8.1
FossilOrigin-Name: 5d014cbe85c713fbe1fccf2a053db4b4474c0eee5e909d7cfd7de58fcbb03f77
diff --git a/ruby/lib/lox/expr.rb b/ruby/lib/lox/expr.rb
index 99bfbd1..762129b 100644
--- a/ruby/lib/lox/expr.rb
+++ b/ruby/lib/lox/expr.rb
@@ -1,12 +1,7 @@
+require_relative "visitable"
+
module Lox
module Expr
- module Visitable
- def accept(visitor)
- klass = self.class.to_s.split("::").last.downcase
- visitor.send("visit_#{klass}", self)
- end
- end
-
Binary = Struct.new(:left, :op, :right) do
include Visitable
end
diff --git a/ruby/lib/lox/interpreter.rb b/ruby/lib/lox/interpreter.rb
index 3e577c6..d842cc9 100644
--- a/ruby/lib/lox/interpreter.rb
+++ b/ruby/lib/lox/interpreter.rb
@@ -1,14 +1,24 @@
module Lox
class Interpreter
-
# The book does printing and error catching here, but
# we're going to do it in the runner instead.
- def interpret(expr)
- value = evaluate(expr)
- stringify(value)
+ def interpret(stmts)
+ stmts.each do |stmt|
+ evaluate(stmt)
+ end
+ end
+
+ def evaluate(stmt) = stmt.accept(self)
+
+ def visit_expr(expr)
+ evaluate(expr.expr)
+ nil
end
- def evaluate(expr) = expr.accept(self)
+ def visit_print(expr)
+ puts stringify(evaluate(expr.expr))
+ nil
+ end
def visit_grouping(expr) = evaluate(expr.expr)
def visit_literal(expr) = expr.value
diff --git a/ruby/lib/lox/parser.rb b/ruby/lib/lox/parser.rb
index 89b0534..7cadd06 100644
--- a/ruby/lib/lox/parser.rb
+++ b/ruby/lib/lox/parser.rb
@@ -1,135 +1,156 @@
require_relative "error"
require_relative "expr"
+require_relative "stmt"
module Lox
class Parser
- class State < Struct.new(:tokens, :current)
- def initialize(tokens)
- super(tokens, 0)
+
+ def initialize(tokens)
+ @tokens = tokens
+ @current = 0
+ end
+
+ def parse!
+ statements = []
+ until eot?
+ statements << statement
end
+ statements
+ end
- def expression = equality
+ private
- def equality
- expr = comparison
+ def statement
+ return print if match?(:PRINT)
- while match?(:BANG_EQUAL, :EQUAL_EQUAL)
- op = prev
- right = comparison
- expr = Expr::Binary.new(expr, op, right)
- end
+ expressionStmt
+ end
- expr
- end
+ def print
+ value = expression
+ consume!(:SEMICOLON, "Expect ';' after value.")
+ Stmt::Print.new(value)
+ end
- def comparison
- expr = term
+ def expressionStmt
+ value = expression
+ consume!(:SEMICOLON, "Expect ';' after value.")
+ Stmt::Expr.new(value)
+ end
- while match?(:GREATER, :GREATER_EQUAL, :LESS, :LESS_EQUAL)
- op = prev
- right = term
- expr = Expr::Binary.new(expr, op, right)
- end
+ def expression = equality
- expr
- end
+ def equality
+ expr = comparison
+ while match?(:BANG_EQUAL, :EQUAL_EQUAL)
+ op = prev
+ right = comparison
+ expr = Expr::Binary.new(expr, op, right)
+ end
- def term
- expr = factor
+ expr
+ end
- while match?(:MINUS, :PLUS)
- op = prev
- right = factor
- expr = Expr::Binary.new(expr, op, right)
- end
+ def comparison
+ expr = term
- expr
+ while match?(:GREATER, :GREATER_EQUAL, :LESS, :LESS_EQUAL)
+ op = prev
+ right = term
+ expr = Expr::Binary.new(expr, op, right)
end
- def factor
- expr = unary
+ expr
+ end
- while match?(:SLASH, :STAR)
- op = prev
- right = unary
- expr = Expr::Binary.new(expr, op, right)
- end
+ def term
+ expr = factor
- expr
+ while match?(:MINUS, :PLUS)
+ op = prev
+ right = factor
+ expr = Expr::Binary.new(expr, op, right)
end
- def unary
- return primary unless match?(:BANG, :MINUS)
+ expr
+ end
+
+ def factor
+ expr = unary
+ while match?(:SLASH, :STAR)
op = prev
right = unary
- Expr::Unary.new(op, right)
+ expr = Expr::Binary.new(expr, op, right)
end
- def primary
- return Expr::Literal.new(false) if match?(:FALSE)
- return Expr::Literal.new(true) if match?(:TRUE)
- return Expr::Literal.new(nil) if match?(:NIL)
- return Expr::Literal.new(prev.literal) if match?(:NUMBER, :STRING)
-
- if match?(:LEFT_PAREN)
- expr = expression
- consume!(:RIGHT_PAREN, "Expect ')' after expression.")
- return Expr::Grouping.new(expr)
- end
+ expr
+ end
- raise ParseError.new(peek, "Expect expression.")
- end
+ def unary
+ return primary unless match?(:BANG, :MINUS)
- private
+ op = prev
+ right = unary
+ Expr::Unary.new(op, right)
+ end
- def match?(*types)
- return false unless check?(*types)
+ def primary
+ return Expr::Literal.new(false) if match?(:FALSE)
+ return Expr::Literal.new(true) if match?(:TRUE)
+ return Expr::Literal.new(nil) if match?(:NIL)
+ return Expr::Literal.new(prev.literal) if match?(:NUMBER, :STRING)
- advance!
- return true
+ if match?(:LEFT_PAREN)
+ expr = expression
+ consume!(:RIGHT_PAREN, "Expect ')' after expression.")
+ return Expr::Grouping.new(expr)
end
- def consume!(type, message)
- raise ParseError.new(peek, message) unless check?(type)
+ raise ParseError.new(peek, "Expect expression.")
+ end
- advance!
- end
+ private
- def check?(*types)
- return false if eot?
+ def match?(*types)
+ return false unless check?(*types)
- types.include?(peek.type)
- end
+ advance!
+ return true
+ end
- def advance!
- self.current += 1 unless eot?
- prev
- end
+ def consume!(type, message)
+ raise ParseError.new(peek, message) unless check?(type)
- def eot? = peek.type === :EOF
- def peek = tokens.fetch(current)
- def prev = tokens.fetch(current - 1)
+ advance!
+ end
- def synchronize!
- advance!
+ def check?(*types)
+ return false if eot?
- until eot?
- return if prev.type == :SEMICOLON
- return if %i[CLASS FUN VAR FOR IF WHILE PRINT RETURN].include?(peek.type)
+ types.include?(peek.type)
+ end
- advance!
- end
- end
+ def advance!
+ @current += 1 unless eot?
+ prev
end
- # In the book, this returns nil when there's an error, but
- # that feels weird so let's move that error handling up the
- # stack for now.
- def parse(tokens)
- state = State.new(tokens)
- state.expression
+ def eot? = peek.type === :EOF
+ def peek = @tokens.fetch(@current)
+ def prev = @tokens.fetch(@current - 1)
+
+ def synchronize!
+ advance!
+
+ until eot?
+ return if prev.type == :SEMICOLON
+ return if %i[CLASS FUN VAR FOR IF WHILE PRINT RETURN].include?(peek.type)
+
+ advance!
+ end
end
end
+
end
diff --git a/ruby/lib/lox/stmt.rb b/ruby/lib/lox/stmt.rb
new file mode 100644
index 0000000..acbebef
--- /dev/null
+++ b/ruby/lib/lox/stmt.rb
@@ -0,0 +1,13 @@
+require_relative "visitable"
+
+module Lox
+ module Stmt
+ Expr = Struct.new(:expr) do
+ include Visitable
+ end
+
+ Print = Struct.new(:expr) do
+ include Visitable
+ end
+ end
+end
diff --git a/ruby/lib/lox/visitable.rb b/ruby/lib/lox/visitable.rb
new file mode 100644
index 0000000..d9866b9
--- /dev/null
+++ b/ruby/lib/lox/visitable.rb
@@ -0,0 +1,8 @@
+module Lox
+ module Visitable
+ def accept(visitor)
+ klass = self.class.to_s.split("::").last.downcase
+ visitor.send("visit_#{klass}", self)
+ end
+ end
+end
diff --git a/ruby/test/lox/test_interpreter.rb b/ruby/test/lox/test_interpreter.rb
index f588f63..d26cc96 100644
--- a/ruby/test/lox/test_interpreter.rb
+++ b/ruby/test/lox/test_interpreter.rb
@@ -7,62 +7,61 @@ require "lox/scanner"
class TestInterpreter < Lox::Test
def setup
@scanner = Lox::Scanner.new
- @parser = Lox::Parser.new
@interpreter = Lox::Interpreter.new
end
- def test_literal
- assert_interpreted(42.0, "42")
- end
+ # def test_literal
+ # assert_evaluated(42.0, "42")
+ # end
def test_grouping
- assert_interpreted(42.0, "(42)")
+ assert_evaluated "42", "(42)"
end
def test_unary
- assert_interpreted(-42.0, "-42")
- assert_interpreted(false, "!42")
- assert_interpreted(false, "!true")
- assert_interpreted(true, "!false")
- assert_interpreted(true, "!nil")
+ assert_evaluated "-42", "-42"
+ assert_evaluated "false", "!42"
+ assert_evaluated "false", "!true"
+ assert_evaluated "true", "!false"
+ assert_evaluated "true", "!nil"
end
def test_binary
- assert_interpreted(42.0, "100 - 58")
- assert_interpreted(42.0, "84 / 2")
- assert_interpreted(42.0, "21 * 2")
+ assert_evaluated "42", "100 - 58"
+ assert_evaluated "42", "84 / 2"
+ assert_evaluated "42", "21 * 2"
# precedence
- assert_interpreted(42.0, "2 * 25 - 8")
+ assert_evaluated "42", "2 * 25 - 8"
- assert_interpreted(42.0, "40 + 2")
- assert_interpreted("42", "\"4\" + \"2\"")
+ assert_evaluated "42", "40 + 2"
+ assert_evaluated "42", "\"4\" + \"2\""
- assert_interpreted(true, "1 > 0")
- assert_interpreted(false, "0 > 0")
- assert_interpreted(false, "0 > 1")
+ assert_evaluated "true", "1 > 0"
+ assert_evaluated "false", "0 > 0"
+ assert_evaluated "false", "0 > 1"
- assert_interpreted(true, "1 >= 0")
- assert_interpreted(true, "0 >= 0")
- assert_interpreted(false, "0 >= 1")
+ assert_evaluated "true", "1 >= 0"
+ assert_evaluated "true", "0 >= 0"
+ assert_evaluated "false", "0 >= 1"
- assert_interpreted(false, "1 < 0")
- assert_interpreted(false, "0 < 0")
- assert_interpreted(true, "0 < 1")
+ assert_evaluated "false", "1 < 0"
+ assert_evaluated "false", "0 < 0"
+ assert_evaluated "true", "0 < 1"
- assert_interpreted(false, "1 <= 0")
- assert_interpreted(true, "0 <= 0")
- assert_interpreted(true, "0 <= 1")
+ assert_evaluated "false", "1 <= 0"
+ assert_evaluated "true", "0 <= 0"
+ assert_evaluated "true", "0 <= 1"
- assert_interpreted(true, "0 != 1")
- assert_interpreted(false, "0 != 0")
- assert_interpreted(false, "nil != nil")
- assert_interpreted(true, "nil != 1")
+ assert_evaluated "true", "0 != 1"
+ assert_evaluated "false", "0 != 0"
+ assert_evaluated "false", "nil != nil"
+ assert_evaluated "true", "nil != 1"
- assert_interpreted(false, "0 == 1")
- assert_interpreted(true, "0 == 0")
- assert_interpreted(true, "nil == nil")
- assert_interpreted(false, "nil == 1")
+ assert_evaluated "false", "0 == 1"
+ assert_evaluated "true", "0 == 0"
+ assert_evaluated "true", "nil == nil"
+ assert_evaluated "false", "nil == 1"
end
def test_errors
@@ -74,31 +73,53 @@ class TestInterpreter < Lox::Test
"false + 23",
].each do |src|
assert_raises Lox::RuntimeError do
- evaluate(src)
+ assert_evaluated nil, src
end
end
end
def test_stringify
- assert_equal "nil", interpret("nil")
- assert_equal "42", interpret("42")
- assert_equal "42.1", interpret("42.1")
- assert_equal "foo", interpret("\"foo\"")
+ assert_evaluated "nil", "nil"
+ assert_evaluated "42", "42"
+ assert_evaluated "42.1", "42.1"
+ assert_evaluated "foo", "\"foo\""
+ end
+
+ def test_multiple_statements
+ assert_interpreted <<~EXPECTED.chomp, <<~SRC
+ one
+ true
+ 3
+ EXPECTED
+ print "one";
+ print true;
+ print 2 + 1;
+ SRC
end
private
- def evaluate(src)
- expr = @parser.parse(@scanner.scan(src))
- @interpreter.evaluate(expr)
+ def assert_interpreted(expected, src)
+ output = with_stdout {
+ stmts = Lox::Parser.new(@scanner.scan(src)).parse!
+ @interpreter.interpret(stmts)
+ }
+ assert_equal expected, output
end
- def interpret(src)
- expr = @parser.parse(@scanner.scan(src))
- @interpreter.interpret(expr)
+ def assert_evaluated(expected, src)
+ assert_interpreted(expected, "print #{src};")
end
- def assert_interpreted(expected, src)
- assert_equal expected, evaluate(src)
+ def with_stdout
+ original = $stdout
+ $stdout = StringIO.new
+
+ yield
+
+ output = $stdout.string.chomp
+ $stdout = original
+ output
end
+
end
diff --git a/ruby/test/lox/test_parser.rb b/ruby/test/lox/test_parser.rb
index 7da6084..646d071 100644
--- a/ruby/test/lox/test_parser.rb
+++ b/ruby/test/lox/test_parser.rb
@@ -56,7 +56,7 @@ class TestParser < Lox::Test
def parse(src, name)
tokens = @scanner.scan(src)
- Lox::Parser::State.new(tokens).send(name)
+ Lox::Parser.new(tokens).send(name)
end
def assert_parsed(expected, name, src)