loxxy 0.1.06 → 0.1.11
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/CHANGELOG.md +64 -3
- data/README.md +4 -7
- data/lib/loxxy/ast/all_lox_nodes.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +41 -3
- data/lib/loxxy/ast/ast_visitor.rb +10 -2
- data/lib/loxxy/ast/lox_block_stmt.rb +0 -2
- data/lib/loxxy/ast/lox_class_stmt.rb +27 -0
- data/lib/loxxy/ast/lox_fun_stmt.rb +1 -3
- data/lib/loxxy/ast/lox_grouping_expr.rb +0 -2
- data/lib/loxxy/ast/lox_return_stmt.rb +2 -1
- data/lib/loxxy/ast/lox_seq_decl.rb +0 -2
- data/lib/loxxy/ast/lox_var_stmt.rb +2 -2
- data/lib/loxxy/back_end/engine.rb +45 -10
- data/lib/loxxy/back_end/environment.rb +8 -1
- data/lib/loxxy/back_end/lox_class.rb +41 -0
- data/lib/loxxy/back_end/lox_function.rb +10 -8
- data/lib/loxxy/back_end/resolver.rb +204 -0
- data/lib/loxxy/back_end/symbol_table.rb +19 -9
- data/lib/loxxy/front_end/grammar.rb +8 -6
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +10 -0
- data/spec/interpreter_spec.rb +47 -22
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aee9fb6c5101bc39682ab401ffc0434aea9e6c4115f74a4f641d0df2d68a03ad
|
4
|
+
data.tar.gz: ccc11c428ef206db9129fabbb97526df3477e03c5dce0b1b5a2f758425c8f9cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbb1a28eb85868636b304aa3ec3c2fb71f3f37f5c573740bd7d577ed4e88374ce1fea5446e39429168228e68bedbddfccd554bc6c88ec7996621956e11a42a3c
|
7
|
+
data.tar.gz: 3b4c1d6b0996a351cfd7dae5d8f80683fcbdd1eeecc17c3a7932b0675285700dfecdabecc31f505243ffaf2f1bbc1bd8babb604c154f53dad0019c4ad96b007a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,64 @@
|
|
1
|
+
## [0.1.11] - 2021-04-03
|
2
|
+
- Intermediate version: `Loxxy` does class declarations
|
3
|
+
|
4
|
+
### New
|
5
|
+
- Class `Ast::LoxClassStmt` a syntax node that represents a class declaration
|
6
|
+
- Method `Ast::ASTBuilder#reduce_class_decl` creates a `LoxClassStmt` instance
|
7
|
+
- Method `Ast::ASTBuilder#reduce_class_name`
|
8
|
+
- Method `Ast::ASTBuilder#reduce_reduce_class_body` collect the methods of the class
|
9
|
+
- Method `Ast::ASTBuilder#reduce_method_plus_more` for dealing with methods
|
10
|
+
- Method `Ast::ASTBuilder#reduce_method_plus_end`
|
11
|
+
- Method `Ast::ASTVisitor#visit_class_stmt` for visiting an `Ast::LoxClassStmt` node
|
12
|
+
- Method `Ast::LoxBlockStmt#empty?` returns true if the code block is empty
|
13
|
+
- Method `BackEnd::Engine#after_class_stmt`
|
14
|
+
- Method `BackEnd::Resolver#after_class_stmt`
|
15
|
+
- Method `BackEnd::Resolver#before_class_stmt`
|
16
|
+
- Class `BackEnd::LoxClass` implementation of a Lox class.
|
17
|
+
|
18
|
+
### CHANGED
|
19
|
+
- File `grammar.rb` refactoring of class declaration syntax rules
|
20
|
+
|
21
|
+
## [0.1.10] - 2021-03-31
|
22
|
+
- Flag return statements occurring outside functions as an error
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
- Class `BackEnd::Resolver` Added attribute `current_function` to know whether the visited parse node is located inside a function
|
26
|
+
|
27
|
+
|
28
|
+
## [0.1.09] - 2021-03-28
|
29
|
+
- Fix and test suite for return statements
|
30
|
+
|
31
|
+
### Changed
|
32
|
+
- `Loxxy` reports an error when a return statement occurs in top-level scope
|
33
|
+
|
34
|
+
### Fixed
|
35
|
+
- A return without explicit value genrated an exception in some cases.
|
36
|
+
|
37
|
+
## [0.1.08] - 2021-03-27
|
38
|
+
- `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
|
39
|
+
|
40
|
+
### New
|
41
|
+
- Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
|
42
|
+
|
43
|
+
### Changed
|
44
|
+
- Class `Ast::Visitor` changes in some method signatures
|
45
|
+
- Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
|
46
|
+
- Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
|
47
|
+
|
48
|
+
## [0.1.07] - 2021-03-14
|
49
|
+
- `Loxxy` now supports nested functions and closures
|
50
|
+
|
51
|
+
### Changed
|
52
|
+
- Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
|
53
|
+
- Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
|
54
|
+
- Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
|
55
|
+
- Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
|
56
|
+
- Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
|
57
|
+
|
58
|
+
### Fixed
|
59
|
+
- Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
|
60
|
+
|
61
|
+
|
1
62
|
## [0.1.06] - 2021-03-06
|
2
63
|
- Parameters/arguments checks in function declaration and call
|
3
64
|
|
@@ -69,9 +130,9 @@
|
|
69
130
|
- Method `Ast::ASTVisitor#visit_fun_stmt` for visiting an `Ast::LoxFunStmt` node
|
70
131
|
- Method `Ast::LoxBlockStmt#empty?` returns true if the code block is empty
|
71
132
|
- Method `BackEnd::Engine#after_fun_stmt`
|
72
|
-
- Method `
|
73
|
-
- Method `
|
74
|
-
- Method `
|
133
|
+
- Method `BackEnd::NativeFunction#call`
|
134
|
+
- Method `BackEnd::NativeFunction#to_str`
|
135
|
+
- Method `BackEnd::LoxFunction` implementation of a function object.
|
75
136
|
|
76
137
|
### Changed
|
77
138
|
- Method `BackEnd::Engine#after_call_expr`
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
### What is loxxy?
|
6
6
|
A Ruby implementation of the [Lox programming language](https://craftinginterpreters.com/the-lox-language.html ),
|
7
|
-
a simple language
|
7
|
+
a simple language defined in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/ ).
|
8
8
|
|
9
9
|
### Purpose of this project:
|
10
10
|
- To deliver an open source example of a programming language fully implemented in Ruby
|
@@ -13,12 +13,9 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
|
|
13
13
|
a Lox interpreter written in Lox.
|
14
14
|
|
15
15
|
### Current status
|
16
|
-
The
|
17
|
-
|
18
|
-
|
19
|
-
- Classes and objects.
|
20
|
-
|
21
|
-
These will be implemented soon.
|
16
|
+
The interpreter currently can execute all allowed __Lox__ expressions and statements except
|
17
|
+
object-oriented feaures (classes and objects).
|
18
|
+
The goal is to implement these missing features in Q2 2021.
|
22
19
|
|
23
20
|
|
24
21
|
## What's the fuss about Lox?
|
@@ -163,6 +163,31 @@ module Loxxy
|
|
163
163
|
[theChildren[0]]
|
164
164
|
end
|
165
165
|
|
166
|
+
# rule('classDecl' => 'CLASS classNaming class_body')
|
167
|
+
def reduce_class_decl(_production, _range, _tokens, theChildren)
|
168
|
+
Ast::LoxClassStmt.new(tokens[1].position, theChildren[1], theChildren[2])
|
169
|
+
end
|
170
|
+
|
171
|
+
# rule('classNaming' => 'IDENTIFIER')
|
172
|
+
def reduce_class_name(_production, _range, _tokens, theChildren)
|
173
|
+
theChildren[0].token.lexeme
|
174
|
+
end
|
175
|
+
|
176
|
+
# rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
|
177
|
+
def reduce_class_body(_production, _range, _tokens, theChildren)
|
178
|
+
theChildren[1]
|
179
|
+
end
|
180
|
+
|
181
|
+
# rule('method_plus' => 'method_plus function')
|
182
|
+
def reduce_method_plus_more(_production, _range, _tokens, theChildren)
|
183
|
+
theChildren[0] << theChildren[1]
|
184
|
+
end
|
185
|
+
|
186
|
+
# rule('method_plus' => 'function')
|
187
|
+
def reduce_method_plus_end(_production, _range, _tokens, theChildren)
|
188
|
+
theChildren
|
189
|
+
end
|
190
|
+
|
166
191
|
# rule('funDecl' => 'FUN function')
|
167
192
|
def reduce_fun_decl(_production, _range, _tokens, theChildren)
|
168
193
|
theChildren[1]
|
@@ -260,13 +285,26 @@ module Loxxy
|
|
260
285
|
|
261
286
|
# rule('call' => 'primary refinement_plus').as 'call_expr'
|
262
287
|
def reduce_call_expr(_production, _range, _tokens, theChildren)
|
263
|
-
|
264
|
-
|
288
|
+
members = theChildren.flatten
|
289
|
+
call_expr = nil
|
290
|
+
loop do
|
291
|
+
(callee, call_expr) = members.shift(2)
|
292
|
+
call_expr.callee = callee
|
293
|
+
members.unshift(call_expr)
|
294
|
+
break if members.size == 1
|
295
|
+
end
|
296
|
+
|
297
|
+
call_expr
|
298
|
+
end
|
299
|
+
|
300
|
+
# rule('refinement_plus' => 'refinement_plus refinement')
|
301
|
+
def reduce_refinement_plus_more(_production, _range, _tokens, theChildren)
|
302
|
+
theChildren[0] << theChildren[1]
|
265
303
|
end
|
266
304
|
|
267
305
|
# rule('refinement_plus' => 'refinement').
|
268
306
|
def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
|
269
|
-
theChildren
|
307
|
+
theChildren
|
270
308
|
end
|
271
309
|
|
272
310
|
# rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
|
@@ -67,6 +67,14 @@ module Loxxy
|
|
67
67
|
broadcast(:after_var_stmt, aVarStmt)
|
68
68
|
end
|
69
69
|
|
70
|
+
# Visit event. The visitor is about to visit a class declaration.
|
71
|
+
# @param aXlassStmt [AST::LOXClassStmt] the for statement node to visit
|
72
|
+
def visit_class_stmt(aClassStmt)
|
73
|
+
broadcast(:before_class_stmt, aClassStmt)
|
74
|
+
traverse_subnodes(aClassStmt) # The methods are visited here...
|
75
|
+
broadcast(:after_class_stmt, aClassStmt, self)
|
76
|
+
end
|
77
|
+
|
70
78
|
# Visit event. The visitor is about to visit a for statement.
|
71
79
|
# @param aForStmt [AST::LOXForStmt] the for statement node to visit
|
72
80
|
def visit_for_stmt(aForStmt)
|
@@ -120,7 +128,7 @@ module Loxxy
|
|
120
128
|
def visit_assign_expr(anAssignExpr)
|
121
129
|
broadcast(:before_assign_expr, anAssignExpr)
|
122
130
|
traverse_subnodes(anAssignExpr)
|
123
|
-
broadcast(:after_assign_expr, anAssignExpr)
|
131
|
+
broadcast(:after_assign_expr, anAssignExpr, self)
|
124
132
|
end
|
125
133
|
|
126
134
|
# Visit event. The visitor is about to visit a logical expression.
|
@@ -194,7 +202,7 @@ module Loxxy
|
|
194
202
|
# Visit event. The visitor is about to visit a function statement node.
|
195
203
|
# @param aFunStmt [AST::LoxFunStmt] function declaration to visit
|
196
204
|
def visit_fun_stmt(aFunStmt)
|
197
|
-
broadcast(:before_fun_stmt, aFunStmt)
|
205
|
+
broadcast(:before_fun_stmt, aFunStmt, self)
|
198
206
|
traverse_subnodes(aFunStmt)
|
199
207
|
broadcast(:after_fun_stmt, aFunStmt, self)
|
200
208
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxClassStmt < LoxCompoundExpr
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
11
|
+
# @param condExpr [Loxxy::Ast::LoxNode] iteration condition
|
12
|
+
# @param theBody [Loxxy::Ast::LoxNode]
|
13
|
+
def initialize(aPosition, aName, theMethods)
|
14
|
+
super(aPosition, theMethods)
|
15
|
+
@name = aName.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
19
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
20
|
+
def accept(visitor)
|
21
|
+
visitor.visit_class_stmt(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
alias body subnodes
|
25
|
+
end # class
|
26
|
+
end # module
|
27
|
+
end # module
|
@@ -16,7 +16,7 @@ module Loxxy
|
|
16
16
|
# @param body [Ast::LoxBlockStmt]
|
17
17
|
def initialize(aPosition, aName, paramList, aBody)
|
18
18
|
super(aPosition, [])
|
19
|
-
@name = aName
|
19
|
+
@name = aName.dup
|
20
20
|
@params = paramList
|
21
21
|
@body = aBody
|
22
22
|
end
|
@@ -26,8 +26,6 @@ module Loxxy
|
|
26
26
|
def accept(visitor)
|
27
27
|
visitor.visit_fun_stmt(self)
|
28
28
|
end
|
29
|
-
|
30
|
-
alias operands subnodes
|
31
29
|
end # class
|
32
30
|
# rubocop: enable Style/AccessorGrouping
|
33
31
|
end # module
|
@@ -8,7 +8,8 @@ module Loxxy
|
|
8
8
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
9
9
|
# @param anExpression [Ast::LoxNode] expression to return
|
10
10
|
def initialize(aPosition, anExpression)
|
11
|
-
|
11
|
+
expr = anExpression || Datatype::Nil.instance
|
12
|
+
super(aPosition, [expr])
|
12
13
|
end
|
13
14
|
|
14
15
|
# Part of the 'visitee' role in Visitor design pattern.
|
@@ -13,8 +13,8 @@ module Loxxy
|
|
13
13
|
# @param aName [String] name of the variable
|
14
14
|
# @param aValue [Loxxy::Ast::LoxNode, NilClass] initial value for the variable
|
15
15
|
def initialize(aPosition, aName, aValue)
|
16
|
-
initial_value = aValue
|
17
|
-
super(aPosition, initial_value)
|
16
|
+
initial_value = aValue || Datatype::Nil.instance
|
17
|
+
super(aPosition, [initial_value])
|
18
18
|
@name = aName
|
19
19
|
end
|
20
20
|
|
@@ -3,7 +3,9 @@
|
|
3
3
|
# Load all the classes implementing AST nodes
|
4
4
|
require_relative '../ast/all_lox_nodes'
|
5
5
|
require_relative 'binary_operator'
|
6
|
+
require_relative 'lox_class'
|
6
7
|
require_relative 'lox_function'
|
8
|
+
require_relative 'resolver'
|
7
9
|
require_relative 'symbol_table'
|
8
10
|
require_relative 'unary_operator'
|
9
11
|
|
@@ -11,7 +13,6 @@ module Loxxy
|
|
11
13
|
module BackEnd
|
12
14
|
# An instance of this class executes the statements as when they
|
13
15
|
# occur during the abstract syntax tree walking.
|
14
|
-
# @note WIP: very crude implementation.
|
15
16
|
class Engine
|
16
17
|
# @return [Hash] A set of configuration options
|
17
18
|
attr_reader :config
|
@@ -28,6 +29,9 @@ module Loxxy
|
|
28
29
|
# @return [Hash { Symbol => BinaryOperator}]
|
29
30
|
attr_reader :binary_operators
|
30
31
|
|
32
|
+
# @return [BackEnd::Resolver]
|
33
|
+
attr_reader :resolver
|
34
|
+
|
31
35
|
# @param theOptions [Hash]
|
32
36
|
def initialize(theOptions)
|
33
37
|
@config = theOptions
|
@@ -47,6 +51,10 @@ module Loxxy
|
|
47
51
|
# @param aVisitor [AST::ASTVisitor]
|
48
52
|
# @return [Loxxy::Datatype::BuiltinDatatype]
|
49
53
|
def execute(aVisitor)
|
54
|
+
# Do variable resolution pass first
|
55
|
+
@resolver = BackEnd::Resolver.new
|
56
|
+
resolver.analyze(aVisitor)
|
57
|
+
|
50
58
|
aVisitor.subscribe(self)
|
51
59
|
aVisitor.start
|
52
60
|
aVisitor.unsubscribe(self)
|
@@ -61,11 +69,26 @@ module Loxxy
|
|
61
69
|
# Do nothing, subnodes were already evaluated
|
62
70
|
end
|
63
71
|
|
64
|
-
def
|
65
|
-
|
72
|
+
def after_class_stmt(aClassStmt, _visitor)
|
73
|
+
klass = LoxClass.new(aClassStmt.name, aClassStmt.methods, self)
|
74
|
+
new_var = Variable.new(aClassStmt.name, klass)
|
75
|
+
symbol_table.insert(new_var)
|
76
|
+
end
|
77
|
+
|
78
|
+
def before_var_stmt(aVarStmt)
|
79
|
+
new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
|
66
80
|
symbol_table.insert(new_var)
|
67
81
|
end
|
68
82
|
|
83
|
+
def after_var_stmt(aVarStmt)
|
84
|
+
var_name = aVarStmt.name
|
85
|
+
variable = symbol_table.lookup(var_name)
|
86
|
+
raise StandardError, "Unknown variable #{var_name}" unless variable
|
87
|
+
|
88
|
+
value = stack.pop
|
89
|
+
variable.assign(value)
|
90
|
+
end
|
91
|
+
|
69
92
|
def before_for_stmt(aForStmt)
|
70
93
|
before_block_stmt(aForStmt)
|
71
94
|
end
|
@@ -121,14 +144,13 @@ module Loxxy
|
|
121
144
|
symbol_table.leave_environment
|
122
145
|
end
|
123
146
|
|
124
|
-
def after_assign_expr(anAssignExpr)
|
147
|
+
def after_assign_expr(anAssignExpr, _visitor)
|
125
148
|
var_name = anAssignExpr.name
|
126
|
-
variable =
|
149
|
+
variable = variable_lookup(anAssignExpr)
|
127
150
|
raise StandardError, "Unknown variable #{var_name}" unless variable
|
128
151
|
|
129
|
-
value = stack.
|
152
|
+
value = stack.last # ToS remains since an assignment produces a value
|
130
153
|
variable.assign(value)
|
131
|
-
stack.push value # An expression produces a value
|
132
154
|
end
|
133
155
|
|
134
156
|
def after_logical_expr(aLogicalExpr, visitor)
|
@@ -217,10 +239,10 @@ module Loxxy
|
|
217
239
|
|
218
240
|
def after_variable_expr(aVarExpr, aVisitor)
|
219
241
|
var_name = aVarExpr.name
|
220
|
-
var =
|
242
|
+
var = variable_lookup(aVarExpr)
|
221
243
|
raise StandardError, "Unknown variable #{var_name}" unless var
|
222
244
|
|
223
|
-
var.value.accept(aVisitor) # Evaluate
|
245
|
+
var.value.accept(aVisitor) # Evaluate variable value then push on stack
|
224
246
|
end
|
225
247
|
|
226
248
|
# @param literalExpr [Ast::LoxLiteralExpr]
|
@@ -234,13 +256,26 @@ module Loxxy
|
|
234
256
|
end
|
235
257
|
|
236
258
|
def after_fun_stmt(aFunStmt, _visitor)
|
237
|
-
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body,
|
259
|
+
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
|
238
260
|
new_var = Variable.new(aFunStmt.name, function)
|
239
261
|
symbol_table.insert(new_var)
|
240
262
|
end
|
241
263
|
|
242
264
|
private
|
243
265
|
|
266
|
+
def variable_lookup(aVarNode)
|
267
|
+
env = nil
|
268
|
+
offset = resolver.locals[aVarNode]
|
269
|
+
if offset.nil?
|
270
|
+
env = symbol_table.root
|
271
|
+
else
|
272
|
+
env = symbol_table.current_env
|
273
|
+
offset.times { env = env.enclosing }
|
274
|
+
end
|
275
|
+
|
276
|
+
env.defns[aVarNode.name]
|
277
|
+
end
|
278
|
+
|
244
279
|
NativeFunction = Struct.new(:callable, :interp) do
|
245
280
|
def accept(_visitor)
|
246
281
|
interp.stack.push self
|
@@ -9,10 +9,17 @@ module Loxxy
|
|
9
9
|
# of a relation or a relation definition.
|
10
10
|
# It contains a map of names to the objects they name (e.g. logical var)
|
11
11
|
class Environment
|
12
|
-
# The
|
12
|
+
# The enclosing (parent) environment.
|
13
13
|
# @return [Environment, NilClass]
|
14
14
|
attr_accessor :enclosing
|
15
15
|
|
16
|
+
# The previous environment in the environment chain.
|
17
|
+
# @return [Environment, NilClass]
|
18
|
+
attr_accessor :predecessor
|
19
|
+
|
20
|
+
# @return [Boolean] true if this environment is part of a closure (contains an embedded function)
|
21
|
+
attr_accessor :embedding
|
22
|
+
|
16
23
|
# Mapping from user-defined name to related definition
|
17
24
|
# (say, a variable object)
|
18
25
|
# @return [Hash{String => Variable}] Pairs of the kind
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../datatype/all_datatypes'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# Representation of a Lox class.
|
8
|
+
class LoxClass
|
9
|
+
# @return [String] The name of the class
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# @return [Array<>] the list of methods
|
13
|
+
attr_reader :methods
|
14
|
+
attr_reader :stack
|
15
|
+
|
16
|
+
# Create a class with given name
|
17
|
+
# @param aName [String] The name of the class
|
18
|
+
def initialize(aName, theMethods, anEngine)
|
19
|
+
@name = aName.dup
|
20
|
+
@methods = theMethods
|
21
|
+
@stack = anEngine.stack
|
22
|
+
end
|
23
|
+
|
24
|
+
def accept(_visitor)
|
25
|
+
stack.push self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Logical negation.
|
29
|
+
# As a function is a truthy thing, its negation is thus false.
|
30
|
+
# @return [Datatype::False]
|
31
|
+
def !
|
32
|
+
Datatype::False.instance
|
33
|
+
end
|
34
|
+
|
35
|
+
# Text representation of a Lox function
|
36
|
+
def to_str
|
37
|
+
name
|
38
|
+
end
|
39
|
+
end # class
|
40
|
+
end # module
|
41
|
+
end # module
|
@@ -6,24 +6,25 @@ module Loxxy
|
|
6
6
|
module BackEnd
|
7
7
|
# rubocop: disable Style/AccessorGrouping
|
8
8
|
# Representation of a Lox function.
|
9
|
-
# It is a named slot that can be associated with a value at the time.
|
10
9
|
class LoxFunction
|
11
|
-
# @return [String]
|
10
|
+
# @return [String] The name of the function (if any)
|
12
11
|
attr_reader :name
|
13
12
|
|
14
13
|
# @return [Array<>] the parameters
|
15
14
|
attr_reader :parameters
|
16
15
|
attr_reader :body
|
17
16
|
attr_reader :stack
|
17
|
+
attr_reader :closure
|
18
18
|
|
19
|
-
# Create a
|
20
|
-
# @param aName [String] The name of the
|
21
|
-
|
22
|
-
def initialize(aName, parameterList, aBody, aStack)
|
19
|
+
# Create a function with given name
|
20
|
+
# @param aName [String] The name of the function
|
21
|
+
def initialize(aName, parameterList, aBody, anEngine)
|
23
22
|
@name = aName.dup
|
24
23
|
@parameters = parameterList
|
25
24
|
@body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
|
26
|
-
@stack =
|
25
|
+
@stack = anEngine.stack
|
26
|
+
@closure = anEngine.symbol_table.current_env
|
27
|
+
anEngine.symbol_table.current_env.embedding = true
|
27
28
|
end
|
28
29
|
|
29
30
|
def arity
|
@@ -35,7 +36,8 @@ module Loxxy
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def call(engine, aVisitor)
|
38
|
-
new_env = Environment.new(engine.symbol_table.current_env)
|
39
|
+
# new_env = Environment.new(engine.symbol_table.current_env)
|
40
|
+
new_env = Environment.new(closure)
|
39
41
|
engine.symbol_table.enter_environment(new_env)
|
40
42
|
|
41
43
|
parameters&.each do |param_name|
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Load all the classes implementing AST nodes
|
4
|
+
require_relative '../ast/all_lox_nodes'
|
5
|
+
require_relative 'binary_operator'
|
6
|
+
require_relative 'lox_function'
|
7
|
+
require_relative 'symbol_table'
|
8
|
+
require_relative 'unary_operator'
|
9
|
+
|
10
|
+
module Loxxy
|
11
|
+
module BackEnd
|
12
|
+
# A class aimed to perform variable resolution when it visits the parse tree.
|
13
|
+
# Resolving means retrieve the declaration of a variable/function everywhere it
|
14
|
+
# is referenced.
|
15
|
+
class Resolver
|
16
|
+
# A stack of Hashes of the form String => Boolean
|
17
|
+
# @return [Array<Hash{String => Boolean}>]
|
18
|
+
attr_reader :scopes
|
19
|
+
|
20
|
+
# A map from a LoxNode involving a variable and the number of enclosing scopes
|
21
|
+
# where it is declared.
|
22
|
+
# @return [Hash {LoxNode => Integer}]
|
23
|
+
attr_reader :locals
|
24
|
+
|
25
|
+
# An indicator that tells we're in the middle of a function declaration
|
26
|
+
# @return [Symbol] must be one of: :none, :function
|
27
|
+
attr_reader :current_function
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@scopes = []
|
31
|
+
@locals = {}
|
32
|
+
@current_function = :none
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given an abstract syntax parse tree visitor, launch the visit
|
36
|
+
# and execute the visit events in the output stream.
|
37
|
+
# @param aVisitor [AST::ASTVisitor]
|
38
|
+
# @return [Loxxy::Datatype::BuiltinDatatype]
|
39
|
+
def analyze(aVisitor)
|
40
|
+
begin_scope
|
41
|
+
aVisitor.subscribe(self)
|
42
|
+
aVisitor.start
|
43
|
+
aVisitor.unsubscribe(self)
|
44
|
+
end_scope
|
45
|
+
end
|
46
|
+
|
47
|
+
# block statement introduces a new scope
|
48
|
+
def before_block_stmt(_aBlockStmt)
|
49
|
+
begin_scope
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_block_stmt(_aBlockStmt)
|
53
|
+
end_scope
|
54
|
+
end
|
55
|
+
|
56
|
+
# A class declaration adds a new variable to current scope
|
57
|
+
def before_class_stmt(aClassStmt)
|
58
|
+
declare(aClassStmt.name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def after_class_stmt(aClassStmt, _visitor)
|
62
|
+
define(aClassStmt.name)
|
63
|
+
end
|
64
|
+
|
65
|
+
def before_for_stmt(aForStmt)
|
66
|
+
before_block_stmt(aForStmt)
|
67
|
+
end
|
68
|
+
|
69
|
+
def after_for_stmt(aForStmt, aVisitor)
|
70
|
+
aForStmt.test_expr.accept(aVisitor)
|
71
|
+
aForStmt.body_stmt.accept(aVisitor)
|
72
|
+
aForStmt.update_expr&.accept(aVisitor)
|
73
|
+
after_block_stmt(aForStmt)
|
74
|
+
end
|
75
|
+
|
76
|
+
def after_if_stmt(anIfStmt, aVisitor)
|
77
|
+
anIfStmt.then_stmt.accept(aVisitor)
|
78
|
+
anIfStmt.else_stmt&.accept(aVisitor)
|
79
|
+
end
|
80
|
+
|
81
|
+
def before_return_stmt(_returnStmt)
|
82
|
+
if scopes.size < 2
|
83
|
+
msg = "Error at 'return': Can't return from top-level code."
|
84
|
+
raise StandardError, msg
|
85
|
+
end
|
86
|
+
|
87
|
+
if current_function == :none
|
88
|
+
msg = "Error at 'return': Can't return from outside a function."
|
89
|
+
raise StandardError, msg
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def after_while_stmt(aWhileStmt, aVisitor)
|
94
|
+
aWhileStmt.body.accept(aVisitor)
|
95
|
+
aWhileStmt.condition.accept(aVisitor)
|
96
|
+
end
|
97
|
+
|
98
|
+
# A variable declaration adds a new variable to current scope
|
99
|
+
def before_var_stmt(aVarStmt)
|
100
|
+
declare(aVarStmt.name)
|
101
|
+
end
|
102
|
+
|
103
|
+
def after_var_stmt(aVarStmt)
|
104
|
+
define(aVarStmt.name)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Assignment expressions require their variables resolved
|
108
|
+
def after_assign_expr(anAssignExpr, aVisitor)
|
109
|
+
resolve_local(anAssignExpr, aVisitor)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Variable expressions require their variables resolved
|
113
|
+
def before_variable_expr(aVarExpr)
|
114
|
+
var_name = aVarExpr.name
|
115
|
+
if !scopes.empty? && (scopes.last[var_name] == false)
|
116
|
+
raise StandardError, "Can't read variable #{var_name} in its own initializer"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def after_variable_expr(aVarExpr, aVisitor)
|
121
|
+
resolve_local(aVarExpr, aVisitor)
|
122
|
+
end
|
123
|
+
|
124
|
+
def after_call_expr(aCallExpr, aVisitor)
|
125
|
+
# Evaluate callee part
|
126
|
+
aCallExpr.callee.accept(aVisitor)
|
127
|
+
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
128
|
+
end
|
129
|
+
|
130
|
+
# function declaration creates a new scope for its body & binds its parameters for that scope
|
131
|
+
def before_fun_stmt(aFunStmt, aVisitor)
|
132
|
+
declare(aFunStmt.name)
|
133
|
+
define(aFunStmt.name)
|
134
|
+
resolve_function(aFunStmt, :function, aVisitor)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def begin_scope
|
140
|
+
scopes.push({})
|
141
|
+
end
|
142
|
+
|
143
|
+
def end_scope
|
144
|
+
scopes.pop
|
145
|
+
end
|
146
|
+
|
147
|
+
def declare(aVarName)
|
148
|
+
return if scopes.empty?
|
149
|
+
|
150
|
+
curr_scope = scopes.last
|
151
|
+
if curr_scope.include?(aVarName)
|
152
|
+
msg = "Error at '#{aVarName}': Already variable with this name in this scope."
|
153
|
+
raise StandardError, msg
|
154
|
+
end
|
155
|
+
|
156
|
+
# The initializer is not yet processed.
|
157
|
+
# Mark the variable as 'not yet ready' = exists but may not be referenced yet
|
158
|
+
curr_scope[aVarName] = false
|
159
|
+
end
|
160
|
+
|
161
|
+
def define(aVarName)
|
162
|
+
return if scopes.empty?
|
163
|
+
|
164
|
+
curr_scope = scopes.last
|
165
|
+
|
166
|
+
# The initializer (if any) was processed.
|
167
|
+
# Mark the variable as alive (= can be referenced in an expression)
|
168
|
+
curr_scope[aVarName] = true
|
169
|
+
end
|
170
|
+
|
171
|
+
def resolve_local(aVarExpr, _visitor)
|
172
|
+
max_i = i = scopes.size - 1
|
173
|
+
scopes.reverse_each do |scp|
|
174
|
+
if scp.include?(aVarExpr.name)
|
175
|
+
# Keep track of the difference of nesting levels between current scope
|
176
|
+
# and the scope where the variable is declared
|
177
|
+
@locals[aVarExpr] = max_i - i
|
178
|
+
break
|
179
|
+
end
|
180
|
+
i -= 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def resolve_function(aFunStmt, funVisitState, aVisitor)
|
185
|
+
enclosing_function = current_function
|
186
|
+
@current_function = funVisitState
|
187
|
+
begin_scope
|
188
|
+
|
189
|
+
aFunStmt.params&.each do |param_name|
|
190
|
+
declare(param_name)
|
191
|
+
define(param_name)
|
192
|
+
end
|
193
|
+
|
194
|
+
body = aFunStmt.body
|
195
|
+
unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
|
196
|
+
body.subnodes.first&.accept(aVisitor)
|
197
|
+
end
|
198
|
+
|
199
|
+
end_scope
|
200
|
+
@current_function = enclosing_function
|
201
|
+
end
|
202
|
+
end # class
|
203
|
+
end # mmodule
|
204
|
+
end # module
|
@@ -44,23 +44,33 @@ module Loxxy
|
|
44
44
|
# to be a child of current environment and to be itself the new current environment.
|
45
45
|
# @param anEnv [BackEnd::Environment] the Environment that
|
46
46
|
def enter_environment(anEnv)
|
47
|
-
anEnv.enclosing
|
47
|
+
if anEnv.enclosing && (anEnv.enclosing != current_env)
|
48
|
+
anEnv.predecessor = current_env
|
49
|
+
else
|
50
|
+
anEnv.enclosing = current_env
|
51
|
+
end
|
48
52
|
@current_env = anEnv
|
49
53
|
end
|
50
54
|
|
51
55
|
def leave_environment
|
52
|
-
current_env.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
unless current_env.embedding
|
57
|
+
current_env.defns.each_pair do |nm, _item|
|
58
|
+
environments = name2envs[nm]
|
59
|
+
if environments.size == 1
|
60
|
+
name2envs.delete(nm)
|
61
|
+
else
|
62
|
+
environments.pop
|
63
|
+
name2envs[nm] = environments
|
64
|
+
end
|
59
65
|
end
|
60
66
|
end
|
61
67
|
raise StandardError, 'Cannot remove root environment.' if current_env == root
|
62
68
|
|
63
|
-
|
69
|
+
if current_env.predecessor
|
70
|
+
@current_env = current_env.predecessor
|
71
|
+
else
|
72
|
+
@current_env = current_env.enclosing
|
73
|
+
end
|
64
74
|
end
|
65
75
|
|
66
76
|
# Add an entry with given name to current environment.
|
@@ -37,12 +37,14 @@ module Loxxy
|
|
37
37
|
rule('declaration' => 'varDecl')
|
38
38
|
rule('declaration' => 'statement')
|
39
39
|
|
40
|
-
rule('classDecl' => 'CLASS classNaming class_body')
|
40
|
+
rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
|
41
41
|
rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
42
|
-
rule('classNaming' => 'IDENTIFIER')
|
43
|
-
rule('class_body' => 'LEFT_BRACE
|
44
|
-
rule('
|
45
|
-
rule('
|
42
|
+
rule('classNaming' => 'IDENTIFIER').as 'class_name'
|
43
|
+
rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
|
44
|
+
rule('methods_opt' => 'method_plus')
|
45
|
+
rule('methods_opt' => [])
|
46
|
+
rule('method_plus' => 'method_plus function').as 'method_plus_more'
|
47
|
+
rule('method_plus' => 'function').as 'method_plus_end'
|
46
48
|
|
47
49
|
rule('funDecl' => 'FUN function').as 'fun_decl'
|
48
50
|
|
@@ -127,7 +129,7 @@ module Loxxy
|
|
127
129
|
rule('unaryOp' => 'MINUS')
|
128
130
|
rule('call' => 'primary')
|
129
131
|
rule('call' => 'primary refinement_plus').as 'call_expr'
|
130
|
-
rule('refinement_plus' => 'refinement_plus refinement')
|
132
|
+
rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
|
131
133
|
rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
|
132
134
|
rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
|
133
135
|
rule('refinement' => 'DOT IDENTIFIER')
|
data/lib/loxxy/version.rb
CHANGED
@@ -34,8 +34,18 @@ module Loxxy
|
|
34
34
|
let(:var_decl) { Ast::LoxVarStmt.new(sample_pos, 'greeting', greeting) }
|
35
35
|
let(:lit_expr) { Ast::LoxLiteralExpr.new(sample_pos, greeting) }
|
36
36
|
|
37
|
+
it "should react to 'before_var_stmt' event" do
|
38
|
+
expect { subject.before_var_stmt(var_decl) }.not_to raise_error
|
39
|
+
current_env = subject.symbol_table.current_env
|
40
|
+
expect(current_env.defns['greeting']).to be_kind_of(Variable)
|
41
|
+
end
|
37
42
|
|
38
43
|
it "should react to 'after_var_stmt' event" do
|
44
|
+
# Precondition: `before_var_stmt` is called...
|
45
|
+
expect { subject.before_var_stmt(var_decl) }.not_to raise_error
|
46
|
+
# Precondition: value to assign is on top of stack
|
47
|
+
subject.stack.push(greeting)
|
48
|
+
|
39
49
|
expect { subject.after_var_stmt(var_decl) }.not_to raise_error
|
40
50
|
current_env = subject.symbol_table.current_env
|
41
51
|
expect(current_env.defns['greeting']).to be_kind_of(Variable)
|
data/spec/interpreter_spec.rb
CHANGED
@@ -53,15 +53,6 @@ module Loxxy
|
|
53
53
|
end # context
|
54
54
|
|
55
55
|
context 'Evaluating Lox code:' do
|
56
|
-
let(:hello_world) do
|
57
|
-
lox = <<-LOX_END
|
58
|
-
var greeting = "Hello"; // Declaring a variable
|
59
|
-
print greeting + ", " + "world!"; // ... Playing with concatenation
|
60
|
-
LOX_END
|
61
|
-
|
62
|
-
lox
|
63
|
-
end
|
64
|
-
|
65
56
|
it 'should evaluate core data types' do
|
66
57
|
result = subject.evaluate('true; // Not false')
|
67
58
|
expect(result).to be_kind_of(Loxxy::Datatype::True)
|
@@ -269,9 +260,9 @@ LOX_END
|
|
269
260
|
|
270
261
|
it 'should accept variable mention' do
|
271
262
|
program = <<-LOX_END
|
272
|
-
|
273
|
-
|
274
|
-
LOX_END
|
263
|
+
var foo = "bar";
|
264
|
+
print foo; // => bar
|
265
|
+
LOX_END
|
275
266
|
expect { subject.evaluate(program) }.not_to raise_error
|
276
267
|
expect(sample_cfg[:ostream].string).to eq('bar')
|
277
268
|
end
|
@@ -438,18 +429,52 @@ LOX_END
|
|
438
429
|
expect(result).to eq(3)
|
439
430
|
end
|
440
431
|
|
441
|
-
|
442
|
-
|
443
|
-
|
432
|
+
# rubocop: disable Style/StringConcatenation
|
433
|
+
it 'should support local functions and closures' do
|
434
|
+
program = <<-LOX_END
|
435
|
+
fun makeCounter() {
|
436
|
+
var i = 0;
|
437
|
+
fun count() {
|
438
|
+
i = i + 1;
|
439
|
+
print i;
|
440
|
+
}
|
441
|
+
|
442
|
+
return count;
|
443
|
+
}
|
444
|
+
|
445
|
+
var counter = makeCounter();
|
446
|
+
counter(); // "1".
|
447
|
+
counter(); // "2".
|
448
|
+
LOX_END
|
449
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
450
|
+
expect(sample_cfg[:ostream].string).to eq('1' + '2')
|
451
|
+
end
|
452
|
+
# rubocop: enable Style/StringConcatenation
|
453
|
+
|
454
|
+
it 'should support class declaration' do
|
455
|
+
program = <<-LOX_END
|
456
|
+
class Duck {
|
457
|
+
noise() {
|
458
|
+
quack();
|
459
|
+
}
|
460
|
+
|
461
|
+
quack() {
|
462
|
+
print "quack";
|
463
|
+
}
|
464
|
+
}
|
465
|
+
print Duck;
|
466
|
+
LOX_END
|
467
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
468
|
+
expect(sample_cfg[:ostream].string).to eq('Duck')
|
444
469
|
end
|
445
|
-
end # context
|
446
470
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
expect { subject.evaluate(
|
471
|
+
it 'should print the hello world message' do
|
472
|
+
program = <<-LOX_END
|
473
|
+
var greeting = "Hello"; // Declaring a variable
|
474
|
+
print greeting + ", " + "world!"; // ... Playing with concatenation
|
475
|
+
LOX_END
|
476
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
477
|
+
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|
453
478
|
end
|
454
479
|
end # context
|
455
480
|
end # describe
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loxxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitri Geshef
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03
|
11
|
+
date: 2021-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- lib/loxxy/ast/lox_binary_expr.rb
|
94
94
|
- lib/loxxy/ast/lox_block_stmt.rb
|
95
95
|
- lib/loxxy/ast/lox_call_expr.rb
|
96
|
+
- lib/loxxy/ast/lox_class_stmt.rb
|
96
97
|
- lib/loxxy/ast/lox_compound_expr.rb
|
97
98
|
- lib/loxxy/ast/lox_for_stmt.rb
|
98
99
|
- lib/loxxy/ast/lox_fun_stmt.rb
|
@@ -113,7 +114,9 @@ files:
|
|
113
114
|
- lib/loxxy/back_end/engine.rb
|
114
115
|
- lib/loxxy/back_end/entry.rb
|
115
116
|
- lib/loxxy/back_end/environment.rb
|
117
|
+
- lib/loxxy/back_end/lox_class.rb
|
116
118
|
- lib/loxxy/back_end/lox_function.rb
|
119
|
+
- lib/loxxy/back_end/resolver.rb
|
117
120
|
- lib/loxxy/back_end/symbol_table.rb
|
118
121
|
- lib/loxxy/back_end/unary_operator.rb
|
119
122
|
- lib/loxxy/back_end/variable.rb
|