loxxy 0.1.12 → 0.1.17
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 +69 -0
- data/README.md +2 -26
- data/lib/loxxy/ast/all_lox_nodes.rb +3 -0
- data/lib/loxxy/ast/ast_builder.rb +35 -4
- data/lib/loxxy/ast/ast_visitor.rb +21 -2
- data/lib/loxxy/ast/lox_class_stmt.rb +7 -4
- data/lib/loxxy/ast/lox_fun_stmt.rb +5 -3
- data/lib/loxxy/ast/lox_get_expr.rb +30 -0
- data/lib/loxxy/ast/lox_set_expr.rb +28 -0
- data/lib/loxxy/ast/lox_this_expr.rb +22 -0
- data/lib/loxxy/back_end/engine.rb +48 -16
- data/lib/loxxy/back_end/environment.rb +0 -1
- data/lib/loxxy/back_end/lox_class.rb +20 -5
- data/lib/loxxy/back_end/lox_function.rb +16 -0
- data/lib/loxxy/back_end/lox_instance.rb +27 -4
- data/lib/loxxy/back_end/resolver.rb +44 -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/spec/back_end/engine_spec.rb +0 -8
- data/spec/front_end/parser_spec.rb +77 -0
- data/spec/front_end/raw_parser_spec.rb +5 -2
- data/spec/interpreter_spec.rb +122 -21
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c7d720e2638407882bfd3f34384748a872086e1b1f7a3849f0e029e8672fc1c
|
4
|
+
data.tar.gz: c3aa7ebe4a625ef1b0a257cc750430b9a0aa404bb39dab900b409b2987e09dea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1eb8bb6575dd39834c55ce3ad359fbdb9a2129f91d25bc39ea093c5a4ca2aa38300ab28e673b39991678c304aea64ffda473707f9d317980b976c8f98fc3a55f
|
7
|
+
data.tar.gz: d3dd5baae665ab27a60a13b7392e9d57ef32ff72a37892d8f08e314888a1bfcc3db9ceeb071a28c00dda44f4bce7afed6aadf6c33c411a93d561a30fef833edd
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,72 @@
|
|
1
|
+
## [0.1.17] - 2021-04-11
|
2
|
+
- TODO
|
3
|
+
|
4
|
+
### Fixed
|
5
|
+
- TODO
|
6
|
+
|
7
|
+
## [0.1.16] - 2021-04-10
|
8
|
+
- Fixed an issue in name lookup. All the `this` test suite is passing.
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
- Method `BackEnd::Engine#after_var_stmt` now it creates the variable and pouts it in the symbol table
|
12
|
+
|
13
|
+
### Removed
|
14
|
+
- Method `BackEnd::Engine#before_var_stmt` it generated bug when assigning a value to a var, when that var name occurred elsewhere
|
15
|
+
|
16
|
+
## [0.1.15] - 2021-04-08
|
17
|
+
- Fixed the `dangling else`by tweaking the grammar rules
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
- Method `Ast::ASTBuilder#reduce_if__else_stmt` parse action specific for if with else branch
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- File `grammar.rb` changed rules to cope with `dangling else` issue
|
24
|
+
|
25
|
+
### Changed
|
26
|
+
- Method `Ast::ASTBuilder#reduce_if_stmt` parse action for if without else branch
|
27
|
+
- File `README.md` removed the section about the `dangling else` issue.
|
28
|
+
|
29
|
+
|
30
|
+
## [0.1.14] - 2021-04-05
|
31
|
+
- `Loxxy` now implements the 'this' keyword
|
32
|
+
|
33
|
+
### New
|
34
|
+
- Class `Ast::LoxThisExpr` a syntax node that represents an occurrence of the `this` keyword
|
35
|
+
- Method `Ast::ASTBuilder#reduce_this_expr` parse action for this keyword
|
36
|
+
- Method `Ast::Visitor#visit_this_expr` visit of an `Ast::LoxThisExpr` node
|
37
|
+
- Method `BackEnd::Engine#after_this_expr` runtime action for this keyword (i.e. refers to current instance)
|
38
|
+
- Method `BackEnd::LoxFunction#bind` implementation of bound method
|
39
|
+
|
40
|
+
### Changed
|
41
|
+
- Class `BackEnd::Resolver` implementing semantic actions for `this` keyword
|
42
|
+
- File `grammar.rb` added name to a syntax rule
|
43
|
+
|
44
|
+
## [0.1.13] - 2021-04-05
|
45
|
+
- `Loxxy` now implements method calls
|
46
|
+
|
47
|
+
### New
|
48
|
+
- Class `Ast::LoxGetExpr` a syntax node that represents a read access to an object property
|
49
|
+
- Class `Ast::LoxSetExpr` a syntax node that represents a write access to an object property
|
50
|
+
- Method `Ast::ASTBuilder#reduce_set_expr` parse action for write access to an object property
|
51
|
+
- Method `Ast::ASTBuilder#reduce_get_expr` parse action for read access to an object property
|
52
|
+
- Method `Ast::Visitor#visit_set_expr` visit of an `Ast::LoxSetExpr` node
|
53
|
+
- Method `Ast::Visitor#visit_get_expr` visit of an `Ast::LoxGetExpr` node
|
54
|
+
- Method `BackEnd::Engine#after_set_expr` runtime action for property setting
|
55
|
+
- Method `BackEnd::Engine#after_get_expr` runtime action for property getting
|
56
|
+
- Method `BackEnd::LoxInstance#set` implementation of write accessor
|
57
|
+
- Method `BackEnd::LoxInstance#get` implementation of read accessor
|
58
|
+
- Method `BackEnd::Resolver#after_set_expr` resolve action for property setting
|
59
|
+
- Method `BackEnd::Resolver#after_get_expr` resolve action for property getting
|
60
|
+
|
61
|
+
### Changed
|
62
|
+
- Method `Ast::ASTBuilder#reduce_assign_expr` expanded to support write access to an object property
|
63
|
+
- Class `LoxClassStmt`: methods are now aggregate under the `body` attribute
|
64
|
+
- Class `LoxFunStmt`: has a new attribute `is_method` and inherits from `Ast::LoxNode`
|
65
|
+
- Method `BackEnd::Engine#after_class_stmt` methods are aggregated into the classes
|
66
|
+
- Method `BackEnd::Engine#after_fun_stmt` extension for method
|
67
|
+
- File `grammar.rb` added names to two syntax rules
|
68
|
+
|
69
|
+
|
1
70
|
## [0.1.12] - 2021-04-03
|
2
71
|
- Intermediate version: `Loxxy` does instance creation (default constructor)
|
3
72
|
|
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
|
|
@@ -1,14 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'lox_fun_stmt'
|
4
|
+
require_relative 'lox_this_expr'
|
4
5
|
require_relative 'lox_variable_expr'
|
5
6
|
require_relative 'lox_literal_expr'
|
6
7
|
require_relative 'lox_noop_expr'
|
8
|
+
require_relative 'lox_get_expr'
|
7
9
|
require_relative 'lox_call_expr'
|
8
10
|
require_relative 'lox_grouping_expr'
|
9
11
|
require_relative 'lox_unary_expr'
|
10
12
|
require_relative 'lox_binary_expr'
|
11
13
|
require_relative 'lox_logical_expr'
|
14
|
+
require_relative 'lox_set_expr'
|
12
15
|
require_relative 'lox_assign_expr'
|
13
16
|
require_relative 'lox_block_stmt'
|
14
17
|
require_relative 'lox_while_stmt'
|
@@ -233,11 +233,20 @@ module Loxxy
|
|
233
233
|
return_first_child(range, tokens, theChildren)
|
234
234
|
end
|
235
235
|
|
236
|
-
# rule('ifStmt' => 'IF ifCondition statement
|
236
|
+
# rule('ifStmt' => 'IF ifCondition statement ELSE statement')
|
237
|
+
# rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt')
|
238
|
+
def reduce_if_else_stmt(_production, _range, tokens, theChildren)
|
239
|
+
condition = theChildren[1]
|
240
|
+
then_stmt = theChildren[2]
|
241
|
+
else_stmt = theChildren[4]
|
242
|
+
LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
|
243
|
+
end
|
244
|
+
|
245
|
+
# rule('unbalancedStmt' => 'IF ifCondition stmt').as ''
|
237
246
|
def reduce_if_stmt(_production, _range, tokens, theChildren)
|
238
247
|
condition = theChildren[1]
|
239
248
|
then_stmt = theChildren[2]
|
240
|
-
else_stmt =
|
249
|
+
else_stmt = nil
|
241
250
|
LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
|
242
251
|
end
|
243
252
|
|
@@ -269,10 +278,22 @@ module Loxxy
|
|
269
278
|
|
270
279
|
# rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment')
|
271
280
|
def reduce_assign_expr(_production, _range, tokens, theChildren)
|
272
|
-
|
273
|
-
|
281
|
+
name_assignee = theChildren[1].token.lexeme.dup
|
282
|
+
if theChildren[0].kind_of?(Ast::LoxSetExpr)
|
283
|
+
theChildren[0].property = name_assignee
|
284
|
+
theChildren[0].subnodes << theChildren[3]
|
285
|
+
theChildren[0]
|
286
|
+
else
|
287
|
+
Ast::LoxAssignExpr.new(tokens[1].position, name_assignee, theChildren[3])
|
288
|
+
end
|
274
289
|
end
|
275
290
|
|
291
|
+
# rule('owner_opt' => 'call DOT')
|
292
|
+
def reduce_set_expr(_production, _range, tokens, theChildren)
|
293
|
+
Ast::LoxSetExpr.new(tokens[1].position, theChildren[0])
|
294
|
+
end
|
295
|
+
|
296
|
+
|
276
297
|
# rule('comparisonTest_plus' => 'comparisonTest_plus comparisonTest term').as 'comparison_t_plus_more'
|
277
298
|
# TODO: is it meaningful to implement this rule?
|
278
299
|
|
@@ -317,6 +338,11 @@ module Loxxy
|
|
317
338
|
LoxCallExpr.new(tokens[0].position, args)
|
318
339
|
end
|
319
340
|
|
341
|
+
# rule('refinement' => 'DOT IDENTIFIER').as 'get_expr'
|
342
|
+
def reduce_get_expr(_production, _range, tokens, theChildren)
|
343
|
+
LoxGetExpr.new(tokens[0].position, theChildren[1].token.lexeme)
|
344
|
+
end
|
345
|
+
|
320
346
|
# rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN')
|
321
347
|
def reduce_grouping_expr(_production, _range, tokens, theChildren)
|
322
348
|
subexpr = theChildren[1]
|
@@ -337,6 +363,11 @@ module Loxxy
|
|
337
363
|
LoxVariableExpr.new(tokens[0].position, var_name)
|
338
364
|
end
|
339
365
|
|
366
|
+
# rule('primary' => 'THIS')
|
367
|
+
def reduce_this_expr(_production, _range, tokens, _children)
|
368
|
+
LoxThisExpr.new(tokens[0].position)
|
369
|
+
end
|
370
|
+
|
340
371
|
# rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
|
341
372
|
def reduce_function(_production, _range, _tokens, theChildren)
|
342
373
|
first_child = theChildren.first
|
@@ -68,7 +68,7 @@ module Loxxy
|
|
68
68
|
end
|
69
69
|
|
70
70
|
# Visit event. The visitor is about to visit a class declaration.
|
71
|
-
# @param
|
71
|
+
# @param aClassStmt [AST::LOXClassStmt] the for statement node to visit
|
72
72
|
def visit_class_stmt(aClassStmt)
|
73
73
|
broadcast(:before_class_stmt, aClassStmt)
|
74
74
|
traverse_subnodes(aClassStmt) # The methods are visited here...
|
@@ -131,6 +131,13 @@ module Loxxy
|
|
131
131
|
broadcast(:after_assign_expr, anAssignExpr, self)
|
132
132
|
end
|
133
133
|
|
134
|
+
# @param aSetExpr [AST::LOXGetExpr] the get expression node to visit
|
135
|
+
def visit_set_expr(aSetExpr)
|
136
|
+
broadcast(:before_set_expr, aSetExpr)
|
137
|
+
traverse_subnodes(aSetExpr)
|
138
|
+
broadcast(:after_set_expr, aSetExpr, self)
|
139
|
+
end
|
140
|
+
|
134
141
|
# Visit event. The visitor is about to visit a logical expression.
|
135
142
|
# Since logical expressions may take shorcuts by not evaluating all their
|
136
143
|
# sub-expressiosns, they are responsible for visiting or not their children.
|
@@ -169,6 +176,12 @@ module Loxxy
|
|
169
176
|
broadcast(:after_call_expr, aCallExpr, self)
|
170
177
|
end
|
171
178
|
|
179
|
+
# @param aGetExpr [AST::LOXGetExpr] the get expression node to visit
|
180
|
+
def visit_get_expr(aGetExpr)
|
181
|
+
broadcast(:before_get_expr, aGetExpr)
|
182
|
+
broadcast(:after_get_expr, aGetExpr, self)
|
183
|
+
end
|
184
|
+
|
172
185
|
# Visit event. The visitor is about to visit a grouping expression.
|
173
186
|
# @param aGroupingExpr [AST::LoxGroupingExpr] grouping expression to visit
|
174
187
|
def visit_grouping_expr(aGroupingExpr)
|
@@ -192,6 +205,13 @@ module Loxxy
|
|
192
205
|
broadcast(:after_variable_expr, aVariableExpr, self)
|
193
206
|
end
|
194
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
|
+
|
195
215
|
# Visit event. The visitor is about to visit the given terminal datatype value.
|
196
216
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
197
217
|
def visit_builtin(aValue)
|
@@ -203,7 +223,6 @@ module Loxxy
|
|
203
223
|
# @param aFunStmt [AST::LoxFunStmt] function declaration to visit
|
204
224
|
def visit_fun_stmt(aFunStmt)
|
205
225
|
broadcast(:before_fun_stmt, aFunStmt, self)
|
206
|
-
traverse_subnodes(aFunStmt)
|
207
226
|
broadcast(:after_fun_stmt, aFunStmt, self)
|
208
227
|
end
|
209
228
|
|
@@ -5,14 +5,19 @@ require_relative 'lox_compound_expr'
|
|
5
5
|
module Loxxy
|
6
6
|
module Ast
|
7
7
|
class LoxClassStmt < LoxCompoundExpr
|
8
|
+
# @return [String] the class name
|
8
9
|
attr_reader :name
|
9
10
|
|
11
|
+
# @return [Array<Ast::LoxFunStmt>] the methods
|
12
|
+
attr_reader :body
|
13
|
+
|
10
14
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
11
15
|
# @param condExpr [Loxxy::Ast::LoxNode] iteration condition
|
12
|
-
# @param theBody [Loxxy::Ast::LoxNode]
|
16
|
+
# @param theBody [Array<Loxxy::Ast::LoxNode>]
|
13
17
|
def initialize(aPosition, aName, theMethods)
|
14
|
-
super(aPosition,
|
18
|
+
super(aPosition, [])
|
15
19
|
@name = aName.dup
|
20
|
+
@body = theMethods
|
16
21
|
end
|
17
22
|
|
18
23
|
# Part of the 'visitee' role in Visitor design pattern.
|
@@ -20,8 +25,6 @@ module Loxxy
|
|
20
25
|
def accept(visitor)
|
21
26
|
visitor.visit_class_stmt(self)
|
22
27
|
end
|
23
|
-
|
24
|
-
alias body subnodes
|
25
28
|
end # class
|
26
29
|
end # module
|
27
30
|
end # module
|
@@ -1,24 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'lox_node'
|
4
4
|
|
5
5
|
module Loxxy
|
6
6
|
module Ast
|
7
7
|
# rubocop: disable Style/AccessorGrouping
|
8
|
-
class LoxFunStmt <
|
8
|
+
class LoxFunStmt < LoxNode
|
9
9
|
attr_reader :name
|
10
10
|
attr_reader :params
|
11
11
|
attr_reader :body
|
12
|
+
attr_accessor :is_method
|
12
13
|
|
13
14
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
14
15
|
# @param aName [String]
|
15
16
|
# @param arguments [Array<String>]
|
16
17
|
# @param body [Ast::LoxBlockStmt]
|
17
18
|
def initialize(aPosition, aName, paramList, aBody)
|
18
|
-
super(aPosition
|
19
|
+
super(aPosition)
|
19
20
|
@name = aName.dup
|
20
21
|
@params = paramList
|
21
22
|
@body = aBody
|
23
|
+
@is_method = false
|
22
24
|
end
|
23
25
|
|
24
26
|
# Part of the 'visitee' role in Visitor design pattern.
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_node'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxGetExpr < LoxNode
|
8
|
+
# @return [Ast::LoxNode] the object to which the property belongs to
|
9
|
+
attr_accessor :object
|
10
|
+
|
11
|
+
# @return [String] Name of an object property
|
12
|
+
attr_reader :property
|
13
|
+
|
14
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
15
|
+
# @param aPropertyName [String] Name of an object property
|
16
|
+
def initialize(aPosition, aPropertyName)
|
17
|
+
super(aPosition)
|
18
|
+
@property = aPropertyName
|
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_get_expr(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias callee= object=
|
28
|
+
end # class
|
29
|
+
end # module
|
30
|
+
end # module
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxSetExpr < LoxCompoundExpr
|
8
|
+
# @return [Ast::LoxNode] the object to which the property belongs to
|
9
|
+
attr_reader :object
|
10
|
+
|
11
|
+
# @return [String] Name of an object property
|
12
|
+
attr_accessor :property
|
13
|
+
|
14
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
15
|
+
# @param anObject [Ast::LoxNode] The object which the given property is being set
|
16
|
+
def initialize(aPosition, anObject)
|
17
|
+
super(aPosition, [])
|
18
|
+
@object = anObject
|
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_set_expr(self)
|
25
|
+
end
|
26
|
+
end # class
|
27
|
+
end # module
|
28
|
+
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
|
@@ -69,24 +69,27 @@ module Loxxy
|
|
69
69
|
# Do nothing, subnodes were already evaluated
|
70
70
|
end
|
71
71
|
|
72
|
-
def after_class_stmt(aClassStmt,
|
73
|
-
|
72
|
+
def after_class_stmt(aClassStmt, aVisitor)
|
73
|
+
# Convert LoxFunStmt into LoxFunction
|
74
|
+
meths = aClassStmt.body.map do |func_node|
|
75
|
+
func_node.is_method = true
|
76
|
+
func_node.accept(aVisitor)
|
77
|
+
mth = stack.pop
|
78
|
+
mth.is_initializer = true if mth.name == 'init'
|
79
|
+
mth
|
80
|
+
end
|
81
|
+
|
82
|
+
klass = LoxClass.new(aClassStmt.name, meths, self)
|
74
83
|
new_var = Variable.new(aClassStmt.name, klass)
|
75
84
|
symbol_table.insert(new_var)
|
76
85
|
end
|
77
86
|
|
78
|
-
def
|
87
|
+
def after_var_stmt(aVarStmt)
|
79
88
|
new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
|
80
89
|
symbol_table.insert(new_var)
|
81
|
-
end
|
82
|
-
|
83
|
-
def after_var_stmt(aVarStmt)
|
84
|
-
var_name = aVarStmt.name
|
85
|
-
variable = symbol_table.lookup(var_name)
|
86
|
-
raise StandardError, "Unknown variable #{var_name}" unless variable
|
87
90
|
|
88
91
|
value = stack.pop
|
89
|
-
|
92
|
+
new_var.assign(value)
|
90
93
|
end
|
91
94
|
|
92
95
|
def before_for_stmt(aForStmt)
|
@@ -153,6 +156,18 @@ module Loxxy
|
|
153
156
|
variable.assign(value)
|
154
157
|
end
|
155
158
|
|
159
|
+
def after_set_expr(aSetExpr, aVisitor)
|
160
|
+
value = stack.pop
|
161
|
+
# Evaluate object part
|
162
|
+
aSetExpr.object.accept(aVisitor)
|
163
|
+
assignee = stack.pop
|
164
|
+
unless assignee.kind_of?(LoxInstance)
|
165
|
+
raise StandardError, 'Only instances have fields.'
|
166
|
+
end
|
167
|
+
|
168
|
+
assignee.set(aSetExpr.property, value)
|
169
|
+
end
|
170
|
+
|
156
171
|
def after_logical_expr(aLogicalExpr, visitor)
|
157
172
|
op = aLogicalExpr.operator
|
158
173
|
operand1 = stack.pop # only first operand was evaluated
|
@@ -221,20 +236,28 @@ module Loxxy
|
|
221
236
|
case callee
|
222
237
|
when NativeFunction
|
223
238
|
stack.push callee.call # Pass arguments
|
224
|
-
when LoxFunction
|
239
|
+
when LoxFunction, LoxClass
|
225
240
|
arg_count = aCallExpr.arguments.size
|
226
241
|
if arg_count != callee.arity
|
227
242
|
msg = "Expected #{callee.arity} arguments but got #{arg_count}."
|
228
243
|
raise Loxxy::RuntimeError, msg
|
229
244
|
end
|
230
245
|
callee.call(self, aVisitor)
|
231
|
-
when LoxClass
|
232
|
-
callee.call(self, aVisitor)
|
233
246
|
else
|
234
247
|
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
235
248
|
end
|
236
249
|
end
|
237
250
|
|
251
|
+
def after_get_expr(aGetExpr, aVisitor)
|
252
|
+
aGetExpr.object.accept(aVisitor)
|
253
|
+
instance = stack.pop
|
254
|
+
unless instance.kind_of?(LoxInstance)
|
255
|
+
raise StandardError, 'Only instances have properties.'
|
256
|
+
end
|
257
|
+
|
258
|
+
stack.push instance.get(aGetExpr.property)
|
259
|
+
end
|
260
|
+
|
238
261
|
def after_grouping_expr(_groupingExpr)
|
239
262
|
# Do nothing: work was already done by visiting /evaluating the subexpression
|
240
263
|
end
|
@@ -242,7 +265,7 @@ module Loxxy
|
|
242
265
|
def after_variable_expr(aVarExpr, aVisitor)
|
243
266
|
var_name = aVarExpr.name
|
244
267
|
var = variable_lookup(aVarExpr)
|
245
|
-
raise StandardError, "
|
268
|
+
raise StandardError, "Undefined variable '#{var_name}'." unless var
|
246
269
|
|
247
270
|
var.value.accept(aVisitor) # Evaluate variable value then push on stack
|
248
271
|
end
|
@@ -252,6 +275,11 @@ module Loxxy
|
|
252
275
|
stack.push(literalExpr.literal)
|
253
276
|
end
|
254
277
|
|
278
|
+
def after_this_expr(aThisExpr, aVisitor)
|
279
|
+
var = variable_lookup(aThisExpr)
|
280
|
+
var.value.accept(aVisitor) # Evaluate this value then push on stack
|
281
|
+
end
|
282
|
+
|
255
283
|
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
256
284
|
def before_visit_builtin(aValue)
|
257
285
|
stack.push(aValue)
|
@@ -259,8 +287,12 @@ module Loxxy
|
|
259
287
|
|
260
288
|
def after_fun_stmt(aFunStmt, _visitor)
|
261
289
|
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
|
262
|
-
|
263
|
-
|
290
|
+
if aFunStmt.is_method
|
291
|
+
stack.push function
|
292
|
+
else
|
293
|
+
new_var = Variable.new(aFunStmt.name, function)
|
294
|
+
symbol_table.insert(new_var)
|
295
|
+
end
|
264
296
|
end
|
265
297
|
|
266
298
|
private
|
@@ -10,15 +10,18 @@ module Loxxy
|
|
10
10
|
# @return [String] The name of the class
|
11
11
|
attr_reader :name
|
12
12
|
|
13
|
-
# @return [
|
14
|
-
attr_reader :
|
13
|
+
# @return [Hash{String => LoxFunction}] the list of methods
|
14
|
+
attr_reader :meths
|
15
15
|
attr_reader :stack
|
16
16
|
|
17
17
|
# Create a class with given name
|
18
18
|
# @param aName [String] The name of the class
|
19
19
|
def initialize(aName, theMethods, anEngine)
|
20
20
|
@name = aName.dup
|
21
|
-
@
|
21
|
+
@meths = {}
|
22
|
+
theMethods.each do |func|
|
23
|
+
meths[func.name] = func
|
24
|
+
end
|
22
25
|
@stack = anEngine.stack
|
23
26
|
end
|
24
27
|
|
@@ -27,14 +30,26 @@ module Loxxy
|
|
27
30
|
end
|
28
31
|
|
29
32
|
def arity
|
30
|
-
|
33
|
+
initializer = find_method('init')
|
34
|
+
initializer ? initializer.arity : 0
|
31
35
|
end
|
32
36
|
|
33
|
-
def call(engine,
|
37
|
+
def call(engine, visitor)
|
34
38
|
instance = LoxInstance.new(self, engine)
|
39
|
+
initializer = find_method('init')
|
40
|
+
if initializer
|
41
|
+
constructor = initializer.bind(instance)
|
42
|
+
constructor.call(engine, visitor)
|
43
|
+
end
|
44
|
+
|
35
45
|
engine.stack.push(instance)
|
36
46
|
end
|
37
47
|
|
48
|
+
# @param aName [String] the method name to search for
|
49
|
+
def find_method(aName)
|
50
|
+
meths[aName]
|
51
|
+
end
|
52
|
+
|
38
53
|
# Logical negation.
|
39
54
|
# As a function is a truthy thing, its negation is thus false.
|
40
55
|
# @return [Datatype::False]
|
@@ -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]
|
@@ -6,26 +6,49 @@ module Loxxy
|
|
6
6
|
module BackEnd
|
7
7
|
# Runtime representation of a Lox object (instance).
|
8
8
|
class LoxInstance
|
9
|
-
# @return BackEnd::LoxClass] the class
|
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
|
+
|
14
|
+
# @return [Hash{String => BuiltinDatatype | LoxFunction | LoxInstance }]
|
15
|
+
attr_reader :fields
|
13
16
|
|
14
17
|
# Create an instance from given class
|
15
18
|
# @param aClass [BackEnd::LoxClass] the class this this object belong
|
16
19
|
def initialize(aClass, anEngine)
|
17
20
|
@klass = aClass
|
18
|
-
@
|
21
|
+
@engine = anEngine
|
22
|
+
@fields = {}
|
19
23
|
end
|
20
24
|
|
21
25
|
def accept(_visitor)
|
22
|
-
stack.push self
|
26
|
+
engine.stack.push self
|
23
27
|
end
|
24
28
|
|
25
29
|
# Text representation of a Lox instance
|
26
30
|
def to_str
|
27
31
|
"#{klass.to_str} instance"
|
28
32
|
end
|
33
|
+
|
34
|
+
# Look up the value of property with given name
|
35
|
+
# aName [String] name of object property
|
36
|
+
def get(aName)
|
37
|
+
return fields[aName] if fields.include? aName
|
38
|
+
|
39
|
+
method = klass.find_method(aName)
|
40
|
+
unless method
|
41
|
+
raise StandardError, "Undefined property '#{aName}'."
|
42
|
+
end
|
43
|
+
|
44
|
+
method.bind(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the value of property with given name
|
48
|
+
# aName [String] name of object property
|
49
|
+
def set(aName, aValue)
|
50
|
+
fields[aName] = aValue
|
51
|
+
end
|
29
52
|
end # class
|
30
53
|
end # module
|
31
54
|
end # module
|
@@ -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
|
@@ -58,8 +63,18 @@ module Loxxy
|
|
58
63
|
declare(aClassStmt.name)
|
59
64
|
end
|
60
65
|
|
61
|
-
def after_class_stmt(aClassStmt,
|
66
|
+
def after_class_stmt(aClassStmt, aVisitor)
|
67
|
+
previous_class = current_class
|
68
|
+
@current_class = :class
|
62
69
|
define(aClassStmt.name)
|
70
|
+
begin_scope
|
71
|
+
define('this')
|
72
|
+
aClassStmt.body.each do |fun_stmt|
|
73
|
+
mth_type = fun_stmt.name == 'init' ? :initializer : :method
|
74
|
+
resolve_function(fun_stmt, mth_type, aVisitor)
|
75
|
+
end
|
76
|
+
end_scope
|
77
|
+
@current_class = previous_class
|
63
78
|
end
|
64
79
|
|
65
80
|
def before_for_stmt(aForStmt)
|
@@ -78,7 +93,7 @@ module Loxxy
|
|
78
93
|
anIfStmt.else_stmt&.accept(aVisitor)
|
79
94
|
end
|
80
95
|
|
81
|
-
def before_return_stmt(
|
96
|
+
def before_return_stmt(returnStmt)
|
82
97
|
if scopes.size < 2
|
83
98
|
msg = "Error at 'return': Can't return from top-level code."
|
84
99
|
raise StandardError, msg
|
@@ -88,6 +103,11 @@ module Loxxy
|
|
88
103
|
msg = "Error at 'return': Can't return from outside a function."
|
89
104
|
raise StandardError, msg
|
90
105
|
end
|
106
|
+
|
107
|
+
if current_function == :initializer
|
108
|
+
msg = "Error at 'return': Can't return a value from an initializer."
|
109
|
+
raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
|
110
|
+
end
|
91
111
|
end
|
92
112
|
|
93
113
|
def after_while_stmt(aWhileStmt, aVisitor)
|
@@ -109,6 +129,11 @@ module Loxxy
|
|
109
129
|
resolve_local(anAssignExpr, aVisitor)
|
110
130
|
end
|
111
131
|
|
132
|
+
def after_set_expr(aSetExpr, aVisitor)
|
133
|
+
# Evaluate object part
|
134
|
+
aSetExpr.object.accept(aVisitor)
|
135
|
+
end
|
136
|
+
|
112
137
|
# Variable expressions require their variables resolved
|
113
138
|
def before_variable_expr(aVarExpr)
|
114
139
|
var_name = aVarExpr.name
|
@@ -127,6 +152,23 @@ module Loxxy
|
|
127
152
|
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
128
153
|
end
|
129
154
|
|
155
|
+
def after_get_expr(aGetExpr, aVisitor)
|
156
|
+
# Evaluate object part
|
157
|
+
aGetExpr.object.accept(aVisitor)
|
158
|
+
end
|
159
|
+
|
160
|
+
def before_this_expr(_thisExpr)
|
161
|
+
if current_class == :none
|
162
|
+
msg = "Error at 'this': Can't use 'this' outside of a class."
|
163
|
+
raise StandardError, msg
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def after_this_expr(aThisExpr, aVisitor)
|
168
|
+
# 'this' behaves closely to a local variable
|
169
|
+
resolve_local(aThisExpr, aVisitor)
|
170
|
+
end
|
171
|
+
|
130
172
|
# function declaration creates a new scope for its body & binds its parameters for that scope
|
131
173
|
def before_fun_stmt(aFunStmt, aVisitor)
|
132
174
|
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,7 +35,7 @@ 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
41
|
rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
@@ -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'
|
@@ -87,7 +89,7 @@ module Loxxy
|
|
87
89
|
rule('expression' => 'assignment')
|
88
90
|
rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment').as 'assign_expr'
|
89
91
|
rule('assignment' => 'logic_or')
|
90
|
-
rule('owner_opt' => 'call DOT')
|
92
|
+
rule('owner_opt' => 'call DOT').as 'set_expr'
|
91
93
|
rule('owner_opt' => [])
|
92
94
|
rule('logic_or' => 'logic_and')
|
93
95
|
rule('logic_or' => 'logic_and disjunct_plus').as 'logical_expr'
|
@@ -132,11 +134,11 @@ module Loxxy
|
|
132
134
|
rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
|
133
135
|
rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
|
134
136
|
rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
|
135
|
-
rule('refinement' => 'DOT IDENTIFIER')
|
137
|
+
rule('refinement' => 'DOT IDENTIFIER').as 'get_expr'
|
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'
|
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
@@ -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
|
|
@@ -305,6 +305,83 @@ LOX_END
|
|
305
305
|
expect(expr.operands[1].operands[1].literal.value).to eq(5)
|
306
306
|
end
|
307
307
|
end # context
|
308
|
+
|
309
|
+
context 'Object orientation:' do
|
310
|
+
it 'should parse object property get access' do
|
311
|
+
input = 'print someObject.someProperty;'
|
312
|
+
ptree = subject.parse(input)
|
313
|
+
expr = ptree.root.subnodes[0]
|
314
|
+
expect(expr).to be_kind_of(Ast::LoxPrintStmt)
|
315
|
+
get_expr = expr.subnodes[0]
|
316
|
+
expect(get_expr).to be_kind_of(Ast::LoxGetExpr)
|
317
|
+
expect(get_expr.object.name).to eq('someObject')
|
318
|
+
expect(get_expr.property).to eq('someProperty')
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'should parse nested call expressions' do
|
322
|
+
input = 'print egg.scramble(3).with(cheddar);'
|
323
|
+
# From section 12.3.1, one expects something like:
|
324
|
+
# LoxCallExpr
|
325
|
+
# +- arguments = ['cheddar']
|
326
|
+
# +- callee = LoxGetExpr
|
327
|
+
# +- property = 'with'
|
328
|
+
# +- object = LoxCallExpr
|
329
|
+
# +- arguments = [3]
|
330
|
+
# +- callee = LoxGetExpr
|
331
|
+
# +- property = 'scramble'
|
332
|
+
# +- object = variable 'egg'
|
333
|
+
ptree = subject.parse(input)
|
334
|
+
print_stmt = ptree.root.subnodes[0]
|
335
|
+
expect(print_stmt).to be_kind_of(Ast::LoxPrintStmt)
|
336
|
+
outer_call = print_stmt.subnodes[0]
|
337
|
+
expect(outer_call).to be_kind_of(Ast::LoxCallExpr)
|
338
|
+
expect(outer_call.arguments[0].name).to eq('cheddar')
|
339
|
+
expect(outer_call.callee).to be_kind_of(Ast::LoxGetExpr)
|
340
|
+
expect(outer_call.callee.property).to eq('with')
|
341
|
+
inner_call = outer_call.callee.object
|
342
|
+
expect(inner_call).to be_kind_of(Ast::LoxCallExpr)
|
343
|
+
expect(inner_call.arguments[0].literal).to eq(3)
|
344
|
+
expect(inner_call.callee).to be_kind_of(Ast::LoxGetExpr)
|
345
|
+
expect(inner_call.callee.property).to eq('scramble')
|
346
|
+
expect(inner_call.callee.object.name).to eq('egg')
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'should parse object property set access' do
|
350
|
+
input = 'someObject.someProperty = value;'
|
351
|
+
ptree = subject.parse(input)
|
352
|
+
expr = ptree.root.subnodes[0]
|
353
|
+
expect(expr).to be_kind_of(Ast::LoxSetExpr)
|
354
|
+
expect(expr.object.name).to eq('someObject')
|
355
|
+
expect(expr.property).to eq('someProperty')
|
356
|
+
expect(expr.subnodes[0]).to be_kind_of(Ast::LoxVariableExpr)
|
357
|
+
expect(expr.subnodes[0].name).to eq('value')
|
358
|
+
end
|
359
|
+
|
360
|
+
it 'should parse complex set access' do
|
361
|
+
input = 'breakfast.omelette.filling.meat = ham;'
|
362
|
+
# From section 12.3.2, one expects something like:
|
363
|
+
# LoxSetExpr
|
364
|
+
# +- property = 'meat'
|
365
|
+
# +- subnodes[0] = LoxVariableExpr 'ham'
|
366
|
+
# +- object = LoxGetExpr
|
367
|
+
# +- property = 'filling'
|
368
|
+
# +- object = LoxGetExpr
|
369
|
+
# +- property = 'omelette'
|
370
|
+
# +- object = LoxVariableExpr 'breakfast'
|
371
|
+
ptree = subject.parse(input)
|
372
|
+
expr = ptree.root.subnodes[0]
|
373
|
+
expect(expr).to be_kind_of(Ast::LoxSetExpr)
|
374
|
+
expect(expr.property).to eq('meat')
|
375
|
+
expect(expr.subnodes[0]).to be_kind_of(Ast::LoxVariableExpr)
|
376
|
+
expect(expr.subnodes[0].name).to eq('ham')
|
377
|
+
expect(expr.object).to be_kind_of(Ast::LoxGetExpr)
|
378
|
+
expect(expr.object.property).to eq('filling')
|
379
|
+
expect(expr.object.object).to be_kind_of(Ast::LoxGetExpr)
|
380
|
+
expect(expr.object.object.property).to eq('omelette')
|
381
|
+
expect(expr.object.object.object).to be_kind_of(Ast::LoxVariableExpr)
|
382
|
+
expect(expr.object.object.object.name).to eq('breakfast')
|
383
|
+
end
|
384
|
+
end # context
|
308
385
|
end # describe
|
309
386
|
end # module
|
310
387
|
end # module
|
@@ -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
@@ -227,19 +227,20 @@ module Loxxy
|
|
227
227
|
# Evaluate the 'then' expression if the condition is true.
|
228
228
|
['if (true) print "then-branch";', 'then-branch'],
|
229
229
|
['if (false) print "ignored";', ''],
|
230
|
-
|
231
|
-
|
230
|
+
['if (nil) print "ignored";', ''],
|
231
|
+
['if (true) { print "block"; }', 'block'],
|
232
|
+
['var a = false; if (a = true) print a;', 'true'],
|
232
233
|
|
233
234
|
# Evaluate the 'else' expression if the condition is false.
|
234
235
|
['if (true) print "then-branch"; else print "else-branch";', 'then-branch'],
|
235
236
|
['if (false) print "then-branch"; else print "else-branch";', 'else-branch'],
|
236
237
|
['if (0) print "then-branch"; else print "else-branch";', 'then-branch'],
|
237
|
-
['if (nil) print "then-branch"; else print "else-branch";', 'else-branch']
|
238
|
-
|
238
|
+
['if (nil) print "then-branch"; else print "else-branch";', 'else-branch'],
|
239
|
+
['if (false) nil; else { print "else-branch"; }', 'else-branch'],
|
239
240
|
|
240
|
-
#
|
241
|
-
|
242
|
-
|
241
|
+
# A dangling else binds to the right-most if.
|
242
|
+
['if (true) if (false) print "bad"; else print "good";', 'good'],
|
243
|
+
['if (false) if (true) print "bad"; else print "worse";', '']
|
243
244
|
].each do |(source, predicted)|
|
244
245
|
io = StringIO.new
|
245
246
|
cfg = { ostream: io }
|
@@ -451,44 +452,144 @@ LOX_END
|
|
451
452
|
end
|
452
453
|
# rubocop: enable Style/StringConcatenation
|
453
454
|
|
454
|
-
it 'should
|
455
|
+
it 'should print the hello world message' do
|
455
456
|
program = <<-LOX_END
|
457
|
+
var greeting = "Hello"; // Declaring a variable
|
458
|
+
print greeting + ", " + "world!"; // ... Playing with concatenation
|
459
|
+
LOX_END
|
460
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
461
|
+
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|
462
|
+
end
|
463
|
+
end # context
|
464
|
+
|
465
|
+
context 'Object orientation:' do
|
466
|
+
let(:duck_class) do
|
467
|
+
snippet = <<-LOX_END
|
456
468
|
class Duck {
|
457
469
|
noise() {
|
458
|
-
quack();
|
470
|
+
this.quack();
|
459
471
|
}
|
460
472
|
|
461
473
|
quack() {
|
462
474
|
print "quack";
|
463
475
|
}
|
464
476
|
}
|
465
|
-
|
477
|
+
LOX_END
|
478
|
+
|
479
|
+
snippet
|
480
|
+
end
|
481
|
+
|
482
|
+
it 'should support class declaration' do
|
483
|
+
program = <<-LOX_END
|
484
|
+
#{duck_class}
|
485
|
+
|
486
|
+
print Duck; // Class names can appear in statements
|
466
487
|
LOX_END
|
467
488
|
expect { subject.evaluate(program) }.not_to raise_error
|
468
489
|
expect(sample_cfg[:ostream].string).to eq('Duck')
|
469
490
|
end
|
470
491
|
|
471
|
-
it 'should support instance creation' do
|
492
|
+
it 'should support default instance creation' do
|
472
493
|
program = <<-LOX_END
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
}
|
477
|
-
}
|
478
|
-
var daffy = Duck();
|
494
|
+
#{duck_class}
|
495
|
+
|
496
|
+
var daffy = Duck(); // Default constructor
|
479
497
|
print daffy;
|
480
498
|
LOX_END
|
481
499
|
expect { subject.evaluate(program) }.not_to raise_error
|
482
500
|
expect(sample_cfg[:ostream].string).to eq('Duck instance')
|
483
501
|
end
|
484
502
|
|
485
|
-
it 'should
|
503
|
+
it 'should support calls to method' do
|
486
504
|
program = <<-LOX_END
|
487
|
-
|
488
|
-
|
505
|
+
#{duck_class}
|
506
|
+
|
507
|
+
var daffy = Duck(); // Default constructor
|
508
|
+
daffy.quack();
|
489
509
|
LOX_END
|
490
510
|
expect { subject.evaluate(program) }.not_to raise_error
|
491
|
-
expect(sample_cfg[:ostream].string).to eq('
|
511
|
+
expect(sample_cfg[:ostream].string).to eq('quack')
|
512
|
+
end
|
513
|
+
|
514
|
+
it "should support the 'this' keyword" do
|
515
|
+
program = <<-LOX_END
|
516
|
+
class Egotist {
|
517
|
+
speak() {
|
518
|
+
print this;
|
519
|
+
}
|
520
|
+
}
|
521
|
+
|
522
|
+
var method = Egotist().speak;
|
523
|
+
method(); // Output: Egotist instance
|
524
|
+
LOX_END
|
525
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
526
|
+
expect(sample_cfg[:ostream].string).to eq('Egotist instance')
|
527
|
+
end
|
528
|
+
|
529
|
+
it 'should support a closure nested in a method' do
|
530
|
+
lox_snippet = <<-LOX_END
|
531
|
+
class Foo {
|
532
|
+
getClosure() {
|
533
|
+
fun closure() {
|
534
|
+
return this.toString();
|
535
|
+
}
|
536
|
+
return closure;
|
537
|
+
}
|
538
|
+
|
539
|
+
toString() { return "foo"; }
|
540
|
+
}
|
541
|
+
|
542
|
+
var closure = Foo().getClosure();
|
543
|
+
closure;
|
544
|
+
LOX_END
|
545
|
+
# Expected result: Backend::LoxFunction('closure')
|
546
|
+
# Expected function's closure (environment layout):
|
547
|
+
# Environment('global')
|
548
|
+
# defns
|
549
|
+
# +- ['clock'] => BackEnd::Engine::NativeFunction
|
550
|
+
# *- ['Foo'] => BackEnd::LoxClass
|
551
|
+
# Environment
|
552
|
+
# defns
|
553
|
+
# ['this'] => BackEnd::LoxInstance
|
554
|
+
# Environment
|
555
|
+
# defns
|
556
|
+
# +- ['closure'] => Backend::LoxFunction
|
557
|
+
result = subject.evaluate(lox_snippet)
|
558
|
+
expect(result).to be_kind_of(BackEnd::LoxFunction)
|
559
|
+
expect(result.name).to eq('closure')
|
560
|
+
closure = result.closure
|
561
|
+
expect(closure).to be_kind_of(Loxxy::BackEnd::Environment)
|
562
|
+
expect(closure.defns['closure'].value).to eq(result)
|
563
|
+
expect(closure.enclosing).to be_kind_of(Loxxy::BackEnd::Environment)
|
564
|
+
expect(closure.enclosing.defns['this'].value).to be_kind_of(Loxxy::BackEnd::LoxInstance)
|
565
|
+
global_env = closure.enclosing.enclosing
|
566
|
+
expect(global_env).to be_kind_of(Loxxy::BackEnd::Environment)
|
567
|
+
expect(global_env.defns['clock'].value).to be_kind_of(BackEnd::Engine::NativeFunction)
|
568
|
+
expect(global_env.defns['Foo'].value).to be_kind_of(BackEnd::LoxClass)
|
569
|
+
end
|
570
|
+
|
571
|
+
it 'should support custom initializer' do
|
572
|
+
lox_snippet = <<-LOX_END
|
573
|
+
// From section 3.9.5
|
574
|
+
class Breakfast {
|
575
|
+
init(meat, bread) {
|
576
|
+
this.meat = meat;
|
577
|
+
this.bread = bread;
|
578
|
+
}
|
579
|
+
|
580
|
+
serve(who) {
|
581
|
+
print "Enjoy your " + this.meat + " and " +
|
582
|
+
this.bread + ", " + who + ".";
|
583
|
+
}
|
584
|
+
}
|
585
|
+
|
586
|
+
var baconAndToast = Breakfast("bacon", "toast");
|
587
|
+
baconAndToast.serve("Dear Reader");
|
588
|
+
// Output: "Enjoy your bacon and toast, Dear Reader."
|
589
|
+
LOX_END
|
590
|
+
expect { subject.evaluate(lox_snippet) }.not_to raise_error
|
591
|
+
predicted = 'Enjoy your bacon and toast, Dear Reader.'
|
592
|
+
expect(sample_cfg[:ostream].string).to eq(predicted)
|
492
593
|
end
|
493
594
|
end # context
|
494
595
|
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.17
|
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-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- lib/loxxy/ast/lox_compound_expr.rb
|
98
98
|
- lib/loxxy/ast/lox_for_stmt.rb
|
99
99
|
- lib/loxxy/ast/lox_fun_stmt.rb
|
100
|
+
- lib/loxxy/ast/lox_get_expr.rb
|
100
101
|
- lib/loxxy/ast/lox_grouping_expr.rb
|
101
102
|
- lib/loxxy/ast/lox_if_stmt.rb
|
102
103
|
- lib/loxxy/ast/lox_literal_expr.rb
|
@@ -106,6 +107,8 @@ files:
|
|
106
107
|
- lib/loxxy/ast/lox_print_stmt.rb
|
107
108
|
- lib/loxxy/ast/lox_return_stmt.rb
|
108
109
|
- lib/loxxy/ast/lox_seq_decl.rb
|
110
|
+
- lib/loxxy/ast/lox_set_expr.rb
|
111
|
+
- lib/loxxy/ast/lox_this_expr.rb
|
109
112
|
- lib/loxxy/ast/lox_unary_expr.rb
|
110
113
|
- lib/loxxy/ast/lox_var_stmt.rb
|
111
114
|
- lib/loxxy/ast/lox_variable_expr.rb
|