Class: Alda::REPL

Inherits:
Object
  • Object
show all
Defined in:
lib/alda-rb/repl.rb

Overview

An instance of this class is an REPL session.

It provides an Alda::REPL::TempScore for you to operate on. To see what methods you can call in an REPL session, see instance methods of Alda::REPL::TempScore.

The session uses “> ” to indicate your input. Your input should be ruby codes, and the codes will be sent to an Alda::REPL::TempScore and executed.

After executing the ruby codes, if the score is not empty, it is played, and the translated alda codes are printed.

Note that every time your ruby codes input is executed, the score is cleared beforehand. To check the result of your previous input, run puts history.

Unlike IRB, this REPL does not print the result of the executed codes. Use p or puts if you want.

Interrupt and SystemExit exceptions are rescued and will not cause the process terminating. exit terminates the REPL session instead of the process.

To start an REPL session in a ruby program, use #run. To start an REPL session conveniently from command line, run command alda-irb. For details about this command line tool, run alda-irb --help.

$ alda-irb
> p processes.last
{:id=>"dus", :port=>34317, :state=>nil, :expiry=>nil, :type=>:repl_server}
> piano_; c d e f
piano: [c d e f]
> 5.times do
.   c
>   end
c c c c c
> score_text
piano: [c d e f]
c c c c c
> play
Playing...
> save 'temp.alda'
> puts `cat temp.alda`
piano: [c d e f]
c c c c c
> system 'rm temp.alda'
> exit

Notice that there is a significant difference between Alda 1 REPL and Alda 2 REPL. In short, Alda 2 has a much more powerful REPL than Alda 1, so it dropped the --history option in the alda play command line interface (alda-lang/alda#367). It has an nREPL server, and this class simply functions by sending messages to the nREPL server. However, for Alda 1, this class maintains necessary information in the memory of the Ruby program, and the REPL is implemented by repeatedly running alda play in command line. Therefore, this class functions differently for Alda 1 and Alda 2 and you thus should not modify Alda::generation during an REPL session.

It is also possible to use this class as a Ruby wrapper of APIs of the Alda nREPL server in Alda 2. In this usage, you never need to call #run, and you call #message or #raw_message instead.

repl = Alda::REPL.new
repl.message :eval_and_play, code: 'piano: c d e f' # => nil
repl.message :eval_and_play, code: 'g a b > c' # => nil
repl.message :score_text # => "piano: [c d e f]\ng a b > c\n"
repl.message :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)

Defined Under Namespace

Classes: TempScore

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(color: true, preview: true, reline: true, **opts) ⇒ REPL

:call-seq:

new(**opts) -> Alda::REPL

