loxxy 0.1.13 → 0.2.00
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 +52 -3
- data/README.md +2 -26
- data/lib/loxxy/ast/all_lox_nodes.rb +2 -0
- data/lib/loxxy/ast/ast_builder.rb +36 -3
- data/lib/loxxy/ast/ast_visitor.rb +14 -0
- data/lib/loxxy/ast/lox_class_stmt.rb +5 -1
- data/lib/loxxy/ast/lox_super_expr.rb +35 -0
- data/lib/loxxy/ast/lox_this_expr.rb +22 -0
- data/lib/loxxy/back_end/engine.rb +46 -13
- data/lib/loxxy/back_end/environment.rb +0 -1
- data/lib/loxxy/back_end/lox_class.rb +22 -4
- data/lib/loxxy/back_end/lox_function.rb +16 -0
- data/lib/loxxy/back_end/lox_instance.rb +4 -8
- data/lib/loxxy/back_end/resolver.rb +61 -2
- data/lib/loxxy/back_end/symbol_table.rb +1 -18
- data/lib/loxxy/front_end/grammar.rb +9 -7
- data/lib/loxxy/interpreter.rb +12 -1
- data/lib/loxxy/version.rb +1 -1
- data/loxxy.gemspec +2 -2
- data/spec/back_end/engine_spec.rb +0 -8
- data/spec/front_end/raw_parser_spec.rb +5 -2
- data/spec/interpreter_spec.rb +118 -8
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a071e03ebadb94c255a118d3bebd75227a2d36c535495a79ad2525e4fc2bf7c
|
|
4
|
+
data.tar.gz: 35aa1fc822287f7b86628acac39e5c3ca1c12c478cbb330d302705597ff2f13b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51b1a4622ee11c20582fa9ffa72f499a64b280c0092b82a8f07d922309c800f787409e8c273a5316d09de594d82026af0d979e2ad7f52ee6d3eb02233549ebf5
|
|
7
|
+
data.tar.gz: 2649d33c23f73351f9ecb3b9b4b9899541398c94fa4effa8ca4f4c766589df575b1f68685a039f595ca1efb83a515d0a3620afa3721a0604e187b321449b9716
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
|
+
## [0.1.17] - 2021-04-11
|
|
2
|
+
- `Loxxy` now support custom initializer.
|
|
3
|
+
|
|
4
|
+
### Changed
|
|
5
|
+
- Method `BackEnd::Class#call` updated for custom initializer.
|
|
6
|
+
- Class `BackEnd::LoxFunction` added an attribute `is_initializer`
|
|
7
|
+
- Class `BackEnd::Resolver#before_return_stmt` added a check that return in initializer may not return a value
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Method `BackEnd::Engine#after_call_expr` now does arity checking also for initalizer.
|
|
11
|
+
- Method `BackEnd::LoxInstance#set` removed the check of field existence that prevented the creation of ... fields
|
|
12
|
+
|
|
13
|
+
## [0.1.16] - 2021-04-10
|
|
14
|
+
- Fixed an issue in name lookup. All the `this` test suite is passing.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Method `BackEnd::Engine#after_var_stmt` now it creates the variable and pouts it in the symbol table
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
- Method `BackEnd::Engine#before_var_stmt` it generated bug when assigning a value to a var, when that var name occurred elsewhere
|
|
21
|
+
|
|
22
|
+
## [0.1.15] - 2021-04-08
|
|
23
|
+
- Fixed the `dangling else`by tweaking the grammar rules
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Method `Ast::ASTBuilder#reduce_if__else_stmt` parse action specific for if with else branch
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- File `grammar.rb` changed rules to cope with `dangling else` issue
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Method `Ast::ASTBuilder#reduce_if_stmt` parse action for if without else branch
|
|
33
|
+
- File `README.md` removed the section about the `dangling else` issue.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## [0.1.14] - 2021-04-05
|
|
37
|
+
- `Loxxy` now implements the 'this' keyword
|
|
38
|
+
|
|
39
|
+
### New
|
|
40
|
+
- Class `Ast::LoxThisExpr` a syntax node that represents an occurrence of the `this` keyword
|
|
41
|
+
- Method `Ast::ASTBuilder#reduce_this_expr` parse action for this keyword
|
|
42
|
+
- Method `Ast::Visitor#visit_this_expr` visit of an `Ast::LoxThisExpr` node
|
|
43
|
+
- Method `BackEnd::Engine#after_this_expr` runtime action for this keyword (i.e. refers to current instance)
|
|
44
|
+
- Method `BackEnd::LoxFunction#bind` implementation of bound method
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- Class `BackEnd::Resolver` implementing semantic actions for `this` keyword
|
|
48
|
+
- File `grammar.rb` added name to a syntax rule
|
|
49
|
+
|
|
1
50
|
## [0.1.13] - 2021-04-05
|
|
2
51
|
- `Loxxy` now implements method calls
|
|
3
52
|
|
|
4
|
-
|
|
53
|
+
### New
|
|
5
54
|
- Class `Ast::LoxGetExpr` a syntax node that represents a read access to an object property
|
|
6
55
|
- Class `Ast::LoxSetExpr` a syntax node that represents a write access to an object property
|
|
7
56
|
- Method `Ast::ASTBuilder#reduce_set_expr` parse action for write access to an object property
|
|
@@ -11,11 +60,11 @@
|
|
|
11
60
|
- Method `BackEnd::Engine#after_set_expr` runtime action for property setting
|
|
12
61
|
- Method `BackEnd::Engine#after_get_expr` runtime action for property getting
|
|
13
62
|
- Method `BackEnd::LoxInstance#set` implementation of write accessor
|
|
14
|
-
- Method `BackEnd::LoxInstance#
|
|
63
|
+
- Method `BackEnd::LoxInstance#get` implementation of read accessor
|
|
15
64
|
- Method `BackEnd::Resolver#after_set_expr` resolve action for property setting
|
|
16
65
|
- Method `BackEnd::Resolver#after_get_expr` resolve action for property getting
|
|
17
66
|
|
|
18
|
-
|
|
67
|
+
### Changed
|
|
19
68
|
- Method `Ast::ASTBuilder#reduce_assign_expr` expanded to support write access to an object property
|
|
20
69
|
- Class `LoxClassStmt`: methods are now aggregate under the `body` attribute
|
|
21
70
|
- Class `LoxFunStmt`: has a new attribute `is_method` and inherits from `Ast::LoxNode`
|
data/README.md
CHANGED
|
@@ -177,8 +177,8 @@ Loxxy supports single line C-style comments.
|
|
|
177
177
|
### Keywords
|
|
178
178
|
Loxxy implements the following __Lox__ reserved keywords:
|
|
179
179
|
```lang-none
|
|
180
|
-
and, else, false, for, fun, if,
|
|
181
|
-
nil, or, print, true, var, while
|
|
180
|
+
and, class, else, false, for, fun, if,
|
|
181
|
+
nil, or, print, return, this, true, var, while
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
### Datatypes
|
|
@@ -331,30 +331,6 @@ print "else-branch";
|
|
|
331
331
|
```
|
|
332
332
|
|
|
333
333
|
As for other languages, the `else` part is optional.
|
|
334
|
-
##### Warning: nested `if`...`else`
|
|
335
|
-
Call it a bug ... Nested `if` `else` control flow structure aren't yet supported by __Loxxy__.
|
|
336
|
-
The culprit has a name: [the dangling else](https://en.wikipedia.org/wiki/Dangling_else).
|
|
337
|
-
|
|
338
|
-
The problem in a nutshell: in a nested if ... else ... statement like this:
|
|
339
|
-
``` javascript
|
|
340
|
-
'if (true) if (false) print "bad"; else print "good";
|
|
341
|
-
```
|
|
342
|
-
... there is an ambiguity.
|
|
343
|
-
Indeed, according to the __Lox__ grammar, the `else` could be bound
|
|
344
|
-
either to the first `if` or to the second one.
|
|
345
|
-
This ambiguity is usually lifted by applying an ad-hoc rule: an `else` is aways bound to the most
|
|
346
|
-
recent (rightmost) `if`.
|
|
347
|
-
Being a generic parsing library, `Rley` doesn't apply any of these supplemental rules.
|
|
348
|
-
As a consequence,it complains about the found ambiguity and stops the parsing...
|
|
349
|
-
Although `Rley` can cope with ambiguities, this requires the use of an advanced data structure
|
|
350
|
-
called `Shared Packed Parse Forest (SPPF)`.
|
|
351
|
-
SPPF are much more complex to handle than the "common" parse trees present in most compiler or interpreter books.
|
|
352
|
-
Therefore, a future version of `Rley` will incorporate the capability to define disambuiguation rules.
|
|
353
|
-
|
|
354
|
-
In the meantime, the `Loxxy` will progress on other __Lox__ features like:
|
|
355
|
-
- Block structures...
|
|
356
|
-
- Iteration structures (`for` and `while` loops)
|
|
357
|
-
|
|
358
334
|
|
|
359
335
|
#### Print Statement
|
|
360
336
|
|
|
@@ -165,7 +165,14 @@ module Loxxy
|
|
|
165
165
|
|
|
166
166
|
# rule('classDecl' => 'CLASS classNaming class_body')
|
|
167
167
|
def reduce_class_decl(_production, _range, _tokens, theChildren)
|
|
168
|
-
|
|
168
|
+
if theChildren[1].kind_of?(Array)
|
|
169
|
+
name = theChildren[1].first
|
|
170
|
+
parent = theChildren[1].last
|
|
171
|
+
else
|
|
172
|
+
name = theChildren[1]
|
|
173
|
+
parent = nil
|
|
174
|
+
end
|
|
175
|
+
Ast::LoxClassStmt.new(tokens[1].position, name, parent, theChildren[2])
|
|
169
176
|
end
|
|
170
177
|
|
|
171
178
|
# rule('classNaming' => 'IDENTIFIER')
|
|
@@ -173,6 +180,13 @@ module Loxxy
|
|
|
173
180
|
theChildren[0].token.lexeme
|
|
174
181
|
end
|
|
175
182
|
|
|
183
|
+
# rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
|
184
|
+
def reduce_class_subclassing(_production, _range, _tokens, theChildren)
|
|
185
|
+
super_token = theChildren[2].token
|
|
186
|
+
super_var = LoxVariableExpr.new(super_token.position, super_token.lexeme)
|
|
187
|
+
[theChildren[0].token.lexeme, super_var]
|
|
188
|
+
end
|
|
189
|
+
|
|
176
190
|
# rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
|
|
177
191
|
def reduce_class_body(_production, _range, _tokens, theChildren)
|
|
178
192
|
theChildren[1].nil? ? [] : theChildren[1]
|
|
@@ -233,11 +247,20 @@ module Loxxy
|
|
|
233
247
|
return_first_child(range, tokens, theChildren)
|
|
234
248
|
end
|
|
235
249
|
|
|
236
|
-
# rule('ifStmt' => 'IF ifCondition statement
|
|
250
|
+
# rule('ifStmt' => 'IF ifCondition statement ELSE statement')
|
|
251
|
+
# rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt')
|
|
252
|
+
def reduce_if_else_stmt(_production, _range, tokens, theChildren)
|
|
253
|
+
condition = theChildren[1]
|
|
254
|
+
then_stmt = theChildren[2]
|
|
255
|
+
else_stmt = theChildren[4]
|
|
256
|
+
LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# rule('unbalancedStmt' => 'IF ifCondition stmt').as ''
|
|
237
260
|
def reduce_if_stmt(_production, _range, tokens, theChildren)
|
|
238
261
|
condition = theChildren[1]
|
|
239
262
|
then_stmt = theChildren[2]
|
|
240
|
-
else_stmt =
|
|
263
|
+
else_stmt = nil
|
|
241
264
|
LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
|
|
242
265
|
end
|
|
243
266
|
|
|
@@ -354,6 +377,16 @@ module Loxxy
|
|
|
354
377
|
LoxVariableExpr.new(tokens[0].position, var_name)
|
|
355
378
|
end
|
|
356
379
|
|
|
380
|
+
# rule('primary' => 'THIS')
|
|
381
|
+
def reduce_this_expr(_production, _range, tokens, _children)
|
|
382
|
+
LoxThisExpr.new(tokens[0].position)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# rule('primary' => 'SUPER DOT IDENTIFIER')
|
|
386
|
+
def reduce_super_expr(_production, _range, _tokens, theChildren)
|
|
387
|
+
LoxSuperExpr.new(theChildren[0].token.position, theChildren[2].token.lexeme)
|
|
388
|
+
end
|
|
389
|
+
|
|
357
390
|
# rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
|
|
358
391
|
def reduce_function(_production, _range, _tokens, theChildren)
|
|
359
392
|
first_child = theChildren.first
|
|
@@ -205,6 +205,20 @@ module Loxxy
|
|
|
205
205
|
broadcast(:after_variable_expr, aVariableExpr, self)
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# Visit event. The visitor is about to visit the this keyword.
|
|
209
|
+
# @param aThisExpr [Ast::LoxThisExpr] this expression
|
|
210
|
+
def visit_this_expr(aThisExpr)
|
|
211
|
+
broadcast(:before_this_expr, aThisExpr)
|
|
212
|
+
broadcast(:after_this_expr, aThisExpr, self)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Visit event. The visitor is about to visit the super keyword.
|
|
216
|
+
# @param aSuperExpr [Ast::LoxSuperExpr] super expression
|
|
217
|
+
def visit_super_expr(aSuperExpr)
|
|
218
|
+
broadcast(:before_super_expr, aSuperExpr)
|
|
219
|
+
broadcast(:after_super_expr, aSuperExpr, self)
|
|
220
|
+
end
|
|
221
|
+
|
|
208
222
|
# Visit event. The visitor is about to visit the given terminal datatype value.
|
|
209
223
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
|
210
224
|
def visit_builtin(aValue)
|
|
@@ -8,15 +8,19 @@ module Loxxy
|
|
|
8
8
|
# @return [String] the class name
|
|
9
9
|
attr_reader :name
|
|
10
10
|
|
|
11
|
+
# @return [Ast::LoxVariableExpr] variable referencing the superclass (if any)
|
|
12
|
+
attr_reader :superclass
|
|
13
|
+
|
|
11
14
|
# @return [Array<Ast::LoxFunStmt>] the methods
|
|
12
15
|
attr_reader :body
|
|
13
16
|
|
|
14
17
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
|
15
18
|
# @param condExpr [Loxxy::Ast::LoxNode] iteration condition
|
|
16
19
|
# @param theBody [Array<Loxxy::Ast::LoxNode>]
|
|
17
|
-
def initialize(aPosition, aName, theMethods)
|
|
20
|
+
def initialize(aPosition, aName, aSuperclassName, theMethods)
|
|
18
21
|
super(aPosition, [])
|
|
19
22
|
@name = aName.dup
|
|
23
|
+
@superclass = aSuperclassName
|
|
20
24
|
@body = theMethods
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lox_node'
|
|
4
|
+
|
|
5
|
+
module Loxxy
|
|
6
|
+
module Ast
|
|
7
|
+
class LoxSuperExpr < LoxNode
|
|
8
|
+
# @return [Ast::LoxNode] the object to which the property belongs to
|
|
9
|
+
attr_accessor :object
|
|
10
|
+
|
|
11
|
+
# @return [String] Name of a method name
|
|
12
|
+
attr_reader :property
|
|
13
|
+
|
|
14
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
|
15
|
+
# @param aMethodName [String] Name of a method
|
|
16
|
+
def initialize(aPosition, aMethodName)
|
|
17
|
+
super(aPosition)
|
|
18
|
+
@property = aMethodName
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
|
22
|
+
# @param visitor [ASTVisitor] the visitor
|
|
23
|
+
def accept(visitor)
|
|
24
|
+
visitor.visit_super_expr(self)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Quack like a LoxVariableExpr
|
|
28
|
+
# @return [String] the `super` keyword
|
|
29
|
+
def name
|
|
30
|
+
'super'
|
|
31
|
+
end
|
|
32
|
+
alias callee= object=
|
|
33
|
+
end # class
|
|
34
|
+
end # module
|
|
35
|
+
end # module
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lox_node'
|
|
4
|
+
|
|
5
|
+
module Loxxy
|
|
6
|
+
module Ast
|
|
7
|
+
# A node in a parse tree that represents the occurrence of 'this' keyword.
|
|
8
|
+
class LoxThisExpr < LoxNode
|
|
9
|
+
# Duck-typing: behaves like a LoxVarExpr
|
|
10
|
+
# @return [String] return the this keyword
|
|
11
|
+
def name
|
|
12
|
+
'this'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
|
16
|
+
# @param _visitor [LoxxyTreeVisitor] the visitor
|
|
17
|
+
def accept(aVisitor)
|
|
18
|
+
aVisitor.visit_this_expr(self)
|
|
19
|
+
end
|
|
20
|
+
end # class
|
|
21
|
+
end # module
|
|
22
|
+
end # module
|
|
@@ -70,30 +70,46 @@ module Loxxy
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def after_class_stmt(aClassStmt, aVisitor)
|
|
73
|
+
if aClassStmt.superclass
|
|
74
|
+
aClassStmt.superclass.accept(aVisitor)
|
|
75
|
+
parent = stack.pop
|
|
76
|
+
unless parent.kind_of?(LoxClass)
|
|
77
|
+
raise StandardError, 'Superclass must be a class.'
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
parent = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if parent # Create an environment specific for 'super'
|
|
84
|
+
super_env = Environment.new(symbol_table.current_env)
|
|
85
|
+
symbol_table.enter_environment(super_env)
|
|
86
|
+
end
|
|
87
|
+
|
|
73
88
|
# Convert LoxFunStmt into LoxFunction
|
|
74
89
|
meths = aClassStmt.body.map do |func_node|
|
|
75
90
|
func_node.is_method = true
|
|
76
91
|
func_node.accept(aVisitor)
|
|
77
|
-
stack.pop
|
|
92
|
+
mth = stack.pop
|
|
93
|
+
mth.is_initializer = true if mth.name == 'init'
|
|
94
|
+
mth
|
|
78
95
|
end
|
|
79
96
|
|
|
80
|
-
klass = LoxClass.new(aClassStmt.name, meths, self)
|
|
97
|
+
klass = LoxClass.new(aClassStmt.name, parent, meths, self)
|
|
98
|
+
if parent
|
|
99
|
+
super_var = Variable.new('super', klass)
|
|
100
|
+
symbol_table.insert(super_var)
|
|
101
|
+
symbol_table.leave_environment
|
|
102
|
+
end
|
|
81
103
|
new_var = Variable.new(aClassStmt.name, klass)
|
|
82
104
|
symbol_table.insert(new_var)
|
|
83
105
|
end
|
|
84
106
|
|
|
85
|
-
def
|
|
107
|
+
def after_var_stmt(aVarStmt)
|
|
86
108
|
new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
|
|
87
109
|
symbol_table.insert(new_var)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def after_var_stmt(aVarStmt)
|
|
91
|
-
var_name = aVarStmt.name
|
|
92
|
-
variable = symbol_table.lookup(var_name)
|
|
93
|
-
raise StandardError, "Unknown variable #{var_name}" unless variable
|
|
94
110
|
|
|
95
111
|
value = stack.pop
|
|
96
|
-
|
|
112
|
+
new_var.assign(value)
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
def before_for_stmt(aForStmt)
|
|
@@ -240,15 +256,13 @@ module Loxxy
|
|
|
240
256
|
case callee
|
|
241
257
|
when NativeFunction
|
|
242
258
|
stack.push callee.call # Pass arguments
|
|
243
|
-
when LoxFunction
|
|
259
|
+
when LoxFunction, LoxClass
|
|
244
260
|
arg_count = aCallExpr.arguments.size
|
|
245
261
|
if arg_count != callee.arity
|
|
246
262
|
msg = "Expected #{callee.arity} arguments but got #{arg_count}."
|
|
247
263
|
raise Loxxy::RuntimeError, msg
|
|
248
264
|
end
|
|
249
265
|
callee.call(self, aVisitor)
|
|
250
|
-
when LoxClass
|
|
251
|
-
callee.call(self, aVisitor)
|
|
252
266
|
else
|
|
253
267
|
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
|
254
268
|
end
|
|
@@ -281,6 +295,25 @@ module Loxxy
|
|
|
281
295
|
stack.push(literalExpr.literal)
|
|
282
296
|
end
|
|
283
297
|
|
|
298
|
+
def after_this_expr(aThisExpr, aVisitor)
|
|
299
|
+
var = variable_lookup(aThisExpr)
|
|
300
|
+
var.value.accept(aVisitor) # Evaluate this value then push on stack
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def after_super_expr(aSuperExpr, aVisitor)
|
|
304
|
+
offset = resolver.locals[aSuperExpr]
|
|
305
|
+
env = symbol_table.current_env
|
|
306
|
+
(offset - 1).times { env = env.enclosing }
|
|
307
|
+
instance = env.defns['this'].value.accept(aVisitor)[0]
|
|
308
|
+
superklass = variable_lookup(aSuperExpr).value.superclass
|
|
309
|
+
method = superklass.find_method(aSuperExpr.property)
|
|
310
|
+
unless method
|
|
311
|
+
raise StandardError, "Undefined property '#{aSuperExpr.property}'."
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
stack.push method.bind(instance)
|
|
315
|
+
end
|
|
316
|
+
|
|
284
317
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
|
285
318
|
def before_visit_builtin(aValue)
|
|
286
319
|
stack.push(aValue)
|
|
@@ -7,17 +7,23 @@ module Loxxy
|
|
|
7
7
|
module BackEnd
|
|
8
8
|
# Runtime representation of a Lox class.
|
|
9
9
|
class LoxClass
|
|
10
|
+
# rubocop: disable Style/AccessorGrouping
|
|
11
|
+
|
|
10
12
|
# @return [String] The name of the class
|
|
11
13
|
attr_reader :name
|
|
14
|
+
attr_reader :superclass
|
|
12
15
|
|
|
13
16
|
# @return [Hash{String => LoxFunction}] the list of methods
|
|
14
17
|
attr_reader :meths
|
|
15
18
|
attr_reader :stack
|
|
16
19
|
|
|
20
|
+
# rubocop: enable Style/AccessorGrouping
|
|
21
|
+
|
|
17
22
|
# Create a class with given name
|
|
18
23
|
# @param aName [String] The name of the class
|
|
19
|
-
def initialize(aName, theMethods, anEngine)
|
|
24
|
+
def initialize(aName, aSuperclass, theMethods, anEngine)
|
|
20
25
|
@name = aName.dup
|
|
26
|
+
@superclass = aSuperclass
|
|
21
27
|
@meths = {}
|
|
22
28
|
theMethods.each do |func|
|
|
23
29
|
meths[func.name] = func
|
|
@@ -30,17 +36,29 @@ module Loxxy
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def arity
|
|
33
|
-
|
|
39
|
+
initializer = find_method('init')
|
|
40
|
+
initializer ? initializer.arity : 0
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def call(engine,
|
|
43
|
+
def call(engine, visitor)
|
|
37
44
|
instance = LoxInstance.new(self, engine)
|
|
45
|
+
initializer = find_method('init')
|
|
46
|
+
if initializer
|
|
47
|
+
constructor = initializer.bind(instance)
|
|
48
|
+
constructor.call(engine, visitor)
|
|
49
|
+
end
|
|
50
|
+
|
|
38
51
|
engine.stack.push(instance)
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
# @param aName [String] the method name to search for
|
|
42
55
|
def find_method(aName)
|
|
43
|
-
meths[aName]
|
|
56
|
+
found = meths[aName]
|
|
57
|
+
unless found || superclass.nil?
|
|
58
|
+
found = superclass.find_method(aName)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
found
|
|
44
62
|
end
|
|
45
63
|
|
|
46
64
|
# Logical negation.
|
|
@@ -15,6 +15,7 @@ module Loxxy
|
|
|
15
15
|
attr_reader :body
|
|
16
16
|
attr_reader :stack
|
|
17
17
|
attr_reader :closure
|
|
18
|
+
attr_accessor :is_initializer
|
|
18
19
|
|
|
19
20
|
# Create a function with given name
|
|
20
21
|
# @param aName [String] The name of the function
|
|
@@ -24,6 +25,7 @@ module Loxxy
|
|
|
24
25
|
@body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
|
|
25
26
|
@stack = anEngine.stack
|
|
26
27
|
@closure = anEngine.symbol_table.current_env
|
|
28
|
+
@is_initializer = false
|
|
27
29
|
anEngine.symbol_table.current_env.embedding = true
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -48,10 +50,24 @@ module Loxxy
|
|
|
48
50
|
(body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
|
|
49
51
|
throw(:return)
|
|
50
52
|
end
|
|
53
|
+
if is_initializer
|
|
54
|
+
enclosing_env = engine.symbol_table.current_env.enclosing
|
|
55
|
+
engine.stack.push(enclosing_env.defns['this'].value)
|
|
56
|
+
end
|
|
51
57
|
|
|
52
58
|
engine.symbol_table.leave_environment
|
|
53
59
|
end
|
|
54
60
|
|
|
61
|
+
def bind(anInstance)
|
|
62
|
+
new_env = Environment.new(closure)
|
|
63
|
+
this = Variable.new('this', anInstance)
|
|
64
|
+
new_env.insert(this)
|
|
65
|
+
bound_method = dup
|
|
66
|
+
bound_method.instance_variable_set(:@closure, new_env)
|
|
67
|
+
|
|
68
|
+
bound_method
|
|
69
|
+
end
|
|
70
|
+
|
|
55
71
|
# Logical negation.
|
|
56
72
|
# As a function is a truthy thing, its negation is thus false.
|
|
57
73
|
# @return [Datatype::False]
|
|
@@ -9,7 +9,7 @@ module Loxxy
|
|
|
9
9
|
# @return BackEnd::LoxClass] the class that this object is an instance of
|
|
10
10
|
attr_reader :klass
|
|
11
11
|
|
|
12
|
-
attr_reader :
|
|
12
|
+
attr_reader :engine
|
|
13
13
|
|
|
14
14
|
# @return [Hash{String => BuiltinDatatype | LoxFunction | LoxInstance }]
|
|
15
15
|
attr_reader :fields
|
|
@@ -18,12 +18,12 @@ module Loxxy
|
|
|
18
18
|
# @param aClass [BackEnd::LoxClass] the class this this object belong
|
|
19
19
|
def initialize(aClass, anEngine)
|
|
20
20
|
@klass = aClass
|
|
21
|
-
@
|
|
21
|
+
@engine = anEngine
|
|
22
22
|
@fields = {}
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def accept(_visitor)
|
|
26
|
-
stack.push self
|
|
26
|
+
engine.stack.push self
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Text representation of a Lox instance
|
|
@@ -41,16 +41,12 @@ module Loxxy
|
|
|
41
41
|
raise StandardError, "Undefined property '#{aName}'."
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
method
|
|
44
|
+
method.bind(self)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Set the value of property with given name
|
|
48
48
|
# aName [String] name of object property
|
|
49
49
|
def set(aName, aValue)
|
|
50
|
-
unless fields.include? aName
|
|
51
|
-
raise StandardError, "Undefined property '#{aName}'."
|
|
52
|
-
end
|
|
53
|
-
|
|
54
50
|
fields[aName] = aValue
|
|
55
51
|
end
|
|
56
52
|
end # class
|
|
@@ -26,10 +26,15 @@ module Loxxy
|
|
|
26
26
|
# @return [Symbol] must be one of: :none, :function
|
|
27
27
|
attr_reader :current_function
|
|
28
28
|
|
|
29
|
+
# An indicator that tells we're in the middle of a class declaration
|
|
30
|
+
# @return [Symbol] must be one of: :none, :class
|
|
31
|
+
attr_reader :current_class
|
|
32
|
+
|
|
29
33
|
def initialize
|
|
30
34
|
@scopes = []
|
|
31
35
|
@locals = {}
|
|
32
36
|
@current_function = :none
|
|
37
|
+
@current_class = :none
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
# Given an abstract syntax parse tree visitor, launch the visit
|
|
@@ -59,10 +64,28 @@ module Loxxy
|
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def after_class_stmt(aClassStmt, aVisitor)
|
|
67
|
+
previous_class = current_class
|
|
68
|
+
@current_class = :class
|
|
62
69
|
define(aClassStmt.name)
|
|
70
|
+
if aClassStmt.superclass
|
|
71
|
+
if aClassStmt.name == aClassStmt.superclass.name
|
|
72
|
+
raise StandardError, "'A class can't inherit from itself."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@current_class = :subclass
|
|
76
|
+
aClassStmt.superclass.accept(aVisitor)
|
|
77
|
+
begin_scope
|
|
78
|
+
define('super')
|
|
79
|
+
end
|
|
80
|
+
begin_scope
|
|
81
|
+
define('this')
|
|
63
82
|
aClassStmt.body.each do |fun_stmt|
|
|
64
|
-
|
|
83
|
+
mth_type = fun_stmt.name == 'init' ? :initializer : :method
|
|
84
|
+
resolve_function(fun_stmt, mth_type, aVisitor)
|
|
65
85
|
end
|
|
86
|
+
end_scope
|
|
87
|
+
end_scope if aClassStmt.superclass
|
|
88
|
+
@current_class = previous_class
|
|
66
89
|
end
|
|
67
90
|
|
|
68
91
|
def before_for_stmt(aForStmt)
|
|
@@ -81,7 +104,7 @@ module Loxxy
|
|
|
81
104
|
anIfStmt.else_stmt&.accept(aVisitor)
|
|
82
105
|
end
|
|
83
106
|
|
|
84
|
-
def before_return_stmt(
|
|
107
|
+
def before_return_stmt(returnStmt)
|
|
85
108
|
if scopes.size < 2
|
|
86
109
|
msg = "Error at 'return': Can't return from top-level code."
|
|
87
110
|
raise StandardError, msg
|
|
@@ -91,6 +114,11 @@ module Loxxy
|
|
|
91
114
|
msg = "Error at 'return': Can't return from outside a function."
|
|
92
115
|
raise StandardError, msg
|
|
93
116
|
end
|
|
117
|
+
|
|
118
|
+
if current_function == :initializer
|
|
119
|
+
msg = "Error at 'return': Can't return a value from an initializer."
|
|
120
|
+
raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
|
|
121
|
+
end
|
|
94
122
|
end
|
|
95
123
|
|
|
96
124
|
def after_while_stmt(aWhileStmt, aVisitor)
|
|
@@ -140,6 +168,37 @@ module Loxxy
|
|
|
140
168
|
aGetExpr.object.accept(aVisitor)
|
|
141
169
|
end
|
|
142
170
|
|
|
171
|
+
def before_this_expr(_thisExpr)
|
|
172
|
+
if current_class == :none
|
|
173
|
+
msg = "Error at 'this': Can't use 'this' outside of a class."
|
|
174
|
+
raise StandardError, msg
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def after_this_expr(aThisExpr, aVisitor)
|
|
179
|
+
# 'this' behaves closely to a local variable
|
|
180
|
+
resolve_local(aThisExpr, aVisitor)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# rubocop: disable Style/CaseLikeIf
|
|
184
|
+
# rubocop: disable Style/StringConcatenation
|
|
185
|
+
def after_super_expr(aSuperExpr, aVisitor)
|
|
186
|
+
msg_prefix = "Error at 'super': Can't use 'super' "
|
|
187
|
+
if current_class == :none
|
|
188
|
+
err_msg = msg_prefix + 'outside of a class.'
|
|
189
|
+
raise StandardError, err_msg
|
|
190
|
+
|
|
191
|
+
elsif current_class == :class
|
|
192
|
+
err_msg = msg_prefix + 'in a class without superclass.'
|
|
193
|
+
raise StandardError, err_msg
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
# 'super' behaves closely to a local variable
|
|
197
|
+
resolve_local(aSuperExpr, aVisitor)
|
|
198
|
+
end
|
|
199
|
+
# rubocop: enable Style/StringConcatenation
|
|
200
|
+
# rubocop: enable Style/CaseLikeIf
|
|
201
|
+
|
|
143
202
|
# function declaration creates a new scope for its body & binds its parameters for that scope
|
|
144
203
|
def before_fun_stmt(aFunStmt, aVisitor)
|
|
145
204
|
declare(aFunStmt.name)
|
|
@@ -85,7 +85,7 @@ module Loxxy
|
|
|
85
85
|
name2envs[name] = [current_env]
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
anEntry.name
|
|
88
|
+
anEntry.name
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# Search for the object with the given name
|
|
@@ -99,23 +99,6 @@ module Loxxy
|
|
|
99
99
|
sc.defns[aName]
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Search for the object with the given i_name
|
|
103
|
-
# @param anIName [String]
|
|
104
|
-
# @return [BackEnd::Variable]
|
|
105
|
-
# def lookup_i_name(anIName)
|
|
106
|
-
# found = nil
|
|
107
|
-
# environment = current_env
|
|
108
|
-
|
|
109
|
-
# begin
|
|
110
|
-
# found = environment.defns.values.find { |e| e.i_name == anIName }
|
|
111
|
-
# break if found
|
|
112
|
-
|
|
113
|
-
# environment = environment.parent
|
|
114
|
-
# end while environment
|
|
115
|
-
|
|
116
|
-
# found
|
|
117
|
-
# end
|
|
118
|
-
|
|
119
102
|
# Return all variables defined in the current .. root chain.
|
|
120
103
|
# Variables are sorted top-down and left-to-right.
|
|
121
104
|
def all_variables
|
|
@@ -35,10 +35,10 @@ module Loxxy
|
|
|
35
35
|
rule('declaration' => 'classDecl')
|
|
36
36
|
rule('declaration' => 'funDecl')
|
|
37
37
|
rule('declaration' => 'varDecl')
|
|
38
|
-
rule('declaration' => '
|
|
38
|
+
rule('declaration' => 'stmt')
|
|
39
39
|
|
|
40
40
|
rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
|
|
41
|
-
rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
|
41
|
+
rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER').as 'class_subclassing'
|
|
42
42
|
rule('classNaming' => 'IDENTIFIER').as 'class_name'
|
|
43
43
|
rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
|
|
44
44
|
rule('methods_opt' => 'method_plus')
|
|
@@ -52,6 +52,8 @@ module Loxxy
|
|
|
52
52
|
rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON').as 'var_initialization'
|
|
53
53
|
|
|
54
54
|
# Statements: produce side effects, but don't introduce bindings
|
|
55
|
+
rule('stmt' => 'statement')
|
|
56
|
+
rule('stmt' => 'unbalancedStmt') # Tweak to cope with "dangling else" problem
|
|
55
57
|
rule('statement' => 'exprStmt')
|
|
56
58
|
rule('statement' => 'forStmt')
|
|
57
59
|
rule('statement' => 'ifStmt')
|
|
@@ -70,10 +72,10 @@ module Loxxy
|
|
|
70
72
|
rule('forTest' => 'expression_opt SEMICOLON').as 'for_test'
|
|
71
73
|
rule('forUpdate' => 'expression_opt')
|
|
72
74
|
|
|
73
|
-
rule('ifStmt' => 'IF ifCondition statement
|
|
75
|
+
rule('ifStmt' => 'IF ifCondition statement ELSE statement').as 'if_else_stmt'
|
|
76
|
+
rule('unbalancedStmt' => 'IF ifCondition stmt').as 'if_stmt'
|
|
77
|
+
rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt').as 'if_else_stmt'
|
|
74
78
|
rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN').as 'keep_symbol2'
|
|
75
|
-
rule('elsePart_opt' => 'ELSE statement').as 'keep_symbol2'
|
|
76
|
-
rule('elsePart_opt' => [])
|
|
77
79
|
|
|
78
80
|
rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
|
|
79
81
|
rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
|
|
@@ -136,12 +138,12 @@ module Loxxy
|
|
|
136
138
|
rule('primary' => 'TRUE').as 'literal_expr'
|
|
137
139
|
rule('primary' => 'FALSE').as 'literal_expr'
|
|
138
140
|
rule('primary' => 'NIL').as 'literal_expr'
|
|
139
|
-
rule('primary' => 'THIS')
|
|
141
|
+
rule('primary' => 'THIS').as 'this_expr'
|
|
140
142
|
rule('primary' => 'NUMBER').as 'literal_expr'
|
|
141
143
|
rule('primary' => 'STRING').as 'literal_expr'
|
|
142
144
|
rule('primary' => 'IDENTIFIER').as 'variable_expr'
|
|
143
145
|
rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN').as 'grouping_expr'
|
|
144
|
-
rule('primary' => 'SUPER DOT IDENTIFIER')
|
|
146
|
+
rule('primary' => 'SUPER DOT IDENTIFIER').as 'super_expr'
|
|
145
147
|
|
|
146
148
|
# Utility rules
|
|
147
149
|
rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
|
data/lib/loxxy/interpreter.rb
CHANGED
|
@@ -24,6 +24,15 @@ module Loxxy
|
|
|
24
24
|
# @param lox_input [String] Lox program to evaluate
|
|
25
25
|
# @return [Loxxy::Datatype::BuiltinDatatype]
|
|
26
26
|
def evaluate(lox_input)
|
|
27
|
+
raw_evaluate(lox_input).first
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Evaluate the given Lox program.
|
|
31
|
+
# Return the pair [result, a BackEnd::Engine instance]
|
|
32
|
+
# where result is the value of the last executed expression (if any)
|
|
33
|
+
# @param lox_input [String] Lox program to evaluate
|
|
34
|
+
# @return Loxxy::Datatype::BuiltinDatatype, Loxxy::BackEnd::Engine]
|
|
35
|
+
def raw_evaluate(lox_input)
|
|
27
36
|
# Front-end scans, parses the input and blurps an AST...
|
|
28
37
|
parser = FrontEnd::Parser.new
|
|
29
38
|
|
|
@@ -34,7 +43,9 @@ module Loxxy
|
|
|
34
43
|
# Back-end launches the tree walking & responds to visit events
|
|
35
44
|
# by executing the code determined by the visited AST node.
|
|
36
45
|
engine = BackEnd::Engine.new(config)
|
|
37
|
-
engine.execute(visitor)
|
|
46
|
+
result = engine.execute(visitor)
|
|
47
|
+
|
|
48
|
+
[result, engine]
|
|
38
49
|
end
|
|
39
50
|
end # class
|
|
40
51
|
end # module
|
data/lib/loxxy/version.rb
CHANGED
data/loxxy.gemspec
CHANGED
|
@@ -40,8 +40,8 @@ Gem::Specification.new do |spec|
|
|
|
40
40
|
spec.version = Loxxy::VERSION
|
|
41
41
|
spec.authors = ['Dimitri Geshef']
|
|
42
42
|
spec.email = ['famished.tiger@yahoo.com']
|
|
43
|
-
spec.summary = 'An implementation of the Lox programming language.
|
|
44
|
-
spec.description = 'An implementation of the Lox programming language.
|
|
43
|
+
spec.summary = 'An implementation of the Lox programming language.'
|
|
44
|
+
spec.description = 'An implementation of the Lox programming language.'
|
|
45
45
|
spec.homepage = 'https://github.com/famished-tiger/loxxy'
|
|
46
46
|
spec.license = 'MIT'
|
|
47
47
|
spec.required_ruby_version = '~> 2.4'
|
|
@@ -34,15 +34,7 @@ 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
|
|
42
|
-
|
|
43
37
|
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
38
|
# Precondition: value to assign is on top of stack
|
|
47
39
|
subject.stack.push(greeting)
|
|
48
40
|
|
|
@@ -76,8 +76,11 @@ LOX_END
|
|
|
76
76
|
expect(decls.symbol.name).to eq('declaration_plus')
|
|
77
77
|
stmt = decls.subnodes[0].subnodes[0]
|
|
78
78
|
expect(stmt).to be_kind_of(Rley::PTree::NonTerminalNode)
|
|
79
|
-
expect(stmt.symbol.name).to eq('
|
|
80
|
-
|
|
79
|
+
expect(stmt.symbol.name).to eq('stmt')
|
|
80
|
+
statement = stmt.subnodes[0]
|
|
81
|
+
expect(statement).to be_kind_of(Rley::PTree::NonTerminalNode)
|
|
82
|
+
expect(statement.symbol.name).to eq('statement')
|
|
83
|
+
prnt_stmt = statement.subnodes[0]
|
|
81
84
|
expect(prnt_stmt).to be_kind_of(Rley::PTree::NonTerminalNode)
|
|
82
85
|
expect(prnt_stmt.subnodes.size).to eq(3)
|
|
83
86
|
expect(prnt_stmt.subnodes[0]).to be_kind_of(Rley::PTree::TerminalNode)
|
data/spec/interpreter_spec.rb
CHANGED
|
@@ -6,6 +6,7 @@ require 'stringio'
|
|
|
6
6
|
# Load the class under test
|
|
7
7
|
require_relative '../lib/loxxy/interpreter'
|
|
8
8
|
|
|
9
|
+
# rubocop: disable Metrics/ModuleLength
|
|
9
10
|
module Loxxy
|
|
10
11
|
# This spec contains the bare bones test for the Interpreter class.
|
|
11
12
|
# The execution of Lox code is tested elsewhere.
|
|
@@ -227,19 +228,20 @@ module Loxxy
|
|
|
227
228
|
# Evaluate the 'then' expression if the condition is true.
|
|
228
229
|
['if (true) print "then-branch";', 'then-branch'],
|
|
229
230
|
['if (false) print "ignored";', ''],
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
['if (nil) print "ignored";', ''],
|
|
232
|
+
['if (true) { print "block"; }', 'block'],
|
|
233
|
+
['var a = false; if (a = true) print a;', 'true'],
|
|
232
234
|
|
|
233
235
|
# Evaluate the 'else' expression if the condition is false.
|
|
234
236
|
['if (true) print "then-branch"; else print "else-branch";', 'then-branch'],
|
|
235
237
|
['if (false) print "then-branch"; else print "else-branch";', 'else-branch'],
|
|
236
238
|
['if (0) print "then-branch"; else print "else-branch";', 'then-branch'],
|
|
237
|
-
['if (nil) print "then-branch"; else print "else-branch";', 'else-branch']
|
|
238
|
-
|
|
239
|
+
['if (nil) print "then-branch"; else print "else-branch";', 'else-branch'],
|
|
240
|
+
['if (false) nil; else { print "else-branch"; }', 'else-branch'],
|
|
239
241
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
# A dangling else binds to the right-most if.
|
|
243
|
+
['if (true) if (false) print "bad"; else print "good";', 'good'],
|
|
244
|
+
['if (false) if (true) print "bad"; else print "worse";', '']
|
|
243
245
|
].each do |(source, predicted)|
|
|
244
246
|
io = StringIO.new
|
|
245
247
|
cfg = { ostream: io }
|
|
@@ -466,7 +468,7 @@ LOX_END
|
|
|
466
468
|
snippet = <<-LOX_END
|
|
467
469
|
class Duck {
|
|
468
470
|
noise() {
|
|
469
|
-
quack();
|
|
471
|
+
this.quack();
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
quack() {
|
|
@@ -509,7 +511,115 @@ LOX_END
|
|
|
509
511
|
expect { subject.evaluate(program) }.not_to raise_error
|
|
510
512
|
expect(sample_cfg[:ostream].string).to eq('quack')
|
|
511
513
|
end
|
|
514
|
+
|
|
515
|
+
it "should support the 'this' keyword" do
|
|
516
|
+
program = <<-LOX_END
|
|
517
|
+
class Egotist {
|
|
518
|
+
speak() {
|
|
519
|
+
print this;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
var method = Egotist().speak;
|
|
524
|
+
method(); // Output: Egotist instance
|
|
525
|
+
LOX_END
|
|
526
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
|
527
|
+
expect(sample_cfg[:ostream].string).to eq('Egotist instance')
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
it 'should support a closure nested in a method' do
|
|
531
|
+
lox_snippet = <<-LOX_END
|
|
532
|
+
class Foo {
|
|
533
|
+
getClosure() {
|
|
534
|
+
fun closure() {
|
|
535
|
+
return this.toString();
|
|
536
|
+
}
|
|
537
|
+
return closure;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
toString() { return "foo"; }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
var closure = Foo().getClosure();
|
|
544
|
+
closure;
|
|
545
|
+
LOX_END
|
|
546
|
+
# Expected result: Backend::LoxFunction('closure')
|
|
547
|
+
# Expected function's closure (environment layout):
|
|
548
|
+
# Environment('global')
|
|
549
|
+
# defns
|
|
550
|
+
# +- ['clock'] => BackEnd::Engine::NativeFunction
|
|
551
|
+
# *- ['Foo'] => BackEnd::LoxClass
|
|
552
|
+
# Environment
|
|
553
|
+
# defns
|
|
554
|
+
# ['this'] => BackEnd::LoxInstance
|
|
555
|
+
# Environment
|
|
556
|
+
# defns
|
|
557
|
+
# +- ['closure'] => Backend::LoxFunction
|
|
558
|
+
result = subject.evaluate(lox_snippet)
|
|
559
|
+
expect(result).to be_kind_of(BackEnd::LoxFunction)
|
|
560
|
+
expect(result.name).to eq('closure')
|
|
561
|
+
closure = result.closure
|
|
562
|
+
expect(closure).to be_kind_of(Loxxy::BackEnd::Environment)
|
|
563
|
+
expect(closure.defns['closure'].value).to eq(result)
|
|
564
|
+
expect(closure.enclosing).to be_kind_of(Loxxy::BackEnd::Environment)
|
|
565
|
+
expect(closure.enclosing.defns['this'].value).to be_kind_of(Loxxy::BackEnd::LoxInstance)
|
|
566
|
+
global_env = closure.enclosing.enclosing
|
|
567
|
+
expect(global_env).to be_kind_of(Loxxy::BackEnd::Environment)
|
|
568
|
+
expect(global_env.defns['clock'].value).to be_kind_of(BackEnd::Engine::NativeFunction)
|
|
569
|
+
expect(global_env.defns['Foo'].value).to be_kind_of(BackEnd::LoxClass)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
it 'should support custom initializer' do
|
|
573
|
+
lox_snippet = <<-LOX_END
|
|
574
|
+
// From section 3.9.5
|
|
575
|
+
class Breakfast {
|
|
576
|
+
init(meat, bread) {
|
|
577
|
+
this.meat = meat;
|
|
578
|
+
this.bread = bread;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
serve(who) {
|
|
582
|
+
print "Enjoy your " + this.meat + " and " +
|
|
583
|
+
this.bread + ", " + who + ".";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
var baconAndToast = Breakfast("bacon", "toast");
|
|
588
|
+
baconAndToast.serve("Dear Reader");
|
|
589
|
+
// Output: "Enjoy your bacon and toast, Dear Reader."
|
|
590
|
+
LOX_END
|
|
591
|
+
expect { subject.evaluate(lox_snippet) }.not_to raise_error
|
|
592
|
+
predicted = 'Enjoy your bacon and toast, Dear Reader.'
|
|
593
|
+
expect(sample_cfg[:ostream].string).to eq(predicted)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it 'should support class inheritance and super keyword' do
|
|
597
|
+
lox_snippet = <<-LOX_END
|
|
598
|
+
class A {
|
|
599
|
+
method() {
|
|
600
|
+
print "A method";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
class B < A {
|
|
605
|
+
method() {
|
|
606
|
+
print "B method";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
test() {
|
|
610
|
+
super.method();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
class C < B {}
|
|
615
|
+
|
|
616
|
+
C().test();
|
|
617
|
+
LOX_END
|
|
618
|
+
expect { subject.evaluate(lox_snippet) }.not_to raise_error
|
|
619
|
+
expect(sample_cfg[:ostream].string).to eq('A method')
|
|
620
|
+
end
|
|
512
621
|
end # context
|
|
513
622
|
end # describe
|
|
514
623
|
# rubocop: enable Metrics/BlockLength
|
|
515
624
|
end # module
|
|
625
|
+
# rubocop: enable Metrics/ModuleLength
|
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.
|
|
4
|
+
version: 0.2.00
|
|
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-04-
|
|
11
|
+
date: 2021-04-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rley
|
|
@@ -66,7 +66,7 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '3.0'
|
|
69
|
-
description: An implementation of the Lox programming language.
|
|
69
|
+
description: An implementation of the Lox programming language.
|
|
70
70
|
email:
|
|
71
71
|
- famished.tiger@yahoo.com
|
|
72
72
|
executables:
|
|
@@ -108,6 +108,8 @@ files:
|
|
|
108
108
|
- lib/loxxy/ast/lox_return_stmt.rb
|
|
109
109
|
- lib/loxxy/ast/lox_seq_decl.rb
|
|
110
110
|
- lib/loxxy/ast/lox_set_expr.rb
|
|
111
|
+
- lib/loxxy/ast/lox_super_expr.rb
|
|
112
|
+
- lib/loxxy/ast/lox_this_expr.rb
|
|
111
113
|
- lib/loxxy/ast/lox_unary_expr.rb
|
|
112
114
|
- lib/loxxy/ast/lox_var_stmt.rb
|
|
113
115
|
- lib/loxxy/ast/lox_variable_expr.rb
|
|
@@ -177,7 +179,7 @@ requirements: []
|
|
|
177
179
|
rubygems_version: 3.1.4
|
|
178
180
|
signing_key:
|
|
179
181
|
specification_version: 4
|
|
180
|
-
summary: An implementation of the Lox programming language.
|
|
182
|
+
summary: An implementation of the Lox programming language.
|
|
181
183
|
test_files:
|
|
182
184
|
- spec/back_end/engine_spec.rb
|
|
183
185
|
- spec/back_end/environment_spec.rb
|