chapter 12
This commit is contained in:
8
examples/simple_class.lox
Normal file
8
examples/simple_class.lox
Normal file
@@ -0,0 +1,8 @@
|
||||
class DevonshireCream {
|
||||
fn serveOn() {
|
||||
return "Scones";
|
||||
}
|
||||
}
|
||||
|
||||
print(DevonshireCream);
|
||||
print(DevonshireCream());
|
4
main.rb
4
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]"
|
||||
|
23
rlox/call.rb
23
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
|
||||
|
82
rlox/classes.rb
Normal file
82
rlox/classes.rb
Normal file
@@ -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()
|
||||
"<instance of '#{@klass}'>"
|
||||
end
|
||||
end
|
||||
|
||||
class LoxClass
|
||||
def initialize(name, methods)
|
||||
@name = name
|
||||
@methods = methods
|
||||
end
|
||||
|
||||
def to_s()
|
||||
if @name
|
||||
@name.to_s
|
||||
else
|
||||
"<anonymous class>"
|
||||
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
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -14,7 +14,7 @@ KEYWORDS = [
|
||||
"or",
|
||||
"return",
|
||||
"super",
|
||||
"this",
|
||||
"self",
|
||||
"true",
|
||||
"let",
|
||||
"while",
|
||||
|
Reference in New Issue
Block a user