diff --git a/ruby/Rakefile b/ruby/Rakefile
index ca9dddc..bf533d4 100644
--- a/ruby/Rakefile
+++ b/ruby/Rakefile
@@ -7,12 +7,7 @@ namespace :test do
desc "Run tests on source changes"
task :watch do
loop do
- sh <<~SH
- while true
- do
- fd .*.rb | entr -d rake test:isolated
- done
- SH
+ sh "fd .*.rb | entr -d rake test:isolated"
end
end
end
diff --git a/ruby/lib/lox/ast_printer.rb b/ruby/lib/lox/ast_printer.rb
index f7578b4..6fb8f45 100644
--- a/ruby/lib/lox/ast_printer.rb
+++ b/ruby/lib/lox/ast_printer.rb
@@ -45,6 +45,10 @@ module Lox
parenthesize("return", *exprs)
end
+ def visit_while(stmt)
+ parenthesize("while", stmt.cond, stmt.body)
+ end
+
private
def parenthesize(name, *exprs)
diff --git a/ruby/lib/lox/environment.rb b/ruby/lib/lox/environment.rb
index f962e90..8df68f2 100644
--- a/ruby/lib/lox/environment.rb
+++ b/ruby/lib/lox/environment.rb
@@ -48,7 +48,7 @@ module Lox
end
def assign_at(distance, name, value)
- ancestor(distance).values[name] = value
+ ancestor(distance).values[name.lexeme] = value
end
end
end
diff --git a/ruby/lib/lox/interpreter.rb b/ruby/lib/lox/interpreter.rb
index c36e7cb..826e260 100644
--- a/ruby/lib/lox/interpreter.rb
+++ b/ruby/lib/lox/interpreter.rb
@@ -30,7 +30,11 @@ module Lox
def execute(stmt) = stmt.accept(self)
def resolve(expr, depth)
- @locals[expr] = depth
+ # HACK We use the object id here since we do **NOT** want
+ # struct equality for getting the depth of a local since
+ # we can have multiple locals referencing the same variable
+ # in different scopes on a single line.
+ @locals[expr.object_id] = depth
end
def visit_block(stmt)
@@ -125,8 +129,8 @@ module Lox
end
def lookup_var(name, expr)
- if @locals.has_key?(expr)
- distance = @locals.fetch(expr)
+ if @locals.has_key?(expr.object_id)
+ distance = @locals.fetch(expr.object_id)
@env.get_at(distance, name.lexeme)
else
@globals.get(name)
@@ -136,8 +140,8 @@ module Lox
def visit_assign(expr)
value = evaluate(expr.value)
- if @locals.has_key?(expr)
- distance = @locals.get(expr)
+ if @locals.has_key?(expr.object_id)
+ distance = @locals.fetch(expr.object_id)
@env.assign_at(distance, expr.name, value)
else
@globals.assign(expr.name, value)
diff --git a/ruby/lib/lox/resolver.rb b/ruby/lib/lox/resolver.rb
index 4a376d7..19520d6 100644
--- a/ruby/lib/lox/resolver.rb
+++ b/ruby/lib/lox/resolver.rb
@@ -24,7 +24,6 @@ module Lox
end
def visit_expr(stmt) = resolve(stmt.expr)
- end
def visit_function(stmt)
declare(stmt.name)
@@ -35,12 +34,14 @@ module Lox
end
def visit_if(stmt)
- resolve(stmt.condition, stmt.then)
+ resolve(stmt.cond, stmt.then)
resolve(stmt.else) if stmt.else
end
def visit_print(stmt) = resolve(stmt.expr)
- def visit_return(stmt) = resolve(stmt.value) if stmt.value
+ def visit_return(stmt)
+ resolve(stmt.value) if stmt.value
+ end
def visit_var(stmt)
declare(stmt.name)
@@ -49,7 +50,7 @@ module Lox
nil
end
- def visit_while(stmt) = resolve(stmt.condition, stmt.body)
+ def visit_while(stmt) = resolve(stmt.cond, stmt.body)
def visit_variable(expr)
if !@scopes.empty? && @scopes.last[expr.name.lexeme] == false
@@ -75,16 +76,18 @@ module Lox
private
- def with_block
- @scopes.unshift({})
+ def with_scope
+ @scopes.push({})
yield
- @scopes.shift
+ @scopes.pop
end
def declare(name)
scope = @scopes.last
return if scope.nil?
+ raise ResolverError.new(name, "Already a variable with this name in this scope") if scope.has_key?(name.lexeme)
+
scope[name.lexeme] = false
end
@@ -92,11 +95,11 @@ module Lox
scope = @scopes.last
return if scope.nil?
- scopes[name.lexeme] = true
+ scope[name.lexeme] = true
end
def resolve_local(expr, name)
- scope_and_depth = @scopes.each.with_index.find {|scope, depth| scope.has_key?(name.lexeme) }
+ scope_and_depth = @scopes.reverse.each.with_index.find {|scope, depth| scope.has_key?(name.lexeme) }
return unless scope_and_depth
scope, depth = scope_and_depth
@@ -109,7 +112,7 @@ module Lox
declare(param)
define(param)
end
- resolve(fn.body)
+ resolve(*fn.body)
end
end
diff --git a/ruby/test/lox/test_environment.rb b/ruby/test/lox/test_environment.rb
index fd5b3ee..c53e580 100644
--- a/ruby/test/lox/test_environment.rb
+++ b/ruby/test/lox/test_environment.rb
@@ -56,4 +56,14 @@ class TestEnvironment < Lox::Test
assert_equal "foo", @env.get_at(0, "name")
end
+ def test_assign_at
+ enclosed = Lox::Environment.new(@env)
+
+ enclosed.assign_at(0, NAME_TOKEN, "foo")
+ assert_equal "foo", enclosed.get_at(0, "name")
+
+ enclosed.assign_at(1, NAME_TOKEN, "bar")
+ assert_equal "bar", enclosed.get_at(1, "name")
+ end
+
end
diff --git a/ruby/test/lox/test_interpreter.rb b/ruby/test/lox/test_interpreter.rb
index 0799786..f2cfcf3 100644
--- a/ruby/test/lox/test_interpreter.rb
+++ b/ruby/test/lox/test_interpreter.rb
@@ -2,12 +2,12 @@ require_relative "../test_helper"
require "lox/interpreter"
require "lox/parser"
+require "lox/resolver"
require "lox/scanner"
class TestInterpreter < Lox::Test
def setup
@scanner = Lox::Scanner.new
- @interpreter = Lox::Interpreter.new
end
# def test_literal
@@ -234,9 +234,25 @@ class TestInterpreter < Lox::Test
sayHi("Dear", "Reader");
SRC
- end
- def test_function
+ assert_interpreted <<~EXPECTED.chomp, <<~SRC
+ 0
+ 1
+ 2
+ EXPECTED
+ {
+ var i = 0; while (i < 3) { { print i; } i = i + 1; }
+ }
+ SRC
+
+ assert_interpreted <<~EXPECTED.chomp, <<~SRC
+ 0
+ 1
+ 2
+ EXPECTED
+ for (var i = 0; i < 3; i = i + 1) print i;
+ SRC
+
assert_interpreted <<~EXPECTED.chomp, <<~SRC
0
1
@@ -278,6 +294,18 @@ class TestInterpreter < Lox::Test
SRC
end
+ def test_resolver
+ assert_interpreted <<~EXPECTED.chomp, <<~SRC
+ 1
+ EXPECTED
+ var foo = 1;
+ {
+ print foo;
+ var foo = 2;
+ }
+ SRC
+ end
+
private
def assert_interpreted(expected, src)
@@ -303,7 +331,12 @@ class TestInterpreter < Lox::Test
def interpret(src)
with_stdout {
stmts = Lox::Parser.new(@scanner.scan(src)).parse!
- @interpreter.interpret(stmts)
+ interpreter = Lox::Interpreter.new
+
+ resolver = Lox::Resolver.new(interpreter);
+ resolver.resolve(*stmts);
+
+ interpreter.interpret(stmts)
}
end
diff --git a/ruby/test/lox/test_parser.rb b/ruby/test/lox/test_parser.rb
index 7aee4d2..f59cddc 100644
--- a/ruby/test/lox/test_parser.rb
+++ b/ruby/test/lox/test_parser.rb
@@ -86,6 +86,12 @@ class TestParser < Lox::Test
assert_parsed "(return (var foo))", :statement, "return foo;"
end
+ def test_for
+ assert_parsed <<~AST.chomp, :statement, "for(var i=0; i<3; i=i+1) print i;"
+ (block (var i 0.0) (while (< (var i) 3.0) (block (print (var i)) (assign i (+ (var i) 1.0)))))
+ AST
+ end
+
private
def assert_parsed(expected, name, src)
diff --git a/ruby/test/lox/test_resolver.rb b/ruby/test/lox/test_resolver.rb
new file mode 100644
index 0000000..8d50cae
--- /dev/null
+++ b/ruby/test/lox/test_resolver.rb
@@ -0,0 +1,73 @@
+require_relative "../test_helper"
+
+require "lox/ast_printer"
+require "lox/parser"
+require "lox/resolver"
+require "lox/scanner"
+
+class TestResolver < Lox::Test
+
+ def setup
+ @ast_printer = Lox::AstPrinter.new
+ @scanner = Lox::Scanner.new
+ @interpreter = Interpreter.new
+ @resolver = Lox::Resolver.new(@interpreter)
+ end
+
+ def test_resolver
+ assert_resolved [
+ ["(var j)", 0],
+ ["(var j)", 2],
+ ["(var j)", 1],
+ ["(assign j (+ (var j) 1.0))", 1],
+ ["(var k)", 0],
+ ["(var k)", 2],
+ ["(var k)", 1],
+ ["(assign k (+ (var k) 1.0))", 1],
+ ], <<~SRC
+ var i = 0;
+ while (i < 3) {
+ print i;
+ i = i + 1;
+ }
+
+ for(var j=0; j<3; j=j+1) {
+ print j;
+ }
+
+ {
+ var k = 0;
+ while (k < 3) {
+ {
+ print k;
+ }
+ k = k + 1;
+ }
+ }
+ SRC
+ end
+
+ private
+
+ def assert_resolved(expected, src)
+ stmts = Lox::Parser.new(@scanner.scan(src)).parse!
+ @resolver.resolve(*stmts);
+
+ assert_equal expected, @interpreter.resolves.map {|expr, depth|
+ [@ast_printer.print(expr), depth]
+ }
+ end
+
+ class Interpreter
+ attr_reader :resolves
+
+ def initialize
+ @resolves = []
+ end
+
+ def resolve(expr, depth)
+ resolves << [expr, depth]
+ end
+ end
+
+end