loxxy 0.0.27 → 0.1.03
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 +64 -0
- data/README.md +56 -10
- data/bin/loxxy +11 -0
- data/lib/loxxy.rb +0 -2
- data/lib/loxxy/ast/all_lox_nodes.rb +2 -0
- data/lib/loxxy/ast/ast_builder.rb +56 -83
- data/lib/loxxy/ast/ast_visitor.rb +28 -10
- data/lib/loxxy/ast/lox_block_stmt.rb +5 -1
- data/lib/loxxy/ast/lox_call_expr.rb +25 -0
- data/lib/loxxy/ast/lox_for_stmt.rb +0 -1
- data/lib/loxxy/ast/lox_fun_stmt.rb +34 -0
- data/lib/loxxy/ast/lox_if_stmt.rb +3 -1
- data/lib/loxxy/back_end/binary_operator.rb +57 -0
- data/lib/loxxy/back_end/engine.rb +126 -5
- data/lib/loxxy/back_end/function.rb +44 -0
- data/lib/loxxy/back_end/unary_operator.rb +41 -0
- data/lib/loxxy/error.rb +9 -0
- data/lib/loxxy/front_end/grammar.rb +28 -28
- data/lib/loxxy/front_end/literal.rb +1 -1
- data/lib/loxxy/version.rb +1 -1
- data/loxxy.gemspec +2 -2
- data/spec/datatype/lx_string_spec.rb +2 -0
- data/spec/datatype/number_spec.rb +3 -1
- data/spec/front_end/scanner_spec.rb +2 -0
- data/spec/interpreter_spec.rb +45 -7
- metadata +12 -4
@@ -6,11 +6,15 @@ module Loxxy
|
|
6
6
|
module Ast
|
7
7
|
class LoxBlockStmt < LoxCompoundExpr
|
8
8
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
9
|
-
# @param
|
9
|
+
# @param decls [Loxxy::Ast::LoxSeqDecl]
|
10
10
|
def initialize(aPosition, decls)
|
11
11
|
super(aPosition, [decls])
|
12
12
|
end
|
13
13
|
|
14
|
+
def empty?
|
15
|
+
subnodes.size == 1 && subnodes[0].nil?
|
16
|
+
end
|
17
|
+
|
14
18
|
# Part of the 'visitee' role in Visitor design pattern.
|
15
19
|
# @param visitor [Ast::ASTVisitor] the visitor
|
16
20
|
def accept(visitor)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
class LoxCallExpr < LoxCompoundExpr
|
8
|
+
attr_accessor :callee
|
9
|
+
attr_reader :arguments
|
10
|
+
|
11
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
12
|
+
# @param argList [Array<Loxxy::Ast::LoxNode>]
|
13
|
+
def initialize(aPosition, argList)
|
14
|
+
super(aPosition, [])
|
15
|
+
@arguments = argList
|
16
|
+
end
|
17
|
+
|
18
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
19
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
20
|
+
def accept(visitor)
|
21
|
+
visitor.visit_call_expr(self)
|
22
|
+
end
|
23
|
+
end # class
|
24
|
+
end # module
|
25
|
+
end # module
|
@@ -18,7 +18,6 @@ module Loxxy
|
|
18
18
|
# @param initialization [Loxxy::Ast::LoxNode]
|
19
19
|
# @param testExpr [Loxxy::Ast::LoxNode]
|
20
20
|
# @param updateExpr [Loxxy::Ast::LoxNode]
|
21
|
-
# @param body [Loxxy::Ast::LoxNode]
|
22
21
|
def initialize(aPosition, initialization, testExpr, updateExpr)
|
23
22
|
child = initialization ? [initialization] : []
|
24
23
|
super(aPosition, child)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lox_compound_expr'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
# rubocop: disable Style/AccessorGrouping
|
8
|
+
class LoxFunStmt < LoxCompoundExpr
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :params
|
11
|
+
attr_reader :body
|
12
|
+
|
13
|
+
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
14
|
+
# @param aName [String]
|
15
|
+
# @param arguments [Array<String>]
|
16
|
+
# @param body [Ast::LoxBlockStmt]
|
17
|
+
def initialize(aPosition, aName, paramList, aBody)
|
18
|
+
super(aPosition, [])
|
19
|
+
@name = aName
|
20
|
+
@params = paramList
|
21
|
+
@body = aBody
|
22
|
+
end
|
23
|
+
|
24
|
+
# Part of the 'visitee' role in Visitor design pattern.
|
25
|
+
# @param visitor [Ast::ASTVisitor] the visitor
|
26
|
+
def accept(visitor)
|
27
|
+
visitor.visit_fun_stmt(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
alias operands subnodes
|
31
|
+
end # class
|
32
|
+
# rubocop: enable Style/AccessorGrouping
|
33
|
+
end # module
|
34
|
+
end # module
|
@@ -12,7 +12,9 @@ module Loxxy
|
|
12
12
|
attr_reader :else_stmt
|
13
13
|
|
14
14
|
# @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
|
15
|
-
# @param
|
15
|
+
# @param condExpr [Loxxy::Ast::LoxNode]
|
16
|
+
# @param thenStmt [Loxxy::Ast::LoxNode]
|
17
|
+
# @param elseStmt [Loxxy::Ast::LoxNode]
|
16
18
|
def initialize(aPosition, condExpr, thenStmt, elseStmt)
|
17
19
|
super(aPosition, [condExpr])
|
18
20
|
@then_stmt = thenStmt
|
@@ -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,7 +2,10 @@
|
|
2
2
|
|
3
3
|
# Load all the classes implementing AST nodes
|
4
4
|
require_relative '../ast/all_lox_nodes'
|
5
|
+
require_relative 'binary_operator'
|
6
|
+
require_relative 'function'
|
5
7
|
require_relative 'symbol_table'
|
8
|
+
require_relative 'unary_operator'
|
6
9
|
|
7
10
|
module Loxxy
|
8
11
|
module BackEnd
|
@@ -19,12 +22,24 @@ module Loxxy
|
|
19
22
|
# @return [Array<Datatype::BuiltinDatatyp>] Stack for the values of expr
|
20
23
|
attr_reader :stack
|
21
24
|
|
25
|
+
# @return [Hash { Symbol => UnaryOperator}]
|
26
|
+
attr_reader :unary_operators
|
27
|
+
|
28
|
+
# @return [Hash { Symbol => BinaryOperator}]
|
29
|
+
attr_reader :binary_operators
|
30
|
+
|
22
31
|
# @param theOptions [Hash]
|
23
32
|
def initialize(theOptions)
|
24
33
|
@config = theOptions
|
25
34
|
@ostream = config.include?(:ostream) ? config[:ostream] : $stdout
|
26
35
|
@symbol_table = SymbolTable.new
|
27
36
|
@stack = []
|
37
|
+
@unary_operators = {}
|
38
|
+
@binary_operators = {}
|
39
|
+
|
40
|
+
init_unary_operators
|
41
|
+
init_binary_operators
|
42
|
+
init_globals
|
28
43
|
end
|
29
44
|
|
30
45
|
# Given an abstract syntax parse tree visitor, launch the visit
|
@@ -80,7 +95,7 @@ module Loxxy
|
|
80
95
|
|
81
96
|
def after_print_stmt(_printStmt)
|
82
97
|
tos = stack.pop
|
83
|
-
@ostream.print tos.to_str
|
98
|
+
@ostream.print tos ? tos.to_str : 'nil'
|
84
99
|
end
|
85
100
|
|
86
101
|
def after_while_stmt(aWhileStmt, aVisitor)
|
@@ -145,9 +160,11 @@ module Loxxy
|
|
145
160
|
end
|
146
161
|
|
147
162
|
def after_binary_expr(aBinaryExpr)
|
148
|
-
op = aBinaryExpr.operator
|
149
163
|
operand2 = stack.pop
|
150
164
|
operand1 = stack.pop
|
165
|
+
op = aBinaryExpr.operator
|
166
|
+
operator = binary_operators[op]
|
167
|
+
operator.validate_operands(operand1, operand2)
|
151
168
|
if operand1.respond_to?(op)
|
152
169
|
stack.push operand1.send(op, operand2)
|
153
170
|
else
|
@@ -157,16 +174,39 @@ module Loxxy
|
|
157
174
|
end
|
158
175
|
|
159
176
|
def after_unary_expr(anUnaryExpr)
|
160
|
-
op = anUnaryExpr.operator
|
161
177
|
operand = stack.pop
|
178
|
+
op = anUnaryExpr.operator
|
179
|
+
operator = unary_operators[op]
|
180
|
+
operator.validate_operand(operand)
|
162
181
|
if operand.respond_to?(op)
|
163
182
|
stack.push operand.send(op)
|
164
183
|
else
|
165
|
-
msg1 = "`#{op}': Unimplemented operator for a #{
|
184
|
+
msg1 = "`#{op}': Unimplemented operator for a #{operand.class}."
|
166
185
|
raise StandardError, msg1
|
167
186
|
end
|
168
187
|
end
|
169
188
|
|
189
|
+
def after_call_expr(aCallExpr, aVisitor)
|
190
|
+
# Evaluate callee part
|
191
|
+
aCallExpr.callee.accept(aVisitor)
|
192
|
+
callee = stack.pop
|
193
|
+
aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
|
194
|
+
|
195
|
+
if callee.kind_of?(NativeFunction)
|
196
|
+
stack.push callee.call # Pass arguments
|
197
|
+
else
|
198
|
+
new_env = Environment.new(symbol_table.current_env)
|
199
|
+
symbol_table.enter_environment(new_env)
|
200
|
+
callee.parameters&.each do |param_name|
|
201
|
+
local = Variable.new(param_name, stack.pop)
|
202
|
+
symbol_table.insert(local)
|
203
|
+
end
|
204
|
+
callee.call(aVisitor)
|
205
|
+
|
206
|
+
symbol_table.leave_environment
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
170
210
|
def after_grouping_expr(_groupingExpr)
|
171
211
|
# Do nothing: work was already done by visiting /evaluating the subexpression
|
172
212
|
end
|
@@ -184,10 +224,91 @@ module Loxxy
|
|
184
224
|
stack.push(literalExpr.literal)
|
185
225
|
end
|
186
226
|
|
187
|
-
# @param
|
227
|
+
# @param aValue [Ast::BuiltinDattype] the built-in datatype value
|
188
228
|
def before_visit_builtin(aValue)
|
189
229
|
stack.push(aValue)
|
190
230
|
end
|
231
|
+
|
232
|
+
def after_fun_stmt(aFunStmt, _visitor)
|
233
|
+
function = Function.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, stack)
|
234
|
+
new_var = Variable.new(aFunStmt.name, function)
|
235
|
+
symbol_table.insert(new_var)
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
NativeFunction = Struct.new(:callable, :interp) do
|
241
|
+
def accept(_visitor)
|
242
|
+
interp.stack.push self
|
243
|
+
end
|
244
|
+
|
245
|
+
def call
|
246
|
+
callable.call
|
247
|
+
end
|
248
|
+
|
249
|
+
def to_str
|
250
|
+
'<native fn>'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def init_unary_operators
|
255
|
+
negate_op = UnaryOperator.new('-', [Datatype::Number])
|
256
|
+
unary_operators[:-@] = negate_op
|
257
|
+
|
258
|
+
negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype])
|
259
|
+
unary_operators[:!] = negation_op
|
260
|
+
end
|
261
|
+
|
262
|
+
def init_binary_operators
|
263
|
+
plus_op = BinaryOperator.new('+', [[Datatype::Number, :idem],
|
264
|
+
[Datatype::LXString, :idem]])
|
265
|
+
binary_operators[:+] = plus_op
|
266
|
+
|
267
|
+
minus_op = BinaryOperator.new('-', [[Datatype::Number, :idem]])
|
268
|
+
binary_operators[:-] = minus_op
|
269
|
+
|
270
|
+
star_op = BinaryOperator.new('*', [[Datatype::Number, :idem]])
|
271
|
+
binary_operators[:*] = star_op
|
272
|
+
|
273
|
+
slash_op = BinaryOperator.new('/', [[Datatype::Number, :idem]])
|
274
|
+
binary_operators[:/] = slash_op
|
275
|
+
|
276
|
+
equal_equal_op = BinaryOperator.new('==', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
|
277
|
+
binary_operators[:==] = equal_equal_op
|
278
|
+
|
279
|
+
not_equal_op = BinaryOperator.new('!=', [[Datatype::BuiltinDatatype, Datatype::BuiltinDatatype]])
|
280
|
+
binary_operators[:!=] = not_equal_op
|
281
|
+
|
282
|
+
less_op = BinaryOperator.new('<', [[Datatype::Number, :idem]])
|
283
|
+
binary_operators[:<] = less_op
|
284
|
+
|
285
|
+
less_equal_op = BinaryOperator.new('<=', [[Datatype::Number, :idem]])
|
286
|
+
binary_operators[:<=] = less_equal_op
|
287
|
+
|
288
|
+
greater_op = BinaryOperator.new('>', [[Datatype::Number, :idem]])
|
289
|
+
binary_operators[:>] = greater_op
|
290
|
+
|
291
|
+
greater_equal_op = BinaryOperator.new('>=', [[Datatype::Number, :idem]])
|
292
|
+
binary_operators[:>=] = greater_equal_op
|
293
|
+
end
|
294
|
+
|
295
|
+
def init_globals
|
296
|
+
add_native_fun('clock', native_clock)
|
297
|
+
end
|
298
|
+
|
299
|
+
def add_native_fun(aName, aProc)
|
300
|
+
native_fun = Variable.new(aName, NativeFunction.new(aProc, self))
|
301
|
+
symbol_table.insert(native_fun)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Ruby-native function that returns (as float) the number of seconds since
|
305
|
+
# a given time reference.
|
306
|
+
def native_clock
|
307
|
+
proc do
|
308
|
+
now = Time.now.to_f
|
309
|
+
Datatype::Number.new(now)
|
310
|
+
end
|
311
|
+
end
|
191
312
|
end # class
|
192
313
|
end # module
|
193
314
|
end # module
|
@@ -0,0 +1,44 @@
|
|
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
|
@@ -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