loxxy 0.1.03 → 0.1.08

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bf956c6e2d6736535acd1e9b0cb29df6b55a8d9002be073cb70bbf753fb06c9
4
- data.tar.gz: 1f635dc69e588f01406cd988bf98d12b9ea398f36e249113a37b089788ef21cb
3
+ metadata.gz: 1724fce16b30fe2328ba31a725e92cc03f43ac7dee60fd54c68642c5a5639471
4
+ data.tar.gz: 0f045add2ebf55375ee63e94f3a7316984588d0133200f3f4ce45de3ae118157
5
5
  SHA512:
6
- metadata.gz: dce93451a9efc548df45683f5772a5d5509c25a57969465f3ff9f48f8b5adcc642b7bb92b25d43658869b5b62d3108796964226c35130cb1492335dc6eaf8fe1
7
- data.tar.gz: 881dfd881db0d2ec3e56b91ad14bc6dd0e0b91ba3839fa1efe09c3636aac575856e3e8c4d4a64a55e913a757a4f1d275cc843734ceda3a23ef597591cb282f3f
6
+ metadata.gz: 5e4a67161f2d6f6a89b237bdda4e1d096df1183eeb3a9b8ff4f1506d8b053bae265de73857faf82a225b77fad5d32faddfe07873d74e0e71a2839f113659c61b
7
+ data.tar.gz: 29d09adcb56231c4f7b7c911c16b978e4ec3e0608f754e8b80b971293b99ea96fcf2bb6cd55dec62a8acac00bde0fb9b93e86cc37ee11024faea8e8eb0a70d3f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
+ ## [0.1.08] - 2021-03-27
2
+ - `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
3
+
4
+ ### New
5
+ - Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
6
+
7
+ ### Changed
8
+ - Class `Ast::Visitor` changes in some method signatures
9
+ - Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
10
+ - Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
11
+
12
+ ## [0.1.07] - 2021-03-14
13
+ - `Loxxy` now supports nested functions and closures
14
+
15
+ ### Changed
16
+ - Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
17
+ - Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
18
+ - Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
19
+ - Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
20
+ - Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
21
+
22
+ ### Fixed
23
+ - Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
24
+
25
+
26
+ ## [0.1.06] - 2021-03-06
27
+ - Parameters/arguments checks in function declaration and call
28
+
29
+ ### Changed
30
+ - Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
31
+ - Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
32
+
33
+ - Class `BackEnd::Function` renamed to `LoxFunction`
34
+
35
+ ## [0.1.05] - 2021-03-05
36
+ - Test for Fibbonacci recursive function is now passing.
37
+
38
+ ### Fixed
39
+ - Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
40
+
41
+ ## [0.1.04] - 2021-02-28
42
+
43
+ ### Added
44
+ - Class `Ast::LoxReturnStmt` a node that represents a return statement
45
+ - Method `Ast::ASTBuilder#reduce_return_stmt`
46
+ - Method `Ast::ASTVisitor#visit_return_stmt` for visiting an `Ast::LoxReturnStmt` node
47
+ - Method `BackEnd::Engine#after_return_stmt` to handle return statement
48
+ - Method `BackEnd::Function#!` implementing the logical negation of a function (as value).
49
+ - Test suite for logical operators (in project repository)
50
+ - Test suite for block code
51
+ - Test suite for call and function declaration (initial)
52
+
53
+ ### Changed
54
+ - Method `BackEnd::Engine#after_call_expr` now generate a `catch` and `throw` events
55
+
1
56
  ## [0.1.03] - 2021-02-26
2
- - Runtime argument chacking for arithmetic and comparison operators
57
+ - Runtime argument checking for arithmetic and comparison operators
3
58
 
4
59
  ### Added
5
60
  - Test suite for arithmetic and comparison operators (in project repository)
@@ -221,11 +276,11 @@
221
276
  - The interpreter can evaluate substraction between two numbers.
222
277
 
223
278
  ### Added
224
- - Method `Datatype::Number#-` implmenting the subtraction operation
279
+ - Method `Datatype::Number#-` implementing the subtraction operation
225
280
 
226
281
  ### Changed
227
282
  - File `README.md` minor editorial changes.
