8.1
FossilOrigin-Name: 5d014cbe85c713fbe1fccf2a053db4b4474c0eee5e909d7cfd7de58fcbb03f77
change pnyklrwqstpkolyyrywuvzxztlptpkmz
commit b0237fbcf2d0ac91ddd385ef60f49b87efb112cb
author alpha <alpha@kejadlen.dev>
date
parent twzsvlqs
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)