loxxy 0.2.03 → 0.3.01
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 +68 -0
- data/README.md +6 -0
- data/lib/loxxy/ast/all_lox_nodes.rb +0 -1
- data/lib/loxxy/ast/ast_builder.rb +27 -4
- data/lib/loxxy/ast/ast_visitor.rb +1 -9
- data/lib/loxxy/back_end/engine.rb +18 -20
- data/lib/loxxy/back_end/resolver.rb +17 -21
- data/lib/loxxy/datatype/number.rb +19 -4
- data/lib/loxxy/front_end/parser.rb +1 -1
- data/lib/loxxy/front_end/scanner.rb +10 -12
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/environment_spec.rb +0 -14
- data/spec/back_end/symbol_table_spec.rb +0 -19
- data/spec/back_end/variable_spec.rb +0 -35
- data/spec/front_end/scanner_spec.rb +35 -7
- data/spec/interpreter_spec.rb +24 -0
- metadata +2 -3
- data/lib/loxxy/ast/lox_for_stmt.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6a77cf0f7f4cd0c445d597c534ecc1de82a31222d54a3d0da30330ccbcb216e
|
|
4
|
+
data.tar.gz: c10cab44247a3f7d75382c7a6bbfbc7938bce368ba14ea48878643ea7778aca5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f76a5a66ff5ecdc76207802e195c7becdaf214cb687f9a62be1b58dc56d3ea0a9b3320bb2b8a105a72e3ff79801e3065eb3c10cab6246de2a4a5e74f4ebb722
|
|
7
|
+
data.tar.gz: eafb0e81fa6c6b5c8c3c2a017907b14e3a4ebadd04ec51cb9d79030905562e86ecf37d9e68511ff52ab4ae45eb598f8c7de4fa6344f90034f21635b8f2c983ff
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
+
## [0.3.01] - 2021-05-08
|
|
2
|
+
- Fix in `Scanner` class, added more tests in test suite.
|
|
3
|
+
|
|
4
|
+
### New
|
|
5
|
+
- Added the new subfolder `extra` under `test_suite`. It will contain tests for non-standard features or tests not covered in standard test suite.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Class `FrontEnd::Scanner`: Couldn't correctly recognize a plus preceding directly a number literal
|
|
9
|
+
|
|
10
|
+
## [0.3.00] - 2021-05-07
|
|
11
|
+
- Milestone: `Loxxy` passes all reference test suite.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Method `BackEnd::Resolver#before_variable_expr`: Standard `Lox` allows re-declaration of a variable at top-level scope
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.2.06] - 2021-05-04
|
|
18
|
+
- Nearly passing the 'official' test suite, fixing non-compliant behavior, specialized exceptions for errors
|
|
19
|
+
|
|
20
|
+
### New
|
|
21
|
+
- Module `LoxFileTester` module that hosts methods that simplify the tests of `Lox` source file.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Folder `test_suite` vastly reorganized. Sub-folder `baseline` contains spec files testing the `Lox` files from official implementation
|
|
25
|
+
- Class `BackEnd::Engine` replaced most `StandardError` by `Loxxy::RuntimeError` exception.
|
|
26
|
+
- Class `BackEnd::Resolver` replaced most `StandardError` by `Loxxy::RuntimeError` exception.
|
|
27
|
+
- Method `Datatype::Number#/` now handles correctly expression like `0/0` (integer divide)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- `0/0` expression results in a ZeroDivisionError exception, in Lox this result to a NaN (Not a Number). Now, `Loxxy` is aligned to standard `Lox`
|
|
31
|
+
- `FrontEnd::Scanner` now always treats expression like `-123` as the unary or binary minus operator applied to a positive number.
|
|
32
|
+
|
|
33
|
+
## [0.2.05] - 2021-04-26
|
|
34
|
+
- `Loxxy` now transforms for loops into while loops (desugaring), fix in Scanner class
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Method `Ast::ASTBuilder#reduce_for_stmt` converts 'for' loops into 'while' loops
|
|
38
|
+
- Method `Ast::ASTBuilder#reduce_for_control takes care of case for(expr1;;expr2) now test expression is set to true
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Method `FrontEnd::Scanner#next_token` keyword recognition was case insensitive
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
- Method `Ast::Visitor#visitor_for_stmt`
|
|
45
|
+
- Method `BackEnd::Engine#after_for_stmt`
|
|
46
|
+
- Method `BackEnd::Resolver#before_for_stmt`
|
|
47
|
+
- Method `BackEnd::Resolver#after_for_stmt`
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## [0.2.04] - 2021-04-25
|
|
51
|
+
- `Loxxy` passes the test suite for `for` statements
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Method `BackEnd::Engine#after_for_stmt` now a for(;;) executes the body once; a for without test will execute forever
|
|
55
|
+
- Method `BackEnd::Resolver#after_for_stmt now accepts nil test expression
|
|
56
|
+
|
|
57
|
+
## [0.2.03] - 2021-04-24
|
|
58
|
+
- Fixes for the set (field) expressions, `accept` methods for AST nodes are meta-programmed
|
|
59
|
+
|
|
60
|
+
### New
|
|
61
|
+
- Module `Ast::Visitee` provides the `define_accept` method that generate `accept` method with meta-programming
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- Method `BackEnd::Engine#before_set_expr` methos method that ensure that the receiver is evaluated first, then the assigned value
|
|
65
|
+
- Method `BackEnd::Engine#after_set_expr` now pushes the value assigned to the field also onto the stack
|
|
66
|
+
- Class `BackEnd::Engine` a number of StnadardError exceptions are replaced by Loxxy::RuntimeError
|
|
67
|
+
|
|
68
|
+
|
|
1
69
|
## [0.2.02] - 2021-04-21
|
|
2
70
|
- Improvements in the scanner class (escape sequence for quotes and newlines), error messages closer to jlox.
|
|
3
71
|
|
data/README.md
CHANGED
|
@@ -19,11 +19,17 @@ Although __Lox__ is fairly simple, it is far from being a toy language:
|
|
|
19
19
|
### Loxxy gem features
|
|
20
20
|
- Complete tree-walking interpreter including lexer, parser and resolver
|
|
21
21
|
- 100% pure Ruby with clean design (not a port from some other language)
|
|
22
|
+
- Passes the `jox` (THE reference `Lox` implementation) test suite
|
|
22
23
|
- Minimal runtime dependency (Rley gem). Won't drag a bunch of gems...
|
|
23
24
|
- Ruby API for integrating a Lox interpreter with your code.
|
|
24
25
|
- A command-line interpreter `loxxy`
|
|
25
26
|
- Open for your language extensions...
|
|
26
27
|
|
|
28
|
+
### Why `Loxxy` ?
|
|
29
|
+
- If programming languages are one of your subject interest...
|
|
30
|
+
- ... and you wanted learn how to implement one in Ruby...
|
|
31
|
+
- ... then `Loxxy` can help to understand and experiment in this rewarding craft.
|
|
32
|
+
|
|
27
33
|
## How to start in 1, 2, 3...?
|
|
28
34
|
... in less than 3 minutes...
|
|
29
35
|
|
|
@@ -19,7 +19,6 @@ require_relative 'lox_while_stmt'
|
|
|
19
19
|
require_relative 'lox_return_stmt'
|
|
20
20
|
require_relative 'lox_print_stmt'
|
|
21
21
|
require_relative 'lox_if_stmt'
|
|
22
|
-
require_relative 'lox_for_stmt'
|
|
23
22
|
require_relative 'lox_var_stmt'
|
|
24
23
|
require_relative 'lox_class_stmt'
|
|
25
24
|
require_relative 'lox_seq_decl'
|
|
@@ -225,16 +225,39 @@ module Loxxy
|
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
# rule('forStmt' => 'FOR LEFT_PAREN forControl RIGHT_PAREN statement')
|
|
228
|
-
def reduce_for_stmt(_production, _range,
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
def reduce_for_stmt(_production, _range, tokens, theChildren)
|
|
229
|
+
# Following 'Crafting Interpreters', we replace the for statement by a while loop
|
|
230
|
+
return theChildren[4] if theChildren[2].compact.empty? # for(;;) => execute body once
|
|
231
|
+
|
|
232
|
+
(init, test, update) = theChildren[2]
|
|
233
|
+
if update
|
|
234
|
+
new_body = LoxSeqDecl.new(tokens[0].position, [theChildren[4], update])
|
|
235
|
+
stmt = Ast::LoxBlockStmt.new(tokens[1].position, new_body)
|
|
236
|
+
else
|
|
237
|
+
stmt = theChildren[4]
|
|
238
|
+
end
|
|
239
|
+
while_stmt = Ast::LoxWhileStmt.new(tokens[0].position, test, stmt)
|
|
240
|
+
|
|
241
|
+
if init
|
|
242
|
+
block_body = LoxSeqDecl.new(tokens[0].position, [init, while_stmt])
|
|
243
|
+
for_stmt = Ast::LoxBlockStmt.new(tokens[1].position, block_body)
|
|
244
|
+
else
|
|
245
|
+
for_stmt = while_stmt
|
|
246
|
+
end
|
|
247
|
+
|
|
231
248
|
for_stmt
|
|
232
249
|
end
|
|
233
250
|
|
|
234
251
|
# rule('forControl' => 'forInitialization forTest forUpdate')
|
|
235
252
|
def reduce_for_control(_production, _range, tokens, theChildren)
|
|
236
253
|
(init, test, update) = theChildren
|
|
237
|
-
|
|
254
|
+
if test.nil? && update
|
|
255
|
+
# when test expr is nil but update expr is not, then force test to be true
|
|
256
|
+
test = LoxLiteralExpr.new(tokens[0].position, Datatype::True.instance)
|
|
257
|
+
[init, test, update]
|
|
258
|
+
else
|
|
259
|
+
theChildren
|
|
260
|
+
end
|
|
238
261
|
end
|
|
239
262
|
|
|
240
263
|
# rule('forInitialization' => 'SEMICOLON')
|
|
@@ -75,14 +75,6 @@ module Loxxy
|
|
|
75
75
|
broadcast(:after_class_stmt, aClassStmt, self)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Visit event. The visitor is about to visit a for statement.
|
|
79
|
-
# @param aForStmt [AST::LOXForStmt] the for statement node to visit
|
|
80
|
-
def visit_for_stmt(aForStmt)
|
|
81
|
-
broadcast(:before_for_stmt, aForStmt)
|
|
82
|
-
traverse_subnodes(aForStmt) # The condition is visited/evaluated here...
|
|
83
|
-
broadcast(:after_for_stmt, aForStmt, self)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
78
|
# Visit event. The visitor is about to visit a if statement.
|
|
87
79
|
# @param anIfStmt [AST::LOXIfStmt] the if statement node to visit
|
|
88
80
|
def visit_if_stmt(anIfStmt)
|
|
@@ -111,7 +103,7 @@ module Loxxy
|
|
|
111
103
|
# @param aWhileStmt [AST::LOXWhileStmt] the while statement node to visit
|
|
112
104
|
def visit_while_stmt(aWhileStmt)
|
|
113
105
|
broadcast(:before_while_stmt, aWhileStmt)
|
|
114
|
-
traverse_subnodes(aWhileStmt) # The condition is visited/evaluated here...
|
|
106
|
+
traverse_subnodes(aWhileStmt) if aWhileStmt.condition # The condition is visited/evaluated here...
|
|
115
107
|
broadcast(:after_while_stmt, aWhileStmt, self)
|
|
116
108
|
end
|
|
117
109
|
|
|
@@ -74,7 +74,7 @@ module Loxxy
|
|
|
74
74
|
aClassStmt.superclass.accept(aVisitor)
|
|
75
75
|
parent = stack.pop
|
|
76
76
|
unless parent.kind_of?(LoxClass)
|
|
77
|
-
raise
|
|
77
|
+
raise Loxxy::RuntimeError, 'Superclass must be a class.'
|
|
78
78
|
end
|
|
79
79
|
else
|
|
80
80
|
parent = nil
|
|
@@ -116,19 +116,6 @@ module Loxxy
|
|
|
116
116
|
before_block_stmt(aForStmt)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def after_for_stmt(aForStmt, aVisitor)
|
|
120
|
-
loop do
|
|
121
|
-
aForStmt.test_expr.accept(aVisitor)
|
|
122
|
-
condition = stack.pop
|
|
123
|
-
break unless condition.truthy?
|
|
124
|
-
|
|
125
|
-
aForStmt.body_stmt.accept(aVisitor)
|
|
126
|
-
aForStmt.update_expr&.accept(aVisitor)
|
|
127
|
-
stack.pop
|
|
128
|
-
end
|
|
129
|
-
after_block_stmt(aForStmt)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
119
|
def after_if_stmt(anIfStmt, aVisitor)
|
|
133
120
|
# Retrieve the result of the condition evaluation
|
|
134
121
|
condition = stack.pop
|
|
@@ -154,7 +141,7 @@ module Loxxy
|
|
|
154
141
|
break unless condition.truthy?
|
|
155
142
|
|
|
156
143
|
aWhileStmt.body.accept(aVisitor)
|
|
157
|
-
aWhileStmt.condition
|
|
144
|
+
aWhileStmt.condition&.accept(aVisitor)
|
|
158
145
|
end
|
|
159
146
|
end
|
|
160
147
|
|
|
@@ -170,7 +157,7 @@ module Loxxy
|
|
|
170
157
|
def after_assign_expr(anAssignExpr, _visitor)
|
|
171
158
|
var_name = anAssignExpr.name
|
|
172
159
|
variable = variable_lookup(anAssignExpr)
|
|
173
|
-
raise
|
|
160
|
+
raise Loxxy::RuntimeError, "Undefined variable '#{var_name}'." unless variable
|
|
174
161
|
|
|
175
162
|
value = stack.last # ToS remains since an assignment produces a value
|
|
176
163
|
variable.assign(value)
|
|
@@ -231,7 +218,8 @@ module Loxxy
|
|
|
231
218
|
operator = binary_operators[op]
|
|
232
219
|
operator.validate_operands(operand1, operand2)
|
|
233
220
|
if operand1.respond_to?(op)
|
|
234
|
-
|
|
221
|
+
result = operand1.send(op, operand2)
|
|
222
|
+
stack.push convert2lox_datatype(result)
|
|
235
223
|
else
|
|
236
224
|
msg1 = "`#{op}': Unimplemented operator for a #{operand1.class}."
|
|
237
225
|
raise StandardError, msg1
|
|
@@ -244,7 +232,8 @@ module Loxxy
|
|
|
244
232
|
operator = unary_operators[op]
|
|
245
233
|
operator.validate_operand(operand)
|
|
246
234
|
if operand.respond_to?(op)
|
|
247
|
-
|
|
235
|
+
result = operand.send(op)
|
|
236
|
+
stack.push convert2lox_datatype(result)
|
|
248
237
|
else
|
|
249
238
|
msg1 = "`#{op}': Unimplemented operator for a #{operand.class}."
|
|
250
239
|
raise StandardError, msg1
|
|
@@ -312,7 +301,7 @@ module Loxxy
|
|
|
312
301
|
superklass = variable_lookup(aSuperExpr).value.superclass
|
|
313
302
|
method = superklass.find_method(aSuperExpr.property)
|
|
314
303
|
unless method
|
|
315
|
-
raise
|
|
304
|
+
raise Loxxy::RuntimeError, "Undefined property '#{aSuperExpr.property}'."
|
|
316
305
|
end
|
|
317
306
|
|
|
318
307
|
stack.push method.bind(instance)
|
|
@@ -367,7 +356,7 @@ module Loxxy
|
|
|
367
356
|
unary_operators[:-@] = negate_op
|
|
368
357
|
|
|
369
358
|
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
|
|
370
|
-
BackEnd::LoxFunction])
|
|
359
|
+
BackEnd::LoxInstance, BackEnd::LoxFunction, BackEnd::LoxClass])
|
|
371
360
|
unary_operators[:!] = negation_op
|
|
372
361
|
end
|
|
373
362
|
|
|
@@ -421,6 +410,15 @@ module Loxxy
|
|
|
421
410
|
Datatype::Number.new(now)
|
|
422
411
|
end
|
|
423
412
|
end
|
|
413
|
+
|
|
414
|
+
def convert2lox_datatype(item)
|
|
415
|
+
case item
|
|
416
|
+
when TrueClass then Datatype::True.instance
|
|
417
|
+
when FalseClass then Datatype::False.instance
|
|
418
|
+
else
|
|
419
|
+
item
|
|
420
|
+
end
|
|
421
|
+
end
|
|
424
422
|
end # class
|
|
425
423
|
end # module
|
|
426
424
|
end # module
|
|
@@ -69,7 +69,7 @@ module Loxxy
|
|
|
69
69
|
define(aClassStmt.name)
|
|
70
70
|
if aClassStmt.superclass
|
|
71
71
|
if aClassStmt.name == aClassStmt.superclass.name
|
|
72
|
-
raise
|
|
72
|
+
raise Loxxy::RuntimeError, "'A class can't inherit from itself."
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
@current_class = :subclass
|
|
@@ -88,17 +88,6 @@ module Loxxy
|
|
|
88
88
|
@current_class = previous_class
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
def before_for_stmt(aForStmt)
|
|
92
|
-
before_block_stmt(aForStmt)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def after_for_stmt(aForStmt, aVisitor)
|
|
96
|
-
aForStmt.test_expr.accept(aVisitor)
|
|
97
|
-
aForStmt.body_stmt.accept(aVisitor)
|
|
98
|
-
aForStmt.update_expr&.accept(aVisitor)
|
|
99
|
-
after_block_stmt(aForStmt)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
91
|
def after_if_stmt(anIfStmt, aVisitor)
|
|
103
92
|
anIfStmt.then_stmt.accept(aVisitor)
|
|
104
93
|
anIfStmt.else_stmt&.accept(aVisitor)
|
|
@@ -107,27 +96,30 @@ module Loxxy
|
|
|
107
96
|
def before_return_stmt(returnStmt)
|
|
108
97
|
if scopes.size < 2
|
|
109
98
|
msg = "Error at 'return': Can't return from top-level code."
|
|
110
|
-
raise
|
|
99
|
+
raise Loxxy::RuntimeError, msg
|
|
111
100
|
end
|
|
112
101
|
|
|
113
102
|
if current_function == :none
|
|
114
103
|
msg = "Error at 'return': Can't return from outside a function."
|
|
115
|
-
raise
|
|
104
|
+
raise Loxxy::RuntimeError, msg
|
|
116
105
|
end
|
|
117
106
|
|
|
118
107
|
if current_function == :initializer
|
|
119
108
|
msg = "Error at 'return': Can't return a value from an initializer."
|
|
120
|
-
raise
|
|
109
|
+
raise Loxxy::RuntimeError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
|
|
121
110
|
end
|
|
122
111
|
end
|
|
123
112
|
|
|
124
113
|
def after_while_stmt(aWhileStmt, aVisitor)
|
|
125
114
|
aWhileStmt.body.accept(aVisitor)
|
|
126
|
-
aWhileStmt.condition
|
|
115
|
+
aWhileStmt.condition&.accept(aVisitor)
|
|
127
116
|
end
|
|
128
117
|
|
|
129
118
|
# A variable declaration adds a new variable to current scope
|
|
130
119
|
def before_var_stmt(aVarStmt)
|
|
120
|
+
# Oddly enough, Lox allows the re-definition of a variable at top-level scope
|
|
121
|
+
return if scopes.size == 1 && scopes.last[aVarStmt.name]
|
|
122
|
+
|
|
131
123
|
declare(aVarStmt.name)
|
|
132
124
|
end
|
|
133
125
|
|
|
@@ -149,7 +141,7 @@ module Loxxy
|
|
|
149
141
|
def before_variable_expr(aVarExpr)
|
|
150
142
|
var_name = aVarExpr.name
|
|
151
143
|
if !scopes.empty? && (scopes.last[var_name] == false)
|
|
152
|
-
raise
|
|
144
|
+
raise Loxxy::RuntimeError, "Can't read variable #{var_name} in its own initializer"
|
|
153
145
|
end
|
|
154
146
|
end
|
|
155
147
|
|
|
@@ -171,7 +163,7 @@ module Loxxy
|
|
|
171
163
|
def before_this_expr(_thisExpr)
|
|
172
164
|
if current_class == :none
|
|
173
165
|
msg = "Error at 'this': Can't use 'this' outside of a class."
|
|
174
|
-
raise
|
|
166
|
+
raise Loxxy::RuntimeError, msg
|
|
175
167
|
end
|
|
176
168
|
end
|
|
177
169
|
|
|
@@ -186,11 +178,11 @@ module Loxxy
|
|
|
186
178
|
msg_prefix = "Error at 'super': Can't use 'super' "
|
|
187
179
|
if current_class == :none
|
|
188
180
|
err_msg = msg_prefix + 'outside of a class.'
|
|
189
|
-
raise
|
|
181
|
+
raise Loxxy::RuntimeError, err_msg
|
|
190
182
|
|
|
191
183
|
elsif current_class == :class
|
|
192
184
|
err_msg = msg_prefix + 'in a class without superclass.'
|
|
193
|
-
raise
|
|
185
|
+
raise Loxxy::RuntimeError, err_msg
|
|
194
186
|
|
|
195
187
|
end
|
|
196
188
|
# 'super' behaves closely to a local variable
|
|
@@ -220,9 +212,13 @@ module Loxxy
|
|
|
220
212
|
return if scopes.empty?
|
|
221
213
|
|
|
222
214
|
curr_scope = scopes.last
|
|
215
|
+
# Oddly enough, Lox allows variable re-declaration at top-level
|
|
223
216
|
if curr_scope.include?(aVarName)
|
|
224
217
|
msg = "Error at '#{aVarName}': Already variable with this name in this scope."
|
|
225
|
-
raise
|
|
218
|
+
raise Loxxy::RuntimeError, msg
|
|
219
|
+
elsif curr_scope.size == 255 && current_function != :none
|
|
220
|
+
msg = "Error at '#{aVarName}': Too many local variables in function."
|
|
221
|
+
raise Loxxy::RuntimeError, msg
|
|
226
222
|
end
|
|
227
223
|
|
|
228
224
|
# The initializer is not yet processed.
|
|
@@ -7,6 +7,10 @@ module Loxxy
|
|
|
7
7
|
module Datatype
|
|
8
8
|
# Class for representing a Lox numeric value.
|
|
9
9
|
class Number < BuiltinDatatype
|
|
10
|
+
def zero?
|
|
11
|
+
value.zero?
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
# Perform the addition of two Lox numbers or
|
|
11
15
|
# one Lox number and a Ruby Numeric
|
|
12
16
|
# @param other [Loxxy::Datatype::Number, Numeric]
|
|
@@ -59,17 +63,28 @@ module Loxxy
|
|
|
59
63
|
# one Lox number and a Ruby Numeric
|
|
60
64
|
# @param other [Loxxy::Datatype::Number, Numeric]
|
|
61
65
|
# @return [Loxxy::Datatype::Number]
|
|
66
|
+
# rubocop: disable Lint/BinaryOperatorWithIdenticalOperands
|
|
62
67
|
def /(other)
|
|
63
68
|
case other
|
|
64
|
-
when Number
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
when Number, Numeric
|
|
70
|
+
if other.zero?
|
|
71
|
+
if zero?
|
|
72
|
+
# NaN case detected
|
|
73
|
+
self.class.new(0.0 / 0.0)
|
|
74
|
+
else
|
|
75
|
+
raise ZeroDivisionError
|
|
76
|
+
end
|
|
77
|
+
elsif other.kind_of?(Number)
|
|
78
|
+
self.class.new(value / other.value)
|
|
79
|
+
else
|
|
80
|
+
self.class.new(value / other)
|
|
81
|
+
end
|
|
68
82
|
else
|
|
69
83
|
err_msg = "'/': Operands must be numbers."
|
|
70
84
|
raise TypeError, err_msg
|
|
71
85
|
end
|
|
72
86
|
end
|
|
87
|
+
# rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
|
|
73
88
|
|
|
74
89
|
# Unary minus (return value with changed sign)
|
|
75
90
|
# @return [Loxxy::Datatype::Number]
|
|
@@ -46,7 +46,7 @@ module Loxxy
|
|
|
46
46
|
# Stop if the parse failed...
|
|
47
47
|
line1 = "Parsing failed\n"
|
|
48
48
|
line2 = "Reason: #{result.failure_reason.message}"
|
|
49
|
-
raise
|
|
49
|
+
raise SyntaxError, line1 + line2
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
return engine.convert(result) # engine.to_ptree(result)
|
|
@@ -54,11 +54,11 @@ module Loxxy
|
|
|
54
54
|
'<=' => 'LESS_EQUAL'
|
|
55
55
|
}.freeze
|
|
56
56
|
|
|
57
|
-
# Here are all the implemented Lox keywords
|
|
57
|
+
# Here are all the implemented Lox keywords
|
|
58
58
|
# These are enumerated in section 4.2.1 Token type
|
|
59
59
|
@@keywords = %w[
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
and class else false fun for if nil or
|
|
61
|
+
print return super this true var while
|
|
62
62
|
].map { |x| [x, x] }.to_h
|
|
63
63
|
|
|
64
64
|
# Constructor. Initialize a tokenizer for Lox input.
|
|
@@ -84,7 +84,7 @@ module Loxxy
|
|
|
84
84
|
token = _next_token
|
|
85
85
|
tok_sequence << token unless token.nil?
|
|
86
86
|
end
|
|
87
|
-
tok_sequence << build_token('EOF',
|
|
87
|
+
tok_sequence << build_token('EOF', nil)
|
|
88
88
|
|
|
89
89
|
return tok_sequence
|
|
90
90
|
end
|
|
@@ -99,22 +99,19 @@ module Loxxy
|
|
|
99
99
|
|
|
100
100
|
token = nil
|
|
101
101
|
|
|
102
|
-
if '(){}
|
|
102
|
+
if '(){},.;+-/*'.include? curr_ch
|
|
103
103
|
# Single delimiter or separator character
|
|
104
104
|
token = build_token(@@lexeme2name[curr_ch], scanner.getch)
|
|
105
|
-
elsif (lexeme = scanner.scan(/[+\-](?!\d)/))
|
|
106
|
-
# Minus or plus character not preceding a digit
|
|
107
|
-
token = build_token(@@lexeme2name[lexeme], lexeme)
|
|
108
105
|
elsif (lexeme = scanner.scan(/[!=><]=?/))
|
|
109
106
|
# One or two special character tokens
|
|
110
107
|
token = build_token(@@lexeme2name[lexeme], lexeme)
|
|
111
|
-
elsif (lexeme = scanner.scan(
|
|
108
|
+
elsif (lexeme = scanner.scan(/\d+(?:\.\d+)?/))
|
|
112
109
|
token = build_token('NUMBER', lexeme)
|
|
113
110
|
elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
|
|
114
111
|
token = build_token('STRING', lexeme)
|
|
115
112
|
elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
|
|
116
|
-
keyw = @@keywords[lexeme
|
|
117
|
-
tok_type = keyw
|
|
113
|
+
keyw = @@keywords[lexeme]
|
|
114
|
+
tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
|
|
118
115
|
token = build_token(tok_type, lexeme)
|
|
119
116
|
elsif scanner.scan(/"(?:\\"|[^"])*\z/)
|
|
120
117
|
# Error: unterminated string...
|
|
@@ -133,7 +130,8 @@ module Loxxy
|
|
|
133
130
|
def build_token(aSymbolName, aLexeme)
|
|
134
131
|
begin
|
|
135
132
|
(value, symb) = convert_to(aLexeme, aSymbolName)
|
|
136
|
-
|
|
133
|
+
lex_length = aLexeme ? aLexeme.size : 0
|
|
134
|
+
col = scanner.pos - lex_length - @line_start + 1
|
|
137
135
|
pos = Rley::Lexical::Position.new(@lineno, col)
|
|
138
136
|
if value
|
|
139
137
|
token = Literal.new(value, aLexeme.dup, symb, pos)
|
data/lib/loxxy/version.rb
CHANGED
|
@@ -54,20 +54,6 @@ module Loxxy
|
|
|
54
54
|
expect(var_b).to be_kind_of(Variable)
|
|
55
55
|
expect(var_b.name).to eq('b')
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
# it 'should set the suffix of just created variable' do
|
|
59
|
-
# subject.insert(var('a'))
|
|
60
|
-
# var_a = subject.defns['a']
|
|
61
|
-
# expect(var_a.suffix).to eq("_#{subject.object_id.to_s(16)}")
|
|
62
|
-
# end
|
|
63
|
-
|
|
64
|
-
# it 'should complain when variable names collide' do
|
|
65
|
-
# subject.insert(var('c'))
|
|
66
|
-
# expect(subject.defns['c']).to be_kind_of(Datatype::Variable)
|
|
67
|
-
# err = StandardError
|
|
68
|
-
# err_msg = "Variable with name 'c' already exists."
|
|
69
|
-
# expect { subject.insert(var('c')) }.to raise_error(err, err_msg)
|
|
70
|
-
# end
|
|
71
57
|
end # context
|
|
72
58
|
end # describe
|
|
73
59
|
end # module
|
|
@@ -106,25 +106,6 @@ module Loxxy
|
|
|
106
106
|
expect(subject.lookup('q')).to eq(subject.current_env.defns['q'])
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# it 'should allow the search of an entry based on its i_name' do
|
|
110
|
-
# subject.insert(var('q'))
|
|
111
|
-
# i_name_x = subject.insert(var('x'))
|
|
112
|
-
# subject.enter_environment(BackEnd::Environment.new)
|
|
113
|
-
# i_name_q2 = subject.insert(var('q'))
|
|
114
|
-
# i_name_y = subject.insert(var('y'))
|
|
115
|
-
|
|
116
|
-
# # Search for unknown i_name
|
|
117
|
-
# expect(subject.lookup_i_name('dummy')).to be_nil
|
|
118
|
-
|
|
119
|
-
# curr_scope = subject.current_env
|
|
120
|
-
# # # Search for existing unique names
|
|
121
|
-
# expect(subject.lookup_i_name(i_name_y)).to eq(curr_scope.defns['y'])
|
|
122
|
-
# expect(subject.lookup_i_name(i_name_x)).to eq(subject.root.defns['x'])
|
|
123
|
-
|
|
124
|
-
# # Search for redefined name
|
|
125
|
-
# expect(subject.lookup_i_name(i_name_q2)).to eq(curr_scope.defns['q'])
|
|
126
|
-
# end
|
|
127
|
-
|
|
128
109
|
it 'should list all the variables defined in all the szcope chain' do
|
|
129
110
|
subject.insert(var('q'))
|
|
130
111
|
subject.enter_environment(BackEnd::Environment.new)
|
|
@@ -38,41 +38,6 @@ module Loxxy
|
|
|
38
38
|
instance = Variable.new(sample_name)
|
|
39
39
|
expect(instance.value).to eq(Datatype::Nil.instance)
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
# it 'should know its default internal name' do
|
|
43
|
-
# # By default: internal name == label
|
|
44
|
-
# expect(subject.i_name).to eq(subject.label)
|
|
45
|
-
# end
|
|
46
|
-
|
|
47
|
-
# it 'should have a nil suffix' do
|
|
48
|
-
# expect(subject.suffix).to be_nil
|
|
49
|
-
# end
|
|
50
|
-
end # context
|
|
51
|
-
|
|
52
|
-
context 'Provided service:' do
|
|
53
|
-
let(:sample_suffix) { 'sample-suffix' }
|
|
54
|
-
it 'should have a label equal to its user-defined name' do
|
|
55
|
-
# expect(subject.label).to eq(subject.name)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
it 'should accept a suffix' do
|
|
59
|
-
# expect { subject.suffix = sample_suffix }.not_to raise_error
|
|
60
|
-
# expect(subject.suffix).to eq(sample_suffix)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it 'should calculate its internal name' do
|
|
64
|
-
# # Rule: empty suffix => internal name == label
|
|
65
|
-
# subject.suffix = ''
|
|
66
|
-
# expect(subject.i_name).to eq(subject.label)
|
|
67
|
-
|
|
68
|
-
# # Rule: suffix starting underscore: internal name = label + suffix
|
|
69
|
-
# subject.suffix = '_10'
|
|
70
|
-
# expect(subject.i_name).to eq(subject.label + subject.suffix)
|
|
71
|
-
|
|
72
|
-
# # Rule: ... otherwise: internal name == suffix
|
|
73
|
-
# subject.suffix = sample_suffix
|
|
74
|
-
# expect(subject.i_name).to eq(subject.suffix)
|
|
75
|
-
end
|
|
76
41
|
end # context
|
|
77
42
|
end # describe
|
|
78
43
|
end # module
|
|
@@ -124,18 +124,15 @@ LOX_END
|
|
|
124
124
|
|
|
125
125
|
it 'should recognize number values' do
|
|
126
126
|
input = <<-LOX_END
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
LOX_END
|
|
127
|
+
123 987654
|
|
128
|
+
0 123.456
|
|
129
|
+
LOX_END
|
|
131
130
|
|
|
132
131
|
expectations = [
|
|
133
132
|
['123', 123],
|
|
134
133
|
['987654', 987654],
|
|
135
134
|
['0', 0],
|
|
136
|
-
['
|
|
137
|
-
['123.456', 123.456],
|
|
138
|
-
['-0.001', -0.001]
|
|
135
|
+
['123.456', 123.456]
|
|
139
136
|
]
|
|
140
137
|
|
|
141
138
|
subject.start_with(input)
|
|
@@ -149,6 +146,30 @@ LOX_END
|
|
|
149
146
|
end
|
|
150
147
|
end
|
|
151
148
|
|
|
149
|
+
it 'should recognize negative number values' do
|
|
150
|
+
input = <<-LOX_END
|
|
151
|
+
-0
|
|
152
|
+
-0.001
|
|
153
|
+
LOX_END
|
|
154
|
+
|
|
155
|
+
expectations = [
|
|
156
|
+
['-', '0'],
|
|
157
|
+
['-', '0.001']
|
|
158
|
+
].flatten
|
|
159
|
+
|
|
160
|
+
subject.start_with(input)
|
|
161
|
+
tokens = subject.tokens
|
|
162
|
+
tokens.pop
|
|
163
|
+
i = 0
|
|
164
|
+
tokens.each_slice(2) do |(sign, lit)|
|
|
165
|
+
expect(sign.terminal).to eq('MINUS')
|
|
166
|
+
expect(sign.lexeme).to eq(expectations[i])
|
|
167
|
+
expect(lit.terminal).to eq('NUMBER')
|
|
168
|
+
expect(lit.lexeme).to eq(expectations[i + 1])
|
|
169
|
+
i += 2
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
152
173
|
it 'should recognize leading and trailing dots as distinct tokens' do
|
|
153
174
|
input = '.456 123.'
|
|
154
175
|
|
|
@@ -217,6 +238,13 @@ LOX_END
|
|
|
217
238
|
expect(token_nil.lexeme).to eq('nil')
|
|
218
239
|
expect(token_nil.value).to be_kind_of(Datatype::Nil)
|
|
219
240
|
end
|
|
241
|
+
|
|
242
|
+
it 'should differentiate nil from variable spelled same' do
|
|
243
|
+
subject.start_with('Nil')
|
|
244
|
+
similar = subject.tokens[0]
|
|
245
|
+
expect(similar.terminal).to eq('IDENTIFIER')
|
|
246
|
+
expect(similar.lexeme).to eq('Nil')
|
|
247
|
+
end
|
|
220
248
|
end # context
|
|
221
249
|
|
|
222
250
|
context 'Handling comments:' do
|
data/spec/interpreter_spec.rb
CHANGED
|
@@ -148,6 +148,19 @@ module Loxxy
|
|
|
148
148
|
end
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
+
it 'should ignore spaces surrounding minus in subtraction of two numbers' do
|
|
152
|
+
[
|
|
153
|
+
['1 - 1;', 0],
|
|
154
|
+
['1 -1;', 0],
|
|
155
|
+
['1- 1;', 0],
|
|
156
|
+
['1-1;', 0]
|
|
157
|
+
].each do |(source, predicted)|
|
|
158
|
+
lox = Loxxy::Interpreter.new
|
|
159
|
+
result = lox.evaluate(source)
|
|
160
|
+
expect(result.value == predicted).to be_truthy
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
151
164
|
it 'should evaluate the negation of an object' do
|
|
152
165
|
[
|
|
153
166
|
['!true;', false],
|
|
@@ -431,6 +444,17 @@ LOX_END
|
|
|
431
444
|
expect(result).to eq(3)
|
|
432
445
|
end
|
|
433
446
|
|
|
447
|
+
it 'should support return within statements inside a function' do
|
|
448
|
+
program = <<-LOX_END
|
|
449
|
+
fun foo() {
|
|
450
|
+
for (;;) return "done";
|
|
451
|
+
}
|
|
452
|
+
print foo(); // output: done
|
|
453
|
+
LOX_END
|
|
454
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
|
455
|
+
expect(sample_cfg[:ostream].string).to eq('done')
|
|
456
|
+
end
|
|
457
|
+
|
|
434
458
|
# rubocop: disable Style/StringConcatenation
|
|
435
459
|
it 'should support local functions and closures' do
|
|
436
460
|
program = <<-LOX_END
|
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.3.01
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dimitri Geshef
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-05-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rley
|
|
@@ -99,7 +99,6 @@ files:
|
|
|
99
99
|
- lib/loxxy/ast/lox_call_expr.rb
|
|
100
100
|
- lib/loxxy/ast/lox_class_stmt.rb
|
|
101
101
|
- lib/loxxy/ast/lox_compound_expr.rb
|
|
102
|
-
- lib/loxxy/ast/lox_for_stmt.rb
|
|
103
102
|
- lib/loxxy/ast/lox_fun_stmt.rb
|
|
104
103
|
- lib/loxxy/ast/lox_get_expr.rb
|
|
105
104
|
- lib/loxxy/ast/lox_grouping_expr.rb
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'lox_compound_expr'
|
|
4
|
-
|
|
5
|
-
module Loxxy
|
|
6
|
-
module Ast
|
|
7
|
-
class LoxForStmt < LoxCompoundExpr
|
|
8
|
-
# @return [LoxNode] test expression
|
|
9
|
-
attr_reader :test_expr
|
|
10
|
-
|
|
11
|
-
# @return [LoxNode] update expression
|
|
12
|
-
attr_reader :update_expr
|
|
13
|
-
|
|
14
|
-
# @return [LoxNode] body statement
|
|
15
|
-
attr_accessor :body_stmt
|
|
16
|
-
|
|
17
|
-
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
|
18
|
-
# @param initialization [Loxxy::Ast::LoxNode]
|
|
19
|
-
# @param testExpr [Loxxy::Ast::LoxNode]
|
|
20
|
-
# @param updateExpr [Loxxy::Ast::LoxNode]
|
|
21
|
-
def initialize(aPosition, initialization, testExpr, updateExpr)
|
|
22
|
-
child = initialization ? [initialization] : []
|
|
23
|
-
super(aPosition, child)
|
|
24
|
-
@test_expr = testExpr
|
|
25
|
-
@update_expr = updateExpr
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Accessor to the condition expression
|
|
29
|
-
# @return [LoxNode]
|
|
30
|
-
def condition
|
|
31
|
-
subnodes[0]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
define_accept # Add `accept` method as found in Visitor design pattern
|
|
35
|
-
end # class
|
|
36
|
-
end # module
|
|
37
|
-
end # module
|