7
FossilOrigin-Name: ae1ca40694300eff97d7ee3961f0e8e0b1f61f275c8226872ac1b502c5343b07
diff --git a/ruby/Gemfile b/ruby/Gemfile
index b7c57df..e76dde0 100644
--- a/ruby/Gemfile
+++ b/ruby/Gemfile
@@ -5,6 +5,5 @@ source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "minitest"
-gem "mocktail"
gem "pry"
gem "rake"
diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock
index dc09b9d..9900ff7 100644
--- a/ruby/Gemfile.lock
+++ b/ruby/Gemfile.lock
@@ -4,7 +4,6 @@ GEM
coderay (1.1.3)
method_source (1.0.0)
minitest (5.16.2)
- mocktail (1.1.3)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -15,7 +14,6 @@ PLATFORMS
DEPENDENCIES
minitest
- mocktail
pry
rake
diff --git a/ruby/bin/lox b/ruby/bin/lox
index c0a2c0a..4cbfcbb 100755
--- a/ruby/bin/lox
+++ b/ruby/bin/lox
@@ -11,17 +11,22 @@ def run_prompt
break if line.nil? || line.empty?
begin
run(line)
- rescue Lox::Error => e
- puts e.message
+ rescue Lox::ParseError => e
+ STDERR.puts e.message
+ rescue Lox::RuntimeError => e
+ STDERR.puts e.message
end
end
end
def run_file(io)
run(io.read)
-rescue Lox::Error => e
- puts e.message
+rescue Lox::ParseError => e
+ STDERR.puts e.message
exit 65
+rescue Lox::RuntimeError => e
+ STDERR.puts e.message, "[line #{e.token.line}]"
+ exit 70
end
def run(src)
diff --git a/ruby/lib/lox.rb b/ruby/lib/lox.rb
index 4212b64..3cb4e82 100644
--- a/ruby/lib/lox.rb
+++ b/ruby/lib/lox.rb
@@ -3,23 +3,25 @@
require_relative "lox/ast_printer"
require_relative "lox/error"
require_relative "lox/expr"
+require_relative "lox/interpreter"
require_relative "lox/parser"
require_relative "lox/scanner"
require_relative "lox/token"
module Lox
class Runner
- def initialize(scanner=Scanner.new, parser=Parser.new)
- @scanner, @parser = scanner, parser
+ def initialize
+ @scanner = Scanner.new
+ @parser = Parser.new
+ @interpreter = Interpreter.new
end
def run(src)
tokens = @scanner.scan(src)
expr = @parser.parse(tokens)
+ value = @interpreter.interpret(expr)
- puts AstPrinter.new.print(expr)
- rescue ParseError => e
- puts e.message
+ puts value
end
end
end
diff --git a/ruby/lib/lox/error.rb b/ruby/lib/lox/error.rb
index f3d8d6a..46897e5 100644
--- a/ruby/lib/lox/error.rb
+++ b/ruby/lib/lox/error.rb
@@ -1,10 +1,23 @@
module Lox
class Error < StandardError
- def initialize(line, where="", message)
+ end
+
+ class ParseError < Error
+ def initialize(token, message)
+ where = token.type == :EOF ? "end" : "'#{token.lexeme}'"
+
error = "Error"
- error << " #{where}" unless where.empty?
+ error << " at #{where}" unless where.empty?
+ super("[line #{token.line}] #{error}: #{message}")
+ end
+ end
+
+ class RuntimeError < Error
+ attr_reader :token
- super("[line #{line}] #{error}: #{message}")
+ def initialize(token, message)
+ @token = token
+ super(message)
end
end
end
diff --git a/ruby/lib/lox/interpreter.rb b/ruby/lib/lox/interpreter.rb
new file mode 100644
index 0000000..3e577c6
--- /dev/null
+++ b/ruby/lib/lox/interpreter.rb
@@ -0,0 +1,88 @@
+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)
+ end
+
+ def evaluate(expr) = expr.accept(self)
+
+ def visit_grouping(expr) = evaluate(expr.expr)
+ def visit_literal(expr) = expr.value
+
+ def visit_unary(expr)
+ right = evaluate(expr.right)
+
+ case expr.op.type
+ when :MINUS
+ check_number_operand!(expr.op, right)
+ -right
+ when :BANG then !truthy?(right)
+ else fail
+ end
+ end
+
+ def visit_binary(expr)
+ left = evaluate(expr.left)
+ right = evaluate(expr.right)
+
+ case expr.op.type
+ when :GREATER
+ check_number_operands!(expr.op, left, right)
+ left > right
+ when :GREATER_EQUAL
+ check_number_operands!(expr.op, left, right)
+ left >= right
+ when :LESS
+ check_number_operands!(expr.op, left, right)
+ left < right
+ when :LESS_EQUAL
+ check_number_operands!(expr.op, left, right)
+ left <= right
+ when :BANG_EQUAL then left != right
+ when :EQUAL_EQUAL then left == right
+ when :MINUS
+ check_number_operands!(expr.op, left, right)
+ left - right
+ when :PLUS
+ unless left.is_a?(Float) && right.is_a?(Float) || left.is_a?(String) && right.is_a?(String)
+ raise RuntimeError.new(expr.op, "Operands must be two numbers or two strings.")
+ end
+ left + right
+ when :SLASH
+ check_number_operands!(expr.op, left, right)
+ left / right
+ when :STAR
+ check_number_operands!(expr.op, left, right)
+ left * right
+ else fail
+ end
+
+ end
+
+ private
+
+ def truthy?(value) = !!value
+
+ def check_number_operand!(token, operand)
+ return if operand.is_a?(Float)
+
+ raise RuntimeError.new(token, "Operand must be a number.")
+ end
+
+ def check_number_operands!(token, left, right)
+ return if left.is_a?(Float) && right.is_a?(Float)
+
+ raise RuntimeError.new(token, "Operands must be numbers.")
+ end
+
+ def stringify(value)
+ return "nil" if value.nil?
+ return value.to_s.sub(/\.0$/, "") if value.is_a?(Float)
+ value.to_s
+ end
+ end
+end
diff --git a/ruby/lib/lox/parser.rb b/ruby/lib/lox/parser.rb
index 3c061f6..89b0534 100644
--- a/ruby/lib/lox/parser.rb
+++ b/ruby/lib/lox/parser.rb
@@ -2,14 +2,6 @@ require_relative "error"
require_relative "expr"
module Lox
- class ParseError < Error
- def initialize(token, message)
- at = token.type == :EOF ? "end" : "'#{token.lexeme}'"
-
- super(token.line, "at #{at}", message)
- end
- end
-
class Parser
class State < Struct.new(:tokens, :current)
def initialize(tokens)
@@ -136,7 +128,7 @@ module Lox
# that feels weird so let's move that error handling up the
# stack for now.
def parse(tokens)
- state = State.new(tokens, 0)
+ state = State.new(tokens)
state.expression
end
end
diff --git a/ruby/lib/lox/scanner.rb b/ruby/lib/lox/scanner.rb
index bcbbe6f..3deca79 100644
--- a/ruby/lib/lox/scanner.rb
+++ b/ruby/lib/lox/scanner.rb
@@ -90,6 +90,7 @@ module Lox
end
end
+ # TODO
fail unless state.errors.empty?
state.add_token(:EOF, text: "")
diff --git a/ruby/test/lox/test_interpreter.rb b/ruby/test/lox/test_interpreter.rb
new file mode 100644
index 0000000..f588f63
--- /dev/null
+++ b/ruby/test/lox/test_interpreter.rb
@@ -0,0 +1,104 @@
+require_relative "../test_helper"
+
+require "lox/interpreter"
+require "lox/parser"
+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_grouping
+ assert_interpreted(42.0, "(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")
+ end
+
+ def test_binary
+ assert_interpreted(42.0, "100 - 58")
+ assert_interpreted(42.0, "84 / 2")
+ assert_interpreted(42.0, "21 * 2")
+
+ # precedence
+ assert_interpreted(42.0, "2 * 25 - 8")
+
+ assert_interpreted(42.0, "40 + 2")
+ assert_interpreted("42", "\"4\" + \"2\"")
+
+ assert_interpreted(true, "1 > 0")
+ assert_interpreted(false, "0 > 0")
+ assert_interpreted(false, "0 > 1")
+
+ assert_interpreted(true, "1 >= 0")
+ assert_interpreted(true, "0 >= 0")
+ assert_interpreted(false, "0 >= 1")
+
+ assert_interpreted(false, "1 < 0")
+ assert_interpreted(false, "0 < 0")
+ assert_interpreted(true, "0 < 1")
+
+ assert_interpreted(false, "1 <= 0")
+ assert_interpreted(true, "0 <= 0")
+ assert_interpreted(true, "0 <= 1")
+
+ assert_interpreted(true, "0 != 1")
+ assert_interpreted(false, "0 != 0")
+ assert_interpreted(false, "nil != nil")
+ assert_interpreted(true, "nil != 1")
+
+ assert_interpreted(false, "0 == 1")
+ assert_interpreted(true, "0 == 0")
+ assert_interpreted(true, "nil == nil")
+ assert_interpreted(false, "nil == 1")
+ end
+
+ def test_errors
+ [
+ "-true",
+ "12 > true",
+ "true < 23",
+ "false * 23",
+ "false + 23",
+ ].each do |src|
+ assert_raises Lox::RuntimeError do
+ evaluate(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\"")
+ end
+
+ private
+
+ def evaluate(src)
+ expr = @parser.parse(@scanner.scan(src))
+ @interpreter.evaluate(expr)
+ end
+
+ def interpret(src)
+ expr = @parser.parse(@scanner.scan(src))
+ @interpreter.interpret(expr)
+ end
+
+ def assert_interpreted(expected, src)
+ assert_equal expected, evaluate(src)
+ end
+end
diff --git a/ruby/test/lox/test_parser.rb b/ruby/test/lox/test_parser.rb
index 2585418..7da6084 100644
--- a/ruby/test/lox/test_parser.rb
+++ b/ruby/test/lox/test_parser.rb
@@ -9,7 +9,6 @@ class TestParser < Lox::Test
def setup
@ast_printer = Lox::AstPrinter.new
@scanner = Lox::Scanner.new
- @parser = Lox::Parser.new
end
def test_expression
diff --git a/ruby/test/test_helper.rb b/ruby/test/test_helper.rb
index 4ed7277..681c495 100644
--- a/ruby/test/test_helper.rb
+++ b/ruby/test/test_helper.rb
@@ -7,32 +7,37 @@ module Lox
def assert_lox(path)
src = File.read(path)
+ # https://github.com/munificent/craftinginterpreters/blob/master/tool/bin/test.dart#L12-L18
expected_out = src.scan(/(?<=\/\/ expect: )(?~\n)/).join("\n")
+ expected_err = src[/(?<=\/\/ expect runtime error: )(?~\n)/]
- out, _err, _status = Open3.capture3(LOX_BIN, path)
+ out, err, _status = Open3.capture3(LOX_BIN, path)
assert_equal expected_out, out
+ assert_equal expected_err, err.lines(chomp: true)[0]
end
end
- book_src = File.expand_path(ENV.fetch("CRAFTING_INTERPRETERS_SRC"))
- Dir.chdir(book_src) do
- lox_tests = Dir["./test/**/*.lox"]
- .group_by {|path| path.rpartition(?/).first.sub(/\.\/test\/?/, "") }
-
- lox_tests.each do |dir, paths|
- klass = Class.new(Test) do
- paths.each do |path|
- name = File.basename(path, ".lox")
- define_method("test_#{name}") do
- assert_lox File.expand_path(path, book_src)
+ if ENV.has_key?("LOX_TEST")
+ book_src = File.expand_path(ENV.fetch("CRAFTING_INTERPRETERS_SRC"))
+ Dir.chdir(book_src) do
+ lox_tests = Dir["./test/**/*.lox"]
+ .group_by {|path| path.rpartition(?/).first.sub(/\.\/test\/?/, "") }
+
+ lox_tests.each do |dir, paths|
+ klass = Class.new(Test) do
+ paths.each do |path|
+ name = File.basename(path, ".lox")
+ define_method("test_#{name}") do
+ assert_lox File.expand_path(path, book_src)
+ end
end
end
- end
- suite = dir.split(?/).map(&:capitalize).join
- suite = "Root" if suite.empty?
- Object.const_set("Test#{suite}", klass)
+ suite = dir.split(?/).map(&:capitalize).join
+ suite = "Root" if suite.empty?
+ Object.const_set("Test#{suite}", klass)
+ end
end
end
end
diff --git a/ruby/test/test_lox.rb b/ruby/test/test_lox.rb
index afdff93..d1d4ed0 100644
--- a/ruby/test/test_lox.rb
+++ b/ruby/test/test_lox.rb
@@ -4,8 +4,6 @@ require "lox"
require "open3"
-require "mocktail"
-
class TestLox < Lox::Test
def test_error_on_more_than_one_arg
lox_path = File.expand_path("../bin/lox", __dir__)
@@ -14,23 +12,3 @@ class TestLox < Lox::Test
assert_equal "Usage: #{lox_path} [script]\n", o
end
end
-
-class TestRunner < Lox::Test
- include Mocktail::DSL
-
- def teardown
- Mocktail.reset
- end
-
- # This test sucks, but we'll live with it just not
- # exploding our runner for now.
- def test_prints
- scanner = Mocktail.of(Lox::Scanner)
- parser = Mocktail.of(Lox::Parser)
- runner = Lox::Runner.new(scanner, parser)
- stubs { scanner.scan("src") }.with { %w[ some tokens ] }
- stubs { parser.parse(%w[ some tokens ]) }.with { Lox::Expr::Literal.new("foo") }
-
- runner.run("src")
- end
-end