class Alda::REPL
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)
Attributes
Whether the output should be colored.
The host of the nREPL server. Only useful in Alda 2.
The port of the nREPL server. Only useful in Alda 2.
Whether a preview of what Alda code will be played everytime you input ruby codes.
Whether to use Reline for input. When it is false, the REPL session will be less buggy but less powerful.
Public Class Methods
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.
# 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
Public Instance Methods
In Alda 1, clears history. In Alda 2, askes the nREPL server to clear its history (start a new score).
# 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
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.
# File lib/alda-rb/repl.rb, line 492 def history if Alda.v1? @history else try_command { message :score_text } end end
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)
# 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
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.
# 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
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.
# 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
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"=>...}
# 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
Reads and returns the next Ruby codes input in the REPL session. It can intelligently continue reading if the code is not complete yet.
# 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
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.
# 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
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.
# 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
Starts the session. Currently does nothing.
# File lib/alda-rb/repl.rb, line 353 def start end
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.
# 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
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).
# 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