FossilOrigin-Name: ae1ca40694300eff97d7ee3961f0e8e0b1f61f275c8226872ac1b502c5343b07
private
alpha 2 years ago
parent dd97e96aca
commit b651c84d9b

@ -5,6 +5,5 @@ source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "minitest" gem "minitest"
gem "mocktail"
gem "pry" gem "pry"
gem "rake" gem "rake"

@ -4,7 +4,6 @@ GEM
coderay (1.1.3) coderay (1.1.3)
method_source (1.0.0) method_source (1.0.0)
minitest (5.16.2) minitest (5.16.2)
mocktail (1.1.3)
pry (0.14.1) pry (0.14.1)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
@ -15,7 +14,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
minitest minitest
mocktail
pry pry
rake rake

@ -11,17 +11,22 @@ def run_prompt
break if line.nil? || line.empty? break if line.nil? || line.empty?
begin begin
run(line) run(line)
rescue Lox::Error => e rescue Lox::ParseError => e
puts e.message STDERR.puts e.message
rescue Lox::RuntimeError => e
STDERR.puts e.message
end end
end end
end end
def run_file(io) def run_file(io)
run(io.read) run(io.read)
rescue Lox::Error => e rescue Lox::ParseError => e
puts e.message STDERR.puts e.message
exit 65 exit 65
rescue Lox::RuntimeError => e
STDERR.puts e.message, "[line #{e.token.line}]"
exit 70
end end
def run(src) def run(src)

@ -3,23 +3,25 @@
require_relative "lox/ast_printer" require_relative "lox/ast_printer"
require_relative "lox/error" require_relative "lox/error"
require_relative "lox/expr" require_relative "lox/expr"
require_relative "lox/interpreter"
require_relative "lox/parser" require_relative "lox/parser"
require_relative "lox/scanner" require_relative "lox/scanner"
require_relative "lox/token" require_relative "lox/token"
module Lox module Lox
class Runner class Runner
def initialize(scanner=Scanner.new, parser=Parser.new) def initialize
@scanner, @parser = scanner, parser @scanner = Scanner.new
@parser = Parser.new
@interpreter = Interpreter.new
end end
def run(src) def run(src)
tokens = @scanner.scan(src) tokens = @scanner.scan(src)
expr = @parser.parse(tokens) expr = @parser.parse(tokens)
value = @interpreter.interpret(expr)
puts AstPrinter.new.print(expr) puts value
rescue ParseError => e
puts e.message
end end
end end
end end

@ -1,10 +1,23 @@
module Lox module Lox
class Error < StandardError 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 = "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 end
end end

@ -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

@ -2,14 +2,6 @@ require_relative "error"
require_relative "expr" require_relative "expr"
module Lox 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 Parser
class State < Struct.new(:tokens, :current) class State < Struct.new(:tokens, :current)
def initialize(tokens) def initialize(tokens)
@ -136,7 +128,7 @@ module Lox
# that feels weird so let's move that error handling up the # that feels weird so let's move that error handling up the
# stack for now. # stack for now.
def parse(tokens) def parse(tokens)
state = State.new(tokens, 0) state = State.new(tokens)
state.expression state.expression
end end
end end

@ -90,6 +90,7 @@ module Lox
end end
end end
# TODO
fail unless state.errors.empty? fail unless state.errors.empty?
state.add_token(:EOF, text: "") state.add_token(:EOF, text: "")

@ -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

@ -9,7 +9,6 @@ class TestParser < Lox::Test
def setup def setup
@ast_printer = Lox::AstPrinter.new @ast_printer = Lox::AstPrinter.new
@scanner = Lox::Scanner.new @scanner = Lox::Scanner.new
@parser = Lox::Parser.new
end end
def test_expression def test_expression

@ -7,32 +7,37 @@ module Lox
def assert_lox(path) def assert_lox(path)
src = File.read(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_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_out, out
assert_equal expected_err, err.lines(chomp: true)[0]
end end
end end
book_src = File.expand_path(ENV.fetch("CRAFTING_INTERPRETERS_SRC")) if ENV.has_key?("LOX_TEST")
Dir.chdir(book_src) do book_src = File.expand_path(ENV.fetch("CRAFTING_INTERPRETERS_SRC"))
lox_tests = Dir["./test/**/*.lox"] Dir.chdir(book_src) do
.group_by {|path| path.rpartition(?/).first.sub(/\.\/test\/?/, "") } lox_tests = Dir["./test/**/*.lox"]
.group_by {|path| path.rpartition(?/).first.sub(/\.\/test\/?/, "") }
lox_tests.each do |dir, paths|
klass = Class.new(Test) do lox_tests.each do |dir, paths|
paths.each do |path| klass = Class.new(Test) do
name = File.basename(path, ".lox") paths.each do |path|
define_method("test_#{name}") do name = File.basename(path, ".lox")
assert_lox File.expand_path(path, book_src) define_method("test_#{name}") do
assert_lox File.expand_path(path, book_src)
end
end end
end end
end
suite = dir.split(?/).map(&:capitalize).join suite = dir.split(?/).map(&:capitalize).join
suite = "Root" if suite.empty? suite = "Root" if suite.empty?
Object.const_set("Test#{suite}", klass) Object.const_set("Test#{suite}", klass)
end
end end
end end
end end

@ -4,8 +4,6 @@ require "lox"
require "open3" require "open3"
require "mocktail"
class TestLox < Lox::Test class TestLox < Lox::Test
def test_error_on_more_than_one_arg def test_error_on_more_than_one_arg
lox_path = File.expand_path("../bin/lox", __dir__) lox_path = File.expand_path("../bin/lox", __dir__)
@ -14,23 +12,3 @@ class TestLox < Lox::Test
assert_equal "Usage: #{lox_path} [script]\n", o assert_equal "Usage: #{lox_path} [script]\n", o
end end
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

Loading…
Cancel
Save