Creates a new Alda::REPL. The parameter color specifies whether the output should be colored (sets #color). The parameter preview specifies whether a preview of what Alda code will be played everytime you input ruby codes (sets #preview). The parameter reline specifies whether to use Reline for input.

The opts are passed to the command line of alda repl. Available options are host, port, etc. Run alda repl --help for more info. If port is specified and host is not or is specified to be "localhost" or "127.0.0.1", then this method will try to connect to an existing Alda REPL server. A new one will be started only if no existing server is found.

The opts are ignored in Alda 1.



249
250
251
252
253
254
255
256
257
258
# File 'lib/alda-rb/repl.rb', line 249

def initialize color: true, preview: true, reline: true, **opts
	@score = TempScore.new self
	@binding = @score.get_binding
	# IRB once changed the API of RubyLex#initialize. Take care of that.
	@lex = RubyLex.new *(RubyLex.instance_method(:initialize).arity == 0 ? [] : [@binding])
	@color = color
	@preview = preview
	@reline = reline
	setup_repl opts
end

Instance Attribute Details

#colorObject

Whether the output should be colored.



219
220
221
# File 'lib/alda-rb/repl.rb', line 219

def color
  @color
end

#hostObject (readonly)

The host of the nREPL server. Only useful in Alda 2.



211
212
213
# File 'lib/alda-rb/repl.rb', line 211

def host
  @host
end

#portObject (readonly)

The port of the nREPL server. Only useful in Alda 2.



215
216
217
# File 'lib/alda-rb/repl.rb', line 215

def port
  @port
end

#previewObject

Whether a preview of what Alda code will be played everytime you input ruby codes.



223
224
225
# File 'lib/alda-rb/repl.rb', line 223

def preview
  @preview
end

#relineObject

Whether to use Reline for input. When it is false, the REPL session will be less buggy but less powerful.



228
229
230
# File 'lib/alda-rb/repl.rb', line 228

def reline
  @reline
end

Instance Method Details

#clear_historyObject

:call-seq:

clear_history() -> nil

In Alda 1, clears #history. In Alda 2, askes the nREPL server to clear its history (start a new score).



506
507
508
509
510
511
512
513
# File 'lib/alda-rb/repl.rb', line 506

def clear_history
	if Alda.v1?
		@history = StringIO.new
	else
		try_command { message :new_score }
	end
	nil
end

#historyObject

:call-seq:

history() -> String

In Alda 1, it is the same as an attribute reader. In Alda 2, it asks the nREPL server for its score text and returns it.



492
493
494
495
496
497
498
# File 'lib/alda-rb/repl.rb', line 492

def history
	if Alda.v1?
		@history
	else
		try_command { message :score_text }
	end
end

#message(op, **params) ⇒ Object

:call-seq:

message(op, **params) -> String or Hash

Sends a message to the nREPL server with the following format, with op being the operation name (the op field in the message), and params being the parameters (other fields in the message). Then, this method analyzes the response. If there is an error, raises Alda::NREPLServerError. Otherwise, if the response contains only one field, return the content of that field (a String). Otherwise, return the whole response as a Hash.

repl = Alda::REPL.new
repl.message :eval_and_play, code: 'piano: c d e f' # => nil
repl.message :eval_and_play, code: 'g a b > c' # => nil
repl.message :score_text # => "piano: [c d e f]\ng a b > c\n"
repl.message :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)


320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/alda-rb/repl.rb', line 320

def message op, **params
	result = raw_message op: Alda::Utils.snake_to_slug(op), **params
	result.transform_keys! { Alda::Utils.slug_to_snake _1 }
	if (status = result.delete :status).include? 'error'
		raise Alda::NREPLServerError.new @host, @port, result.delete(:problems), status
	end
	case result.size
	when 0 then nil
	when 1 then result.values.first
	else result
	end
end

#play_score(code) ⇒ Object

:call-seq:

play_score(code) -> nil

Appends code to the history and plays the code as Alda code. In Alda 1, plays the score by sending code to command line alda. In Alda 2, sends code to the nREPL server for evaluating and playing.



452
453
454
455
456
457
458
459
# File 'lib/alda-rb/repl.rb', line 452

def play_score code
	if Alda.v1?
		Alda.play code: code, history: @history
		@history.puts code
	else
		message :eval_and_play, code: code
	end
end

#process_rb_code(code) ⇒ Object

:call-seq:

process_rb_code(code) -> true or false

Processes the Ruby codes read. Sends it to a score and sends the result to command line alda. Returns false for breaking the REPL main loop, true otherwise.



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/alda-rb/repl.rb', line 411

def process_rb_code code
	@score.clear
	begin
		@binding.eval code
	rescue StandardError, ScriptError, Interrupt => e
		$stderr.print e.full_message
		return true
	rescue SystemExit
		return false
	end
	code = @score.events_alda_codes
	unless code.empty?
		$stdout.puts @color ? code.yellow : code
		try_command { play_score code }
	end
	true
end

#raw_message(contents) ⇒ Object

:call-seq:

raw_message(contents) -> Hash

Sends a message to the nREPL server and returns the response. The parameter contents is a Hash or a JSON string.

repl = Alda::REPL.new
repl.raw_message op: 'describe' # => {"ops"=>...}


296
297
298
299
300
301
# File 'lib/alda-rb/repl.rb', line 296

def raw_message contents
	Alda::GenerationError.assert_generation [:v2]
	contents = JSON.parse contents if contents.is_a? String
	@socket.write contents.bencode
	@bencode_parser.parse!
end

#rb_codeObject

:call-seq:

rb_code() -> String

Reads and returns the next Ruby codes input in the REPL session. It can intelligently continue reading if the code is not complete yet.



362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/alda-rb/repl.rb', line 362