228
- - File `lx_string_spec.rb` Added test for string concatentation
283
+ - File `lx_string_spec.rb` Added test for string concatenation
229
284
  - File `number_spec.rb` Added tests for addition and subtraction operations
230
285
  - File `interpreter_spec.rb` Added tests for subtraction operation
231
286
 
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### What is loxxy?
6
6
  A Ruby implementation of the [Lox programming language](https://craftinginterpreters.com/the-lox-language.html ),
7
- a simple language used in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/ ).
7
+ a simple language defined in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/ ).
8
8
 
9
9
  ### Purpose of this project:
10
10
  - To deliver an open source example of a programming language fully implemented in Ruby
@@ -13,12 +13,10 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
13
13
  a Lox interpreter written in Lox.
14
14
 
15
15
  ### Current status
16
- The project is still in inception and the interpreter is being implemented...
17
- Currently it can execute all allowed __Lox__ expressions and statements except:
18
- - Closures,
19
- - Classes and objects.
16
+ The interpreter currently it can execute all allowed __Lox__ expressions and statements except
17
+ object-oriented feaures (classes and objects).
20
18
 
21
- These will be implemented soon.
19
+ Our intent is implement to these missing features in Q2 2021.
22
20
 
23
21
 
24
22
  ## What's the fuss about Lox?
@@ -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
- theChildren[1].callee = theChildren[0]
259
- theChildren[1]
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[0]
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 StandardError, "Can't have more than 255 arguments."
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)
@@ -112,7 +120,7 @@ module Loxxy
112
120
  def visit_assign_expr(anAssignExpr)
113
121
  broadcast(:before_assign_expr, anAssignExpr)
114
122
  traverse_subnodes(anAssignExpr)
115
- broadcast(:after_assign_expr, anAssignExpr)
123
+ broadcast(:after_assign_expr, anAssignExpr, self)
116
124
  end
117
125
 
118
126
  # Visit event. The visitor is about to visit a logical expression.
@@ -186,7 +194,7 @@ module Loxxy
186
194
  # Visit event. The visitor is about to visit a function statement node.
187
195
  # @param aFunStmt [AST::LoxFunStmt] function declaration to visit
188
196
  def visit_fun_stmt(aFunStmt)
189
- broadcast(:before_fun_stmt, aFunStmt)
197
+ broadcast(:before_fun_stmt, aFunStmt, self)
190
198
  traverse_subnodes(aFunStmt)
191
199
  broadcast(:after_fun_stmt, aFunStmt, self)
192
200
  end
@@ -20,8 +20,6 @@ module Loxxy
20
20
  def accept(visitor)
21
21
  visitor.visit_block_stmt(self)
22
22
  end
23
-
24
- alias operands subnodes
25
23
  end # class
26
24
  end # module
27
25
  end # module
@@ -26,8 +26,6 @@ module Loxxy
26
26
  def accept(visitor)
27
27
  visitor.visit_fun_stmt(self)
28
28
  end
29
-
30
- alias operands subnodes
31
29
  end # class
32
30
  # rubocop: enable Style/AccessorGrouping
33
31
  end # module
@@ -16,8 +16,6 @@ module Loxxy
16
16
  def accept(visitor)
17
17
  visitor.visit_grouping_expr(self)
18
18
  end
19
-
20
- alias operands subnodes
21
19
  end # class
22
20
  end # module
23
21
  end # module
@@ -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
@@ -10,8 +10,6 @@ module Loxxy
10
10
  def accept(visitor)
11
11
  visitor.visit_seq_decl(self)
12
12
  end
13
-
14
- alias operands subnodes
15
13
  end # class
16
14
  end # module
17
15
  end # module
@@ -13,8 +13,8 @@ module Loxxy
13
13
  # @param aName [String] name of the variable
14
14
  # @param aValue [Loxxy::Ast::LoxNode, NilClass] initial value for the variable
15
15
  def initialize(aPosition, aName, aValue)
16
- initial_value = aValue ? [aValue] : [Datatype::Nil.instance]
17
- super(aPosition, initial_value)
16
+ initial_value = aValue || Datatype::Nil.instance
17
+ super(aPosition, [initial_value])
18
18
  @name = aName
