~tim/scheme-vm

bdb7c3ef14856959eae3873ebde65a0da58e75df — Tim Morgan 2 years ago 249d287
Show better error when calling something other than a lambda
5 files changed, 33 insertions(+), 9 deletions(-)

M program.rb
M spec/program_spec.rb
M spec/vm_spec.rb
M vm.rb
M vm/operations.rb
M program.rb => program.rb +5 -5
@@ 76,13 76,13 @@ class Program
    "\n\n#{line}\n\n#{code}\n#{pointer}"
  end

  def print_call_stack(call_stack)
  def print_call_stack(call_stack, message = nil)
    call_stack.reverse.each do |frame|
      next unless (name = frame[:name])
      next unless (name = frame[:name] || frame[:orig_name])
      @stdout.puts "#{name.filename}##{name.line}"
      code = @compiler.source[name.filename].split("\n")[name.line - 1]
      @stdout.puts "  #{code}"
      @stdout.puts " #{' ' * name.column}^"
      @stdout.puts " #{' ' * name.column}^#{' ' + message if message}"
    end
  end



@@ 98,9 98,9 @@ class Program
  end

  def print_fatal_error(e)
    message = "Fatal Error: #{e.message}"
    message = "Error: #{e.message}"
    @stdout.puts(message)
    print_call_stack(e.call_stack)
    print_call_stack(e.call_stack, e.message)
  end

  def print_timings

M spec/program_spec.rb => spec/program_spec.rb +24 -0
@@ 230,6 230,30 @@ describe Program do
      end
    end

    context 'when something other than a lambda is called' do
      let(:code) do
        "; undefined variable\n" \
        "(import (scheme base))\n" \
        "(define (foo) (1))\n" \
        '(foo)'
      end

      it 'sets the exit code to 1' do
        expect(subject.run).to eq(4)
      end

      it 'shows the filename, line and column of the error' do
        subject.run
        stdout.rewind
        expect(stdout.read).to eq(
          "Error: 1 is not callable\n" \
            "#{__FILE__}#4\n" \
            "  (foo)\n" \
            "   ^ 1 is not callable\n"
        )
      end
    end

    context 'exception in macro in another file' do
      let(:code) do
        '(include "./fixtures/bad-macro") ' \

M spec/vm_spec.rb => spec/vm_spec.rb +1 -1
@@ 119,7 119,7 @@ describe VM do
        expect(continuation.ip).to eq(10)
        expect(continuation.call_stack).to eq([
          { args: [], named_args: {} },
          { name: nil, func: 7, return: 13, args: [], named_args: {} }
          { name: nil, orig_name: nil, func: 7, return: 13, args: [], named_args: {} }
        ])
      end
    end

M vm.rb => vm.rb +1 -1
@@ 142,7 142,7 @@ class VM
  end

  def fetch
    raise "heap[#{@ip}] (#{resolve(@ip)}) is not executable" unless executable?(@ip)
    raise VM::FatalError.new(@call_stack, "#{resolve(@ip)} is not callable") unless executable?(@ip)
    instruction = @heap[@ip]
    @ip += 1
    instruction

M vm/operations.rb => vm/operations.rb +2 -2
@@ 149,7 149,7 @@ class VM

    def do_call(new_ip = pop)
      raise VM::FatalError.new(@call_stack, "tried to call undefined value: #{@last_atom}") if new_ip.nil?
      raise "ip #{new_ip.inspect} is invalid" unless new_ip.is_a?(Integer)
      raise VM::FatalError.new(@call_stack, "#{new_ip.inspect} doesn't look like an address") unless new_ip.is_a?(Integer)
      return do_call_continuation(new_ip) if heap[new_ip].is_a?(Continuation)
      name = @last_atom if find_address_for_name(@last_atom) == new_ip
      if @heap[@ip] == RETURN


@@ 157,7 157,7 @@ class VM
        @call_stack.last[:args] = @call_args
        @call_stack.last[:name] = name
      else
        @call_stack << { name: name, func: new_ip, return: @ip, args: @call_args, named_args: {} }
        @call_stack << { name: name, orig_name: name, func: new_ip, return: @ip, args: @call_args, named_args: {} }
      end
      raise CallStackTooDeep.new(@call_stack) if @call_stack.size > MAX_CALL_DEPTH
      @ip = new_ip