loxxy 0.1.15 → 0.2.02
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|