19
19
  end
20
20
 
@@ -3,7 +3,8 @@
3
3
  # Load all the classes implementing AST nodes
4
4
  require_relative '../ast/all_lox_nodes'
5
5
  require_relative 'binary_operator'
6
- require_relative 'function'
6
+ require_relative 'lox_function'
7
+ require_relative 'resolver'
7
8
  require_relative 'symbol_table'
8
9
  require_relative 'unary_operator'
9
10
 
@@ -11,7 +12,6 @@ module Loxxy
11
12
  module BackEnd
12
13
  # An instance of this class executes the statements as when they
13
14
  # occur during the abstract syntax tree walking.
14
- # @note WIP: very crude implementation.
15
15
  class Engine
16
16
  # @return [Hash] A set of configuration options
17
17
  attr_reader :config
@@ -19,7 +19,7 @@ module Loxxy
19
19
  # @return [BackEnd::SymbolTable]
20
20
  attr_reader :symbol_table
21
21
 
22
- # @return [Array<Datatype::BuiltinDatatyp>] Stack for the values of expr
22
+ # @return [Array<Datatype::BuiltinDatatype>] Data stack for the expression results
23
23
  attr_reader :stack
24
24
 
25
25
  # @return [Hash { Symbol => UnaryOperator}]
@@ -28,6 +28,9 @@ module Loxxy
28
28
  # @return [Hash { Symbol => BinaryOperator}]
29
29
  attr_reader :binary_operators
30
30
 
31
+ # @return [BackEnd::Resolver]
32
+ attr_reader :resolver
33
+
31
34
  # @param theOptions [Hash]
32
35
  def initialize(theOptions)
33
36
  @config = theOptions
@@ -47,6 +50,10 @@ module Loxxy
47
50
  # @param aVisitor [AST::ASTVisitor]
48
51
  # @return [Loxxy::Datatype::BuiltinDatatype]
49
52
  def execute(aVisitor)
53
+ # Do variable resolution pass first
54
+ @resolver = BackEnd::Resolver.new
55
+ resolver.analyze(aVisitor)
56
+
50
57
  aVisitor.subscribe(self)
51
58
  aVisitor.start
52
59
  aVisitor.unsubscribe(self)
@@ -61,11 +68,20 @@ module Loxxy
61
68
  # Do nothing, subnodes were already evaluated
62
69
  end
63
70
 
64
- def after_var_stmt(aVarStmt)
65
- new_var = Variable.new(aVarStmt.name, aVarStmt.subnodes[0])
71
+ def before_var_stmt(aVarStmt)
72
+ new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
66
73
  symbol_table.insert(new_var)
67
74
  end
68
75
 
76
+ def after_var_stmt(aVarStmt)
77
+ var_name = aVarStmt.name
78
+ variable = symbol_table.lookup(var_name)
79
+ raise StandardError, "Unknown variable #{var_name}" unless variable
80
+
81
+ value = stack.pop
82
+ variable.assign(value)
83
+ end
84
+
69
85
  def before_for_stmt(aForStmt)
70
86
  before_block_stmt(aForStmt)
71
87
  end
@@ -98,6 +114,10 @@ module Loxxy
98
114
  @ostream.print tos ? tos.to_str : 'nil'
99
115
  end
100
116
 
117
+ def after_return_stmt(_returnStmt, _aVisitor)
118
+ throw(:return)
119
+ end
120
+
101
121
  def after_while_stmt(aWhileStmt, aVisitor)
102
122
  loop do
103
123
  condition = stack.pop
@@ -117,14 +137,13 @@ module Loxxy
117
137
  symbol_table.leave_environment
118
138
  end
119
139
 
120
- def after_assign_expr(anAssignExpr)
140
+ def after_assign_expr(anAssignExpr, _visitor)
121
141
  var_name = anAssignExpr.name
122
- variable = symbol_table.lookup(var_name)
142
+ variable = variable_lookup(anAssignExpr)
123
143
  raise StandardError, "Unknown variable #{var_name}" unless variable
124
144
 
125
- value = stack.pop
145
+ value = stack.last # ToS remains since an assignment produces a value
126
146
  variable.assign(value)
