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

color[RW]

Whether the output should be colored.

host[R]

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

port[R]

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

preview[RW]

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

reline[RW]

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

Public Class Methods

new(**opts) → Alda::REPL click to toggle source

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

clear_history() → nil click to toggle source

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
history() → String click to toggle source

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
message(op, **params) → String or Hash click to toggle source

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
play_score(code) → nil click to toggle source

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
process_rb_code(code) → true or false click to toggle source

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
raw_message(contents) → Hash click to toggle source

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
rb_code() → String click to toggle source

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
readline(indent = 0) → String click to toggle source

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
run() → nil click to toggle source

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

# 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) → nil click to toggle source

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
start() → nil click to toggle source

Starts the session. Currently does nothing.

# File lib/alda-rb/repl.rb, line 353
def start
end
terminate() → nil click to toggle source

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
try_command() { ... } → obj click to toggle source

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