class Koota::Compiler

Compiles an AST as returned by {Koota::Parser#call} into bytecode to be consumed by {Koota::VM#call}.

Public Instance Methods

call(ast, refs = {}) click to toggle source
# File lib/koota/compiler.rb, line 13
def call(ast, refs = {})
  @memory = []
  @refs   = refs
  @links  = {
    calls: {},
    picks: {}
  }
  @name = ''

  compile(ast)

  (@memory << HALT).tap do
    link_calls(@links[:calls])
    link_picks(@links[:picks])
  end
end
compile_atom(atom) click to toggle source
# File lib/koota/compiler.rb, line 123
def compile_atom(atom)
  atom.each_char do |char|
    symbolified_char = char.to_sym

    # The name check ensures we aren't calling ourselves.
    if @refs[symbolified_char] && @name != symbolified_char
      # This will be linked to the real offset in a later stage (aka
      # `link_calls`). For now, we use zero as a placeholder and store the
      # current offset in `@links`. This is needed because we don't know
      # what offset to use before all the code is compiled.
      @links[:calls][@memory.length] = symbolified_char
      add_bytecode(CALL, 0, 0)
    else
      add_bytecode(PUT, *Encode.utf8(char))
    end
  end
end
compile_choice(*choices) click to toggle source
# File lib/koota/compiler.rb, line 75
def compile_choice(*choices)
  # First of all, we need to emit a pick opcode. The offset to the pick
  # list is a placeholder for the same reasons calls use placeholders.
  pick_offset = @memory.length
  add_bytecode(PICK, 0, 0)

  # We now loop through each choice and compile it. We also add a jump to
  # the end of the "choice area" after each choice (except the last, since
  # it's just gonna be a jump into the next opcode) in order to skip the
  # rest of the choices. The jumps use a placeholder too, which will be
  # linked after the loop using the `jumps` array.
  jumps   = []
  offsets = []
  choices.each_with_index do |choice, i|
    # We need the offset for each choice for the pick list.
    offsets << @memory.length
    compile(choice)

    # The last choice doesn't need a jump, since it's just gonna be a jump
    # to the next opcode.
    unless i == choices.length - 1
      jumps << @memory.length
      add_bytecode(JUMP, 0, 0)
    end
  end

  # Link the jumps.
  encoded_jump = Encode.short(@memory.length)
  jumps.each do |offset|
    @memory[offset + 1], @memory[offset + 2] = encoded_jump
  end

  # Add the pick offset and the choice offsets to the pick links to be
  # processed later, in `link_picks`.
  @links[:picks][pick_offset] = offsets
end
compile_maybe(maybe) click to toggle source
# File lib/koota/compiler.rb, line 112
def compile_maybe(maybe)
  # A maybe is compiled down to a jrnd pointing after its contents. We can
  # link the offset immediately since we know the length of the maybe.
  jrnd_offset = @memory.length
  add_bytecode(JRND, 0, 0)

  compile(maybe)

  @memory[jrnd_offset + 1], @memory[jrnd_offset + 2] = Encode.short(@memory.length)
end
compile_pattern(*sequence) click to toggle source
# File lib/koota/compiler.rb, line 71
def compile_pattern(*sequence)
  sequence.each { |it| compile(it) }
end
compile_raw(raw) click to toggle source
# File lib/koota/compiler.rb, line 141
def compile_raw(raw)
  raw.each_char do |char|
    add_bytecode(PUT, *Encode.utf8(char))
  end
end

Private Instance Methods

add_bytecode(*bytecode) click to toggle source
# File lib/koota/compiler.rb, line 149
def add_bytecode(*bytecode)
  @memory.concat(bytecode)
end
compile(ast) click to toggle source
# File lib/koota/compiler.rb, line 153
def compile(ast)
  raise ArgumentError, 'invalid AST' if ast.empty?

  send(:"compile_#{ast[0]}", *ast[1..-1])
end