127
- stack.push value # An expression produces a value
128
147
  end
129
148
 
130
149
  def after_logical_expr(aLogicalExpr, visitor)
@@ -192,18 +211,18 @@ module Loxxy
192
211
  callee = stack.pop
193
212
  aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
194
213
 
195
- if callee.kind_of?(NativeFunction)
214
+ case callee
215
+ when NativeFunction
196
216
  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)
217
+ when LoxFunction
218
+ arg_count = aCallExpr.arguments.size
219
+ if arg_count != callee.arity
220
+ msg = "Expected #{callee.arity} arguments but got #{arg_count}."
221
+ raise Loxxy::RuntimeError, msg
203
222
  end
204
- callee.call(aVisitor)
205
-
206
- symbol_table.leave_environment
223
+ callee.call(self, aVisitor)
224
+ else
225
+ raise Loxxy::RuntimeError, 'Can only call functions and classes.'
207
226
  end
208
227
  end
209
228
 
@@ -213,10 +232,10 @@ module Loxxy
213
232
 
214
233
  def after_variable_expr(aVarExpr, aVisitor)
215
234
  var_name = aVarExpr.name
216
- var = symbol_table.lookup(var_name)
235
+ var = variable_lookup(aVarExpr)
217
236
  raise StandardError, "Unknown variable #{var_name}" unless var
218
237
 
219
- var.value.accept(aVisitor) # Evaluate the variable value
238
+ var.value.accept(aVisitor) # Evaluate variable value then push on stack
220
239
  end
221
240
 
222
241
  # @param literalExpr [Ast::LoxLiteralExpr]
@@ -230,13 +249,26 @@ module Loxxy
230
249
  end
231
250
 
232
251
  def after_fun_stmt(aFunStmt, _visitor)
233
- function = Function.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, stack)
252
+ function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
234
253
  new_var = Variable.new(aFunStmt.name, function)
235
254
  symbol_table.insert(new_var)
236
255
  end
237
256
 
238
257
  private
239
258
 
259
+ def variable_lookup(aVarNode)
260
+ env = nil
261
+ offset = resolver.locals[aVarNode]
262
+ if offset.nil?
263
+ env = symbol_table.root
264
+ else
265
+ env = symbol_table.current_env
266
+ offset.times { env = env.enclosing }
267
+ end
268
+
269
+ env.defns[aVarNode.name]
270
+ end
271
+
240
272
  NativeFunction = Struct.new(:callable, :interp) do
241
273
  def accept(_visitor)
242
274
  interp.stack.push self
@@ -255,7 +287,8 @@ module Loxxy
255
287
  negate_op = UnaryOperator.new('-', [Datatype::Number])
256
288
  unary_operators[:-@] = negate_op
257
289
 
258
- negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype])
290
+ negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
291
+ BackEnd::LoxFunction])
259
292
  unary_operators[:!] = negation_op
260
293
  end
261
294
 
