7
FossilOrigin-Name: ae1ca40694300eff97d7ee3961f0e8e0b1f61f275c8226872ac1b502c5343b07
change rqnlyquzvwspykulztqlmtwnroqrrumq
commit a81d59f29462c1135123d8e8abdfac2f2a33611f
author alpha <alpha@kejadlen.dev>
date
parent nokkoosn
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