loxxy 0.1.04 → 0.1.09
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 +49 -0
- data/README.md +4 -7
- data/lib/loxxy/ast/ast_builder.rb +21 -4
- data/lib/loxxy/ast/ast_visitor.rb +2 -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 +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 +46 -29
- 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 +182 -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 +1 -1
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +10 -0
- data/spec/interpreter_spec.rb +30 -22
- metadata +4 -3
- data/lib/loxxy/back_end/function.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d027baa99cb59b7a102b55d1dec07e75f7cb17a71c42708881507ae8bc83b75
|
4
|
+
data.tar.gz: 6f27ba42c296a5845185fb8d746fd8cf24d9b8b71c2c98d590fa8a9c22cac33f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b71f56915f53a4ece5bcfde6b74ee7d407517b54038b3a00a945813acd3137c5da8800f8ac2fea71220b35557d18eb63fef25ac0b90297b1067a9ed65e70a55
|
7
|
+
data.tar.gz: b6faed818eff75c6beb32137cd97777b332b9a79994ab74ddb70cc4a257778e8a2571ab59d8c612fc89e5bf5f3c7a4e9f3868748ae737b3fe0cce1e404681460
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,52 @@
|
|
1
|
+
## [0.1.09] - 2021-03-28
|
2
|
+
- Fix and test suite for return statements
|
3
|
+
|
4
|
+
### Changed
|
5
|
+
- `Loxxy` reports an error when a return statement occurs in top-level scope
|
6
|
+
|
7
|
+
### Fixed
|
8
|
+
- A return without explicit value genrated an exception in some cases.
|
9
|
+
|
10
|
+
## [0.1.08] - 2021-03-27
|
11
|
+
- `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
|
12
|
+
|
13
|
+
### New
|
14
|
+
- Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- Class `Ast::Visitor` changes in some method signatures
|
18
|
+
- Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
|
19
|
+
- Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
|
20
|
+
|
21
|
+
## [0.1.07] - 2021-03-14
|
22
|
+
- `Loxxy` now supports nested functions and closures
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
- Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
|
26
|
+
- Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
|
27
|
+
- Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
|
28
|
+
- Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
|
29
|
+
- Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
|
30
|
+
|
31
|
+
### Fixed
|
32
|
+
- Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
|
33
|
+
|
34
|
+
|
35
|
+
## [0.1.06] - 2021-03-06
|
36
|
+
- Parameters/arguments checks in function declaration and call
|
37
|
+
|
38
|
+
### Changed
|
39
|
+
- Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
|
40
|
+
- Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
|
41
|
+
|
42
|
+
- Class `BackEnd::Function` renamed to `LoxFunction`
|
43
|
+
|
44
|
+
## [0.1.05] - 2021-03-05
|
45
|
+
- Test for Fibbonacci recursive function is now passing.
|
46
|
+
|
47
|
+
### Fixed
|
48
|
+
- Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
|
49
|
+
|
1
50
|
## [0.1.04] - 2021-02-28
|
2
51
|
|
3
52
|
### Added
|
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?
|
@@ -260,20 +260,33 @@ module Loxxy
|
|
260
260
|
|
261
261
|
# rule('call' => 'primary refinement_plus').as 'call_expr'
|
262
262
|
def reduce_call_expr(_production, _range, _tokens, theChildren)
|
263
|
-
|
264
|
-
|
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]
|
265
278
|
end
|
266
279
|
|
267
280
|
# rule('refinement_plus' => 'refinement').
|
268
281
|
def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
|
269
|
-
theChildren
|
282
|
+
theChildren
|
270
283
|
end
|
271
284
|
|
272
285
|
# rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
|
273
286
|
def reduce_call_arglist(_production, _range, tokens, theChildren)
|
274
287
|
args = theChildren[1] || []
|
275
288
|
if args.size > 255
|
276
|
-
raise
|
289
|
+
raise Loxxy::RuntimeError, "Can't have more than 255 arguments."
|
277
290
|
end
|
278
291
|
|
279
292
|
LoxCallExpr.new(tokens[0].position, args)
|
@@ -303,6 +316,10 @@ module Loxxy
|
|
303
316
|
def reduce_function(_production, _range, _tokens, theChildren)
|
304
317
|
first_child = theChildren.first
|
305
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
|
306
323
|
LoxFunStmt.new(pos, first_child.token.lexeme, theChildren[2], theChildren[4])
|
307
324
|
end
|
308
325
|
|
@@ -120,7 +120,7 @@ module Loxxy
|
|
120
120
|
def visit_assign_expr(anAssignExpr)
|
121
121
|
broadcast(:before_assign_expr, anAssignExpr)
|
122
122
|
traverse_subnodes(anAssignExpr)
|
123
|
-
broadcast(:after_assign_expr, anAssignExpr)
|
123
|
+
broadcast(:after_assign_expr, anAssignExpr, self)
|
124
124
|
end
|
125
125
|
|
126
126
|
# Visit event. The visitor is about to visit a logical expression.
|
@@ -194,7 +194,7 @@ module Loxxy
|
|
194
194
|
# Visit event. The visitor is about to visit a function statement node.
|
195
195
|
# @param aFunStmt [AST::LoxFunStmt] function declaration to visit
|
196
196
|
def visit_fun_stmt(aFunStmt)
|
197
|
-
broadcast(:before_fun_stmt, aFunStmt)
|
197
|
+
broadcast(:before_fun_stmt, aFunStmt, self)
|
198
198
|
traverse_subnodes(aFunStmt)
|
199
199
|
broadcast(:after_fun_stmt, aFunStmt, self)
|
200
200
|
end
|
@@ -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,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
|
@@ -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
|
@@ -121,14 +137,13 @@ module Loxxy
|
|
121
137
|
symbol_table.leave_environment
|
122
138
|
end
|
123
139
|
|
124
|
-
def after_assign_expr(anAssignExpr)
|
140
|
+
def after_assign_expr(anAssignExpr, _visitor)
|
125
141
|
var_name = anAssignExpr.name
|
126
|
-
variable =
|
142
|
+
variable = variable_lookup(anAssignExpr)
|
127
143
|
raise StandardError, "Unknown variable #{var_name}" unless variable
|
128
144
|
|
129
|
-
value = stack.
|
145
|
+
value = stack.last # ToS remains since an assignment produces a value
|
130
146
|
variable.assign(value)
|
131
|
-
stack.push value # An expression produces a value
|
132
147
|
end
|
133
148
|
|
134
149
|
def after_logical_expr(aLogicalExpr, visitor)
|
@@ -199,39 +214,28 @@ module Loxxy
|
|
199
214
|
case callee
|
200
215
|
when NativeFunction
|
201
216
|
stack.push callee.call # Pass arguments
|
202
|
-
when
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
symbol_table.insert(local)
|
208
|
-
end
|
209
|
-
catch(:return) do
|
210
|
-
callee.call(aVisitor)
|
211
|
-
throw(:return)
|
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
|
212
222
|
end
|
213
|
-
|
214
|
-
symbol_table.leave_environment
|
223
|
+
callee.call(self, aVisitor)
|
215
224
|
else
|
216
225
|
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
217
226
|
end
|
218
227
|
end
|
219
228
|
|
220
|
-
def complete_call
|
221
|
-
callee = ret_stack.pop
|
222
|
-
symbol_table.leave_environment if callee.kind_of?(Function)
|
223
|
-
end
|
224
|
-
|
225
229
|
def after_grouping_expr(_groupingExpr)
|
226
230
|
# Do nothing: work was already done by visiting /evaluating the subexpression
|
227
231
|
end
|
228
232
|
|
229
233
|
def after_variable_expr(aVarExpr, aVisitor)
|
230
234
|
var_name = aVarExpr.name
|
231
|
-
var =
|
235
|
+
var = variable_lookup(aVarExpr)
|
232
236
|
raise StandardError, "Unknown variable #{var_name}" unless var
|
233
237
|
|
234
|
-
var.value.accept(aVisitor) # Evaluate
|
238
|
+
var.value.accept(aVisitor) # Evaluate variable value then push on stack
|
235
239
|
end
|
236
240
|
|
237
241
|
# @param literalExpr [Ast::LoxLiteralExpr]
|
@@ -245,13 +249,26 @@ module Loxxy
|
|
245
249
|
end
|
246
250
|
|
247
251
|
def after_fun_stmt(aFunStmt, _visitor)
|
248
|
-
function =
|
252
|
+
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
|
249
253
|
new_var = Variable.new(aFunStmt.name, function)
|
250
254
|
symbol_table.insert(new_var)
|
251
255
|
end
|
252
256
|
|
253
257
|
private
|
254
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
|
+
|
255
272
|
NativeFunction = Struct.new(:callable, :interp) do
|
256
273
|
def accept(_visitor)
|
257
274
|
interp.stack.push self
|
@@ -271,7 +288,7 @@ module Loxxy
|
|
271
288
|
unary_operators[:-@] = negate_op
|
272
289
|
|
273
290
|
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
|
274
|
-
BackEnd::
|
291
|
+
BackEnd::LoxFunction])
|
275
292
|
unary_operators[:!] = negation_op
|
276
293
|
end
|
277
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,182 @@
|
|
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 before_return_stmt(_returnStmt)
|
68
|
+
if scopes.size < 2
|
69
|
+
msg = "Error at 'return': Can't return from top-level code."
|
70
|
+
raise StandardError, msg
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def after_while_stmt(aWhileStmt, aVisitor)
|
75
|
+
aWhileStmt.body.accept(aVisitor)
|
76
|
+
aWhileStmt.condition.accept(aVisitor)
|
77
|
+
end
|
78
|
+
|
79
|
+
# A variable declaration adds a new variable to current scope
|
80
|
+
def before_var_stmt(aVarStmt)
|
81
|
+
declare(aVarStmt.name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def after_var_stmt(aVarStmt)
|
85
|
+
define(aVarStmt.name)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Assignment expressions require their variables resolved
|
89
|
+
def after_assign_expr(anAssignExpr, aVisitor)
|
90
|
+
resolve_local(anAssignExpr, aVisitor)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Variable expressions require their variables resolved
|
94
|
+
def before_variable_expr(aVarExpr)
|
95
|
+
var_name = aVarExpr.name
|
96
|
+
if !scopes.empty? && (scopes.last[var_name] == false)
|
97
|
+
raise StandardError, "Can't read variable #{var_name} in its own initializer"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def after_variable_expr(aVarExpr, aVisitor)
|
102
|
+
resolve_local(aVarExpr, aVisitor)
|
103
|
+
end
|
104
|
+
|
105
|
+
def after_call_expr(aCallExpr, aVisitor)
|
106
|
+
# Evaluate callee part
|
107
|
+
aCallExpr.callee.accept(aVisitor)
|
108
|
+
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# function declaration creates a new scope for its body & binds its parameters for that scope
|
112
|
+
def before_fun_stmt(aFunStmt, aVisitor)
|
113
|
+
declare(aFunStmt.name)
|
114
|
+
define(aFunStmt.name)
|
115
|
+
resolve_function(aFunStmt, aVisitor)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def begin_scope
|
121
|
+
scopes.push({})
|
122
|
+
end
|
123
|
+
|
124
|
+
def end_scope
|
125
|
+
scopes.pop
|
126
|
+
end
|
127
|
+
|
128
|
+
def declare(aVarName)
|
129
|
+
return if scopes.empty?
|
130
|
+
|
131
|
+
curr_scope = scopes.last
|
132
|
+
if curr_scope.include?(aVarName)
|
133
|
+
msg = "Error at '#{aVarName}': Already variable with this name in this scope."
|
134
|
+
raise StandardError, msg
|
135
|
+
end
|
136
|
+
|
137
|
+
# The initializer is not yet processed.
|
138
|
+
# Mark the variable as 'not yet ready' = exists but may not be referenced yet
|
139
|
+
curr_scope[aVarName] = false
|
140
|
+
end
|
141
|
+
|
142
|
+
def define(aVarName)
|
143
|
+
return if scopes.empty?
|
144
|
+
|
145
|
+
curr_scope = scopes.last
|
146
|
+
|
147
|
+
# The initializer (if any) was processed.
|
148
|
+
# Mark the variable as alive (= can be referenced in an expression)
|
149
|
+
curr_scope[aVarName] = true
|
150
|
+
end
|
151
|
+
|
152
|
+
def resolve_local(aVarExpr, _visitor)
|
153
|
+
max_i = i = scopes.size - 1
|
154
|
+
scopes.reverse_each do |scp|
|
155
|
+
if scp.include?(aVarExpr.name)
|
156
|
+
# Keep track of the difference of nesting levels between current scope
|
157
|
+
# and the scope where the variable is declared
|
158
|
+
@locals[aVarExpr] = max_i - i
|
159
|
+
break
|
160
|
+
end
|
161
|
+
i -= 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def resolve_function(aFunStmt, aVisitor)
|
166
|
+
begin_scope
|
167
|
+
|
168
|
+
aFunStmt.params&.each do |param_name|
|
169
|
+
declare(param_name)
|
170
|
+
define(param_name)
|
171
|
+
end
|
172
|
+
|
173
|
+
body = aFunStmt.body
|
174
|
+
unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
|
175
|
+
body.subnodes.first&.accept(aVisitor)
|
176
|
+
end
|
177
|
+
|
178
|
+
end_scope
|
179
|
+
end
|
180
|
+
end # class
|
181
|
+
end # mmodule
|
182
|
+
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
@@ -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')
|
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,35 @@ 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')
|
444
451
|
end
|
445
|
-
|
452
|
+
# rubocop: enable Style/StringConcatenation
|
446
453
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
expect { subject.evaluate(
|
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!')
|
453
461
|
end
|
454
462
|
end # context
|
455
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.09
|
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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -113,7 +113,8 @@ files:
|
|
113
113
|
- lib/loxxy/back_end/engine.rb
|
114
114
|
- lib/loxxy/back_end/entry.rb
|
115
115
|
- lib/loxxy/back_end/environment.rb
|
116
|
-
- lib/loxxy/back_end/
|
116
|
+
- lib/loxxy/back_end/lox_function.rb
|
117
|
+
- lib/loxxy/back_end/resolver.rb
|
117
118
|
- lib/loxxy/back_end/symbol_table.rb
|
118
119
|
- lib/loxxy/back_end/unary_operator.rb
|
119
120
|
- lib/loxxy/back_end/variable.rb
|
@@ -1,51 +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
|
-
# Logical negation.
|
38
|
-
# As a function is a truthy thing, its negation is thus false.
|
39
|
-
# @return [Datatype::False]
|
40
|
-
def !
|
41
|
-
Datatype::False.instance
|
42
|
-
end
|
43
|
-
|
44
|
-
# Text representation of a Lox function
|
45
|
-
def to_str
|
46
|
-
"<fn #{name}>"
|
47
|
-
end
|
48
|
-
end # class
|
49
|
-
# rubocop: enable Style/AccessorGrouping
|
50
|
-
end # module
|
51
|
-
end # module
|