loxxy 0.1.07 → 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 +11 -2
- data/README.md +4 -5
- 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_seq_decl.rb +0 -2
- data/lib/loxxy/ast/lox_var_stmt.rb +2 -2
- data/lib/loxxy/back_end/engine.rb +36 -7
- data/lib/loxxy/back_end/resolver.rb +171 -0
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +8 -0
- metadata +3 -2
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,3 +1,14 @@
|
|
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
|
+
|
1
12
|
## [0.1.07] - 2021-03-14
|
2
13
|
- `Loxxy` now supports nested functions and closures
|
3
14
|
|
@@ -12,8 +23,6 @@
|
|
12
23
|
- Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
|
13
24
|
|
14
25
|
|
15
|
-
|
16
|
-
|
17
26
|
## [0.1.06] - 2021-03-06
|
18
27
|
- Parameters/arguments checks in function declaration and call
|
19
28
|
|
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,11 +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
|
-
- Classes and objects.
|
16
|
+
The interpreter currently it can execute all allowed __Lox__ expressions and statements except
|
17
|
+
object-oriented feaures (classes and objects).
|
19
18
|
|
20
|
-
|
19
|
+
Our intent is implement to these missing features in Q2 2021.
|
21
20
|
|
22
21
|
|
23
22
|
## What's the fuss about Lox?
|
@@ -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
|
@@ -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
|
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require_relative '../ast/all_lox_nodes'
|
5
5
|
require_relative 'binary_operator'
|
6
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,9 +137,9 @@ 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
145
|
value = stack.last # ToS remains since an assignment produces a value
|
@@ -216,10 +232,10 @@ module Loxxy
|
|
216
232
|
|
217
233
|
def after_variable_expr(aVarExpr, aVisitor)
|
218
234
|
var_name = aVarExpr.name
|
219
|
-
var =
|
235
|
+
var = variable_lookup(aVarExpr)
|
220
236
|
raise StandardError, "Unknown variable #{var_name}" unless var
|
221
237
|
|
222
|
-
var.value.accept(aVisitor) # Evaluate
|
238
|
+
var.value.accept(aVisitor) # Evaluate variable value then push on stack
|
223
239
|
end
|
224
240
|
|
225
241
|
# @param literalExpr [Ast::LoxLiteralExpr]
|
@@ -240,6 +256,19 @@ module Loxxy
|
|
240
256
|
|
241
257
|
private
|
242
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
|
+
|
243
272
|
NativeFunction = Struct.new(:callable, :interp) do
|
244
273
|
def accept(_visitor)
|
245
274
|
interp.stack.push self
|
@@ -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
|
data/lib/loxxy/version.rb
CHANGED
@@ -34,10 +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
|
39
46
|
# Precondition: value to assign is on top of stack
|
40
47
|
subject.stack.push(greeting)
|
48
|
+
|
41
49
|
expect { subject.after_var_stmt(var_decl) }.not_to raise_error
|
42
50
|
current_env = subject.symbol_table.current_env
|
43
51
|
expect(current_env.defns['greeting']).to be_kind_of(Variable)
|
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-03-
|
11
|
+
date: 2021-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -114,6 +114,7 @@ files:
|
|
114
114
|
- lib/loxxy/back_end/entry.rb
|
115
115
|
- lib/loxxy/back_end/environment.rb
|
116
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
|