diff --git a/main.rb b/main.rb index b9d0042..ee5884a 100644 --- a/main.rb +++ b/main.rb @@ -8,7 +8,7 @@ case ARGV.length when 1 then begin file ARGV[0] - rescue Error => e + rescue StandardError => e STDERR.puts e end else diff --git a/rlox/environment.rb b/rlox/environment.rb new file mode 100644 index 0000000..4b7dcd3 --- /dev/null +++ b/rlox/environment.rb @@ -0,0 +1,34 @@ +class Environment + def initialize(parent=nil) + @values = {} + @parent = parent + end + + def to_s() + @values.to_s + end + + def define(name, value) + @values[name] = value + end + + def get(name) + if @values.has_key? name.lexeme + return @values[name.lexeme] + end + if @parent != nil + return @parent.get(name) + end + raise ExecError.new(name.line, "Undefined variable '#{name.lexeme}'.") + end + + def assign(name, value) + if @values.has_key? name.lexeme + @values.put(name.lexeme, value) + end + if @parent != nil + return @parent.assign(name, value) + end + raise ExecError.new(name.line, "Undefined variable '#{name.lexeme}'.") + end +end diff --git a/rlox/executor.rb b/rlox/executor.rb index b426735..c5b53f9 100644 --- a/rlox/executor.rb +++ b/rlox/executor.rb @@ -1,13 +1,24 @@ -require './rlox/scan' +require './rlox/environment' require './rlox/parse' +require './rlox/scan' class Executor - def run(source) - scanner = Scanner.new source - tokens = scanner.scan - parser = Parser.new tokens - ast = parser.parse + def initialize() + @env = Environment.new + @scanner = Scanner.new + @parser = Parser.new + end - puts ast.eval + def run(source) + tokens = @scanner.scan_on(source) + ast = @parser.parse_on(tokens) + + ast.each { | stmt | + @env = stmt.eval(@env) + } + end + + def inc_line() + @scanner.inc_line end end diff --git a/rlox/expression.rb b/rlox/expression.rb index 3f1cb22..355a577 100644 --- a/rlox/expression.rb +++ b/rlox/expression.rb @@ -39,14 +39,21 @@ def check_same_type_op(operator, *operands) } end +Assign = Struct.new(:name, :value) do + def eval(env) + val = value.eval(env) + env.assign(name, val) + val + end +end Binary = Struct.new(:left, :operator, :right) do def dbg() parenthesize(operator.lexeme, left, right) end - def eval() - l = left.eval - r = right.eval + def eval(env) + l = left.eval(env) + r = right.eval(env) case operator.type when :minus then @@ -84,8 +91,8 @@ Grouping = Struct.new(:expression) do parenthesize("group", expression) end - def eval(fn) - expression.eval + def eval(env) + expression.eval(env) end end Literal = Struct.new(:value) do @@ -93,7 +100,7 @@ Literal = Struct.new(:value) do value.to_s end - def eval() + def eval(env) value end end @@ -102,8 +109,8 @@ Unary = Struct.new(:operator, :right) do parenthesize(operator.lexeme, right) end - def eval() - r = right.eval + def eval(env) + r = right.eval(env) case operator.type when :bang then is_truthy(r) @@ -114,3 +121,40 @@ Unary = Struct.new(:operator, :right) do end end end +Var = Struct.new(:name) do + def eval(env) + env.get(name) + 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 + def eval(env) + value = nil + if initializer != nil + value = initializer.eval(env) + end + + env.define(name.lexeme, value) + env + end +end +Block = Struct.new(:stmts) do + def eval(env) + child = Environment.new(env) + stmts.each{ | stmt | + stmt.eval(child) + } + env + end +end diff --git a/rlox/parse.rb b/rlox/parse.rb index 77443be..fee246c 100644 --- a/rlox/parse.rb +++ b/rlox/parse.rb @@ -1,8 +1,7 @@ require './rlox/expression' class Parser - def initialize(tokens) - @tokens = tokens + def initialize() @current = 0 end @@ -44,7 +43,7 @@ class Parser end def comma() - expr = equality + expr = assignment while match(:comma) operator = previous @@ -55,6 +54,23 @@ class Parser expr end + def assignment() + expr = equality + + if match(:eq) + equals = previous + value = assignment + if expr.is_a? Var + name = expr.name + return Assign.new(name, value) + end + + error(equals, "Invalid assignment target: '#{expr.dbg}'.") + end + + expr + end + def equality() expr = comparison @@ -126,6 +142,8 @@ class Parser expr = expression consume(:right_paren, "Expect ')' after expression.") Grouping.new(expr) + elsif match(:id) + Var.new(previous) else error(peek, "Expect expression.") end @@ -160,7 +178,7 @@ class Parser end case peek.type - when :class, :fun, :var, :for, :if, :while, :print, :return then + when :class, :fun, :let, :for, :if, :while, :print, :return then return end @@ -168,7 +186,76 @@ 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.") + Expression.new(expr) + end + + def block() + stmts = [] + + while not check(:right_brace) and not is_at_end + stmts << declaration + end + + consume(:right_brace, "Expect '}' after block.") + + return stmts + end + + def statement() + if match(:print) + print_statement + elsif match(:left_brace) + Block.new(block) + else + expression_statement + end + end + + def var_declaration() + name = consume(:id, "Expect variable name.") + + initializer = nil + if match(:eq) + initializer = expression + end + consume(:semicolon, "Expect ';' after variable declaration.") + Variable.new(name, initializer) + end + + def declaration() + begin + if match(:let) + return var_declaration + end + return statement + rescue ParseError => e + synchronize + raise e + end + end + def parse() - expression + statements = [] + while not is_at_end + statements << declaration + end + + statements + end + + def parse_on(tokens) + @current = 0 + @tokens = tokens + + parse end end diff --git a/rlox/prompt.rb b/rlox/prompt.rb index 6ac3655..840f1e4 100644 --- a/rlox/prompt.rb +++ b/rlox/prompt.rb @@ -4,20 +4,26 @@ require './rlox/executor' def prompt() exec = Executor.new - while buf = Readline.readline("> ", true) - Readline::HISTORY.pop if /^\s*$/ =~ buf - begin - if Readline::HISTORY[Readline::HISTORY.length-2] == buf - Readline::HISTORY.pop + f = File.new("#{Dir.home}/.rlox", File::CREAT|File::RDWR) + File.readlines("#{Dir.home}/.rlox").each { | line | + Readline::HISTORY.push line + } + + begin + while buf = Readline.readline("> ", true) + Readline::HISTORY.pop if /^\s*$/ =~ buf + + begin + exec.run buf + f.write("#{buf}\n") + rescue LoxError => err + STDERR.puts err end - rescue IndexError - end - - begin - exec.run buf - rescue LoxError => err - STDERR.puts err + exec.inc_line end + rescue Interrupt + ensure + f.close end end diff --git a/rlox/scan.rb b/rlox/scan.rb index ea0c3dc..292fa76 100644 --- a/rlox/scan.rb +++ b/rlox/scan.rb @@ -22,8 +22,7 @@ KEYWORDS = [ ] class Scanner - def initialize(source) - @source = source + def initialize() @start = 0 @current = 0 @line = 1 @@ -53,7 +52,7 @@ class Scanner def peek() if is_at_end - '\0' + "\0" else @source[@current] end @@ -61,7 +60,7 @@ class Scanner def peek_next() if @current + 1 >= @source.length - '\0' + "\0" else @source[@current+1] end @@ -69,7 +68,7 @@ class Scanner def string() while peek != '"' and not is_at_end - if peek == '\n' + if peek == "\n" @line += 1 end advance @@ -148,7 +147,7 @@ class Scanner when '>' then add_token(match('=') ? :geq : :gt) when '/' then if match('/') - while peek != '\n' and not is_at_end + while peek != "\n" and not is_at_end advance end elsif match('*') @@ -165,8 +164,8 @@ class Scanner else add_token(:slash) end - when ' ', '\r', '\t' then nil - when '\n' then + when ' ', "\r", "\t" then nil + when "\n" then @line += 1 nil when '"' then string @@ -176,7 +175,9 @@ class Scanner elsif is_alpha(c) identifier else - raise ParseError.new(@line, "Unexpected character: #{c}.") + raise ParseError.new( + @line, + "Unexpected character: #{c} (char code: #{c.ord}).") end end end @@ -205,4 +206,16 @@ class Scanner tokens end + + def scan_on(source) + @start = 0 + @current = 0 + @source = source + + scan + end + + def inc_line() + @line += 1 + end end