6eae1b8c6357691b157014cbe33404853d3b11bd — Tim Morgan 1 year, 8 months ago b429293
Refactor for easier porting/debugging

* Remove code paths that allow parsing/compiling/executing *additional* code
  subsequent to initializing the object.
* Remove caching of standard library in specs to feel the full pain of slow
  parsing/compiling.
* Move parsing of code responsibility back up to the Program object --
  Compiler delegates back up to the parent Program if it needs to parse
  new code. This separation of concerns could be further improved.
10 files changed, 459 insertions(+), 407 deletions(-)

M bin/scheme
M compiler.rb
M program.rb
M spec/compiler_spec.rb
M spec/lib_spec.rb
M spec/program_spec.rb
M spec/spec_helper.rb
D spec/support/dumpable_string_io.rb
M spec/vm_spec.rb
M vm/call_stack_printer.rb
M bin/scheme => bin/scheme +2 -1
@@ 31,7 31,8 @@ program = if options[:script]
           end
 
 if program
-  exit program.run(debug: options[:debug])
+  program.debug = options[:debug]
+  exit program.run
 else
   puts opt_parser
 end

M compiler.rb => compiler.rb +10 -12
@@ 17,7 17,8 @@ class Compiler
   include Compiler::Lib::Scheme::ProcessContext
   include Compiler::Lib::Scheme::Write
 
-  def initialize(code = nil, filename:, arguments: {}, load_path: LOAD_PATH)
+  def initialize(ast = nil, filename:, arguments: {}, load_path: LOAD_PATH, program:)
+    @program = program
     @variables = {}
     @filename = filename
     @arguments = arguments


@@ 26,20 27,14 @@ class Compiler
     @locals = {}              # top-level locals (globals)
     @libs = {}                # loaded libraries
     @mangled_identifiers = {} # used for macro hygiene
-    @source = {}              # store source code for each file compiled
-    @source[filename] = code
     @sexps = []
-    @sexps += Parser.new(code, filename: filename).parse if code
+    @sexps += ast if ast
   end
 
-  attr_reader :variables, :arguments, :syntax, :source, :libs
+  attr_reader :variables, :arguments, :syntax, :libs
   attr_accessor :filename
 
-  def compile(code = nil)
-    if code
-      @source[@filename] = code
-      @sexps = Parser.new(code, filename: filename).parse
-    end
+  def compile
     compile_sexps(@sexps, options: { syntax: @syntax, locals: @locals }) + [VM::HALT]
   end
 


@@ 286,6 281,10 @@ class Compiler
     return VM::POP unless options[:use]
   end
 
+  def source
+    @program.source
+  end
+
   def parse_file(filename, relative_to: nil)
     path = if filename.start_with?('.') && relative_to
              File.join(File.dirname(relative_to), filename)


@@ 294,7 293,6 @@ class Compiler
            end
     raise "File #{filename} not found in load path #{@load_path.join(';')}" unless path
     code = File.read(path)
-    @source[filename] = code
-    Parser.new(code, filename: filename).parse
+    @program.parse(code, filename: filename)
   end
 end

M program.rb => program.rb +24 -27
@@ 3,35 3,33 @@ require_relative 'compiler'
 require 'time'
 
 class Program
-  EXIT_CODE_VAR_UNDEFINED  = 1
-  EXIT_CODE_STACK_TOO_DEEP = 2
-  EXIT_CODE_SYNTAX_ERROR   = 3
+  EXIT_CODE_SYNTAX_ERROR   = 1
+  EXIT_CODE_VAR_UNDEFINED  = 2
+  EXIT_CODE_STACK_TOO_DEEP = 3
   EXIT_CODE_FATAL_ERROR    = 4
 
   def initialize(code, filename: '(unknown)', args: [], stdout: $stdout)
     @filename = filename
     @args = args
     @stdout = stdout
-    start_parse = Time.now
-    @compiler = Compiler.new(code, filename: filename)
-    @total_parse = Time.now - start_parse
-  rescue Parser::ParseError => e
-    print_syntax_error(e, code)
-    @error_parsing = true
+    @source = {}
+    @code = code
+    @debug = 0
   end
 
-  def run(code: nil, debug: 0)
-    return EXIT_CODE_SYNTAX_ERROR if @error_parsing
-    start_compile = Time.now
-    @instr = @compiler.compile(code)
-    @total_compile = Time.now - start_compile
-    VM::PrettyPrinter.new(@instr, grouped: true, ip: true).print if debug >= 1
-    vm.debug = debug
-    start_execute = Time.now
+  attr_accessor :debug
+
+  def run
+    @ast = parse(@code)
+    @compiler = Compiler.new(@ast, filename: @filename, program: self)
+    @instr = @compiler.compile
+    VM::PrettyPrinter.new(@instr, grouped: true, ip: true).print if @debug >= 1
+    vm.debug = @debug
     vm.execute(@instr)