@@ -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 enclosing (parent) environment.
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
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all the classes implementing AST nodes
4
+ require_relative '../ast/all_lox_nodes'
5
+ require_relative 'binary_operator'
6
+ require_relative 'lox_function'
7
+ require_relative 'symbol_table'
8
+ require_relative 'unary_operator'
9
+
10
+ module Loxxy
11
+ module BackEnd
12
+ # A class aimed to perform variable resolution when it visits the parse tree.
13
+ # Resolving means retrieve the declaration of a variable/function everywhere it
14
+ # is referenced.
15
+ class Resolver
16
+ # A stack of Hashes of the form String => Boolean
17
+ # @return [Array<Hash{String => Boolean}>]
18
+ attr_reader :scopes
19
+
20
+ # A map from a LoxNode involving a variable and the number of enclosing scopes
21
+ # where it is declared.
22
+ # @return [Hash {LoxNode => Integer}]
23
+ attr_reader :locals
24
+
25
+ def initialize
26
+ @scopes = []
27
+ @locals = {}
28
+ end
29
+
30
+ # Given an abstract syntax parse tree visitor, launch the visit
31
+ # and execute the visit events in the output stream.
32
+ # @param aVisitor [AST::ASTVisitor]
33
+ # @return [Loxxy::Datatype::BuiltinDatatype]
34
+ def analyze(aVisitor)
35
+ begin_scope
36
+ aVisitor.subscribe(self)
37
+ aVisitor.start
38
+ aVisitor.unsubscribe(self)
39
+ end_scope
40
+ end
41
+
42
+ # block statement introduces a new scope
43
+ def before_block_stmt(_aBlockStmt)
44
+ begin_scope
45
+ end
46
+
47
+ def after_block_stmt(_aBlockStmt)
48
+ end_scope
49
+ end
50
+
51
+ def before_for_stmt(aForStmt)
52
+ before_block_stmt(aForStmt)
53
+ end
54
+
55
+ def after_for_stmt(aForStmt, aVisitor)
56
+ aForStmt.test_expr.accept(aVisitor)
57
+ aForStmt.body_stmt.accept(aVisitor)
58
+ aForStmt.update_expr&.accept(aVisitor)
59
+ after_block_stmt(aForStmt)
60
+ end
61
+
62
+ def after_if_stmt(anIfStmt, aVisitor)
63
+ anIfStmt.then_stmt.accept(aVisitor)
64
+ anIfStmt.else_stmt&.accept(aVisitor)
65
+ end
66
+
67
+ def after_while_stmt(aWhileStmt, aVisitor)
68
+ aWhileStmt.body.accept(aVisitor)
69
+ aWhileStmt.condition.accept(aVisitor)
70
+ end
71
+
72
+ # A variable declaration adds a new variable to current scope
73
+ def before_var_stmt(aVarStmt)
74
+ declare(aVarStmt.name)
75
+ end
76
+
77
+ def after_var_stmt(aVarStmt)
78
+ define(aVarStmt.name)
79
+ end
80
+
81
+ # Assignment expressions require their variables resolved
82
+ def after_assign_expr(anAssignExpr, aVisitor)
83
+ resolve_local(anAssignExpr, aVisitor)
84
+ end
85
+
86
+ # Variable expressions require their variables resolved
87
+ def before_variable_expr(aVarExpr)
88
+ var_name = aVarExpr.name
89
+ if !scopes.empty? && (scopes.last[var_name] == false)
90
+ raise StandardError, "Can't read variable #{var_name} in its own initializer"
91
+ end
92
+ end
93
+
94
+ def after_variable_expr(aVarExpr, aVisitor)
95
+ resolve_local(aVarExpr, aVisitor)
96
+ end
97
+
98
+ def after_call_expr(aCallExpr, aVisitor)
99
+ # Evaluate callee part
100
+ aCallExpr.callee.accept(aVisitor)
101
+ aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
102
+ end
103
+
104
+ # function declaration creates a new scope for its body & binds its parameters for that scope
105
+ def before_fun_stmt(aFunStmt, aVisitor)
106
+ declare(aFunStmt.name)
107
+ define(aFunStmt.name)
108
+ resolve_function(aFunStmt, aVisitor)
109
+ end
110
+
111
+ private
112
+
113
+ def begin_scope
114
+ scopes.push({})
115
+ end
116
+
117
+ def end_scope
118
+ scopes.pop
119
+ end
120
+
121
+ def declare(aVarName)
122
+ return if scopes.empty?
123
+
124
+ curr_scope = scopes.last
125
+
126
+ # The initializer is not yet processed.
127
+ # Mark the variable as 'not yet ready' = exists but may not be referenced yet
128
+ curr_scope[aVarName] = false
129
+ end
130
+
131
+ def define(aVarName)
132
+ return if scopes.empty?
133
+
134
+ curr_scope = scopes.last
135
+
136
+ # The initializer (if any) was processed.
137
+ # Mark the variable as alive (= can be referenced in an expression)
138
+ curr_scope[aVarName] = true
139
+ end
140
+
141
+ def resolve_local(aVarExpr, _visitor)
142
+ max_i = i = scopes.size - 1
143
+ scopes.reverse_each do |scp|
144
+ if scp.include?(aVarExpr.name)
145
+ # Keep track of the difference of nesting levels between current scope
146
+ # and the scope where the variable is declared
147
+ @locals[aVarExpr] = max_i - i
148
+ break
149
+ end
150
+ i -= 1
151
+ end
152
+ end
153
+
154
+ def resolve_function(aFunStmt, aVisitor)
155
+ begin_scope
156
+
157
+ aFunStmt.params&.each do |param_name|
158
+ declare(param_name)
159
+ define(param_name)
160
+ end
161
+
162
+ body = aFunStmt.body
163
+ unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
164
+ body.subnodes.first&.accept(aVisitor)
165
+ end
166
+
167
+ end_scope
168
+ end
169
+ end # class
170
+ end # mmodule
171
+ 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 = current_env
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.defns.each_pair do |nm, _item|
53
- environments = name2envs[nm]
54
- if environments.size == 1
55
- name2envs.delete(nm)
56
- else
57
- environments.pop
58
- name2envs[nm] = environments
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
- @current_env = current_env.enclosing
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.
data/lib/loxxy/error.rb CHANGED
@@ -6,4 +6,7 @@ module Loxxy
6
6
 
