loxxy 0.1.03 → 0.1.08
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 +58 -3
- data/README.md +4 -6
- data/lib/loxxy/ast/all_lox_nodes.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +26 -4
- data/lib/loxxy/ast/ast_visitor.rb +10 -2
- data/lib/loxxy/ast/lox_block_stmt.rb +0 -2
- data/lib/loxxy/ast/lox_fun_stmt.rb +0 -2
- data/lib/loxxy/ast/lox_grouping_expr.rb +0 -2
- data/lib/loxxy/ast/lox_return_stmt.rb +21 -0
- 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 +56 -23
- data/lib/loxxy/back_end/environment.rb +8 -1
- data/lib/loxxy/back_end/lox_function.rb +71 -0
- data/lib/loxxy/back_end/resolver.rb +171 -0
- data/lib/loxxy/back_end/symbol_table.rb +19 -9
- data/lib/loxxy/error.rb +3 -0
- data/lib/loxxy/front_end/grammar.rb +2 -2
- data/lib/loxxy/front_end/scanner.rb +2 -0
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +10 -0
- data/spec/interpreter_spec.rb +44 -22
- metadata +5 -3
- data/lib/loxxy/back_end/function.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1724fce16b30fe2328ba31a725e92cc03f43ac7dee60fd54c68642c5a5639471
|
4
|
+
data.tar.gz: 0f045add2ebf55375ee63e94f3a7316984588d0133200f3f4ce45de3ae118157
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e4a67161f2d6f6a89b237bdda4e1d096df1183eeb3a9b8ff4f1506d8b053bae265de73857faf82a225b77fad5d32faddfe07873d74e0e71a2839f113659c61b
|
7
|
+
data.tar.gz: 29d09adcb56231c4f7b7c911c16b978e4ec3e0608f754e8b80b971293b99ea96fcf2bb6cd55dec62a8acac00bde0fb9b93e86cc37ee11024faea8e8eb0a70d3f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,60 @@
|
|
1
|
+
## [0.1.08] - 2021-03-27
|
2
|
+
- `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
|
3
|
+
|
4
|
+
### New
|
5
|
+
- Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
- Class `Ast::Visitor` changes in some method signatures
|
9
|
+
- Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
|
10
|
+
- Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
|
11
|
+
|
12
|
+
## [0.1.07] - 2021-03-14
|
13
|
+
- `Loxxy` now supports nested functions and closures
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
|
17
|
+
- Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
|
18
|
+
- Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
|
19
|
+
- Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
|
20
|
+
- Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
|
24
|
+
|
25
|
+
|
26
|
+
## [0.1.06] - 2021-03-06
|
27
|
+
- Parameters/arguments checks in function declaration and call
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
- Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
|
31
|
+
- Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
|
32
|
+
|
33
|
+
- Class `BackEnd::Function` renamed to `LoxFunction`
|
34
|
+
|
35
|
+
## [0.1.05] - 2021-03-05
|
36
|
+
- Test for Fibbonacci recursive function is now passing.
|
37
|
+
|
38
|
+
### Fixed
|
39
|
+
- Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
|
40
|
+
|
41
|
+
## [0.1.04] - 2021-02-28
|
42
|
+
|
43
|
+
### Added
|
44
|
+
- Class `Ast::LoxReturnStmt` a node that represents a return statement
|
45
|
+
- Method `Ast::ASTBuilder#reduce_return_stmt`
|
46
|
+
- Method `Ast::ASTVisitor#visit_return_stmt` for visiting an `Ast::LoxReturnStmt` node
|
47
|
+
- Method `BackEnd::Engine#after_return_stmt` to handle return statement
|
48
|
+
- Method `BackEnd::Function#!` implementing the logical negation of a function (as value).
|
49
|
+
- Test suite for logical operators (in project repository)
|
50
|
+
- Test suite for block code
|
51
|
+
- Test suite for call and function declaration (initial)
|
52
|
+
|
53
|
+
### Changed
|
54
|
+
- Method `BackEnd::Engine#after_call_expr` now generate a `catch` and `throw` events
|
55
|
+
|
1
56
|
## [0.1.03] - 2021-02-26
|
2
|
-
- Runtime argument
|
57
|
+
- Runtime argument checking for arithmetic and comparison operators
|
3
58
|
|
4
59
|
### Added
|
5
60
|
- Test suite for arithmetic and comparison operators (in project repository)
|
@@ -221,11 +276,11 @@
|
|
221
276
|
- The interpreter can evaluate substraction between two numbers.
|
222
277
|
|
223
278
|
### Added
|
224
|
-
- Method `Datatype::Number#-`
|
279
|
+
- Method `Datatype::Number#-` implementing the subtraction operation
|
225
280
|
|
226
281
|
### Changed
|
227
282
|
- File `README.md` minor editorial changes.
|
228
|
-
- File `lx_string_spec.rb` Added test for string
|
283
|
+
- File `lx_string_spec.rb` Added test for string concatenation
|
229
284
|
- File `number_spec.rb` Added tests for addition and subtraction operations
|
230
285
|
- File `interpreter_spec.rb` Added tests for subtraction operation
|
231
286
|
|
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,10 @@ 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
|
-
- Closures,
|
19
|
-
- Classes and objects.
|
16
|
+
The interpreter currently it can execute all allowed __Lox__ expressions and statements except
|
17
|
+
object-oriented feaures (classes and objects).
|
20
18
|
|
21
|
-
|
19
|
+
Our intent is implement to these missing features in Q2 2021.
|
22
20
|
|
23
21
|
|
24
22
|
## What's the fuss about Lox?
|
@@ -12,6 +12,7 @@ require_relative 'lox_logical_expr'
|
|
12
12
|
require_relative 'lox_assign_expr'
|
13
13
|
require_relative 'lox_block_stmt'
|
14
14
|
require_relative 'lox_while_stmt'
|
15
|
+
require_relative 'lox_return_stmt'
|
15
16
|
require_relative 'lox_print_stmt'
|
16
17
|
require_relative 'lox_if_stmt'
|
17
18
|
require_relative 'lox_for_stmt'
|
@@ -221,6 +221,11 @@ module Loxxy
|
|
221
221
|
Ast::LoxPrintStmt.new(tokens[1].position, theChildren[1])
|
222
222
|
end
|
223
223
|
|
224
|
+
# rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
|
225
|
+
def reduce_return_stmt(_production, _range, tokens, theChildren)
|
226
|
+
Ast::LoxReturnStmt.new(tokens[1].position, theChildren[1])
|
227
|
+
end
|
228
|
+
|
224
229
|
# rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as ''
|
225
230
|
def reduce_while_stmt(_production, _range, tokens, theChildren)
|
226
231
|
Ast::LoxWhileStmt.new(tokens[1].position, theChildren[2], theChildren[4])
|
@@ -255,20 +260,33 @@ module Loxxy
|
|
255
260
|
|
256
261
|
# rule('call' => 'primary refinement_plus').as 'call_expr'
|
257
262
|
def reduce_call_expr(_production, _range, _tokens, theChildren)
|
258
|
-
|
259
|
-
|
263
|
+
members = theChildren.flatten
|
264
|
+
call_expr = nil
|
265
|
+
loop do
|
266
|
+
(callee, call_expr) = members.shift(2)
|
267
|
+
call_expr.callee = callee
|
268
|
+
members.unshift(call_expr)
|
269
|
+
break if members.size == 1
|
270
|
+
end
|
271
|
+
|
272
|
+
call_expr
|
273
|
+
end
|
274
|
+
|
275
|
+
# rule('refinement_plus' => 'refinement_plus refinement')
|
276
|
+
def reduce_refinement_plus_more(_production, _range, _tokens, theChildren)
|
277
|
+
theChildren[0] << theChildren[1]
|
260
278
|
end
|
261
279
|
|
262
280
|
# rule('refinement_plus' => 'refinement').
|
263
281
|
def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
|
264
|
-
theChildren
|
282
|
+
theChildren
|
265
283
|
end
|
266
284
|
|
267
285
|
# rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
|
268
286
|
def reduce_call_arglist(_production, _range, tokens, theChildren)
|
269
287
|
args = theChildren[1] || []
|
270
288
|
if args.size > 255
|
271
|
-
raise
|
289
|
+
raise Loxxy::RuntimeError, "Can't have more than 255 arguments."
|
272
290
|
end
|
273
291
|
|
274
292
|
LoxCallExpr.new(tokens[0].position, args)
|
@@ -298,6 +316,10 @@ module Loxxy
|
|
298
316
|
def reduce_function(_production, _range, _tokens, theChildren)
|
299
317
|
first_child = theChildren.first
|
300
318
|
pos = first_child.token.position
|
319
|
+
if theChildren[2] && theChildren[2].size > 255
|
320
|
+
msg = "Can't have more than 255 parameters."
|
321
|
+
raise Loxxy::SyntaxError, msg
|
322
|
+
end
|
301
323
|
LoxFunStmt.new(pos, first_child.token.lexeme, theChildren[2], theChildren[4])
|
302
324
|
end
|
303
325
|
|
@@ -91,6 +91,14 @@ module Loxxy
|
|
91
91
|
broadcast(:after_print_stmt, aPrintStmt)
|
92
92
|
end
|
93
93
|
|
94
|
+
# Visit event. The visitor is about to visit a return statement.
|
95
|
+
# @param aReturnStmt [AST::LOXReturnStmt] the return statement node to visit
|
96
|
+
def visit_return_stmt(aReturnStmt)
|
97
|
+
broadcast(:before_return_stmt, aReturnStmt)
|
98
|
+
traverse_subnodes(aReturnStmt)
|
99
|
+
broadcast(:after_return_stmt, aReturnStmt, self)
|
100
|
+
end
|
101
|
+
|
94
102
|
# Visit event. The visitor is about to visit a while statement node.
|
95
103
|
# @param aWhileStmt [AST::LOXWhileStmt] the while statement node to visit
|
96
104
|
def visit_while_stmt(aWhileStmt)
|
@@ -112,7 +120,7 @@ module Loxxy
|
|
112
120
|
def visit_assign_expr(anAssignExpr)
|
113
121
|
broadcast(:before_assign_expr, anAssignExpr)
|
114
122
|
traverse_subnodes(anAssignExpr)
|
115
|
-
broadcast(:after_assign_expr, anAssignExpr)
|
123
|
+
broadcast(:after_assign_expr, anAssignExpr, self)
|
116
124
|
end
|
117
125
|
|
118
126
|
# Visit event. The visitor is about to visit a logical expression.
|
@@ -186,7 +194,7 @@ module Loxxy
|
|
186
194
|
# Visit event. The visitor is about to visit a function statement node.
|
187
195
|
# @param aFunStmt [AST::LoxFunStmt] function declaration to visit
|
188
196
|
def visit_fun_stmt(aFunStmt)
|
189
|
-
broadcast(:before_fun_stmt, aFunStmt)
|
197
|
+
broadcast(:before_fun_stmt, aFunStmt, self)
|
190
198
|
traverse_subnodes(aFunStmt)
|
191
199
|
broadcast(:after_fun_stmt, aFunStmt, self)
|
192
200
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxReturnStmt < LoxCompoundExpr
|
8
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
9
|
+
# @param anExpression [Ast::LoxNode] expression to return
|
10
|
+
def initialize(aPosition, anExpression)
|
11
|
+
super(aPosition, [anExpression])
|
12
|
+
end
|
13
|
+
|
14
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
15
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
16
|
+
def accept(visitor)
|
17
|
+
visitor.visit_return_stmt(self)
|
18
|
+
end
|
19
|
+
end # class
|
20
|
+
end # module
|
21
|
+
end # module
|
@@ -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,8 @@
|
|
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 '
|
6
|
+
require_relative 'lox_function'
|
7
|
+
require_relative 'resolver'
|
7
8
|
require_relative 'symbol_table'
|
8
9
|
require_relative 'unary_operator'
|
9
10
|
|
@@ -11,7 +12,6 @@ module Loxxy
|
|
11
12
|
module BackEnd
|
12
13
|
# An instance of this class executes the statements as when they
|
13
14
|
# occur during the abstract syntax tree walking.
|
14
|
-
# @note WIP: very crude implementation.
|
15
15
|
class Engine
|
16
16
|
# @return [Hash] A set of configuration options
|
17
17
|
attr_reader :config
|
@@ -19,7 +19,7 @@ module Loxxy
|
|
19
19
|
# @return [BackEnd::SymbolTable]
|
20
20
|
attr_reader :symbol_table
|
21
21
|
|
22
|
-
# @return [Array<Datatype::
|
22
|
+
# @return [Array<Datatype::BuiltinDatatype>] Data stack for the expression results
|
23
23
|
attr_reader :stack
|
24
24
|
|
25
25
|
# @return [Hash { Symbol => UnaryOperator}]
|
@@ -28,6 +28,9 @@ module Loxxy
|
|
28
28
|
# @return [Hash { Symbol => BinaryOperator}]
|
29
29
|
attr_reader :binary_operators
|
30
30
|
|
31
|
+
# @return [BackEnd::Resolver]
|
32
|
+
attr_reader :resolver
|
33
|
+
|
31
34
|
# @param theOptions [Hash]
|
32
35
|
def initialize(theOptions)
|
33
36
|
@config = theOptions
|
@@ -47,6 +50,10 @@ module Loxxy
|
|
47
50
|
# @param aVisitor [AST::ASTVisitor]
|
48
51
|
# @return [Loxxy::Datatype::BuiltinDatatype]
|
49
52
|
def execute(aVisitor)
|
53
|
+
# Do variable resolution pass first
|
54
|
+
@resolver = BackEnd::Resolver.new
|
55
|
+
resolver.analyze(aVisitor)
|
56
|
+
|
50
57
|
aVisitor.subscribe(self)
|
51
58
|
aVisitor.start
|
52
59
|
aVisitor.unsubscribe(self)
|
@@ -61,11 +68,20 @@ module Loxxy
|
|
61
68
|
# Do nothing, subnodes were already evaluated
|
62
69
|
end
|
63
70
|
|
64
|
-
def
|
65
|
-
new_var = Variable.new(aVarStmt.name,
|
71
|
+
def before_var_stmt(aVarStmt)
|
72
|
+
new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
|
66
73
|
symbol_table.insert(new_var)
|
67
74
|
end
|
68
75
|
|
76
|
+
def after_var_stmt(aVarStmt)
|
77
|
+
var_name = aVarStmt.name
|
78
|
+
variable = symbol_table.lookup(var_name)
|
79
|
+
raise StandardError, "Unknown variable #{var_name}" unless variable
|
80
|
+
|
81
|
+
value = stack.pop
|
82
|
+
variable.assign(value)
|
83
|
+
end
|
84
|
+
|
69
85
|
def before_for_stmt(aForStmt)
|
70
86
|
before_block_stmt(aForStmt)
|
71
87
|
end
|
@@ -98,6 +114,10 @@ module Loxxy
|
|
98
114
|
@ostream.print tos ? tos.to_str : 'nil'
|
99
115
|
end
|
100
116
|
|
117
|
+
def after_return_stmt(_returnStmt, _aVisitor)
|
118
|
+
throw(:return)
|
119
|
+
end
|
120
|
+
|
101
121
|
def after_while_stmt(aWhileStmt, aVisitor)
|
102
122
|
loop do
|
103
123
|
condition = stack.pop
|
@@ -117,14 +137,13 @@ module Loxxy
|
|
117
137
|
symbol_table.leave_environment
|
118
138
|
end
|
119
139
|
|
120
|
-
def after_assign_expr(anAssignExpr)
|
140
|
+
def after_assign_expr(anAssignExpr, _visitor)
|
121
141
|
var_name = anAssignExpr.name
|
122
|
-
variable =
|
142
|
+
variable = variable_lookup(anAssignExpr)
|
123
143
|
raise StandardError, "Unknown variable #{var_name}" unless variable
|
124
144
|
|
125
|
-
value = stack.
|
145
|
+
value = stack.last # ToS remains since an assignment produces a value
|
126
146
|
variable.assign(value)
|
127
|
-
stack.push value # An expression produces a value
|
128
147
|
end
|
129
148
|
|
130
149
|
def after_logical_expr(aLogicalExpr, visitor)
|
@@ -192,18 +211,18 @@ module Loxxy
|
|
192
211
|
callee = stack.pop
|
193
212
|
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
194
213
|
|
195
|
-
|
214
|
+
case callee
|
215
|
+
when NativeFunction
|
196
216
|
stack.push callee.call # Pass arguments
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
symbol_table.insert(local)
|
217
|
+
when LoxFunction
|
218
|
+
arg_count = aCallExpr.arguments.size
|
219
|
+
if arg_count != callee.arity
|
220
|
+
msg = "Expected #{callee.arity} arguments but got #{arg_count}."
|
221
|
+
raise Loxxy::RuntimeError, msg
|
203
222
|
end
|
204
|
-
callee.call(aVisitor)
|
205
|
-
|
206
|
-
|
223
|
+
callee.call(self, aVisitor)
|
224
|
+
else
|
225
|
+
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
207
226
|
end
|
208
227
|
end
|
209
228
|
|
@@ -213,10 +232,10 @@ module Loxxy
|
|
213
232
|
|
214
233
|
def after_variable_expr(aVarExpr, aVisitor)
|
215
234
|
var_name = aVarExpr.name
|
216
|
-
var =
|
235
|
+
var = variable_lookup(aVarExpr)
|
217
236
|
raise StandardError, "Unknown variable #{var_name}" unless var
|
218
237
|
|
219
|
-
var.value.accept(aVisitor) # Evaluate
|
238
|
+
var.value.accept(aVisitor) # Evaluate variable value then push on stack
|
220
239
|
end
|
221
240
|
|
222
241
|
# @param literalExpr [Ast::LoxLiteralExpr]
|
@@ -230,13 +249,26 @@ module Loxxy
|
|
230
249
|
end
|
231
250
|
|
232
251
|
def after_fun_stmt(aFunStmt, _visitor)
|
233
|
-
function =
|
252
|
+
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
|
234
253
|
new_var = Variable.new(aFunStmt.name, function)
|
235
254
|
symbol_table.insert(new_var)
|
236
255
|
end
|
237
256
|
|
238
257
|
private
|
239
258
|
|
259
|
+
def variable_lookup(aVarNode)
|
260
|
+
env = nil
|
261
|
+
offset = resolver.locals[aVarNode]
|
262
|
+
if offset.nil?
|
263
|
+
env = symbol_table.root
|
264
|
+
else
|
265
|
+
env = symbol_table.current_env
|
266
|
+
offset.times { env = env.enclosing }
|
267
|
+
end
|
268
|
+
|
269
|
+
env.defns[aVarNode.name]
|
270
|
+
end
|
271
|
+
|
240
272
|
NativeFunction = Struct.new(:callable, :interp) do
|
241
273
|
def accept(_visitor)
|
242
274
|
interp.stack.push self
|
@@ -255,7 +287,8 @@ module Loxxy
|
|
255
287
|
negate_op = UnaryOperator.new('-', [Datatype::Number])
|
256
288
|
unary_operators[:-@] = negate_op
|
257
289
|
|
258
|
-
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype
|
290
|
+
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
|
291
|
+
BackEnd::LoxFunction])
|
259
292
|
unary_operators[:!] = negation_op
|
260
293
|
end
|
261
294
|
|
@@ -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,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../datatype/all_datatypes'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# rubocop: disable Style/AccessorGrouping
|
8
|
+
# Representation of a Lox function.
|
9
|
+
# It is a named slot that can be associated with a value at the time.
|
10
|
+
class LoxFunction
|
11
|
+
# @return [String] The name of the function (if any)
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @return [Array<>] the parameters
|
15
|
+
attr_reader :parameters
|
16
|
+
attr_reader :body
|
17
|
+
attr_reader :stack
|
18
|
+
attr_reader :closure
|
19
|
+
|
20
|
+
# Create a function with given name
|
21
|
+
# @param aName [String] The name of the variable
|
22
|
+
def initialize(aName, parameterList, aBody, anEngine)
|
23
|
+
@name = aName.dup
|
24
|
+
@parameters = parameterList
|
25
|
+
@body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
|
26
|
+
@stack = anEngine.stack
|
27
|
+
@closure = anEngine.symbol_table.current_env
|
28
|
+
anEngine.symbol_table.current_env.embedding = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def arity
|
32
|
+
parameters ? parameters.size : 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def accept(_visitor)
|
36
|
+
stack.push self
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(engine, aVisitor)
|
40
|
+
# new_env = Environment.new(engine.symbol_table.current_env)
|
41
|
+
new_env = Environment.new(closure)
|
42
|
+
engine.symbol_table.enter_environment(new_env)
|
43
|
+
|
44
|
+
parameters&.each do |param_name|
|
45
|
+
local = Variable.new(param_name, stack.pop)
|
46
|
+
engine.symbol_table.insert(local)
|
47
|
+
end
|
48
|
+
|
49
|
+
catch(:return) do
|
50
|
+
(body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
|
51
|
+
throw(:return)
|
52
|
+
end
|
53
|
+
|
54
|
+
engine.symbol_table.leave_environment
|
55
|
+
end
|
56
|
+
|
57
|
+
# Logical negation.
|
58
|
+
# As a function is a truthy thing, its negation is thus false.
|
59
|
+
# @return [Datatype::False]
|
60
|
+
def !
|
61
|
+
Datatype::False.instance
|
62
|
+
end
|
63
|
+
|
64
|
+
# Text representation of a Lox function
|
65
|
+
def to_str
|
66
|
+
"<fn #{name}>"
|
67
|
+
end
|
68
|
+
end # class
|
69
|
+
# rubocop: enable Style/AccessorGrouping
|
70
|
+
end # module
|
71
|
+
end # module
|
@@ -0,0 +1,171 @@
|
|
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
|
+
def initialize
|
26
|
+
@scopes = []
|
27
|
+
@locals = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Given an abstract syntax parse tree visitor, launch the visit
|
31
|
+
# and execute the visit events in the output stream.
|
32
|
+
# @param aVisitor [AST::ASTVisitor]
|
33
|
+
# @return [Loxxy::Datatype::BuiltinDatatype]
|
34
|
+
def analyze(aVisitor)
|
35
|
+
begin_scope
|
36
|
+
aVisitor.subscribe(self)
|
37
|
+
aVisitor.start
|
38
|
+
aVisitor.unsubscribe(self)
|
39
|
+
end_scope
|
40
|
+
end
|
41
|
+
|
42
|
+
# block statement introduces a new scope
|
43
|
+
def before_block_stmt(_aBlockStmt)
|
44
|
+
begin_scope
|
45
|
+
end
|
46
|
+
|
47
|
+
def after_block_stmt(_aBlockStmt)
|
48
|
+
end_scope
|
49
|
+
end
|
50
|
+
|
51
|
+
def before_for_stmt(aForStmt)
|
52
|
+
before_block_stmt(aForStmt)
|
53
|
+
end
|
54
|
+
|
55
|
+
def after_for_stmt(aForStmt, aVisitor)
|
56
|
+
aForStmt.test_expr.accept(aVisitor)
|
57
|
+
aForStmt.body_stmt.accept(aVisitor)
|
58
|
+
aForStmt.update_expr&.accept(aVisitor)
|
59
|
+
after_block_stmt(aForStmt)
|
60
|
+
end
|
61
|
+
|
62
|
+
def after_if_stmt(anIfStmt, aVisitor)
|
63
|
+
anIfStmt.then_stmt.accept(aVisitor)
|
64
|
+
anIfStmt.else_stmt&.accept(aVisitor)
|
65
|
+
end
|
66
|
+
|
67
|
+
def after_while_stmt(aWhileStmt, aVisitor)
|
68
|
+
aWhileStmt.body.accept(aVisitor)
|
69
|
+
aWhileStmt.condition.accept(aVisitor)
|
70
|
+
end
|
71
|
+
|
72
|
+
# A variable declaration adds a new variable to current scope
|
73
|
+
def before_var_stmt(aVarStmt)
|
74
|
+
declare(aVarStmt.name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def after_var_stmt(aVarStmt)
|
78
|
+
define(aVarStmt.name)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Assignment expressions require their variables resolved
|
82
|
+
def after_assign_expr(anAssignExpr, aVisitor)
|
83
|
+
resolve_local(anAssignExpr, aVisitor)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Variable expressions require their variables resolved
|
87
|
+
def before_variable_expr(aVarExpr)
|
88
|
+
var_name = aVarExpr.name
|
89
|
+
if !scopes.empty? && (scopes.last[var_name] == false)
|
90
|
+
raise StandardError, "Can't read variable #{var_name} in its own initializer"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def after_variable_expr(aVarExpr, aVisitor)
|
95
|
+
resolve_local(aVarExpr, aVisitor)
|
96
|
+
end
|
97
|
+
|
98
|
+
def after_call_expr(aCallExpr, aVisitor)
|
99
|
+
# Evaluate callee part
|
100
|
+
aCallExpr.callee.accept(aVisitor)
|
101
|
+
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
102
|
+
end
|
103
|
+
|
104
|
+
# function declaration creates a new scope for its body & binds its parameters for that scope
|
105
|
+
def before_fun_stmt(aFunStmt, aVisitor)
|
106
|
+
declare(aFunStmt.name)
|
107
|
+
define(aFunStmt.name)
|
108
|
+
resolve_function(aFunStmt, aVisitor)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def begin_scope
|
114
|
+
scopes.push({})
|
115
|
+
end
|
116
|
+
|
117
|
+
def end_scope
|
118
|
+
scopes.pop
|
119
|
+
end
|
120
|
+
|
121
|
+
def declare(aVarName)
|
122
|
+
return if scopes.empty?
|
123
|
+
|
124
|
+
curr_scope = scopes.last
|
125
|
+
|
126
|
+
# The initializer is not yet processed.
|
127
|
+
# Mark the variable as 'not yet ready' = exists but may not be referenced yet
|
128
|
+
curr_scope[aVarName] = false
|
129
|
+
end
|
130
|
+
|
131
|
+
def define(aVarName)
|
132
|
+
return if scopes.empty?
|
133
|
+
|
134
|
+
curr_scope = scopes.last
|
135
|
+
|
136
|
+
# The initializer (if any) was processed.
|
137
|
+
# Mark the variable as alive (= can be referenced in an expression)
|
138
|
+
curr_scope[aVarName] = true
|
139
|
+
end
|
140
|
+
|
141
|
+
def resolve_local(aVarExpr, _visitor)
|
142
|
+
max_i = i = scopes.size - 1
|
143
|
+
scopes.reverse_each do |scp|
|
144
|
+
if scp.include?(aVarExpr.name)
|
145
|
+
# Keep track of the difference of nesting levels between current scope
|
146
|
+
# and the scope where the variable is declared
|
147
|
+
@locals[aVarExpr] = max_i - i
|
148
|
+
break
|
149
|
+
end
|
150
|
+
i -= 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def resolve_function(aFunStmt, aVisitor)
|
155
|
+
begin_scope
|
156
|
+
|
157
|
+
aFunStmt.params&.each do |param_name|
|
158
|
+
declare(param_name)
|
159
|
+
define(param_name)
|
160
|
+
end
|
161
|
+
|
162
|
+
body = aFunStmt.body
|
163
|
+
unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
|
164
|
+
body.subnodes.first&.accept(aVisitor)
|
165
|
+
end
|
166
|
+
|
167
|
+
end_scope
|
168
|
+
end
|
169
|
+
end # class
|
170
|
+
end # mmodule
|
171
|
+
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.
|
data/lib/loxxy/error.rb
CHANGED
@@ -74,7 +74,7 @@ module Loxxy
|
|
74
74
|
rule('elsePart_opt' => [])
|
75
75
|
|
76
76
|
rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
|
77
|
-
rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
|
77
|
+
rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
|
78
78
|
rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as 'while_stmt'
|
79
79
|
rule('block' => 'LEFT_BRACE declaration_plus RIGHT_BRACE').as 'block_stmt'
|
80
80
|
rule('block' => 'LEFT_BRACE RIGHT_BRACE').as 'block_empty'
|
@@ -127,7 +127,7 @@ module Loxxy
|
|
127
127
|
rule('unaryOp' => 'MINUS')
|
128
128
|
rule('call' => 'primary')
|
129
129
|
rule('call' => 'primary refinement_plus').as 'call_expr'
|
130
|
-
rule('refinement_plus' => 'refinement_plus refinement')
|
130
|
+
rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
|
131
131
|
rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
|
132
132
|
rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
|
133
133
|
rule('refinement' => 'DOT IDENTIFIER')
|
@@ -92,6 +92,7 @@ module Loxxy
|
|
92
92
|
|
93
93
|
private
|
94
94
|
|
95
|
+
# rubocop: disable Lint/DuplicateBranch
|
95
96
|
def _next_token
|
96
97
|
skip_intertoken_spaces
|
97
98
|
curr_ch = scanner.peek(1)
|
@@ -125,6 +126,7 @@ module Loxxy
|
|
125
126
|
|
126
127
|
return token
|
127
128
|
end
|
129
|
+
# rubocop: enable Lint/DuplicateBranch
|
128
130
|
|
129
131
|
def build_token(aSymbolName, aLexeme)
|
130
132
|
begin
|
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
|
@@ -424,18 +415,49 @@ LOX_END
|
|
424
415
|
expect(sample_cfg[:ostream].string).to eq('<fn foo><native fn>')
|
425
416
|
end
|
426
417
|
|
427
|
-
it 'should
|
428
|
-
|
429
|
-
|
418
|
+
it 'should support return statements' do
|
419
|
+
program = <<-LOX_END
|
420
|
+
fun max(a, b) {
|
421
|
+
if (a > b) return a;
|
422
|
+
|
423
|
+
return b;
|
424
|
+
}
|
425
|
+
|
426
|
+
max(3, 2);
|
427
|
+
LOX_END
|
428
|
+
result = subject.evaluate(program)
|
429
|
+
expect(result).to eq(3)
|
430
430
|
end
|
431
|
-
end # context
|
432
431
|
|
433
|
-
|
434
|
-
it
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
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 print the hello world message' do
|
455
|
+
program = <<-LOX_END
|
456
|
+
var greeting = "Hello"; // Declaring a variable
|
457
|
+
print greeting + ", " + "world!"; // ... Playing with concatenation
|
458
|
+
LOX_END
|
459
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
460
|
+
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|
439
461
|
end
|
440
462
|
end # context
|
441
463
|
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.08
|
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-
|
11
|
+
date: 2021-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -103,6 +103,7 @@ files:
|
|
103
103
|
- lib/loxxy/ast/lox_node.rb
|
104
104
|
- lib/loxxy/ast/lox_noop_expr.rb
|
105
105
|
- lib/loxxy/ast/lox_print_stmt.rb
|
106
|
+
- lib/loxxy/ast/lox_return_stmt.rb
|
106
107
|
- lib/loxxy/ast/lox_seq_decl.rb
|
107
108
|
- lib/loxxy/ast/lox_unary_expr.rb
|
108
109
|
- lib/loxxy/ast/lox_var_stmt.rb
|
@@ -112,7 +113,8 @@ files:
|
|
112
113
|
- lib/loxxy/back_end/engine.rb
|
113
114
|
- lib/loxxy/back_end/entry.rb
|
114
115
|
- lib/loxxy/back_end/environment.rb
|
115
|
-
- lib/loxxy/back_end/
|
116
|
+
- lib/loxxy/back_end/lox_function.rb
|
117
|
+
- lib/loxxy/back_end/resolver.rb
|
116
118
|
- lib/loxxy/back_end/symbol_table.rb
|
117
119
|
- lib/loxxy/back_end/unary_operator.rb
|
118
120
|
- lib/loxxy/back_end/variable.rb
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../datatype/all_datatypes'
|
4
|
-
|
5
|
-
module Loxxy
|
6
|
-
module BackEnd
|
7
|
-
# rubocop: disable Style/AccessorGrouping
|
8
|
-
# Representation of a Lox function.
|
9
|
-
# It is a named slot that can be associated with a value at the time.
|
10
|
-
class Function
|
11
|
-
# @return [String]
|
12
|
-
attr_reader :name
|
13
|
-
|
14
|
-
# @return [Array<>] the parameters
|
15
|
-
attr_reader :parameters
|
16
|
-
attr_reader :body
|
17
|
-
attr_reader :stack
|
18
|
-
|
19
|
-
# Create a variable with given name and initial value
|
20
|
-
# @param aName [String] The name of the variable
|
21
|
-
# @param aValue [Datatype::BuiltinDatatype] the initial assigned value
|
22
|
-
def initialize(aName, parameterList, aBody, aStack)
|
23
|
-
@name = aName.dup
|
24
|
-
@parameters = parameterList
|
25
|
-
@body = aBody
|
26
|
-
@stack = aStack
|
27
|
-
end
|
28
|
-
|
29
|
-
def accept(_visitor)
|
30
|
-
stack.push self
|
31
|
-
end
|
32
|
-
|
33
|
-
def call(aVisitor)
|
34
|
-
body.empty? ? Datatype::Nil.instance : body.accept(aVisitor)
|
35
|
-
end
|
36
|
-
|
37
|
-
# Text representation of a Lox function
|
38
|
-
def to_str
|
39
|
-
"<fn #{name}>"
|
40
|
-
end
|
41
|
-
end # class
|
42
|
-
# rubocop: enable Style/AccessorGrouping
|
43
|
-
end # module
|
44
|
-
end # module
|