def rb_code
	result = ''
	indent = 0
	begin
		result.concat readline(indent).tap { return unless _1 }, ?\n
		# IRB once changed the API of RubyLex#check_state. Take care of that.
		opts = @lex.method(:check_state).arity.positive? ? {} : { context: @binding }
		ltype, indent, continue, block_open = @lex.check_state result, **opts
	rescue Interrupt
		$stdout.puts
		return ''
	end while ltype || indent.nonzero? || continue || block_open
	result
end

#readline(indent = 0) ⇒ Object

:call-seq:

readline(indent = 0) -> String

Prompts the user to input a line. The parameter indent is the indentation level. Twice the number of spaces is already in the input field before the user fills in if #reline is true. The prompt hint is different for zero indent and nonzero indent. Returns the user input.



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/alda-rb/repl.rb', line 387

def readline indent = 0
	prompt = indent.nonzero? ? '. ' : '> '
	prompt = prompt.green if @color
	if @reline
		Reline.pre_input_hook = -> do
			Reline.insert_text '  ' * indent
			Reline.redisplay
			Reline.pre_input_hook = nil
		end
		Reline.readline prompt, true
	else
		$stdout.print prompt
		$stdout.flush
		$stdin.gets chomp: true
	end
end

#runObject

:call-seq:

run() -> nil

Runs the session. Includes the start (#start), the main loop, and the termination (#terminate).



339
340
341
342
343
344
345
346
# File 'lib/alda-rb/repl.rb', line 339

def run
	start
	while code = rb_code
		next if code.empty?
		break unless process_rb_code code
	end
	terminate
end

#setup_repl(opts) ⇒ Object

:call-seq:

setup_repl(opts) -> nil

Sets up the REPL session. This method is called in ::new. After you #terminate the session, you cannot use the REPL anymore unless you call this method again.



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/alda-rb/repl.rb', line 268

def setup_repl opts
	if Alda.v1?
		@history = StringIO.new
	else
		@port = (opts.fetch :port, -1).to_i
		@host = opts.fetch :host, 'localhost'
		unless @port.positive? && %w[localhost 127.0.0.1].include?(@host) &&
		       Alda.processes.any? { _1[:port] == @port && _1[:type] == :repl_server }
			Alda.env(ALDA_DISABLE_SPAWNING: :no) { @nrepl_pipe = Alda.pipe :repl, **opts, server: true }
			/nrepl:\/\/[a-zA-Z0-9._\-]+:(?<port>\d+)/ =~ @nrepl_pipe.gets
			@port = port.to_i
			Process.detach @nrepl_pipe.pid
		end
		@socket = TCPSocket.new @host, @port
		@bencode_parser = BEncode::Parser.new @socket
	end
	nil
end

#startObject

:call-seq:

start() -> nil

Starts the session. Currently does nothing.



353
354
# File 'lib/alda-rb/repl.rb', line 353

def start
end

#terminateObject

:call-seq:

terminate() -> nil

Terminates the REPL session. In Alda 1, just calls #clear_history. In Alda 2, sends a SIGINT to the nREPL server if it was spawned by the Ruby program.



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/alda-rb/repl.rb', line 468

def terminate
	if Alda.v1?
		clear_history
	else
		if @nrepl_pipe
			if Alda::Utils.win_platform?
				unless IO.popen(['taskkill', '/f', '/pid', @nrepl_pipe.pid.to_s], &:read).include? 'SUCCESS'
					Alda::Warning.warn 'failed to kill nREPL server; may become zombie process'
				end
			else
				Process.kill :INT, @nrepl_pipe.pid
			end
			@nrepl_pipe.close
		end
		@socket.close
	end
end

#try_commandObject

:call-seq:

try_command() { ... } -> obj

Run the block. In Alda 1, catches Alda::CommandLineError. In Alda 2, catches Alda::NREPLServerError. If an error is caught, prints the error message (in red if #color is true).



437
438
439
440
441
442
443
# File 'lib/alda-rb/repl.rb', line 437

def try_command
	begin
		yield
	rescue Alda.v1? ? Alda::CommandLineError : Alda::NREPLServerError => e
		puts @color ? e.message.red : e.message
	end
end