module RubyLisp::Evaluator

Public Instance Methods

eval_ast(input, env) click to toggle source
# File lib/rubylisp/evaluator.rb, line 59
def eval_ast input, env
  # loop forever until a value is returned;
  # this is for tail call optimization
  while true
    # (no-op if this is not a macro call)
    input = macroexpand(input, env)

    case input
    when Array # of ASTs to be evaluated, e.g. multiple expressions in a file
      # discard nils that can be produced by the reader, e.g. when a comment
      # is read
      input = input.compact

      # if input is empty (e.g. only comments), there is nothing to evaluate
      return nil if input.empty?

      # discard the return value of all but the last form
      input[0..-2].each {|form| eval_ast(form, env)}
      # recur; evaluate and return the value of the last form
      input = input.last
    when Hamster::Hash
      return input.map {|k, v| [eval_ast(k, env), eval_ast(v, env)]}
    when Hamster::List
      if input.empty?
        return input
      elsif special_form? input, 'apply'
        # evaluate the last argument
        coll = eval_ast(input[-1], env) || vector()
        # assert that it's enumerable
        sexp = list input.fill(coll, -1, 1)
        assert_arg_type sexp, 'last', Enumerable

        # drop `apply` and pull the last form's contents into the sexp as
        # multiple arguments
        all_but_last_arg = sexp[1..-2].map {|arg| eval_ast arg, env}
        fn, *args = all_but_last_arg + coll.to_list
        sexp = list [Symbol.new("apply"), fn]
        assert_arg_type sexp, 1, [Function, Proc]
        return fn.call(*args)
      # Provides a way to call a Ruby method that expects a block. Blocks
      # are not first-class in Ruby, but they have similar semantics to
      # functions.
      #
      # The last argument to call-with-block can be a Function or
      # Proc that takes any number of arguments; it will be provided to the
      # instance method as a block.
      #
      # ruby: instance.method(arg1, arg2, arg3) { do_something }
      # rbl: (call-with-block .method instance [arg1 arg2 arg3] fn)
      elsif special_form? input, 'call-with-block'
        assert_number_of_args input, 4

        assert_arg_type input, 1, Symbol

        sexp = input[2..-1].map {|form| eval_ast(form, env)}
                           .cons(input[1].value[1..-1].to_sym)
                           .cons(input[0])

        assert_arg_type sexp, 3, [Hamster::List, Hamster::Vector]
        assert_arg_type sexp, 4, [Function, Proc]

        method, receiver, method_args, fn = sexp[1..-1]

        return receiver.send(method, *method_args) do |*block_args|
          fn.call(*block_args)
        end
      elsif special_form? input, 'def'
        assert_arg_type input, 1, Symbol
        k, v = input[1..-1]
        key, val = [k.value, eval_ast(v, env)]
        return env.out_env.set key, val
      elsif special_form? input, 'defmacro*'
        assert_arg_type input, 1, Symbol
        key, val = input[1..-1]
        macro = eval_ast(val, env)
        macro.is_macro = true
        return env.set key.value, macro
      elsif special_form? input, 'do'
        body = input[1..-1]
        # discard the return values of all but the last form
        body[0..-2].each {|form| eval_ast(form, env)}
        # recur; evaluate and return the last form
        input = body.last
      elsif special_form? input, 'eval'
        assert_number_of_args input, 1

        form = eval_ast(input[1], env)
        input = form
      elsif special_form? input, 'fn'
        if input[1].class == Symbol
          fn_name = input[1].value
          more = input[2..-1]
        else
          fn_name = "__fn_#{rand 10000}"
          more = input[1..-1]
        end

        arities = more[0].class == Hamster::Vector ? [list(more)] : more

        return fn = Function.new(fn_name, env, arities) {|*fn_args|
          arity = fn.get_arity(fn_args)
          eval_ast arity.body, fn.gen_env(arity, fn_args, env)
        }
      elsif special_form? input, 'if'
        unless input[1..-1].count > 1
          raise RuntimeError,
                "An `if` form must at least have a 'then' branch."
        end

        cond, then_form, else_form = input[1..-1]

        if (eval_ast cond, env)
          input = then_form
        elsif else_form
          input = else_form
        else
          return nil
        end
      elsif special_form? input, 'in-ns'
        assert_number_of_args input, 1
        input[1] = eval_ast(input[1], env)
        assert_arg_type input, 1, Symbol
        ns_name = input[1].to_s
        # TODO: register ns and switch to it
        env.is_namespace = true
        return env.namespace = ns_name
      elsif special_form? input, 'let'
        assert_arg_type input, 1, Hamster::Vector
        inner_env = Environment.new(outer: env)
        bindings, *body = input[1..-1]
        unless bindings.count.even?
          raise RuntimeError,
                "The bindings vector of `let` must contain an even number " +
                " of forms."
        end

        bindings.each_slice(2) do |(binding, value)|
          inner_env.set binding.value, eval_ast(value, inner_env)
        end

        # discard the return values of all but the last form
        body[0..-2].each {|expr| eval_ast(expr, inner_env)}
        # recur; evaluate and return the last form's value
        env = inner_env
        input = body.last
      elsif special_form? input, 'macroexpand'
        assert_number_of_args input, 1
        form = eval_ast(input[1], env)
        return macroexpand(form, env)
      elsif special_form? input, 'ns'
        # ns will be defined more robustly in rbl.core, but rbl.core also
        # needs the `ns` form in order to declare that it is rbl.core.
        #
        # defining it here in a minimal form where it is equivalent to in-ns,
        # except that you don't have to quote the namespace name
        assert_number_of_args input, 1
        assert_arg_type input, 1, Symbol
        ns_name = input[1].to_s
        # TODO: register ns and switch to it
        env.is_namespace = true
        return env.namespace = ns_name
      elsif special_form? input, 'quasiquote'
        assert_number_of_args input, 1
        ast = input[1]
        input = quasiquote(ast)
      elsif special_form? input, 'quote'
        assert_number_of_args input, 1
        return input[1]
      elsif special_form? input, 'resolve'
        assert_number_of_args input, 1
        symbol = eval_ast(input[1], env)
        sexp = list input.fill(symbol, 1, 1)
        assert_arg_type sexp, 1, Symbol
        return symbol.resolve(env)
      else
        fn, *args = input.map {|value| eval_ast(value, env)}
        if fn.class == Function
          # recur with input and env set to the body of the function and an
          # inner environment where the function's bindings are set to the
          # values of the arguments
          arity = fn.get_arity(args)
          env = fn.gen_env(arity, args, env)
          input = arity.body
        else
          return fn.call(*args)
        end
      end
    when Hamster::Vector
      return input.map {|value| eval_ast(value, env)}
    when Symbol
      return input.resolve(env)
    when Value
      return input.value
    else
      return input
    end
  end
