loxxy 0.1.15 → 0.2.02
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +47 -11
- data/CHANGELOG.md +66 -0
- data/README.md +177 -90
- data/bin/loxxy +54 -6
- data/lib/loxxy.rb +1 -0
- data/lib/loxxy/ast/all_lox_nodes.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +20 -1
- data/lib/loxxy/ast/ast_visitor.rb +7 -0
- data/lib/loxxy/ast/lox_class_stmt.rb +5 -1
- data/lib/loxxy/ast/lox_super_expr.rb +35 -0
- data/lib/loxxy/back_end/engine.rb +41 -13
- data/lib/loxxy/back_end/lox_class.rb +22 -4
- data/lib/loxxy/back_end/lox_function.rb +6 -0
- data/lib/loxxy/back_end/lox_instance.rb +0 -4
- data/lib/loxxy/back_end/resolver.rb +38 -2
- data/lib/loxxy/back_end/symbol_table.rb +1 -18
- data/lib/loxxy/cli_parser.rb +68 -0
- data/lib/loxxy/error.rb +3 -0
- data/lib/loxxy/front_end/grammar.rb +2 -2
- data/lib/loxxy/front_end/scanner.rb +32 -7
- data/lib/loxxy/interpreter.rb +12 -1
- data/lib/loxxy/version.rb +1 -1
- data/loxxy.gemspec +6 -2
- data/spec/back_end/engine_spec.rb +0 -8
- data/spec/front_end/scanner_spec.rb +34 -0
- data/spec/interpreter_spec.rb +94 -0
- metadata +9 -4
data/bin/loxxy
CHANGED
@@ -3,9 +3,57 @@
|
|
3
3
|
|
4
4
|
require 'loxxy'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
class LoxxyRunner
|
7
|
+
DefaultLoxExtension = 'lox'
|
8
|
+
attr_reader(:cli_options)
|
9
|
+
|
10
|
+
def initialize(prog_name, args)
|
11
|
+
my_version = Loxxy::VERSION
|
12
|
+
cli = Loxxy::CLIParser.new(prog_name, my_version)
|
13
|
+
@cli_options = cli.parse!(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run!(file_names)
|
17
|
+
return if file_names.nil? || file_names.empty?
|
18
|
+
|
19
|
+
lox = Loxxy::Interpreter.new
|
20
|
+
begin
|
21
|
+
file_names.each do |lox_file|
|
22
|
+
fname = validate_filename(lox_file)
|
23
|
+
next unless file_exist?(fname)
|
24
|
+
|
25
|
+
File.open(fname, 'r') { |f| lox.evaluate(f.read) }
|
26
|
+
end
|
27
|
+
rescue Loxxy::ScanError => e
|
28
|
+
$stderr.puts e.message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate_filename(raw_fname)
|
35
|
+
# When necessary add extension to file name
|
36
|
+
fname = raw_fname.dup
|
37
|
+
basename = File.basename(fname)
|
38
|
+
has_extension = basename =~ /(?<=[^.])\.[^.]+$/
|
39
|
+
fname << '.' << DefaultLoxExtension unless has_extension
|
40
|
+
|
41
|
+
fname
|
42
|
+
end
|
43
|
+
|
44
|
+
def file_exist?(fname)
|
45
|
+
exists = File.exist?(fname)
|
46
|
+
$stderr.puts "No such file '#{fname}'" unless exists
|
47
|
+
|
48
|
+
exists
|
49
|
+
end
|
50
|
+
end # class
|
51
|
+
|
52
|
+
########################################
|
53
|
+
# ENTRY POINT
|
54
|
+
########################################
|
55
|
+
program = LoxxyRunner.new(File.basename(__FILE__), ARGV)
|
56
|
+
|
57
|
+
# All options from CLI gobbled from ARGV, remains only file name
|
58
|
+
program.run!(ARGV)
|
59
|
+
# End of file
|
data/lib/loxxy.rb
CHANGED
@@ -165,7 +165,14 @@ module Loxxy
|
|
165
165
|
|
166
166
|
# rule('classDecl' => 'CLASS classNaming class_body')
|
167
167
|
def reduce_class_decl(_production, _range, _tokens, theChildren)
|
168
|
-
|
168
|
+
if theChildren[1].kind_of?(Array)
|
169
|
+
name = theChildren[1].first
|
170
|
+
parent = theChildren[1].last
|
171
|
+
else
|
172
|
+
name = theChildren[1]
|
173
|
+
parent = nil
|
174
|
+
end
|
175
|
+
Ast::LoxClassStmt.new(tokens[1].position, name, parent, theChildren[2])
|
169
176
|
end
|
170
177
|
|
171
178
|
# rule('classNaming' => 'IDENTIFIER')
|
@@ -173,6 +180,13 @@ module Loxxy
|
|
173
180
|
theChildren[0].token.lexeme
|
174
181
|
end
|
175
182
|
|
183
|
+
# rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
184
|
+
def reduce_class_subclassing(_production, _range, _tokens, theChildren)
|
185
|
+
super_token = theChildren[2].token
|
186
|
+
super_var = LoxVariableExpr.new(super_token.position, super_token.lexeme)
|
187
|
+
[theChildren[0].token.lexeme, super_var]
|
188
|
+
end
|
189
|
+
|
176
190
|
# rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
|
177
191
|
def reduce_class_body(_production, _range, _tokens, theChildren)
|
178
192
|
theChildren[1].nil? ? [] : theChildren[1]
|
@@ -368,6 +382,11 @@ module Loxxy
|
|
368
382
|
LoxThisExpr.new(tokens[0].position)
|
369
383
|
end
|
370
384
|
|
385
|
+
# rule('primary' => 'SUPER DOT IDENTIFIER')
|
386
|
+
def reduce_super_expr(_production, _range, _tokens, theChildren)
|
387
|
+
LoxSuperExpr.new(theChildren[0].token.position, theChildren[2].token.lexeme)
|
388
|
+
end
|
389
|
+
|
371
390
|
# rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
|
372
391
|
def reduce_function(_production, _range, _tokens, theChildren)
|
373
392
|
first_child = theChildren.first
|
@@ -212,6 +212,13 @@ module Loxxy
|
|
212
212
|
broadcast(:after_this_expr, aThisExpr, self)
|
213
213
|
end
|
214
214
|
|
215
|
+
# Visit event. The visitor is about to visit the super keyword.
|
216
|
+
# @param aSuperExpr [Ast::LoxSuperExpr] super expression
|
217
|
+
def visit_super_expr(aSuperExpr)
|
218
|
+
broadcast(:before_super_expr, aSuperExpr)
|
219
|
+
broadcast(:after_super_expr, aSuperExpr, self)
|
220
|
+
end
|
221
|
+
|
215
222
|
# Visit event. The visitor is about to visit the given terminal datatype value.
|
216
223
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
217
224
|
def visit_builtin(aValue)
|
@@ -8,15 +8,19 @@ module Loxxy
|
|
8
8
|
# @return [String] the class name
|
9
9
|
attr_reader :name
|
10
10
|
|
11
|
+
# @return [Ast::LoxVariableExpr] variable referencing the superclass (if any)
|
12
|
+
attr_reader :superclass
|
13
|
+
|
11
14
|
# @return [Array<Ast::LoxFunStmt>] the methods
|
12
15
|
attr_reader :body
|
13
16
|
|
14
17
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
15
18
|
# @param condExpr [Loxxy::Ast::LoxNode] iteration condition
|
16
19
|
# @param theBody [Array<Loxxy::Ast::LoxNode>]
|
17
|
-
def initialize(aPosition, aName, theMethods)
|
20
|
+
def initialize(aPosition, aName, aSuperclassName, theMethods)
|
18
21
|
super(aPosition, [])
|
19
22
|
@name = aName.dup
|
23
|
+
@superclass = aSuperclassName
|
20
24
|
@body = theMethods
|
21
25
|
end
|
22
26
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_node'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxSuperExpr < LoxNode
|
8
|
+
# @return [Ast::LoxNode] the object to which the property belongs to
|
9
|
+
attr_accessor :object
|
10
|
+
|
11
|
+
# @return [String] Name of a method name
|
12
|
+
attr_reader :property
|
13
|
+
|
14
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
15
|
+
# @param aMethodName [String] Name of a method
|
16
|
+
def initialize(aPosition, aMethodName)
|
17
|
+
super(aPosition)
|
18
|
+
@property = aMethodName
|
19
|
+
end
|
20
|
+
|
21
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
22
|
+
# @param visitor [ASTVisitor] the visitor
|
23
|
+
def accept(visitor)
|
24
|
+
visitor.visit_super_expr(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Quack like a LoxVariableExpr
|
28
|
+
# @return [String] the `super` keyword
|
29
|
+
def name
|
30
|
+
'super'
|
31
|
+
end
|
32
|
+
alias callee= object=
|
33
|
+
end # class
|
34
|
+
end # module
|
35
|
+
end # module
|
@@ -70,30 +70,46 @@ module Loxxy
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def after_class_stmt(aClassStmt, aVisitor)
|
73
|
+
if aClassStmt.superclass
|
74
|
+
aClassStmt.superclass.accept(aVisitor)
|
75
|
+
parent = stack.pop
|
76
|
+
unless parent.kind_of?(LoxClass)
|
77
|
+
raise StandardError, 'Superclass must be a class.'
|
78
|
+
end
|
79
|
+
else
|
80
|
+
parent = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
if parent # Create an environment specific for 'super'
|
84
|
+
super_env = Environment.new(symbol_table.current_env)
|
85
|
+
symbol_table.enter_environment(super_env)
|
86
|
+
end
|
87
|
+
|
73
88
|
# Convert LoxFunStmt into LoxFunction
|
74
89
|
meths = aClassStmt.body.map do |func_node|
|
75
90
|
func_node.is_method = true
|
76
91
|
func_node.accept(aVisitor)
|
77
|
-
stack.pop
|
92
|
+
mth = stack.pop
|
93
|
+
mth.is_initializer = true if mth.name == 'init'
|
94
|
+
mth
|
78
95
|
end
|
79
96
|
|
80
|
-
klass = LoxClass.new(aClassStmt.name, meths, self)
|
97
|
+
klass = LoxClass.new(aClassStmt.name, parent, meths, self)
|
98
|
+
if parent
|
99
|
+
super_var = Variable.new('super', klass)
|
100
|
+
symbol_table.insert(super_var)
|
101
|
+
symbol_table.leave_environment
|
102
|
+
end
|
81
103
|
new_var = Variable.new(aClassStmt.name, klass)
|
82
104
|
symbol_table.insert(new_var)
|
83
105
|
end
|
84
106
|
|
85
|
-
def
|
107
|
+
def after_var_stmt(aVarStmt)
|
86
108
|
new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
|
87
109
|
symbol_table.insert(new_var)
|
88
|
-
end
|
89
|
-
|
90
|
-
def after_var_stmt(aVarStmt)
|
91
|
-
var_name = aVarStmt.name
|
92
|
-
variable = symbol_table.lookup(var_name)
|
93
|
-
raise StandardError, "Unknown variable #{var_name}" unless variable
|
94
110
|
|
95
111
|
value = stack.pop
|
96
|
-
|
112
|
+
new_var.assign(value)
|
97
113
|
end
|
98
114
|
|
99
115
|
def before_for_stmt(aForStmt)
|
@@ -240,15 +256,13 @@ module Loxxy
|
|
240
256
|
case callee
|
241
257
|
when NativeFunction
|
242
258
|
stack.push callee.call # Pass arguments
|
243
|
-
when LoxFunction
|
259
|
+
when LoxFunction, LoxClass
|
244
260
|
arg_count = aCallExpr.arguments.size
|
245
261
|
if arg_count != callee.arity
|
246
262
|
msg = "Expected #{callee.arity} arguments but got #{arg_count}."
|
247
263
|
raise Loxxy::RuntimeError, msg
|
248
264
|
end
|
249
265
|
callee.call(self, aVisitor)
|
250
|
-
when LoxClass
|
251
|
-
callee.call(self, aVisitor)
|
252
266
|
else
|
253
267
|
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
254
268
|
end
|
@@ -286,6 +300,20 @@ module Loxxy
|
|
286
300
|
var.value.accept(aVisitor) # Evaluate this value then push on stack
|
287
301
|
end
|
288
302
|
|
303
|
+
def after_super_expr(aSuperExpr, aVisitor)
|
304
|
+
offset = resolver.locals[aSuperExpr]
|
305
|
+
env = symbol_table.current_env
|
306
|
+
(offset - 1).times { env = env.enclosing }
|
307
|
+
instance = env.defns['this'].value.accept(aVisitor)[0]
|
308
|
+
superklass = variable_lookup(aSuperExpr).value.superclass
|
309
|
+
method = superklass.find_method(aSuperExpr.property)
|
310
|
+
unless method
|
311
|
+
raise StandardError, "Undefined property '#{aSuperExpr.property}'."
|
312
|
+
end
|
313
|
+
|
314
|
+
stack.push method.bind(instance)
|
315
|
+
end
|
316
|
+
|
289
317
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
290
318
|
def before_visit_builtin(aValue)
|
291
319
|
stack.push(aValue)
|
@@ -7,17 +7,23 @@ module Loxxy
|
|
7
7
|
module BackEnd
|
8
8
|
# Runtime representation of a Lox class.
|
9
9
|
class LoxClass
|
10
|
+
# rubocop: disable Style/AccessorGrouping
|
11
|
+
|
10
12
|
# @return [String] The name of the class
|
11
13
|
attr_reader :name
|
14
|
+
attr_reader :superclass
|
12
15
|
|
13
16
|
# @return [Hash{String => LoxFunction}] the list of methods
|
14
17
|
attr_reader :meths
|
15
18
|
attr_reader :stack
|
16
19
|
|
20
|
+
# rubocop: enable Style/AccessorGrouping
|
21
|
+
|
17
22
|
# Create a class with given name
|
18
23
|
# @param aName [String] The name of the class
|
19
|
-
def initialize(aName, theMethods, anEngine)
|
24
|
+
def initialize(aName, aSuperclass, theMethods, anEngine)
|
20
25
|
@name = aName.dup
|
26
|
+
@superclass = aSuperclass
|
21
27
|
@meths = {}
|
22
28
|
theMethods.each do |func|
|
23
29
|
meths[func.name] = func
|
@@ -30,17 +36,29 @@ module Loxxy
|
|
30
36
|
end
|
31
37
|
|
32
38
|
def arity
|
33
|
-
|
39
|
+
initializer = find_method('init')
|
40
|
+
initializer ? initializer.arity : 0
|
34
41
|
end
|
35
42
|
|
36
|
-
def call(engine,
|
43
|
+
def call(engine, visitor)
|
37
44
|
instance = LoxInstance.new(self, engine)
|
45
|
+
initializer = find_method('init')
|
46
|
+
if initializer
|
47
|
+
constructor = initializer.bind(instance)
|
48
|
+
constructor.call(engine, visitor)
|
49
|
+
end
|
50
|
+
|
38
51
|
engine.stack.push(instance)
|
39
52
|
end
|
40
53
|
|
41
54
|
# @param aName [String] the method name to search for
|
42
55
|
def find_method(aName)
|
43
|
-
meths[aName]
|
56
|
+
found = meths[aName]
|
57
|
+
unless found || superclass.nil?
|
58
|
+
found = superclass.find_method(aName)
|
59
|
+
end
|
60
|
+
|
61
|
+
found
|
44
62
|
end
|
45
63
|
|
46
64
|
# Logical negation.
|
@@ -15,6 +15,7 @@ module Loxxy
|
|
15
15
|
attr_reader :body
|
16
16
|
attr_reader :stack
|
17
17
|
attr_reader :closure
|
18
|
+
attr_accessor :is_initializer
|
18
19
|
|
19
20
|
# Create a function with given name
|
20
21
|
# @param aName [String] The name of the function
|
@@ -24,6 +25,7 @@ module Loxxy
|
|
24
25
|
@body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
|
25
26
|
@stack = anEngine.stack
|
26
27
|
@closure = anEngine.symbol_table.current_env
|
28
|
+
@is_initializer = false
|
27
29
|
anEngine.symbol_table.current_env.embedding = true
|
28
30
|
end
|
29
31
|
|
@@ -48,6 +50,10 @@ module Loxxy
|
|
48
50
|
(body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
|
49
51
|
throw(:return)
|
50
52
|
end
|
53
|
+
if is_initializer
|
54
|
+
enclosing_env = engine.symbol_table.current_env.enclosing
|
55
|
+
engine.stack.push(enclosing_env.defns['this'].value)
|
56
|
+
end
|
51
57
|
|
52
58
|
engine.symbol_table.leave_environment
|
53
59
|
end
|
@@ -47,10 +47,6 @@ module Loxxy
|
|
47
47
|
# Set the value of property with given name
|
48
48
|
# aName [String] name of object property
|
49
49
|
def set(aName, aValue)
|
50
|
-
unless fields.include? aName
|
51
|
-
raise StandardError, "Undefined property '#{aName}'."
|
52
|
-
end
|
53
|
-
|
54
50
|
fields[aName] = aValue
|
55
51
|
end
|
56
52
|
end # class
|
@@ -67,12 +67,24 @@ module Loxxy
|
|
67
67
|
previous_class = current_class
|
68
68
|
@current_class = :class
|
69
69
|
define(aClassStmt.name)
|
70
|
+
if aClassStmt.superclass
|
71
|
+
if aClassStmt.name == aClassStmt.superclass.name
|
72
|
+
raise StandardError, "'A class can't inherit from itself."
|
73
|
+
end
|
74
|
+
|
75
|
+
@current_class = :subclass
|
76
|
+
aClassStmt.superclass.accept(aVisitor)
|
77
|
+
begin_scope
|
78
|
+
define('super')
|
79
|
+
end
|
70
80
|
begin_scope
|
71
81
|
define('this')
|
72
82
|
aClassStmt.body.each do |fun_stmt|
|
73
|
-
|
83
|
+
mth_type = fun_stmt.name == 'init' ? :initializer : :method
|
84
|
+
resolve_function(fun_stmt, mth_type, aVisitor)
|
74
85
|
end
|
75
86
|
end_scope
|
87
|
+
end_scope if aClassStmt.superclass
|
76
88
|
@current_class = previous_class
|
77
89
|
end
|
78
90
|
|
@@ -92,7 +104,7 @@ module Loxxy
|
|
92
104
|
anIfStmt.else_stmt&.accept(aVisitor)
|
93
105
|
end
|
94
106
|
|
95
|
-
def before_return_stmt(
|
107
|
+
def before_return_stmt(returnStmt)
|
96
108
|
if scopes.size < 2
|
97
109
|
msg = "Error at 'return': Can't return from top-level code."
|
98
110
|
raise StandardError, msg
|
@@ -102,6 +114,11 @@ module Loxxy
|
|
102
114
|
msg = "Error at 'return': Can't return from outside a function."
|
103
115
|
raise StandardError, msg
|
104
116
|
end
|
117
|
+
|
118
|
+
if current_function == :initializer
|
119
|
+
msg = "Error at 'return': Can't return a value from an initializer."
|
120
|
+
raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
|
121
|
+
end
|
105
122
|
end
|
106
123
|
|
107
124
|
def after_while_stmt(aWhileStmt, aVisitor)
|
@@ -163,6 +180,25 @@ module Loxxy
|
|
163
180
|
resolve_local(aThisExpr, aVisitor)
|
164
181
|
end
|
165
182
|
|
183
|
+
# rubocop: disable Style/CaseLikeIf
|
184
|
+
# rubocop: disable Style/StringConcatenation
|
185
|
+
def after_super_expr(aSuperExpr, aVisitor)
|
186
|
+
msg_prefix = "Error at 'super': Can't use 'super' "
|
187
|
+
if current_class == :none
|
188
|
+
err_msg = msg_prefix + 'outside of a class.'
|
189
|
+
raise StandardError, err_msg
|
190
|
+
|
191
|
+
elsif current_class == :class
|
192
|
+
err_msg = msg_prefix + 'in a class without superclass.'
|
193
|
+
raise StandardError, err_msg
|
194
|
+
|
195
|
+
end
|
196
|
+
# 'super' behaves closely to a local variable
|
197
|
+
resolve_local(aSuperExpr, aVisitor)
|
198
|
+
end
|
199
|
+
# rubocop: enable Style/StringConcatenation
|
200
|
+
# rubocop: enable Style/CaseLikeIf
|
201
|
+
|
166
202
|
# function declaration creates a new scope for its body & binds its parameters for that scope
|
167
203
|
def before_fun_stmt(aFunStmt, aVisitor)
|
168
204
|
declare(aFunStmt.name)
|