diff --git a/README.md b/README.md index ad1e93d..f24092b 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,7 @@ because I want to learn it. It deviates a little from the “canonical” version of Lox. I don’t use code generation for the expressions, and I don’t use the visitor pattern. `var` and `fun` are `let` and `fn`, respectively. And we don’t need parentheses around -branching conditions, instead we require the bodies to be blocks. +branching conditions, instead we require the bodies to be blocks. We also have +closures and anonymous functions (and literals for them), and implicit returns. + +If you wonder what that looks like, you can look at the [examples](/examples). diff --git a/examples/counter.lox b/examples/counter.lox new file mode 100644 index 0000000..c4aa1e9 --- /dev/null +++ b/examples/counter.lox @@ -0,0 +1,10 @@ +fn makeCounter() { + let i = 0; + fn count() { + i = i + 1; + } +} + +let counter = makeCounter(); +print(counter()); // "1". +print(counter()); // "2". diff --git a/examples/fib.lox b/examples/fib.lox new file mode 100644 index 0000000..7dbd103 --- /dev/null +++ b/examples/fib.lox @@ -0,0 +1,8 @@ +fn fibonacci(n) { + if n <= 1 { return n; } + return fibonacci(n - 2) + fibonacci(n - 1); +} + +for let i = 0; i < 25; i = i + 1 { + print(fibonacci(i)); +} diff --git a/examples/thrice.lox b/examples/thrice.lox new file mode 100644 index 0000000..dcbae33 --- /dev/null +++ b/examples/thrice.lox @@ -0,0 +1,9 @@ +fn thrice(f) { + for let i = 1; i <= 3; i = i + 1 { + f(i); + } +} + +thrice(fn (a) { + print(a); +}); diff --git a/rlox/call.rb b/rlox/call.rb new file mode 100644 index 0000000..51307b8 --- /dev/null +++ b/rlox/call.rb @@ -0,0 +1,27 @@ +class Callable + def initialize(name, arity, &block) + @name = name + @arity = arity + @call = block + end + + def to_s() + "#{@name}:#{@arity}" + end + + def name() + @name + end + + def arity() + @arity + end + + def call(args) + begin + @call.call(args) + rescue ReturnError => e + e.value + end + end +end diff --git a/rlox/environment.rb b/rlox/environment.rb index f828b7f..b4e191d 100644 --- a/rlox/environment.rb +++ b/rlox/environment.rb @@ -1,4 +1,14 @@ +require './rlox/call' + class Environment + def self.global() + env = self.new() + + env.define("print", Callable.new("print", 1) { | args | puts args[0] }) + env.define("clock", Callable.new("clock", 0) { | args | Time.now.to_i }) + env + end + def initialize(parent=nil) @values = {} @parent = parent diff --git a/rlox/error.rb b/rlox/error.rb index 9d0c55d..f920c9a 100644 --- a/rlox/error.rb +++ b/rlox/error.rb @@ -20,3 +20,13 @@ end class ExecError < LoxError end + +class ReturnError < StandardError + def initialize(value) + @value = value + end + + def value() + @value + end +end diff --git a/rlox/executor.rb b/rlox/executor.rb index c5b53f9..4b840e6 100644 --- a/rlox/executor.rb +++ b/rlox/executor.rb @@ -4,7 +4,7 @@ require './rlox/scan' class Executor def initialize() - @env = Environment.new + @env = Environment.global @scanner = Scanner.new @parser = Parser.new end @@ -14,7 +14,7 @@ class Executor ast = @parser.parse_on(tokens) ast.each { | stmt | - @env = stmt.eval(@env) + stmt.eval(@env) } end diff --git a/rlox/expression.rb b/rlox/expression.rb index c8af350..3a63f15 100644 --- a/rlox/expression.rb +++ b/rlox/expression.rb @@ -24,6 +24,30 @@ def check_same_type_op(operator, *operands) end } end +Call = Struct.new(:callee, :paren, :arguments) do + def eval(env) + to_call = callee.eval(env) + al = arguments.length + + if not to_call.class.method_defined? :call + raise ExecError.new(paren.line, "Can only call functions and classes.") + end + + if al != to_call.arity + raise ExecError.new( + paren.line, + "#{to_call.name} expected #{to_call.arity} arguments but got #{al}." + ) + end + + args = [] + arguments.each{ | arg | + args << arg.eval(env) + } + + to_call.call(args) + end +end Assign = Struct.new(:name, :value) do def eval(env) @@ -115,17 +139,21 @@ Logical = Struct.new(:left, :operator, :right) do right.eval(env) end end +Fn = Struct.new(:params, :body) do + def eval(env) + Callable.new("fn", params.length) { | args | + nenv = Environment.new(env) + for i in 0..params.length-1 + nenv.define(params[i].lexeme, args[i]) + end + body.eval(nenv) + } + end +end Expression = Struct.new(:expression) do def eval(env) expression.eval(env) - env - end -end -Print = Struct.new(:print) do - def eval(env) - puts print.eval(env) - env end end Variable = Struct.new(:name, :initializer) do @@ -136,16 +164,16 @@ Variable = Struct.new(:name, :initializer) do end env.define(name.lexeme, value) - env end end Block = Struct.new(:stmts) do def eval(env) child = Environment.new(env) + ret = nil stmts.each{ | stmt | - stmt.eval(child) + ret = stmt.eval(child) } - env + ret end end If = Struct.new(:cond, :thn, :els) do @@ -155,14 +183,31 @@ If = Struct.new(:cond, :thn, :els) do elsif els != nil els.eval(env) end - env end end While = Struct.new(:cond, :body) do def eval(env) + ret = nil while truthy? cond.eval(env) - body.eval(env) + ret = body.eval(env) end - env + ret + end +end +FnDef = Struct.new(:name, :params, :body) do + def eval(env) + env.define(name.lexeme, Callable.new(name.lexeme, params.length) { + | args | + nenv = Environment.new(env) + for i in 0..params.length-1 + nenv.define(params[i].lexeme, args[i]) + end + body.eval(nenv) + }) + end +end +Return = Struct.new(:value) do + def eval(env) + raise ReturnError.new(value == nil ? nil : value.eval(env)) end end diff --git a/rlox/parse.rb b/rlox/parse.rb index e90c707..a1df791 100644 --- a/rlox/parse.rb +++ b/rlox/parse.rb @@ -23,6 +23,15 @@ class Parser end end + + def check_next(type) + if is_at_end or peek_next.type == :eof + false + else + peek_next.type == type + end + end + def advance() if !is_at_end @current += 1 @@ -38,20 +47,12 @@ class Parser @tokens[@current] end - def previous() - @tokens[@current-1] + def peek_next() + @tokens[@current+1] end - def comma() - expr = assignment - - while match(:comma) - operator = previous - right = comparison - expr = Binary.new(expr, operator, right) - end - - expr + def previous() + @tokens[@current-1] end def and_expr() @@ -134,7 +135,7 @@ class Parser def comparison() expr = addition - while match(:gt, :geq, :lt, :geq) + while match(:gt, :geq, :lt, :leq) operator = previous right = addition expr = Binary.new(expr, operator, right) @@ -150,7 +151,51 @@ class Parser return Unary.new(operator, right) end - primary + call + end + + def call() + expr = primary + + while true + if match(:left_paren) + expr = finish_call(expr) + else + break + end + end + + expr + end + + def finish_call(callee) + args = [] + + if not check(:right_paren) + begin + args << expression + end while match(:comma) + end + + paren = consume(:right_paren, "Expect ')' after arguments.") + + return Call.new(callee, paren, args) + end + + def anon_fn() + consume(:left_paren, "Expect '(' after 'fn' keyword'.") + + params = [] + if not check(:right_paren) + begin + params << consume(:id, "Expect parameter name") + end while match(:comma) + end + consume(:right_paren, "Expect ')' after parameters.") + + consume(:left_brace, "Expect '{' before fn body.") + body = Block.new(block) + Fn.new(params, body) end def primary() @@ -168,6 +213,8 @@ class Parser Grouping.new(expr) elsif match(:id) Var.new(previous) + elsif match(:fn) + anon_fn else error(peek, "Expect expression.") end @@ -190,7 +237,7 @@ class Parser end def expression() - comma + assignment end def synchronize() @@ -202,7 +249,7 @@ class Parser end case peek.type - when :class, :fun, :let, :for, :if, :while, :print, :return then + when :class, :fun, :let, :for, :if, :while, :return then return end @@ -210,12 +257,6 @@ class Parser end end - def print_statement() - value = expression - consume(:semicolon, "Expect ';' after value.") - Print.new(value) - end - def expression_statement() expr = expression consume(:semicolon, "Expect ';' after expression.") @@ -261,7 +302,7 @@ class Parser def for_statement() init = if match(:semicolon) nil - elsif match(:var) + elsif match(:let) var_declaration else expression_statement @@ -284,30 +325,41 @@ class Parser consume(:left_brace, "Expect '{' after 'if' condition.") body = Block.new(block) - if inc - body = Block.new([body, Expr.new(inc)]) + if inc != nil + body = Block.new([body, Expression.new(inc)]) end - if not cond + if cond == nil cond = Literal.new(true) end - if init - body = Block.new([init, body]) - end - body = While.new(cond, body) + if init != nil + body = Block.new([init, body]) + end + body end + def return_statement() + value = nil + + if !check(:semicolon) + value = expression + end + + consume(:semicolon, "Expect ';' after return value.") + Return.new(value) + end + def statement() if match(:if) if_statement elsif match(:for) for_statement - elsif match(:print) - print_statement + elsif match(:return) + return_statement elsif match(:while) while_statement elsif match(:left_brace) @@ -328,12 +380,33 @@ class Parser Variable.new(name, initializer) end + def function(kind) + consume(:fn, "Expect 'fn' keyword.") + name = consume(:id, "Expect #{kind} name.") + consume(:left_paren, "Expect '(' after #{kind} name.") + + params = [] + if not check(:right_paren) + begin + params << consume(:id, "Expect paramenter name") + end while match(:comma) + end + consume(:right_paren, "Expect ')' after parameters.") + + consume(:left_brace, "Expect '{' before #{kind} body.") + body = Block.new(block) + FnDef.new(name, params, body) + end + def declaration() begin - if match(:let) - return var_declaration + if check(:fn) and check_next(:id) + function("fn") + elsif match(:let) + var_declaration + else + statement end - return statement rescue ParseError => e synchronize raise e @@ -344,6 +417,7 @@ class Parser statements = [] while not is_at_end statements << declaration + match(:semicolon) end statements diff --git a/rlox/prompt.rb b/rlox/prompt.rb index d7bdcbe..adfcd29 100644 --- a/rlox/prompt.rb +++ b/rlox/prompt.rb @@ -14,8 +14,12 @@ def prompt() while buf = Readline.readline("> ", true) Readline::HISTORY.pop if /^\s*$/ =~ buf + if buf[-1] != ';' + buf << ';' + end + begin - exec.run buf + puts exec.run(buf).to_s rescue LoxError => err STDERR.puts err ensure diff --git a/rlox/scan.rb b/rlox/scan.rb index 292fa76..146bce3 100644 --- a/rlox/scan.rb +++ b/rlox/scan.rb @@ -12,7 +12,6 @@ KEYWORDS = [ "if", "nil", "or", - "print", "return", "super", "this",