From 580795cd2b6d9609bd744ccca64e8ad7f175c5c1 Mon Sep 17 00:00:00 2001 From: hellerve Date: Fri, 27 Jul 2018 17:50:56 +0200 Subject: [PATCH] chapter 12 --- examples/simple_class.lox | 8 +++ main.rb | 4 +- rlox/call.rb | 23 ++++++-- rlox/classes.rb | 82 +++++++++++++++++++++++++++ rlox/environment.rb | 10 +++- rlox/executor.rb | 6 +- rlox/expression.rb | 116 ++++++++++++++++++++++++++++++++++++-- rlox/parse.rb | 44 ++++++++++++++- rlox/prompt.rb | 7 ++- rlox/scan.rb | 2 +- 10 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 examples/simple_class.lox create mode 100644 rlox/classes.rb diff --git a/examples/simple_class.lox b/examples/simple_class.lox new file mode 100644 index 0000000..19a06d6 --- /dev/null +++ b/examples/simple_class.lox @@ -0,0 +1,8 @@ +class DevonshireCream { + fn serveOn() { + return "Scones"; + } +} + +print(DevonshireCream); +print(DevonshireCream()); diff --git a/main.rb b/main.rb index ee5884a..4ea178f 100644 --- a/main.rb +++ b/main.rb @@ -8,8 +8,8 @@ case ARGV.length when 1 then begin file ARGV[0] - rescue StandardError => e - STDERR.puts e + #rescue StandardError => e + # STDERR.puts e end else puts "Usage: rlox [script]" diff --git a/rlox/call.rb b/rlox/call.rb index 51307b8..85df3b6 100644 --- a/rlox/call.rb +++ b/rlox/call.rb @@ -1,12 +1,14 @@ class Callable - def initialize(name, arity, &block) + def initialize(name, params, body, env, &block) + @env = env @name = name - @arity = arity + @params = params + @body = body @call = block end def to_s() - "#{@name}:#{@arity}" + "#{@name}:#{arity}" end def name() @@ -14,7 +16,7 @@ class Callable end def arity() - @arity + @params.length end def call(args) @@ -24,4 +26,17 @@ class Callable e.value end end + + def bind(instance) + env = @env.child + env.vars.define("self", instance) + Callable.new(@name, @params, @env, @body) { | args | + nenv = env.child + for i in 0..@params.length-1 + nenv.vars.define(@params[i].lexeme, args[i]) + end + + @body.eval(env) + } + end end diff --git a/rlox/classes.rb b/rlox/classes.rb new file mode 100644 index 0000000..c92e37b --- /dev/null +++ b/rlox/classes.rb @@ -0,0 +1,82 @@ +class LoxInstance + def initialize(klass) + @klass = klass + @fields = {} + end + + def fieldnames() + "(#{@fields.keys.join(", ")})" + end + + def get(name) + if @fields.has_key? name.lexeme + return @fields[name.lexeme] + end + + method = @klass.find_method(self, name) + if method + return method + end + + raise ExecError.new( + name.line, + "Undefined property '#{name.lexeme}' on #{self}, has #{fieldnames}." + ) + end + + def set(name, val) + @fields[name.lexeme] = val + end + + def to_s() + "" + end +end + +class LoxClass + def initialize(name, methods) + @name = name + @methods = methods + end + + def to_s() + if @name + @name.to_s + else + "" + end + end + + def arity() + init = @methods["init"] + if init + init.arity + else + 0 + end + end + + def name() + @name + end + + def call(args) + ins = LoxInstance.new(self) + init = @methods["init"] + if init + init.bind(ins).call(args) + end + + ins + end + + def find_method(instance, name) + if name.lexeme == "init" + raise ExecError.new(name.line, "Cannot re-initialize class.") + end + + if @methods.has_key? name.lexeme + return @methods[name.lexeme].bind(instance) + end + end +end diff --git a/rlox/environment.rb b/rlox/environment.rb index aced30a..5e3a882 100644 --- a/rlox/environment.rb +++ b/rlox/environment.rb @@ -4,8 +4,14 @@ 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.define("print", Callable.new("print", ["obj"], nil, env) { + | args | + puts args[0] + }) + env.define("clock", Callable.new("clock", [], nil, env) { + | args | + Time.now.to_i + }) env end diff --git a/rlox/executor.rb b/rlox/executor.rb index aca6a87..4da723a 100644 --- a/rlox/executor.rb +++ b/rlox/executor.rb @@ -2,7 +2,7 @@ require './rlox/environment' require './rlox/parse' require './rlox/scan' -Env = Struct.new(:map, :scopes, :vars, :fn) do +Env = Struct.new(:map, :scopes, :vars, :fn, :cls) do def child() Env.new(map, scopes, Environment.new(vars), fn) end @@ -19,7 +19,7 @@ end class Executor def initialize() - @env = Env.new({}, [], Environment.global, nil) + @env = Env.new({}, [], Environment.global, nil, nil) @scanner = Scanner.new @parser = Parser.new end @@ -42,7 +42,7 @@ class Executor return end - ast.each { | stmt | + ast.map { | stmt | stmt.eval(@env) } end diff --git a/rlox/expression.rb b/rlox/expression.rb index eaa9a79..c0ddf07 100644 --- a/rlox/expression.rb +++ b/rlox/expression.rb @@ -1,3 +1,5 @@ +require './rlox/classes' + def truthy?(obj) if [nil, false, "", 0].include? obj false @@ -215,7 +217,8 @@ Unary = Struct.new(:operator, :right) do end Var = Struct.new(:name) do def resolve(env) - if env.scopes.length > 0 and env.scopes[-1][name.lexeme][:assigned] == false + if (env.scopes.length > 0 and env.scopes[-1][name.lexeme] and + env.scopes[-1][name.lexeme][:assigned] == false) raise VarError.new( name.line, "Cannot read local variable #{name.lexeme} in its own initializer." @@ -266,7 +269,7 @@ Fn = Struct.new(:params, :body) do end def eval(env) - Callable.new("fn", params.length) { | args | + Callable.new("fn", params, body, env) { | args | nenv = env.child for i in 0..params.length-1 nenv.vars.define(params[i].lexeme, args[i]) @@ -275,6 +278,106 @@ Fn = Struct.new(:params, :body) do } end end +Get = Struct.new(:object, :name) do + def resolve(env) + object.resolve(env) + end + + def eval(env) + obj = object.eval(env) + + if obj.is_a? LoxInstance + return obj.get(name) + end + + raise ExecError.new( + expr.name.line, + "Only instances have properties, #{object.lexeme} is a #{obj.class}." + ) + end +end +Set = Struct.new(:object, :name, :value) do + def resolve(env) + value.resolve(env) + object.resolve(env) + end + + def eval(env) + obj = object.eval(env) + + if not obj.is_a? LoxInstance + raise ExecError.new( + name.line, + "Only instances have fields, #{obj} is a #{obj.class}." + ) + end + + val = value.eval(env) + + obj.set(name, val) + + val + end +end +Klass = Struct.new(:name, :methods) do + def resolve(env) + enclosing = env.cls + env.cls = :class + declare(env.scopes, name) + define(env.scopes, name) + + begin_scope(env.scopes) + dummy = Token.new(:id, "self", nil, 0) + declare(env.scopes, dummy) + define(env.scopes, dummy) + + methods.each { | method | + method.resolve(env, method.name.lexeme == "init" ? :init : :method) + } + + end_scope(env.scopes) + env.cls = enclosing + end + + def eval(env) + if name + env.vars.define(name.lexeme, nil) + end + + methods_dict = {} + methods.each { | method | + fn = Callable.new(method.name.lexeme, method.params, method.body, env) { + | args | + nenv = env.child + for i in 0..method.params.length-1 + nenv.vars.define(method.params[i].lexeme, args[i]) + end + method.body.eval(nenv) + } + methods_dict[method.name.lexeme] = fn + } + + klass = LoxClass.new(name ? name.lexeme : nil, methods_dict) + + if name + env.vars.assign(name, klass) + end + klass + end +end +Self = Struct.new(:keyword) do + def resolve(env) + if env.cls != :class + raise VarError.new(keyword.line, "Cannot use 'self' outside of a class.") + end + + resolve_local(env, self, keyword) + end + + def eval(env) + env.lookup(self, keyword) + end +end Expression = Struct.new(:expression) do def resolve(env) @@ -353,12 +456,12 @@ While = Struct.new(:cond, :body) do end end FnDef = Struct.new(:name, :params, :body) do - def resolve(env) + def resolve(env, fn=:fn) declare(env.scopes, name) define(env.scopes, name) enclosing = env.fn - env.fn = :fn + env.fn = fn begin_scope(env.scopes) params.each{ | param | @@ -372,7 +475,7 @@ FnDef = Struct.new(:name, :params, :body) do end def eval(env) - env.vars.define(name.lexeme, Callable.new(name.lexeme, params.length) { + env.vars.define(name.lexeme, Callable.new(name.lexeme, params, body, env) { | args | nenv = env.child for i in 0..params.length-1 @@ -389,6 +492,9 @@ Return = Struct.new(:keyword, :value) do end if value + if env.fn == :init + raise VarError.new(keyword.line, "Cannot return a value from 'init()'.") + end value.resolve(env) end end diff --git a/rlox/parse.rb b/rlox/parse.rb index 7f121aa..742ba54 100644 --- a/rlox/parse.rb +++ b/rlox/parse.rb @@ -88,6 +88,8 @@ class Parser if expr.is_a? Var name = expr.name return Assign.new(name, value) + elsif expr.is_a? Get + return Set.new(expr.object, expr.name, value) end error(equals, "Invalid assignment target: '#{expr.dbg}'.") @@ -160,6 +162,9 @@ class Parser while true if match(:left_paren) expr = finish_call(expr) + elsif match(:dot) + name = consume(:id, "Expect property name after '.'.") + expr = Get.new(expr, name) else break end @@ -198,6 +203,20 @@ class Parser Fn.new(params, body) end + def anon_class() + consume(:left_brace, "Expect '{' before body of anonymous class.") + + methods = [] + + while not check(:right_brace) and not is_at_end + methods << function("method") + end + + consume(:right_brace, "Expect '}' after class body.") + + Klass.new(nil, methods) + end + def primary() if match(:false) Literal.new(false) @@ -205,6 +224,8 @@ class Parser Literal.new(true) elsif match(:nil) Literal.new(nil) + elsif match(:self) + Self.new(previous) elsif match(:number, :string) Literal.new(previous.literal) elsif match(:left_paren) @@ -215,6 +236,8 @@ class Parser Var.new(previous) elsif match(:fn) anon_fn + elsif match(:class) + anon_class else error(peek, "Expect expression.") end @@ -399,9 +422,28 @@ class Parser FnDef.new(name, params, body) end + def class_declaration() + consume(:class, "Expect 'class' keyword.") + name = consume(:id, "Expect class name.") + + consume(:left_brace, "Expect '{' before body of class '#{name.lexeme}.'") + + methods = [] + + while not check(:right_brace) and not is_at_end + methods << function("method") + end + + consume(:right_brace, "Expect '}' after class body.") + + Klass.new(name, methods) + end + def declaration() begin - if check(:fn) and check_next(:id) + if check(:class) and check_next(:id) + class_declaration + elsif check(:fn) and check_next(:id) function("fn") elsif match(:let) var_declaration diff --git a/rlox/prompt.rb b/rlox/prompt.rb index adfcd29..3213986 100644 --- a/rlox/prompt.rb +++ b/rlox/prompt.rb @@ -19,7 +19,12 @@ def prompt() end begin - puts exec.run(buf).to_s + res = exec.run(buf) + if res + res.each { | res | + puts res.to_s + } + end rescue LoxError => err STDERR.puts err ensure diff --git a/rlox/scan.rb b/rlox/scan.rb index 146bce3..6f94d8a 100644 --- a/rlox/scan.rb +++ b/rlox/scan.rb @@ -14,7 +14,7 @@ KEYWORDS = [ "or", "return", "super", - "this", + "self", "true", "let", "while",