end
macro_call?(ast, env) click to toggle source
# File lib/rubylisp/evaluator.rb, line 12
def macro_call? ast, env
  return false unless ast.is_a? Hamster::List
  symbol = ast[0]
  return false unless symbol.class == Symbol
  return false unless env.find(symbol.value)
  value = symbol.resolve(env)
  value.class == Function && value.is_macro
end
macroexpand(input, env) click to toggle source
# File lib/rubylisp/evaluator.rb, line 21
def macroexpand input, env
  while macro_call? input, env
    macro = input[0].resolve(env)
    input = macro.call(*input[1..-1])
  end
  input
end
pair?(form) click to toggle source
# File lib/rubylisp/evaluator.rb, line 29
def pair? form
  sequential?(form) && form.count > 0
end
quasiquote(ast) click to toggle source
# File lib/rubylisp/evaluator.rb, line 33
def quasiquote ast
  if !pair?(ast)
    list [Symbol.new('quote'), ast]
  elsif vector? ast
    list [Symbol.new('vec'), quasiquote(ast.to_list)]
  else
    elem_a, *elems = ast

    if elem_a.class == Symbol && elem_a.value == 'unquote'
      elems[0]
    elsif pair?(elem_a) &&
          elem_a[0].class == Symbol &&
          elem_a[0].value == 'splice-unquote'
      list [Symbol.new('concat'), elem_a[1], quasiquote(elems)]
    else
      list [Symbol.new('cons'),
            quasiquote(elem_a),
            quasiquote(elems.to_list)]
    end
  end
end
special_form?(input, name) click to toggle source
# File lib/rubylisp/evaluator.rb, line 55
def special_form? input, name
  input[0].class == Symbol && input[0].value == name
end