bdb7c3ef14856959eae3873ebde65a0da58e75df — Tim Morgan 1 year, 8 months 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 @@
     "\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 @@
   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 @@
       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 @@
         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 @@
   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 @@
 
     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 @@
         @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