~tim/scheme-vm

e50204ca9b6172b0c94f55146a67120b6b9733ec — Tim Morgan 4 years ago 782a8e4
Give macro definitions referential transparency

...and fix a handful of other variable and macro scoping issues
M compiler.rb => compiler.rb +22 -7
@@ 96,10 96,8 @@ class Compiler
      send(built_in_name, args, options)
    elsif (macro = options[:syntax][name])
      compile_macro_sexp(sexp, macro, options)
    elsif options[:locals][name]
      call(sexp, options)
    else
      raise VM::VariableUndefined, name
      call(sexp, options)
    end
  end



@@ 144,7 142,11 @@ class Compiler

  def compile_atom(name, options)
    if options[:quote] || options[:quasiquote]
      [VM::PUSH_ATOM, name]
      [
        VM::PUSH_ATOM,
        name,
        pop_maybe(options)
      ]
    else
      [
        push_var(name, options),


@@ 195,10 197,22 @@ class Compiler
      send(method_name, sexp[1..-1], options)
    else
      sexp = Macro.new(macro, self).expand(sexp)
      compile_sexp(sexp, options)
      locals = Hash[macro[:locals].zip([true] * macro[:locals].size)]
      compile_sexp(
        sexp,
        options.merge(
          syntax: options[:syntax].merge(macro[:syntax] || {}),
          locals: options[:locals].merge(locals)
        )
      )
    end
  end

  def unmangled_name(name)
    return name unless (match = name.match(/\A#([^:]+):(.+)\z/))
    match[2]
  end

  def do_debug(_args, _options)
    [VM::DEBUG]
  end


@@ 209,10 223,11 @@ class Compiler
  end

  def call((lambda, *args), options)
    function = compile_sexp(lambda, options.merge(use: true))
    [
      args.map { |arg| compile_sexp(arg, options.merge(use: true)) },
      args.any? ? [VM::PUSH_NUM, args.size, VM::SET_ARGS] : nil,
      compile_sexp(lambda, options.merge(use: true)),
      function,
      VM::CALL
    ]
  end


@@ 227,7 242,7 @@ class Compiler
  end

  def push_var(name, options)
    raise VM::VariableUndefined, name unless options[:locals][name]
    raise VM::VariableUndefined, name unless options[:locals][unmangled_name(name)]
    [
      VM::PUSH_VAR,
      name

M compiler/lib/scheme/base.rb => compiler/lib/scheme/base.rb +3 -1
@@ 42,7 42,9 @@ class Compiler
        def base_define_syntax((name, transformer), options)
          options[:syntax][name] = {
            locals: options[:locals].keys + options[:syntax].keys + [name],
            transformer: transformer
            transformer: transformer,
            syntax: options[:syntax],
            lib: options[:lib]
          }
          []
        end

M compiler/libraries.rb => compiler/libraries.rb +12 -8
@@ 5,7 5,9 @@ class Compiler
    def do_define_native((name, method_name), options)
      options[:syntax][name] = {
        locals: options[:locals].keys + options[:syntax].keys + [name],
        native_transformer: method_name
        native_transformer: method_name,
        syntax: options[:syntax],
        lib: options[:lib]
      }
      []
    end


@@ 97,29 99,31 @@ class Compiler
    end

    def do_define_library((name, *declarations), options)
      exports = @libs[name.join('/')] = {
      name_as_string = name.join('/')
      exports = @libs[name_as_string] = {
        syntax: {},
        bindings: {}
      }
      imports = []
      begins = []
      lib_opts = options.merge(use: true, locals: options[:locals].dup, syntax: options[:syntax].dup)
      # TODO: probably shouldn't dup locals either
      lib_opts = options.merge(use: true, locals: options[:locals].dup, syntax: exports[:syntax], lib: name_as_string)
      declarations.each do |(type, *args)|
        case type
        when 'export'
          exports[:bindings].merge!(library_exports_as_hash(args))
        when 'import'
          do_import(args, name.first.filename, lib_opts)
          imports = do_import(args, name.first.filename, lib_opts)
        when 'begin'
          begins += args
        end
      end
      sexp = [
        VM::SET_LIB, name.join('/'),
      [
        VM::SET_LIB, name_as_string,
        imports,
        begins.map { |s| compile_sexp(s, lib_opts) },
        VM::ENDL
      ]
      exports[:syntax] = lib_opts[:syntax]
      sexp
    end

    def library_exports_as_hash(exports)

M compiler/macro.rb => compiler/macro.rb +12 -2
@@ 79,12 79,22 @@ class Compiler
        # FIXME: won't work with some identifiers and literal elipsis
        identifiers = @template.flatten.select { |name| name =~ /\A[a-z]/ }.uniq
        identifiers.each_with_object({}) do |identifier, hash|
          next if known_identifier?(identifier)
          hash[identifier] = @compiler.mangle_identifier(identifier)
          hash[identifier] = if macro?(identifier)
                               identifier
                             elsif known_identifier?(identifier)
                               @macro[:lib] ? "##{@macro[:lib]}:#{identifier}" : identifier
                             else
                               @compiler.mangle_identifier(identifier)
                             end
        end
      end
    end

    def macro?(name)
      return false if @macro[:syntax].nil?
      @macro[:syntax][name] # TODO: maybe built_in_function should be here -- not sure!
    end

    def known_identifier?(name)
      @locals.include?(name) || @compiler.built_in_function?(name)
    end

M lib/assert.scm => lib/assert.scm +1 -2
@@ 1,6 1,5 @@
(import (only (scheme base) begin define-syntax = eq? eqv? equal? if newline not quote write-string))

(define-library (assert)
  (import (only (scheme base) begin define-syntax = eq? eqv? equal? if newline not quote write-string))
  (export assert)
  (begin
    (define-syntax assert

M spec/compiler_spec.rb => spec/compiler_spec.rb +27 -31
@@ 372,6 372,7 @@ describe Compiler do
        it 'compiles into vm instructions' do
          expect(d(@result)).to eq([
            'VM::PUSH_ATOM', 'foo',
            'VM::POP',
            'VM::HALT'
          ])
        end


@@ 1155,8 1156,8 @@ describe Compiler do
    context 'define-library and import' do
      before do
        @result = subject.compile(<<-END)
          (import (only (scheme base) define))
          (define-library (my-lib 1)
            (import (only (scheme base) define define-syntax))
            (begin
              (define foo "foo")
              (define-syntax macro1


@@ 1164,6 1165,7 @@ describe Compiler do
                  ((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")


@@ 1171,6 1173,7 @@ describe Compiler do
                (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


@@ 1182,37 1185,29 @@ describe Compiler do
      end

      it 'records export names for the libraries' do
        expect(subject.libs['my-lib/1']).to include(
          syntax: include(
            'macro1' => {
              locals: Array,
              transformer: [
                'syntax-rules', [],
                [['macro1'], '1']
              ]
            }
          ),
          bindings: {
            'foo'    => 'foo',
            'macro1' => 'macro1'
          }
        expect(subject.libs['my-lib/1'][:syntax]['macro1']).to include(
          locals: Array,
          transformer: [
            'syntax-rules', [],
            [['macro1'], '1']
          ]
        )
        expect(subject.libs['my-lib/2']).to include(
          syntax: include(
            'macro2' => {
              locals: Array,
              transformer: [
                'syntax-rules', [],
                [['macro2'], 'macro1']
              ]
            }
          ),
          bindings: {
            'foo'    => 'foo',
            'baz'    => 'baz',
            'bar'    => 'foo',
            'macro2' => 'macro2'
          }
        expect(subject.libs['my-lib/1'][:bindings]).to eq(
          'foo'    => 'foo',
          'macro1' => 'macro1'
        )
        expect(subject.libs['my-lib/2'][:syntax]['macro2']).to include(
          locals: Array,
          transformer: [
            'syntax-rules', [],
            [['macro2'], 'macro1']
          ]
        )
        expect(subject.libs['my-lib/2'][:bindings]).to eq(
          'foo'    => 'foo',
          'baz'    => 'baz',
          'bar'    => 'foo',
          'macro2' => 'macro2'
        )
      end



@@ 1224,6 1219,7 @@ describe Compiler do
          'VM::ENDL',

          'VM::SET_LIB', 'my-lib/2',
          'VM::IMPORT_LIB', 'my-lib/1', 'foo', 'foo',
          'VM::PUSH_STR', 'baz',
          'VM::DEFINE_VAR', 'baz',
          'VM::ENDL',

M spec/fixtures/library-test.scm => spec/fixtures/library-test.scm +1 -2
@@ 1,6 1,5 @@
(import (only (scheme base) define define-syntax))

(define-library (fixtures library-test)
  (import (only (scheme base) define define-syntax))
  (export foo macro)
  (begin
    (define (foo)

M spec/program_spec.rb => spec/program_spec.rb +1 -1
@@ 38,7 38,7 @@ describe Program do
      end
    end

    xcontext 'when the program imports other libraries' do
    context 'when the program imports other libraries' do
      let(:code) do
        <<-END
          (define-library (base)

M vm.rb => vm.rb +8 -0
@@ 275,11 275,19 @@ class VM
      frame[:named_args][name]
    elsif (c = find_closure_with_symbol(name))
      c[:locals][name]
    elsif (m = mangled_local(name))
      (lib, name) = m
      @libs[lib][:locals][name]
    elsif raise_if_not_found
      raise VariableUndefined, name
    end
  end

  def mangled_local(name)
    return unless name && (match = name.match(/\A#([^:]+):(.+)\z/))
    match[1..2]
  end

  def args
    @call_stack.last[:args]
  end