loxxy 0.1.02 → 0.1.07
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 +63 -3
- data/README.md +2 -2
- data/bin/loxxy +11 -0
- data/lib/loxxy.rb +0 -2
- data/lib/loxxy/ast/all_lox_nodes.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +26 -4
- data/lib/loxxy/ast/ast_visitor.rb +8 -0
- data/lib/loxxy/ast/lox_return_stmt.rb +21 -0
- data/lib/loxxy/back_end/binary_operator.rb +57 -0
- data/lib/loxxy/back_end/engine.rb +80 -19
- data/lib/loxxy/back_end/environment.rb +8 -1
- data/lib/loxxy/back_end/lox_function.rb +71 -0
- data/lib/loxxy/back_end/symbol_table.rb +19 -9
- data/lib/loxxy/back_end/unary_operator.rb +41 -0
- data/lib/loxxy/error.rb +12 -0
- data/lib/loxxy/front_end/grammar.rb +2 -2
- data/lib/loxxy/front_end/scanner.rb +2 -0
- data/lib/loxxy/version.rb +1 -1
- data/loxxy.gemspec +2 -2
- data/spec/back_end/engine_spec.rb +2 -0
- data/spec/interpreter_spec.rb +44 -13
- metadata +11 -5
- data/lib/loxxy/back_end/function.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f171f0046ff95784ed21b369751ca65d25e0ff757d1b2dccc534087a4b9c4a8d
|
4
|
+
data.tar.gz: 579badf6a30436962cb18e63dfee1ed361cfd152e9fecf54f2bfd25395db086b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0470f022b121ceaff279f75024e07b78a218c3baa4d44e6f9bf54f4ebddf18341b27f697fe68c727eb2d61add2c55e7c102fbf73bfd29ef4fb3965024247f747
|
7
|
+
data.tar.gz: 320dbb075351069f624103f1d93aadf903ec6a5b7bb685871f53227b496760ac67ae9fa1dae85491efd9b94cfeb90eac8f66e104b4f70eaac9b0da081dfde567
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,68 @@
|
|
1
|
+
## [0.1.07] - 2021-03-14
|
2
|
+
- `Loxxy` now supports nested functions and closures
|
3
|
+
|
4
|
+
### Changed
|
5
|
+
- Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
|
6
|
+
- Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
|
7
|
+
- Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
|
8
|
+
- Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
|
9
|
+
- Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
- Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
## [0.1.06] - 2021-03-06
|
18
|
+
- Parameters/arguments checks in function declaration and call
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
|
22
|
+
- Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
|
23
|
+
|
24
|
+
- Class `BackEnd::Function` renamed to `LoxFunction`
|
25
|
+
|
26
|
+
## [0.1.05] - 2021-03-05
|
27
|
+
- Test for Fibbonacci recursive function is now passing.
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
- Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
|
31
|
+
|
32
|
+
## [0.1.04] - 2021-02-28
|
33
|
+
|
34
|
+
### Added
|
35
|
+
- Class `Ast::LoxReturnStmt` a node that represents a return statement
|
36
|
+
- Method `Ast::ASTBuilder#reduce_return_stmt`
|
37
|
+
- Method `Ast::ASTVisitor#visit_return_stmt` for visiting an `Ast::LoxReturnStmt` node
|
38
|
+
- Method `BackEnd::Engine#after_return_stmt` to handle return statement
|
39
|
+
- Method `BackEnd::Function#!` implementing the logical negation of a function (as value).
|
40
|
+
- Test suite for logical operators (in project repository)
|
41
|
+
- Test suite for block code
|
42
|
+
- Test suite for call and function declaration (initial)
|
43
|
+
|
44
|
+
### Changed
|
45
|
+
- Method `BackEnd::Engine#after_call_expr` now generate a `catch` and `throw` events
|
46
|
+
|
47
|
+
## [0.1.03] - 2021-02-26
|
48
|
+
- Runtime argument checking for arithmetic and comparison operators
|
49
|
+
|
50
|
+
### Added
|
51
|
+
- Test suite for arithmetic and comparison operators (in project repository)
|
52
|
+
- Class `BackEnd::UnaryOperator`: runtime argument validation
|
53
|
+
- Class `BackEnd::BinaryOperator`: runtime argument validation
|
54
|
+
|
55
|
+
### Changed
|
56
|
+
- File `console` renamed to `loxxy`. Very basic command-line interface.
|
57
|
+
- Custom exception classes
|
58
|
+
- File `README.md` updated list of supported `Lox` keywords.
|
59
|
+
|
60
|
+
|
1
61
|
## [0.1.02] - 2021-02-21
|
2
62
|
- Function definition and call documented in `README.md`
|
3
63
|
|
4
64
|
### Changed
|
5
|
-
- File `README.md` updated
|
65
|
+
- File `README.md` updated description of function definition and function call.
|
6
66
|
|
7
67
|
### Fixed
|
8
68
|
- Method `BackEnd::Engine#after_print_stmt` now handles of empty stack or nil data.
|
@@ -207,11 +267,11 @@
|
|
207
267
|
- The interpreter can evaluate substraction between two numbers.
|
208
268
|
|
209
269
|
### Added
|
210
|
-
- Method `Datatype::Number#-`
|
270
|
+
- Method `Datatype::Number#-` implementing the subtraction operation
|
211
271
|
|
212
272
|
### Changed
|
213
273
|
- File `README.md` minor editorial changes.
|
214
|
-
- File `lx_string_spec.rb` Added test for string
|
274
|
+
- File `lx_string_spec.rb` Added test for string concatenation
|
215
275
|
- File `number_spec.rb` Added tests for addition and subtraction operations
|
216
276
|
- File `interpreter_spec.rb` Added tests for subtraction operation
|
217
277
|
|
data/README.md
CHANGED
@@ -15,7 +15,6 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
|
|
15
15
|
### Current status
|
16
16
|
The project is still in inception and the interpreter is being implemented...
|
17
17
|
Currently it can execute all allowed __Lox__ expressions and statements except:
|
18
|
-
- Closures,
|
19
18
|
- Classes and objects.
|
20
19
|
|
21
20
|
These will be implemented soon.
|
@@ -180,7 +179,8 @@ Loxxy supports single line C-style comments.
|
|
180
179
|
### Keywords
|
181
180
|
Loxxy implements the following __Lox__ reserved keywords:
|
182
181
|
```lang-none
|
183
|
-
and, false,
|
182
|
+
and, else, false, for, fun, if,
|
183
|
+
nil, or, print, true, var, while
|
184
184
|
```
|
185
185
|
|
186
186
|
### Datatypes
|
data/bin/loxxy
ADDED
data/lib/loxxy.rb
CHANGED
@@ -6,8 +6,6 @@ require_relative 'loxxy/front_end/raw_parser'
|
|
6
6
|
|
7
7
|
# Namespace for all classes and constants of __loxxy__ gem.
|
8
8
|
module Loxxy
|
9
|
-
class Error < StandardError; end
|
10
|
-
|
11
9
|
# Shorthand method. Returns the sole object that represents
|
12
10
|
# a Lox false literal.
|
13
11
|
# @return [Loxxy::Datatype::False]
|
@@ -12,6 +12,7 @@ require_relative 'lox_logical_expr'
|
|
12
12
|
require_relative 'lox_assign_expr'
|
13
13
|
require_relative 'lox_block_stmt'
|
14
14
|
require_relative 'lox_while_stmt'
|
15
|
+
require_relative 'lox_return_stmt'
|
15
16
|
require_relative 'lox_print_stmt'
|
16
17
|
require_relative 'lox_if_stmt'
|
17
18
|
require_relative 'lox_for_stmt'
|
@@ -221,6 +221,11 @@ module Loxxy
|
|
221
221
|
Ast::LoxPrintStmt.new(tokens[1].position, theChildren[1])
|
222
222
|
end
|
223
223
|
|
224
|
+
# rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
|
225
|
+
def reduce_return_stmt(_production, _range, tokens, theChildren)
|
226
|
+
Ast::LoxReturnStmt.new(tokens[1].position, theChildren[1])
|
227
|
+
end
|
228
|
+
|
224
229
|
# rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as ''
|
225
230
|
def reduce_while_stmt(_production, _range, tokens, theChildren)
|
226
231
|
Ast::LoxWhileStmt.new(tokens[1].position, theChildren[2], theChildren[4])
|
@@ -255,20 +260,33 @@ module Loxxy
|
|
255
260
|
|
256
261
|
# rule('call' => 'primary refinement_plus').as 'call_expr'
|
257
262
|
def reduce_call_expr(_production, _range, _tokens, theChildren)
|
258
|
-
|
259
|
-
|
263
|
+
members = theChildren.flatten
|
264
|
+
call_expr = nil
|
265
|
+
loop do
|
266
|
+
(callee, call_expr) = members.shift(2)
|
267
|
+
call_expr.callee = callee
|
268
|
+
members.unshift(call_expr)
|
269
|
+
break if members.size == 1
|
270
|
+
end
|
271
|
+
|
272
|
+
call_expr
|
273
|
+
end
|
274
|
+
|
275
|
+
# rule('refinement_plus' => 'refinement_plus refinement')
|
276
|
+
def reduce_refinement_plus_more(_production, _range, _tokens, theChildren)
|
277
|
+
theChildren[0] << theChildren[1]
|
260
278
|
end
|
261
279
|
|
262
280
|
# rule('refinement_plus' => 'refinement').
|
263
281
|
def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
|
264
|
-
theChildren
|
282
|
+
theChildren
|
265
283
|
end
|
266
284
|
|
267
285
|
# rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
|
268
286
|
def reduce_call_arglist(_production, _range, tokens, theChildren)
|
269
287
|
args = theChildren[1] || []
|
270
288
|
if args.size > 255
|
271
|
-
raise
|
289
|
+
raise Loxxy::RuntimeError, "Can't have more than 255 arguments."
|
272
290
|
end
|
273
291
|
|
274
292
|
LoxCallExpr.new(tokens[0].position, args)
|
@@ -298,6 +316,10 @@ module Loxxy
|
|
298
316
|
def reduce_function(_production, _range, _tokens, theChildren)
|
299
317
|
first_child = theChildren.first
|
300
318
|
pos = first_child.token.position
|
319
|
+
if theChildren[2] && theChildren[2].size > 255
|
320
|
+
msg = "Can't have more than 255 parameters."
|
321
|
+
raise Loxxy::SyntaxError, msg
|
322
|
+
end
|
301
323
|
LoxFunStmt.new(pos, first_child.token.lexeme, theChildren[2], theChildren[4])
|
302
324
|
end
|
303
325
|
|
@@ -91,6 +91,14 @@ module Loxxy
|
|
91
91
|
broadcast(:after_print_stmt, aPrintStmt)
|
92
92
|
end
|
93
93
|
|
94
|
+
# Visit event. The visitor is about to visit a return statement.
|
95
|
+
# @param aReturnStmt [AST::LOXReturnStmt] the return statement node to visit
|
96
|
+
def visit_return_stmt(aReturnStmt)
|
97
|
+
broadcast(:before_return_stmt, aReturnStmt)
|
98
|
+
traverse_subnodes(aReturnStmt)
|
99
|
+
broadcast(:after_return_stmt, aReturnStmt, self)
|
100
|
+
end
|
101
|
+
|
94
102
|
# Visit event. The visitor is about to visit a while statement node.
|
95
103
|
# @param aWhileStmt [AST::LOXWhileStmt] the while statement node to visit
|
96
104
|
def visit_while_stmt(aWhileStmt)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxReturnStmt < LoxCompoundExpr
|
8
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
9
|
+
# @param anExpression [Ast::LoxNode] expression to return
|
10
|
+
def initialize(aPosition, anExpression)
|
11
|
+
super(aPosition, [anExpression])
|
12
|
+
end
|
13
|
+
|
14
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
15
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
16
|
+
def accept(visitor)
|
17
|
+
visitor.visit_return_stmt(self)
|
18
|
+
end
|
19
|
+
end # class
|
20
|
+
end # module
|
21
|
+
end # module
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../error'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
Signature = Struct.new(:parameter_types)
|
8
|
+
|
9
|
+
# A Lox binary operator
|
10
|
+
class BinaryOperator
|
11
|
+
# @return [String] text representation of the operator
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @return [Array<Class>]
|
15
|
+
attr_reader :signatures
|
16
|
+
|
17
|
+
# @param aName [String] "name" of operator
|
18
|
+
# @param theSignatures [Array<Signature>] allowed signatures
|
19
|
+
def initialize(aName, theSignatures)
|
20
|
+
@name = aName
|
21
|
+
@signatures = theSignatures
|
22
|
+
end
|
23
|
+
|
24
|
+
# rubocop: disable Style/ClassEqualityComparison
|
25
|
+
def validate_operands(operand1, operand2)
|
26
|
+
compliant = signatures.find do |(type1, type2)|
|
27
|
+
next unless operand1.kind_of?(type1)
|
28
|
+
|
29
|
+
if type2 == :idem
|
30
|
+
(operand2.class == operand1.class)
|
31
|
+
else
|
32
|
+
operand2.kind_of?(type2)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
# rubocop: enable Style/ClassEqualityComparison
|
36
|
+
|
37
|
+
unless compliant
|
38
|
+
err = Loxxy::RuntimeError
|
39
|
+
if signatures.size == 1 && signatures[0].last == :idem
|
40
|
+
raise err, "Operands must be #{datatype_name(signatures[0].first)}s."
|
41
|
+
elsif signatures.size == 2 && signatures.all? { |(_, second)| second == :idem }
|
42
|
+
type1 = datatype_name(signatures[0].first)
|
43
|
+
type2 = datatype_name(signatures[1].first)
|
44
|
+
raise err, "Operands must be two #{type1}s or two #{type2}s."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def datatype_name(aClass)
|
52
|
+
# (?:(?:[^:](?!:|(?<=LX))))+$
|
53
|
+
aClass.name.sub(/^.+(?=::)::(?:LX)?/, '').downcase
|
54
|
+
end
|
55
|
+
end # class
|
56
|
+
end # module
|
57
|
+
end # module
|
@@ -2,8 +2,10 @@
|
|
2
2
|
|
3
3
|
# Load all the classes implementing AST nodes
|
4
4
|
require_relative '../ast/all_lox_nodes'
|
5
|
-
require_relative '
|
5
|
+
require_relative 'binary_operator'
|
6
|
+
require_relative 'lox_function'
|
6
7
|
require_relative 'symbol_table'
|
8
|
+
require_relative 'unary_operator'
|
7
9
|
|
8
10
|
module Loxxy
|
9
11
|
module BackEnd
|
@@ -17,16 +19,26 @@ module Loxxy
|
|
17
19
|
# @return [BackEnd::SymbolTable]
|
18
20
|
attr_reader :symbol_table
|
19
21
|
|
20
|
-
# @return [Array<Datatype::
|
22
|
+
# @return [Array<Datatype::BuiltinDatatype>] Data stack for the expression results
|
21
23
|
attr_reader :stack
|
22
24
|
|
25
|
+
# @return [Hash { Symbol => UnaryOperator}]
|
26
|
+
attr_reader :unary_operators
|
27
|
+
|
28
|
+
# @return [Hash { Symbol => BinaryOperator}]
|
29
|
+
attr_reader :binary_operators
|
30
|
+
|
23
31
|
# @param theOptions [Hash]
|
24
32
|
def initialize(theOptions)
|
25
33
|
@config = theOptions
|
26
34
|
@ostream = config.include?(:ostream) ? config[:ostream] : $stdout
|
27
35
|
@symbol_table = SymbolTable.new
|
28
36
|
@stack = []
|
37
|
+
@unary_operators = {}
|
38
|
+
@binary_operators = {}
|
29
39
|
|
40
|
+
init_unary_operators
|
41
|
+
init_binary_operators
|
30
42
|
init_globals
|
31
43
|
end
|
32
44
|
|
@@ -50,7 +62,7 @@ module Loxxy
|
|
50
62
|
end
|
51
63
|
|
52
64
|
def after_var_stmt(aVarStmt)
|
53
|
-
new_var = Variable.new(aVarStmt.name,
|
65
|
+
new_var = Variable.new(aVarStmt.name, stack.pop)
|
54
66
|
symbol_table.insert(new_var)
|
55
67
|
end
|
56
68
|
|
@@ -86,6 +98,10 @@ module Loxxy
|
|
86
98
|
@ostream.print tos ? tos.to_str : 'nil'
|
87
99
|
end
|
88
100
|
|
101
|
+
def after_return_stmt(_returnStmt, _aVisitor)
|
102
|
+
throw(:return)
|
103
|
+
end
|
104
|
+
|
89
105
|
def after_while_stmt(aWhileStmt, aVisitor)
|
90
106
|
loop do
|
91
107
|
condition = stack.pop
|
@@ -110,9 +126,8 @@ module Loxxy
|
|
110
126
|
variable = symbol_table.lookup(var_name)
|
111
127
|
raise StandardError, "Unknown variable #{var_name}" unless variable
|
112
128
|
|
113
|
-
value = stack.
|
129
|
+
value = stack.last # ToS remains since an assignment produces a value
|
114
130
|
variable.assign(value)
|
115
|
-
stack.push value # An expression produces a value
|
116
131
|
end
|
117
132
|
|
118
133
|
def after_logical_expr(aLogicalExpr, visitor)
|
@@ -148,9 +163,11 @@ module Loxxy
|
|
148
163
|
end
|
149
164
|
|
150
165
|
def after_binary_expr(aBinaryExpr)
|
151
|
-
op = aBinaryExpr.operator
|
152
166
|
operand2 = stack.pop
|
153
167
|
operand1 = stack.pop
|
168
|
+
op = aBinaryExpr.operator
|
169
|
+
operator = binary_operators[op]
|
170
|
+
operator.validate_operands(operand1, operand2)
|
154
171
|
if operand1.respond_to?(op)
|
155
172
|
stack.push operand1.send(op, operand2)
|
156
173
|
else
|
@@ -160,12 +177,14 @@ module Loxxy
|
|
160
177
|
end
|
161
178
|
|
162
179
|
def after_unary_expr(anUnaryExpr)
|
163
|
-
op = anUnaryExpr.operator
|
164
180
|
operand = stack.pop
|
181
|
+
op = anUnaryExpr.operator
|
182
|
+
operator = unary_operators[op]
|
183
|
+
operator.validate_operand(operand)
|
165
184
|
if operand.respond_to?(op)
|
166
185
|
stack.push operand.send(op)
|
167
186
|
else
|
168
|
-
msg1 = "`#{op}': Unimplemented operator for a #{
|
187
|
+
msg1 = "`#{op}': Unimplemented operator for a #{operand.class}."
|
169
188
|
raise StandardError, msg1
|
170
189
|
end
|
171
190
|
end
|
@@ -176,18 +195,18 @@ module Loxxy
|
|
176
195
|
callee = stack.pop
|
177
196
|
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
178
197
|
|
179
|
-
|
198
|
+
case callee
|
199
|
+
when NativeFunction
|
180
200
|
stack.push callee.call # Pass arguments
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
symbol_table.insert(local)
|
201
|
+
when LoxFunction
|
202
|
+
arg_count = aCallExpr.arguments.size
|
203
|
+
if arg_count != callee.arity
|
204
|
+
msg = "Expected #{callee.arity} arguments but got #{arg_count}."
|
205
|
+
raise Loxxy::RuntimeError, msg
|
187
206
|
end
|
188
|
-
callee.call(aVisitor)
|
189
|
-
|
190
|
-
|
207
|
+
callee.call(self, aVisitor)
|
208
|
+
else
|
209
|
+
raise Loxxy::RuntimeError, 'Can only call functions and classes.'
|
191
210
|
end
|
192
211
|
end
|
193
212
|
|
@@ -214,7 +233,7 @@ module Loxxy
|
|
214
233
|
end
|
215
234
|
|
216
235
|
def after_fun_stmt(aFunStmt, _visitor)
|
217
|
-
function =
|
236
|
+
function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
|
218
237
|
new_var = Variable.new(aFunStmt.name, function)
|
219
238
|
symbol_table.insert(new_var)
|
220
239
|
end
|
@@ -235,6 +254,48 @@ module Loxxy
|
|
235
254
|
end
|
236
255
|
end
|
237
256
|
|
257
|
+
def init_unary_operators
|
258
|
+
negate_op = UnaryOperator.new('-', [Datatype::Number])
|
259
|
+
unary_operators[:-@] = negate_op
|
260
|
+
|
261
|
+
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
|
262
|
+
BackEnd::LoxFunction])
|
263
|
+
unary_operators[:!] = negation_op
|
264
|
+
end
|
265
|
+
|
266
|
+
def init_binary_operators
|
267
|
+
plus_op = BinaryOperator.new('+', [[Datatype::Number, :idem],
|
268
|
+
[Datatype::LXString, :idem]])
|
269
|
+
binary_operators[:+] = plus_op
|
270
|
+
|
271
|
+
minus_op = BinaryOperator.new('-', [[Datatype::Number, :idem]])
|
272
|
+
binary_operators[:-] = minus_op
|
273
|
+
|
274
|
+
star_op = BinaryOperator.new('*', [[Datatype::Number, :idem]])
|
275
|
+
binary_operators[:*] = star_op
|
276
|
+
|
277
|
+
slash_op = BinaryOperator.new('/', [[Datatype::Number, :idem]])
|
278
|
+
binary_operators[:/] = slash_op
|
279
|
+
|
280
|
+
equal_equal_op = BinaryOperator.new('==', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
|
281
|
+
binary_operators[:==] = equal_equal_op
|
282
|
+
|
283
|
+
not_equal_op = BinaryOperator.new('!=', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
|
284
|
+
binary_operators[:!=] = not_equal_op
|
285
|
+
|
286
|
+
less_op = BinaryOperator.new('<', [[Datatype::Number, :idem]])
|
287
|
+
binary_operators[:<] = less_op
|
288
|
+
|
289
|
+
less_equal_op = BinaryOperator.new('<=', [[Datatype::Number, :idem]])
|
290
|
+
binary_operators[:<=] = less_equal_op
|
291
|
+
|
292
|
+
greater_op = BinaryOperator.new('>', [[Datatype::Number, :idem]])
|
293
|
+
binary_operators[:>] = greater_op
|
294
|
+
|
295
|
+
greater_equal_op = BinaryOperator.new('>=', [[Datatype::Number, :idem]])
|
296
|
+
binary_operators[:>=] = greater_equal_op
|
297
|
+
end
|
298
|
+
|
238
299
|
def init_globals
|
239
300
|
add_native_fun('clock', native_clock)
|
240
301
|
end
|
@@ -9,10 +9,17 @@ module Loxxy
|
|
9
9
|
# of a relation or a relation definition.
|
10
10
|
# It contains a map of names to the objects they name (e.g. logical var)
|
11
11
|
class Environment
|
12
|
-
# The
|
12
|
+
# The enclosing (parent) environment.
|
13
13
|
# @return [Environment, NilClass]
|
14
14
|
attr_accessor :enclosing
|
15
15
|
|
16
|
+
# The previous environment in the environment chain.
|
17
|
+
# @return [Environment, NilClass]
|
18
|
+
attr_accessor :predecessor
|
19
|
+
|
20
|
+
# @return [Boolean] true if this environment is part of a closure (contains an embedded function)
|
21
|
+
attr_accessor :embedding
|
22
|
+
|
16
23
|
# Mapping from user-defined name to related definition
|
17
24
|
# (say, a variable object)
|
18
25
|
# @return [Hash{String => Variable}] Pairs of the kind
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../datatype/all_datatypes'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# rubocop: disable Style/AccessorGrouping
|
8
|
+
# Representation of a Lox function.
|
9
|
+
# It is a named slot that can be associated with a value at the time.
|
10
|
+
class LoxFunction
|
11
|
+
# @return [String] The name of the function (if any)
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @return [Array<>] the parameters
|
15
|
+
attr_reader :parameters
|
16
|
+
attr_reader :body
|
17
|
+
attr_reader :stack
|
18
|
+
attr_reader :closure
|
19
|
+
|
20
|
+
# Create a function with given name
|
21
|
+
# @param aName [String] The name of the variable
|
22
|
+
def initialize(aName, parameterList, aBody, anEngine)
|
23
|
+
@name = aName.dup
|
24
|
+
@parameters = parameterList
|
25
|
+
@body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
|
26
|
+
@stack = anEngine.stack
|
27
|
+
@closure = anEngine.symbol_table.current_env
|
28
|
+
anEngine.symbol_table.current_env.embedding = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def arity
|
32
|
+
parameters ? parameters.size : 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def accept(_visitor)
|
36
|
+
stack.push self
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(engine, aVisitor)
|
40
|
+
# new_env = Environment.new(engine.symbol_table.current_env)
|
41
|
+
new_env = Environment.new(closure)
|
42
|
+
engine.symbol_table.enter_environment(new_env)
|
43
|
+
|
44
|
+
parameters&.each do |param_name|
|
45
|
+
local = Variable.new(param_name, stack.pop)
|
46
|
+
engine.symbol_table.insert(local)
|
47
|
+
end
|
48
|
+
|
49
|
+
catch(:return) do
|
50
|
+
(body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
|
51
|
+
throw(:return)
|
52
|
+
end
|
53
|
+
|
54
|
+
engine.symbol_table.leave_environment
|
55
|
+
end
|
56
|
+
|
57
|
+
# Logical negation.
|
58
|
+
# As a function is a truthy thing, its negation is thus false.
|
59
|
+
# @return [Datatype::False]
|
60
|
+
def !
|
61
|
+
Datatype::False.instance
|
62
|
+
end
|
63
|
+
|
64
|
+
# Text representation of a Lox function
|
65
|
+
def to_str
|
66
|
+
"<fn #{name}>"
|
67
|
+
end
|
68
|
+
end # class
|
69
|
+
# rubocop: enable Style/AccessorGrouping
|
70
|
+
end # module
|
71
|
+
end # module
|
@@ -44,23 +44,33 @@ module Loxxy
|
|
44
44
|
# to be a child of current environment and to be itself the new current environment.
|
45
45
|
# @param anEnv [BackEnd::Environment] the Environment that
|
46
46
|
def enter_environment(anEnv)
|
47
|
-
anEnv.enclosing
|
47
|
+
if anEnv.enclosing && (anEnv.enclosing != current_env)
|
48
|
+
anEnv.predecessor = current_env
|
49
|
+
else
|
50
|
+
anEnv.enclosing = current_env
|
51
|
+
end
|
48
52
|
@current_env = anEnv
|
49
53
|
end
|
50
54
|
|
51
55
|
def leave_environment
|
52
|
-
current_env.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
unless current_env.embedding
|
57
|
+
current_env.defns.each_pair do |nm, _item|
|
58
|
+
environments = name2envs[nm]
|
59
|
+
if environments.size == 1
|
60
|
+
name2envs.delete(nm)
|
61
|
+
else
|
62
|
+
environments.pop
|
63
|
+
name2envs[nm] = environments
|
64
|
+
end
|
59
65
|
end
|
60
66
|
end
|
61
67
|
raise StandardError, 'Cannot remove root environment.' if current_env == root
|
62
68
|
|
63
|
-
|
69
|
+
if current_env.predecessor
|
70
|
+
@current_env = current_env.predecessor
|
71
|
+
else
|
72
|
+
@current_env = current_env.enclosing
|
73
|
+
end
|
64
74
|
end
|
65
75
|
|
66
76
|
# Add an entry with given name to current environment.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../error'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module BackEnd
|
7
|
+
# A Lox unary operator
|
8
|
+
class UnaryOperator
|
9
|
+
# @return [String] text representation of the operator
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# @return [Array<Class>]
|
13
|
+
attr_reader :signatures
|
14
|
+
|
15
|
+
# @param aName [String] "name" of operator
|
16
|
+
# @param theSignatures [Array<Class>] allowed signatures
|
17
|
+
def initialize(aName, theSignatures)
|
18
|
+
@name = aName
|
19
|
+
@signatures = theSignatures
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_operand(operand1)
|
23
|
+
compliant = signatures.find { |some_type| operand1.kind_of?(some_type) }
|
24
|
+
|
25
|
+
unless compliant
|
26
|
+
err = Loxxy::RuntimeError
|
27
|
+
# if signatures.size == 1
|
28
|
+
raise err, "Operand must be a #{datatype_name(signatures[0])}."
|
29
|
+
# end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def datatype_name(aClass)
|
36
|
+
# (?:(?:[^:](?!:|(?<=LX))))+$
|
37
|
+
aClass.name.sub(/^.+(?=::)::(?:LX)?/, '').downcase
|
38
|
+
end
|
39
|
+
end # class
|
40
|
+
end # module
|
41
|
+
end # module
|
data/lib/loxxy/error.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Loxxy
|
4
|
+
# Abstract class. Generalization of Loxxy error classes.
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Error occurring while Loxxy executes some invalid Lox code.
|
8
|
+
class RuntimeError < Error; end
|
9
|
+
|
10
|
+
# Error occurring while Loxxy parses some invalid Lox code.
|
11
|
+
class SyntaxError < Error; end
|
12
|
+
end
|
@@ -74,7 +74,7 @@ module Loxxy
|
|
74
74
|
rule('elsePart_opt' => [])
|
75
75
|
|
76
76
|
rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
|
77
|
-
rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
|
77
|
+
rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
|
78
78
|
rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as 'while_stmt'
|
79
79
|
rule('block' => 'LEFT_BRACE declaration_plus RIGHT_BRACE').as 'block_stmt'
|
80
80
|
rule('block' => 'LEFT_BRACE RIGHT_BRACE').as 'block_empty'
|
@@ -127,7 +127,7 @@ module Loxxy
|
|
127
127
|
rule('unaryOp' => 'MINUS')
|
128
128
|
rule('call' => 'primary')
|
129
129
|
rule('call' => 'primary refinement_plus').as 'call_expr'
|
130
|
-
rule('refinement_plus' => 'refinement_plus refinement')
|
130
|
+
rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
|
131
131
|
rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
|
132
132
|
rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
|
133
133
|
rule('refinement' => 'DOT IDENTIFIER')
|
@@ -92,6 +92,7 @@ module Loxxy
|
|
92
92
|
|
93
93
|
private
|
94
94
|
|
95
|
+
# rubocop: disable Lint/DuplicateBranch
|
95
96
|
def _next_token
|
96
97
|
skip_intertoken_spaces
|
97
98
|
curr_ch = scanner.peek(1)
|
@@ -125,6 +126,7 @@ module Loxxy
|
|
125
126
|
|
126
127
|
return token
|
127
128
|
end
|
129
|
+
# rubocop: enable Lint/DuplicateBranch
|
128
130
|
|
129
131
|
def build_token(aSymbolName, aLexeme)
|
130
132
|
begin
|
data/lib/loxxy/version.rb
CHANGED
data/loxxy.gemspec
CHANGED
@@ -46,8 +46,8 @@ Gem::Specification.new do |spec|
|
|
46
46
|
spec.license = 'MIT'
|
47
47
|
spec.required_ruby_version = '~> 2.4'
|
48
48
|
|
49
|
-
spec.bindir = '
|
50
|
-
spec.executables =
|
49
|
+
spec.bindir = 'bin'
|
50
|
+
spec.executables = ['loxxy']
|
51
51
|
spec.require_paths = ['lib']
|
52
52
|
|
53
53
|
PkgExtending.pkg_files(spec)
|
@@ -36,6 +36,8 @@ module Loxxy
|
|
36
36
|
|
37
37
|
|
38
38
|
it "should react to 'after_var_stmt' event" do
|
39
|
+
# Precondition: value to assign is on top of stack
|
40
|
+
subject.stack.push(greeting)
|
39
41
|
expect { subject.after_var_stmt(var_decl) }.not_to raise_error
|
40
42
|
current_env = subject.symbol_table.current_env
|
41
43
|
expect(current_env.defns['greeting']).to be_kind_of(Variable)
|
data/spec/interpreter_spec.rb
CHANGED
@@ -53,15 +53,6 @@ module Loxxy
|
|
53
53
|
end # context
|
54
54
|
|
55
55
|
context 'Evaluating Lox code:' do
|
56
|
-
let(:hello_world) do
|
57
|
-
lox = <<-LOX_END
|
58
|
-
var greeting = "Hello"; // Declaring a variable
|
59
|
-
print greeting + ", " + "world!"; // ... Playing with concatenation
|
60
|
-
LOX_END
|
61
|
-
|
62
|
-
lox
|
63
|
-
end
|
64
|
-
|
65
56
|
it 'should evaluate core data types' do
|
66
57
|
result = subject.evaluate('true; // Not false')
|
67
58
|
expect(result).to be_kind_of(Loxxy::Datatype::True)
|
@@ -269,9 +260,9 @@ LOX_END
|
|
269
260
|
|
270
261
|
it 'should accept variable mention' do
|
271
262
|
program = <<-LOX_END
|
272
|
-
|
273
|
-
|
274
|
-
LOX_END
|
263
|
+
var foo = "bar";
|
264
|
+
print foo; // => bar
|
265
|
+
LOX_END
|
275
266
|
expect { subject.evaluate(program) }.not_to raise_error
|
276
267
|
expect(sample_cfg[:ostream].string).to eq('bar')
|
277
268
|
end
|
@@ -424,8 +415,48 @@ LOX_END
|
|
424
415
|
expect(sample_cfg[:ostream].string).to eq('<fn foo><native fn>')
|
425
416
|
end
|
426
417
|
|
418
|
+
it 'should support return statements' do
|
419
|
+
program = <<-LOX_END
|
420
|
+
fun max(a, b) {
|
421
|
+
if (a > b) return a;
|
422
|
+
|
423
|
+
return b;
|
424
|
+
}
|
425
|
+
|
426
|
+
max(3, 2);
|
427
|
+
LOX_END
|
428
|
+
result = subject.evaluate(program)
|
429
|
+
expect(result).to eq(3)
|
430
|
+
end
|
431
|
+
|
432
|
+
# rubocop: disable Style/StringConcatenation
|
433
|
+
it 'should support local functions and closures' do
|
434
|
+
program = <<-LOX_END
|
435
|
+
fun makeCounter() {
|
436
|
+
var i = 0;
|
437
|
+
fun count() {
|
438
|
+
i = i + 1;
|
439
|
+
print i;
|
440
|
+
}
|
441
|
+
|
442
|
+
return count;
|
443
|
+
}
|
444
|
+
|
445
|
+
var counter = makeCounter();
|
446
|
+
counter(); // "1".
|
447
|
+
counter(); // "2".
|
448
|
+
LOX_END
|
449
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
450
|
+
expect(sample_cfg[:ostream].string).to eq('1' + '2')
|
451
|
+
end
|
452
|
+
# rubocop: enable Style/StringConcatenation
|
453
|
+
|
427
454
|
it 'should print the hello world message' do
|
428
|
-
|
455
|
+
program = <<-LOX_END
|
456
|
+
var greeting = "Hello"; // Declaring a variable
|
457
|
+
print greeting + ", " + "world!"; // ... Playing with concatenation
|
458
|
+
LOX_END
|
459
|
+
expect { subject.evaluate(program) }.not_to raise_error
|
429
460
|
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|
430
461
|
end
|
431
462
|
end # context
|
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.07
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitri Geshef
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -69,7 +69,8 @@ dependencies:
|
|
69
69
|
description: An implementation of the Lox programming language. WIP
|
70
70
|
email:
|
71
71
|
- famished.tiger@yahoo.com
|
72
|
-
executables:
|
72
|
+
executables:
|
73
|
+
- loxxy
|
73
74
|
extensions: []
|
74
75
|
extra_rdoc_files:
|
75
76
|
- README.md
|
@@ -83,6 +84,7 @@ files:
|
|
83
84
|
- LICENSE.txt
|
84
85
|
- README.md
|
85
86
|
- Rakefile
|
87
|
+
- bin/loxxy
|
86
88
|
- lib/loxxy.rb
|
87
89
|
- lib/loxxy/ast/all_lox_nodes.rb
|
88
90
|
- lib/loxxy/ast/ast_builder.rb
|
@@ -101,16 +103,19 @@ files:
|
|
101
103
|
- lib/loxxy/ast/lox_node.rb
|
102
104
|
- lib/loxxy/ast/lox_noop_expr.rb
|
103
105
|
- lib/loxxy/ast/lox_print_stmt.rb
|
106
|
+
- lib/loxxy/ast/lox_return_stmt.rb
|
104
107
|
- lib/loxxy/ast/lox_seq_decl.rb
|
105
108
|
- lib/loxxy/ast/lox_unary_expr.rb
|
106
109
|
- lib/loxxy/ast/lox_var_stmt.rb
|
107
110
|
- lib/loxxy/ast/lox_variable_expr.rb
|
108
111
|
- lib/loxxy/ast/lox_while_stmt.rb
|
112
|
+
- lib/loxxy/back_end/binary_operator.rb
|
109
113
|
- lib/loxxy/back_end/engine.rb
|
110
114
|
- lib/loxxy/back_end/entry.rb
|
111
115
|
- lib/loxxy/back_end/environment.rb
|
112
|
-
- lib/loxxy/back_end/
|
116
|
+
- lib/loxxy/back_end/lox_function.rb
|
113
117
|
- lib/loxxy/back_end/symbol_table.rb
|
118
|
+
- lib/loxxy/back_end/unary_operator.rb
|
114
119
|
- lib/loxxy/back_end/variable.rb
|
115
120
|
- lib/loxxy/datatype/all_datatypes.rb
|
116
121
|
- lib/loxxy/datatype/boolean.rb
|
@@ -120,6 +125,7 @@ files:
|
|
120
125
|
- lib/loxxy/datatype/nil.rb
|
121
126
|
- lib/loxxy/datatype/number.rb
|
122
127
|
- lib/loxxy/datatype/true.rb
|
128
|
+
- lib/loxxy/error.rb
|
123
129
|
- lib/loxxy/front_end/grammar.rb
|
124
130
|
- lib/loxxy/front_end/literal.rb
|
125
131
|
- lib/loxxy/front_end/parser.rb
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../datatype/all_datatypes'
|
4
|
-
|
5
|
-
module Loxxy
|
6
|
-
module BackEnd
|
7
|
-
# rubocop: disable Style/AccessorGrouping
|
8
|
-
# Representation of a Lox function.
|
9
|
-
# It is a named slot that can be associated with a value at the time.
|
10
|
-
class Function
|
11
|
-
# @return [String]
|
12
|
-
attr_reader :name
|
13
|
-
|
14
|
-
# @return [Array<>] the parameters
|
15
|
-
attr_reader :parameters
|
16
|
-
attr_reader :body
|
17
|
-
attr_reader :stack
|
18
|
-
|
19
|
-
# Create a variable with given name and initial value
|
20
|
-
# @param aName [String] The name of the variable
|
21
|
-
# @param aValue [Datatype::BuiltinDatatype] the initial assigned value
|
22
|
-
def initialize(aName, parameterList, aBody, aStack)
|
23
|
-
@name = aName.dup
|
24
|
-
@parameters = parameterList
|
25
|
-
@body = aBody
|
26
|
-
@stack = aStack
|
27
|
-
end
|
28
|
-
|
29
|
-
def accept(_visitor)
|
30
|
-
stack.push self
|
31
|
-
end
|
32
|
-
|
33
|
-
def call(aVisitor)
|
34
|
-
body.empty? ? Datatype::Nil.instance : body.accept(aVisitor)
|
35
|
-
end
|
36
|
-
|
37
|
-
# Text representation of a Lox function
|
38
|
-
def to_str
|
39
|
-
"<fn #{name}>"
|
40
|
-
end
|
41
|
-
end # class
|
42
|
-
# rubocop: enable Style/AccessorGrouping
|
43
|
-
end # module
|
44
|
-
end # module
|