7
7
  # Error occurring while Loxxy executes some invalid Lox code.
8
8
  class RuntimeError < Error; end
9
+
10
+ # Error occurring while Loxxy parses some invalid Lox code.
11
+ class SyntaxError < Error; end
9
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') # .as 'refinement_plus_more'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Loxxy
4
- VERSION = '0.1.03'
4
+ VERSION = '0.1.08'
5
5
  end
@@ -34,8 +34,18 @@ 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
37
42
 
38
43
  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
+ # Precondition: value to assign is on top of stack
47
+ subject.stack.push(greeting)
48
+
39
49
  expect { subject.after_var_stmt(var_decl) }.not_to raise_error
40
50
  current_env = subject.symbol_table.current_env
41
51
  expect(current_env.defns['greeting']).to be_kind_of(Variable)
@@ -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
- var foo = "bar";
273
- print foo; // => bar
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,18 +415,49 @@ LOX_END
424
415
  expect(sample_cfg[:ostream].string).to eq('<fn foo><native fn>')
425
416
  end
426
417
 
427
- it 'should print the hello world message' do
428
- expect { subject.evaluate(hello_world) }.not_to raise_error
429
- expect(sample_cfg[:ostream].string).to eq('Hello, world!')
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
430
  end
431
- end # context
432
431
 
433
- context 'Test suite:' do
434
- it "should complain if one argument isn't a number" do
435
- source = '1 + nil;'
436
- err = Loxxy::RuntimeError
437
- err_msg = 'Operands must be two numbers or two strings.'
438
- expect { subject.evaluate(source) }.to raise_error(err, err_msg)
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
+
454
+ it 'should print the hello world message' do
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
460
+ expect(sample_cfg[:ostream].string).to eq('Hello, world!')
439
461
  end
440
462
  end # context
441
463
  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.03
4
+ version: 0.1.08
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-02-26 00:00:00.000000000 Z
11
+ date: 2021-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -103,6 +103,7 @@ files:
103
103
  - lib/loxxy/ast/lox_node.rb
104
104
  - lib/loxxy/ast/lox_noop_expr.rb
105
105
  - lib/loxxy/ast/lox_print_stmt.rb
106
+ - lib/loxxy/ast/lox_return_stmt.rb
106
107
  - lib/loxxy/ast/lox_seq_decl.rb
107
108
  - lib/loxxy/ast/lox_unary_expr.rb
108
109
  - lib/loxxy/ast/lox_var_stmt.rb
@@ -112,7 +113,8 @@ files:
112
113
  - lib/loxxy/back_end/engine.rb
113
114
  - lib/loxxy/back_end/entry.rb
114
115
  - lib/loxxy/back_end/environment.rb
115
- - lib/loxxy/back_end/function.rb
116
+ - lib/loxxy/back_end/lox_function.rb
117
+ - lib/loxxy/back_end/resolver.rb
116
118
  - lib/loxxy/back_end/symbol_table.rb
117
119
  - lib/loxxy/back_end/unary_operator.rb
118
120
  - lib/loxxy/back_end/variable.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