-    @total_execute = Time.now - start_execute
-    print_timings if ENV['PRINT_TIMINGS']
     vm.return_value
+  rescue Parser::ParseError => e
+    print_syntax_error(e, @code)
+    EXIT_CODE_SYNTAX_ERROR
   rescue VM::VariableUndefined => e
     print_general_error(e)
     EXIT_CODE_VAR_UNDEFINED


@@ 53,6 51,11 @@ class Program
     vm.stdout = io
   end
 
+  def parse(code, filename: @filename)
+    @source[filename] = code
+    Parser.new(code, filename: filename).parse
+  end
+
   private
 
   def vm


@@ 60,7 63,7 @@ class Program
   end
 
   def print_general_error(e)
-    code = @compiler.source[e.filename]
+    code = @source[e.filename]
     VM::SourceCodeErrorPrinter.new(
       title: "Error: #{e.message}",
       code: code,


@@ 80,14 83,8 @@ class Program
     VM::CallStackPrinter.new(
       title: "Error: #{e.message}",
       call_stack: e.call_stack,
-      compiler: @compiler,
+      source: @source,
       message: e.message
     ).print(@stdout)
   end
-
-  def print_timings
-    puts "parse:   #{@total_parse}"
-    puts "compile: #{@total_compile}"
-    puts "execute: #{@total_execute}"
-  end
 end

M spec/compiler_spec.rb => spec/compiler_spec.rb +395 -333
@@ 1,20 1,22 @@
 require_relative './spec_helper'
 
 describe Compiler do
-  before(:all) do
-    base_compiler = described_class.new(filename: __FILE__)
-    base_compiler.compile('(import (scheme base))')
-    @compiler_cache = Marshal.dump(base_compiler)
-  end
+  let(:ast)     { [] }
+  let(:out)     { StringIO.new }
+  let(:program) { Program.new('', stdout: out) }
+
+  subject { described_class.new(ast, filename: __FILE__, program: program) }
 
-  subject { Marshal.load(@compiler_cache) }
+  before do
+    @result = subject.compile
+  end
 
   describe '#compile' do
     context 'number' do
-      before do
-        @result = subject.compile(<<-END)
-          1
-        END
+      let(:ast) do
+        [
+          '1'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 27,11 29,11 @@ describe Compiler do
     end
 
     context '#t' do
-      before do
-        @result = subject.compile(<<-END)
-          #t
-          #true
-        END
+      let(:ast) do
+        [
+          '#t',
+          '#true'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 46,11 48,11 @@ describe Compiler do
     end
 
     context '#f' do
-      before do
-        @result = subject.compile(<<-END)
-          #f
-          #false
-        END
+      let(:ast) do
+        [
+          '#f',
+          '#false'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 65,10 67,10 @@ describe Compiler do
     end
 
     context '"a string"' do
-      before do
-        @result = subject.compile(<<-END)
-          "a string"
-        END
+      let(:ast) do
+        [
+          '"a string"'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 82,10 84,11 @@ describe Compiler do
     end
 
     context 'a . pair' do
-      before do
-        @result = subject.compile(<<-END)
-          (quote (1 . 2))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['quote', ['1', '.', '2']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 100,19 103,19 @@ describe Compiler do
     end
 
     context 'character #\c' do
-      before do
-        @result = subject.compile(<<-END)
-          #\\c
-          #\\space
-          #\\newline
-          #\\alarm
-          #\\backspace
-          #\\delete
-          #\\escape
-          #\\null
-          #\\return
-          #\\tab
-        END
+      let(:ast) do
+        [
+          '#\c',
+          '#\space',
+          '#\newline',
+          '#\alarm',
+          '#\backspace',
+          '#\delete',
+          '#\escape',
+          '#\null',
+          '#\return',
+          '#\tab'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 133,10 136,11 @@ describe Compiler do
     end
 
     context 'list->string' do
-      before do
-        @result = subject.compile(<<-END)
-          (list->string (list #\\a #\\b))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['list->string', ['list', '#\a', '#\b']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 155,10 159,11 @@ describe Compiler do
     end
 
     context 'append' do
-      before do
-        @result = subject.compile(<<-END)
-          (append (list 1 2) (list 3 4))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['append', ['list', '1', '2'], ['list', '3', '4']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 180,10 185,11 @@ describe Compiler do
     end
 
     context 'car' do
-      before do
-        @result = subject.compile(<<-END)
-          (car (list 1 2 3))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['car', ['list', '1', '2', '3']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 201,10 207,11 @@ describe Compiler do
     end
 
     context 'cdr' do
-      before do
-        @result = subject.compile(<<-END)
-          (cdr (list 1 2 3))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['cdr', ['list', '1', '2', '3']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 222,10 229,11 @@ describe Compiler do
     end
 
     context 'cons' do
-      before do
-        @result = subject.compile(<<-END)
-          (cons 1 (list 2 3))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['cons', '1', ['list', '2', '3']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 243,10 251,11 @@ describe Compiler do
     end
 
     context 'set-car!' do
-      before do
-        @result = subject.compile(<<-END)
-          (set-car! (quote (1 . 2)) 3)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['set-car!', ['quote', ['1', '.', '2']], '3']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 262,10 271,11 @@ describe Compiler do
     end
 
     context 'set-cdr!' do
-      before do
-        @result = subject.compile(<<-END)
-          (set-cdr! (quote (1 . 2)) 3)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['set-cdr!', ['quote', ['1', '.', '2']], '3']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 281,10 291,11 @@ describe Compiler do
     end
 
     context 'null?' do
-      before do
-        @result = subject.compile(<<-END)
-          (null? (list))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['null?', ['list']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 299,16 310,17 @@ describe Compiler do
     end
 
     context 'variables' do
-      before do
-        @result = subject.compile(<<-END)
-          (define x 8)
-          ((lambda ()
-            (define y 10)
-            (set! y 11)
-            (set! x 9)
-            y))
-          x
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define', 'x', '8'],
+          [['lambda', [],
+            ['define', 'y', '10'],
+            ['set!', 'y', '11'],
+            ['set!', 'x', '9'],
+            'y']],
+          'x'
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 334,23 346,28 @@ describe Compiler do
     end
 
     context 'variable out of scope' do
+      let(:erroring_ast) do
+        [
+          [VM::Atom.new('include', __FILE__), '"./fixtures/library-test"'],
+          ['lambda', [],
+            'n'],
+          ['define', 'n', '10']
+        ]
+      end
+
       it 'fails to compile' do
-        expect {
-          subject.compile(<<-END)
-            (lambda ()
-              n)
-            (define n 10)
-          END
-        }.to raise_error(VM::VariableUndefined)
+        compiler = described_class.new(erroring_ast, filename: __FILE__, program: program)
+        expect { compiler.compile }.to raise_error(VM::VariableUndefined)
       end
     end
 
     context 'quote' do
       context 'given a list' do
-        before do
-          @result = subject.compile(<<-END)
-            (quote (foo 2 3 (write 4)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quote', ['foo', '2', '3', ['write', '4']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 371,10 388,11 @@ describe Compiler do
       end
 
       context 'given an empty list' do
-        before do
-          @result = subject.compile(<<-END)
-            (quote ())
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quote', []]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 388,10 406,11 @@ describe Compiler do
       end
 
       context 'given an atom' do
-        before do
-          @result = subject.compile(<<-END)
-            (quote foo)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quote', 'foo']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 406,10 425,11 @@ describe Compiler do
 
     context 'quasiquote' do
       context 'given a simple list' do
-        before do
-          @result = subject.compile(<<-END)
-            (quasiquote (foo 2 3 (write 4)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quasiquote', ['foo', '2', '3', ['write', '4']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 430,10 450,11 @@ describe Compiler do
       end
 
       context 'given a list containing an unquote expression' do
-        before do
-          @result = subject.compile(<<-END)
-            (quasiquote (list 1 (unquote (+ 2 3))))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quasiquote', ['list', '1', ['unquote', ['+', '2', '3']]]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 452,10 473,11 @@ describe Compiler do
       end
 
       context 'given a list containing an unquote-splicing expression' do
-        before do
-          @result = subject.compile(<<-END)
-            (quasiquote (list 1 (unquote-splicing (list 2 3))))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['quasiquote', ['list', '1', ['unquote-splicing', ['list', '2', '3']]]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 473,11 495,12 @@ describe Compiler do
       end
 
       context 'given a list containing an unquoted variable' do
-        before do
-          @result = subject.compile(<<-END)
-            (define foo 2)
-            (quasiquote (list 1 (unquote foo)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'foo', '2'],
+            ['quasiquote', ['list', '1', ['unquote', 'foo']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 498,10 521,11 @@ describe Compiler do
 
     context 'define' do
       context '<variable> <expression>' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x 1)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x', '1']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 514,11 538,12 @@ describe Compiler do
       end
 
       context '(<variable> <formals>) <body>' do
-        before do
-          @result = subject.compile(<<-END)
-            (define (fn x y)
-              (list x y))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', ['fn', 'x', 'y'],
+              ['list', 'x', 'y']]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 541,11 566,12 @@ describe Compiler do
       end
 
       context '(<variable> . <formal>) <body>' do
-        before do
-          @result = subject.compile(<<-END)
-            (define (fn . x)
-              x)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', ['fn', '.', 'x'],
+              'x']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 565,11 591,12 @@ describe Compiler do
 
     context 'lambda' do
       context 'not storing in a variable or passing to a function' do
-        before do
-          @result = subject.compile(<<-END)
-            (lambda ()
-              (define x 1))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['lambda', [],
+              ['define', 'x', '1']]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 586,12 613,13 @@ describe Compiler do
       end
 
       context 'storing in a variable' do
-        before do
-          @result = subject.compile(<<-END)
-            (define myfn
-              (lambda ()
-                (define x 1)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'myfn',
+              ['lambda', [],
+                ['define', 'x', '1']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 608,15 636,16 @@ describe Compiler do
       end
 
       context 'mixed variable storage' do
-        before do
-          @result = subject.compile(<<-END)
-            (define one
-              (lambda ()
-                (lambda () 1)
-                (define two
-                  (lambda () 2))
-                (lambda () 3)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'one',
+              ['lambda', [],
+                ['lambda', [], '1'],
+                ['define', 'two',
+                  ['lambda', [], '2']],
+                ['lambda', [], '3']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 645,12 674,13 @@ describe Compiler do
       end
 
       context 'with return value' do
-        before do
-          @result = subject.compile(<<-END)
-            (lambda ()
-              1
-              2)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['lambda', [],
+              '1',
+              '2']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 670,13 700,14 @@ describe Compiler do
 
     context 'call' do
       context 'without args' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x
-              (lambda ()
-                1))
-            (x)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x',
+              ['lambda', [],
+                '1']],
+            ['x']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 694,12 725,13 @@ describe Compiler do
       end
 
       context 'with args' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x
-              (lambda (y z) ()))
-            (x 2 4)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x',
+              ['lambda', ['y', 'z'], []]],
+            ['x', '2', '4']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 725,12 757,13 @@ describe Compiler do
       end
 
       context 'with variable args' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x
-              (lambda args ()))
-            (x 2 4)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x',
+              ['lambda', 'args', []]],
+            ['x', '2', '4']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 754,12 787,13 @@ describe Compiler do
       end
 
       context 'with destructuring args' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x
-              (lambda (first second . rest) ()))
-            (x 2 3 4 5)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x',
+              ['lambda', ['first', 'second', '.', 'rest'], []]],
+            ['x', '2', '3', '4', '5']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 789,10 823,11 @@ describe Compiler do
       end
 
       context 'calling immediately' do
-        before do
-          @result = subject.compile(<<-END)
-            ((lambda (x) x) 1)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            [['lambda', ['x'], 'x'], '1']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 813,12 848,13 @@ describe Compiler do
       end
 
       context 'calling self' do
-        before do
-          @result = subject.compile(<<-END)
-            (define x
-              (lambda ()
-                (x)))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'x',
+              ['lambda', [],
+                ['x']]]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 836,10 872,11 @@ describe Compiler do
     end
 
     context 'call/cc' do
-      before do
-        @result = subject.compile(<<-END)
-          (call/cc (lambda (k) 1))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['call/cc', ['lambda', ['k'], '1']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 857,11 894,12 @@ describe Compiler do
     end
 
     context 'apply' do
-      before do
-        @result = subject.compile(<<-END)
-          (define (foo))
-          (apply foo (list 1 2))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define', ['foo']],
+          ['apply', 'foo', ['list', '1', '2']]
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 884,10 922,11 @@ describe Compiler do
     end
 
     context 'list' do
-      before do
-        @result = subject.compile(<<-END)
-          (list 1 2)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['list', '1', '2']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 903,10 942,11 @@ describe Compiler do
     end
 
     context 'eq?' do
-      before do
-        @result = subject.compile(<<-END)
-          (eq? 1 1)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['eq?', '1', '1']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 922,11 962,12 @@ describe Compiler do
 
     context 'if' do
       context 'given value is not used' do
-        before do
-          @result = subject.compile(<<-END)
-            (if #t 2 3)
-            0
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['if', '#t', '2', '3'],
+            '0'
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 945,10 986,11 @@ describe Compiler do
       end
 
       context 'given value is used' do
-        before do
-          @result = subject.compile(<<-END)
-            (write-string (if #t 2 3))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['write-string', ['if', '#t', '2', '3']]
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 968,11 1010,12 @@ describe Compiler do
       end
 
       context 'inside a function body' do
-        before do
-          @result = subject.compile(<<-END)
-            (lambda ()
-              (if #t 2 3))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['lambda', [],
+              ['if', '#t', '2', '3']]
+          ]
         end
 
         it 'compiles into vm instructions, optimizing out the JUMP with a RETURN' do


@@ 993,10 1036,11 @@ describe Compiler do
       end
 
       context 'without else' do
-        before do
-          @result = subject.compile(<<-END)
-            (if #f #f)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['if', '#f', '#f']
+          ]
         end
 
         it 'compiles into vm instructions, adding PUSH_UNDEF for else' do


@@ 1015,16 1059,17 @@ describe Compiler do
 
     context 'define-syntax' do
       context 'at the top level' do
-        before do
-          @result = subject.compile(<<-END)
-            (define local-foo 1)
-            (define-syntax macro-foo (syntax-rules () ()))
-
-            (define-syntax and
-              (syntax-rules ()
-                ((and) #t)))
-            (and)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['define', 'local-foo', '1'],
+            ['define-syntax', 'macro-foo', ['syntax-rules', [], []]],
+
+            ['define-syntax', 'and',
+              ['syntax-rules', [],
+                [['and'], '#t']]],
+            ['and']
+          ]
         end
 
         it 'stores the transformer in the top-level syntax accessor' do


@@ 1051,14 1096,15 @@ describe Compiler do
       end
 
       context 'inside a lambda' do
-        before do
-          @result = subject.compile(<<-END)
-            (lambda ()
-              (define-syntax my-and
-                (syntax-rules ()
-                  ((my-and) #t)))
-              (and))
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            ['lambda', [],
+              ['define-syntax', 'my-and',
+                ['syntax-rules', [],
+                  [['my-and'], '#t']]],
+              ['and']]
+          ]
         end
 
         it 'does not store the transformer in the top-level syntax accessor' do


@@ 1080,15 1126,16 @@ describe Compiler do
 
     context 'include' do
       context 'given a path' do
-        before do
-          @result = subject.compile(<<-END)
-            (include "./fixtures/include-test")
-            "hello from main"
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+            [VM::Atom.new('include', __FILE__), '"./fixtures/include-test"'],
+            '"hello from main"'
+          ]
         end
 
         it 'includes the code' do
-          expect(d(@result)).to eq([
+          expected = [
             'VM::IMPORT_LIB', 'scheme/base', 'write-string', 'write-string',
             'VM::PUSH_STR', 'hello from include-test',
             'VM::PUSH_NUM', 1,


@@ 1098,26 1145,33 @@ describe Compiler do
             'VM::PUSH_STR', 'hello from main',
             'VM::POP',
             'VM::HALT'
-          ])
+          ]
+          expect(d(@result, skip_libs: false)[-expected.size..-1]).to eq(expected)
         end
       end
 
       context 'given a path to a library' do
+        let(:erroring_ast) do
+          [
+            [VM::Atom.new('include', __FILE__), '"./fixtures/library-test"'],
+            ['macro']
+          ]
+        end
+
         it 'does not import macros into the current namespace' do
-          expect {
-            subject.compile('(include "./fixtures/library-test") (macro)')
-          }.to raise_error(VM::VariableUndefined)
+          compiler = described_class.new(erroring_ast, filename: __FILE__, program: program)
+          expect { compiler.compile }.to raise_error(VM::VariableUndefined)
         end
       end
     end
 
     context 'exit' do
       context 'given no arguments' do
-        before do
-          @result = subject.compile(<<-END)
-            (import (only (scheme process-context) exit))
-            (exit)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', __FILE__), ['only', ['scheme', 'process-context'], 'exit']],
+            ['exit']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 1129,11 1183,11 @@ describe Compiler do
       end
 
       context 'given an integer argument' do
-        before do
-          @result = subject.compile(<<-END)
-            (import (only (scheme process-context) exit))
-            (exit 10)
-          END
+        let(:ast) do
+          [
+            [VM::Atom.new('import', __FILE__), ['only', ['scheme', 'process-context'], 'exit']],
+            ['exit', '10']
+          ]
         end
 
         it 'compiles into vm instructions' do


@@ 1147,16 1201,17 @@ describe Compiler do
     end
 
     context 'macro expansion' do
-      before do
-        @result = subject.compile(<<-END)
-          (define-syntax foo
-            (syntax-rules ()
-              ((foo ((name1 val1) ...))
-                (list
-                  (list "names" (quote name1)) ...
-                  (list "vals" (quote val1)) ...))))
-          (foo ((x "foo") (y "bar") (z "baz")))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define-syntax', 'foo',
+            ['syntax-rules', [],
+              [['foo', [['name1', 'val1'], '...']],
+                ['list',
+                  ['list', '"names"', ['quote', VM::Atom.new('name1')]], '...',
+                  ['list', '"vals"', ['quote', VM::Atom.new('val1')]], '...']]]],
+          ['foo', [['x', '"foo"'], ['y', '"bar"'], ['z', '"baz"']]]
+        ]
       end
 
       it 'expands the macro' do


@@ 1200,34 1255,37 @@ describe Compiler do
     end
 
     context 'define-library and import' do
-      before do
-        @result = subject.compile(<<-END)
-          (define-library (my-lib 1)
-            (import (only (scheme base) define define-syntax))
-            (begin
-              (define foo "foo")
-              (define-syntax macro1
-                (syntax-rules ()
-                  ((macro1) 1))))
-            (export foo macro1))
-          (define-library (my-lib 2)
-            (import (only (scheme base) define define-syntax))
-            (import (my-lib 1))
-            (begin
-              (define baz "baz")
-              (define-syntax macro2
-                (syntax-rules ()
-                  ((macro2) macro1))))
-            (export foo baz macro2 (rename foo bar)))
-          (import (only (scheme base) define))
-          (import (my-lib 1) (my-lib 2))
-          (import (only (my-lib 1) foo))
-          (import (only (my-lib 1) bar)) ; wrong name
-          (import (prefix (my-lib 1) my-))
-          (import (rename (my-lib 1) (foo baz)))
-          (import (except (my-lib 2) bar))
-          (import (rename (prefix (except (my-lib 2) bar) my-) (my-foo my-baz)))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define-library', [VM::Atom.new('my-lib', filename: ''), '1'],
+            ['import', ['only', ['scheme', 'base'], 'define', 'define-syntax']],
+            ['begin',
+              ['define', 'foo', '"foo"'],
+              ['define-syntax', 'macro1',
+                ['syntax-rules', [],
+                  [['macro1'], '1']]]],
+            ['export', 'foo', 'macro1']],
+          ['define-library', [VM::Atom.new('my-lib', filename: ''), '2'],
+            ['import', ['only', ['scheme', 'base'], 'define', 'define-syntax']],
+            ['import', ['my-lib', '1']],
+            ['begin',
+              ['define', 'baz', '"baz"'],
+              ['define-syntax', 'macro2',
+                ['syntax-rules', [],
+                  [['macro2'], 'macro1']]]],
+            ['export', 'foo', 'baz', 'macro2', ['rename', 'foo', 'bar']]],
+          [VM::Atom.new('import', filename: ''), ['only', ['scheme', 'base'], 'define']],
+          [VM::Atom.new('import', filename: ''), ['my-lib', '1'], ['my-lib', '2']],
+          [VM::Atom.new('import', filename: ''), ['only', ['my-lib', '1'], 'foo']],
+          [VM::Atom.new('import', filename: ''), ['only', ['my-lib', '1'], 'bar']], # wrong name
+          [VM::Atom.new('import', filename: ''), ['prefix', ['my-lib', '1'], 'my-']],
+          [VM::Atom.new('import', filename: ''), ['rename', ['my-lib', '1'], ['foo', 'baz']]],
+          [VM::Atom.new('import', filename: ''), ['except', ['my-lib', '2'], 'bar']],
+          [VM::Atom.new('import', filename: ''), ['rename',
+                                                  ['prefix', ['except', ['my-lib', '2'], 'bar'], 'my-'],
+                                                  ['my-foo', 'my-baz']]]
+        ]
       end
 
       it 'records export names for the libraries' do


@@ 1258,7 1316,7 @@ describe Compiler do
       end
 
       it 'compiles into vm instructions' do
-        expect(d(@result, skip_libs: false)).to eq([
+        expected = [
           'VM::SET_LIB', 'my-lib/1',
           'VM::PUSH_STR', 'foo',
           'VM::DEFINE_VAR', 'foo',


@@ 1283,20 1341,22 @@ describe Compiler do
           'VM::IMPORT_LIB', 'my-lib/2', 'baz', 'my-baz',
 
           'VM::HALT'
-        ])
+        ]
+        expect(d(@result, skip_libs: false)[-expected.size..-1]).to eq(expected)
       end
     end
   end
 
   context 'define-syntax and syntax-rules' do
     context 'given a template with two arguments and a nested template' do
-      before do
-        @result = subject.compile(<<-END)
-          (define-syntax listify
-            (syntax-rules ()
-              ((listify first second) (list (list first) (list second)))))
-          (listify 1 2)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define-syntax', 'listify',
+            ['syntax-rules', [],
+             [['listify', 'first', 'second'], ['list', ['list', VM::Atom.new('first')], ['list', VM::Atom.new('second')]]]]],
+          ['listify', '1', '2']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 1316,16 1376,17 @@ describe Compiler do
     end
 
     context 'given multiple templates, recursive expansion' do
-      before do
-        @result = subject.compile(<<-END)
-          (define-syntax and
-            (syntax-rules ()
-              ((and) #t)
-              ((and test) test)
-              ((and test1 test2 ...)
-                (if test1 (and test2 ...) #f))))
-          (and 1 2 3)
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define-syntax', 'and',
+            ['syntax-rules', [],
+              [['and'], '#t'],
+              [['and', 'test'], 'test'],
+              [['and', 'test1', 'test2', '...'],
+                 ['if', VM::Atom.new('test1'), ['and', VM::Atom.new('test2'), '...'], '#f']]]],
+          ['and', '1', '2', '3']
+        ]
       end
 
       it 'compiles into vm instructions' do


@@ 1346,15 1407,16 @@ describe Compiler do
     end
 
     context 'given multiple templates, recursive expansion' do
-      before do
-        @result = subject.compile(<<-END)
-          (define-syntax let
-            (syntax-rules ()
-              ((let ((name val) ...) body1 body2 ...)
-                ((lambda (name ...) body1 body2 ...)
-                val ...))))
-          (let ((x 1) (y 2) (z 3)) (list x y z))
-        END
+      let(:ast) do
+        [
+          [VM::Atom.new('import', filename: ''), ['scheme', 'base']],
+          ['define-syntax', 'let',
+            ['syntax-rules', [],
+              [['let', [['name', 'val'], '...'], 'body1', 'body2', '...'],
+                [['lambda', [VM::Atom.new('name'), '...'], VM::Atom.new('body1'), VM::Atom.new('body2'), '...'],
+                VM::Atom.new('val'), '...']]]],
+          ['let', [['x', '1'], ['y', '2'], ['z', '3']], ['list', 'x', 'y', 'z']]
+        ]
       end
 
       it 'compiles into vm instructions' do

M spec/lib_spec.rb => spec/lib_spec.rb +5 -12
@@ 1,24 1,17 @@
 require_relative './spec_helper'
-require_relative './support/dumpable_string_io'
 require 'stringio'
 
-out = DumpableStringIO.new
-program = Program.new('(import (scheme base) (assert))', stdout: out)
-program.run
-cached_program = Marshal.dump(program)
-
 Dir[File.expand_path('../lib/**/*.scm', __FILE__)].each do |path|
   describe File.split(path).last do
-    code = File.read(path).sub(/\(import \(scheme base\)\s+\(assert\)\s*\)/, '')
+    code = File.read(path)
     focus = !(code =~ /^;; focus/).nil? || ENV['SCM_FILE'] == path[-ENV['SCM_FILE'].to_s.size..-1]
     skip = !(code =~ /^;; skip/).nil?
     debug = code =~ /^;; debug/ ? 2 : 0
     it 'passes all tests', focus: focus, skip: skip do
-      out = DumpableStringIO.new
-      program = Marshal.load(cached_program)
-      program.filename = path
-      program.stdout = out
-      program.run(code: code, debug: debug)
+      out = StringIO.new
+      program = Program.new(code, filename: path, stdout: out)
+      program.debug = debug
+      program.run
       out.rewind
       result = out.read
       raise result unless result == ''

M spec/program_spec.rb => spec/program_spec.rb +6 -6
@@ 116,8 116,8 @@ describe Program do
     context 'when the program fails' do
       let(:code) { 'foo' }
 
-      it 'returns 1' do
-        expect(subject.run).to eq(1)
+      it 'returns 2' do
+        expect(subject.run).to eq(2)
       end
     end
 


@@ 196,8 196,8 @@ describe Program do
         '(foo)'
       end
 
-      it 'sets the exit code to 1' do
-        expect(subject.run).to eq(1)
+      it 'sets the exit code to 2' do
+        expect(subject.run).to eq(2)
       end
 
       it 'shows the filename, line and column of the error' do


@@ 280,8 280,8 @@ describe Program do
         '      (()'
       end
 
-      it 'sets the exit code to 3' do
-        expect(subject.run).to eq(3)
+      it 'sets the exit code to 1' do
+        expect(subject.run).to eq(1)
       end
 
       it 'shows the filename, line and column of the error' do

M spec/spec_helper.rb => spec/spec_helper.rb +6 -1
@@ 10,7 10,12 @@ RSpec.configure do |c|
   def d(instructions, skip_libs: true)
     pretty = VM::PrettyPrinter.new(instructions).format
     if skip_libs
-      pretty.slice_after('VM::ENDL').to_a.last
+      pretty = pretty.slice_after('VM::ENDL').to_a.last
+      if (import_index = pretty.rindex('VM::IMPORT_LIB'))
+        pretty[(import_index + 4)..-1]
+      else
+        pretty
+      end
     else
       pretty
     end

D spec/support/dumpable_string_io.rb => spec/support/dumpable_string_io.rb +0 -8
@@ 1,8 0,0 @@
-# we don't actually care to dump or load this
-class DumpableStringIO < StringIO
-  def marshal_dump
-    []
-  end
-
-  def marshal_load(_data); end
-end

M spec/vm_spec.rb => spec/vm_spec.rb +8 -4
@@ 1167,7 1167,7 @@ describe VM do
   describe 'tail call elimination' do
     context 'when the return is in the true position of an if' do
       before do
-        c = Compiler.new(<<-END, filename: 'tce.scm')
+        ast = Parser.new(<<-END, filename: 'tce.scm').parse
           (import (only (scheme base) >= - define if))
           (import (only (scheme process-context) exit))
           (define (fn n)


@@ 1177,6 1177,7 @@ describe VM do
               n))
           (fn 2)
         END
+        c = Compiler.new(ast, filename: 'tce.scm', program: Program.new(''))
         instr = c.compile
         subject.execute(instr)
       end


@@ 1195,7 1196,7 @@ describe VM do
 
     context 'when the return is in the false position of an if' do
       before do
-        c = Compiler.new(<<-END, filename: 'tce.scm')
+        ast = Parser.new(<<-END, filename: 'tce.scm').parse
           (import (only (scheme base) < - define if))
           (import (only (scheme process-context) exit))
           (define (fn n)


@@ 1205,6 1206,7 @@ describe VM do
               (fn (- n 1))))
           (fn 2)
         END
+        c = Compiler.new(ast, filename: 'tce.scm', program: Program.new(''))
         instr = c.compile
         subject.execute(instr)
       end


@@ 1223,7 1225,7 @@ describe VM do
 
     context 'inside nested ifs' do
       before do
-        c = Compiler.new(<<-END, filename: 'tce.scm')
+        ast = Parser.new(<<-END, filename: 'tce.scm').parse
           (import (only (scheme base) >= - define if))
           (import (only (scheme process-context) exit))
           (define (fn n)


@@ 1235,6 1237,7 @@ describe VM do
               #f))
           (fn 2)
         END
+        c = Compiler.new(ast, filename: 'tce.scm', program: Program.new(''))
         instr = c.compile
         subject.execute(instr)
       end


@@ 1253,7 1256,7 @@ describe VM do
 
     context 'when the call uses apply' do
       before do
-        c = Compiler.new(<<-END, filename: 'tce.scm')
+        ast = Parser.new(<<-END, filename: 'tce.scm').parse
           (import (only (scheme base) >= - define if apply list))
           (import (only (scheme process-context) exit))
           (define (fn n)


@@ 1263,6 1266,7 @@ describe VM do
               n))
           (fn 2)
         END
+        c = Compiler.new(ast, filename: 'tce.scm', program: Program.new(''))
         instr = c.compile
         subject.execute(instr)
       end

M vm/call_stack_printer.rb => vm/call_stack_printer.rb +3 -3
@@ 1,9 1,9 @@
 class VM
   class CallStackPrinter
-    def initialize(title: 'Error', call_stack:, compiler:, message:)
+    def initialize(title: 'Error', call_stack:, source:, message:)
       @title = title
       @call_stack = call_stack
-      @compiler = compiler
+      @source = source
       @message = message
     end
 


@@ 12,7 12,7 @@ class VM
       @call_stack.reverse.each do |frame|
         next unless (name = frame[:name] || frame[:orig_name])
         out << "#{name.filename}##{name.line}"
-        code = @compiler.source[name.filename].split("\n")[name.line - 1]
+        code = @source[name.filename].split("\n")[name.line - 1]
         out << "  #{code}"
         out << " #{' ' * name.column}^#{' ' + @message if @message}"
       end