loxxy 0.1.04 → 0.1.09

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: 16614a276ddf1553d4373e5da9ab78d7f03f2fc8cadec6190c28bb27bdd95b99
4
- data.tar.gz: 98903f6ec0c9e010fd429f6b1753b34f52b4cfc67c7eafd21b1103c6ac429bba
3
+ metadata.gz: 1d027baa99cb59b7a102b55d1dec07e75f7cb17a71c42708881507ae8bc83b75
4
+ data.tar.gz: 6f27ba42c296a5845185fb8d746fd8cf24d9b8b71c2c98d590fa8a9c22cac33f
5
5
  SHA512:
6
- metadata.gz: 8278546ef246d8f4d5db72b18d5c91dfa3dc1d7f18f81b7491ca01df38a64495a3b5eb26df0fa18b652f59d56784efb8fe9348b5b65d091aa27a09e6b8f144a4
7
- data.tar.gz: deaa7b43f225788b9366610572f5994eec8950a8e66c8159b83a18b874139d322270f274c4753dc28b6926b7c295d42a2f77600211dfbd0df0e24b82e1a155b5
6
+ metadata.gz: 0b71f56915f53a4ece5bcfde6b74ee7d407517b54038b3a00a945813acd3137c5da8800f8ac2fea71220b35557d18eb63fef25ac0b90297b1067a9ed65e70a55
7
+ data.tar.gz: b6faed818eff75c6beb32137cd97777b332b9a79994ab74ddb70cc4a257778e8a2571ab59d8c612fc89e5bf5f3c7a4e9f3868748ae737b3fe0cce1e404681460
data/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ ## [0.1.09] - 2021-03-28
2
+ - Fix and test suite for return statements
3
+
4
+ ### Changed
5
+ - `Loxxy` reports an error when a return statement occurs in top-level scope
6
+
7
+ ### Fixed
8
+ - A return without explicit value genrated an exception in some cases.
9
+
10
+ ## [0.1.08] - 2021-03-27
11
+ - `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
12
+
13
+ ### New
14
+ - Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
15
+
16
+ ### Changed
17
+ - Class `Ast::Visitor` changes in some method signatures
18
+ - Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
19
+ - Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
20
+
21
+ ## [0.1.07] - 2021-03-14
22
+ - `Loxxy` now supports nested functions and closures
23
+
24
+ ### Changed
25
+ - Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
26
+ - Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
27
+ - Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
28
+ - Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
29
+ - Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
30
+
31
+ ### Fixed
32
+ - Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
33
+
34
+
35
+ ## [0.1.06] - 2021-03-06
36
+ - Parameters/arguments checks in function declaration and call
37
+
38
+ ### Changed
39
+ - Method `Ast::AstBuilder#reduce_call_arglist` raises a `Loxxy::RuntimeError` when more than 255 arguments are used.
40
+ - Method `BackEnd::Engine#after_call_expr` raises a `Loxxy::RuntimeError` when argument count doesn't match the arity of function.
41
+
42
+ - Class `BackEnd::Function` renamed to `LoxFunction`
43
+
44
+ ## [0.1.05] - 2021-03-05
45
+ - Test for Fibbonacci recursive function is now passing.
46
+
47
+ ### Fixed
48
+ - Method `BackEnd::Function#call` a call doesn't no more generate of TWO scopes
49
+
1
50
  ## [0.1.04] - 2021-02-28
2
51
 
3
52
  ### Added
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,9 @@ 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.
20
-
21
- These will be implemented soon.
16
+ The interpreter currently can execute all allowed __Lox__ expressions and statements except
17
+ object-oriented feaures (classes and objects).
18
+ The goal is to implement these missing features in Q2 2021.
22
19
 
23
20
 
24
21
  ## What's the fuss about Lox?
@@ -260,20 +260,33 @@ module Loxxy
260
260
 
261
261
  # rule('call' => 'primary refinement_plus').as 'call_expr'
262
262
  def reduce_call_expr(_production, _range, _tokens, theChildren)
263
- theChildren[1].callee = theChildren[0]
264
- 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]
265
278
  end
266
279
 
267
280
  # rule('refinement_plus' => 'refinement').
268
281
  def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
269
- theChildren[0]
282
+ theChildren
270
283
  end
271
284
 
272
285
  # rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
273
286
  def reduce_call_arglist(_production, _range, tokens, theChildren)
274
287
  args = theChildren[1] || []
275
288
  if args.size > 255
276
- raise StandardError, "Can't have more than 255 arguments."
289
+ raise Loxxy::RuntimeError, "Can't have more than 255 arguments."
277
290
  end
278
291
 
279
292
  LoxCallExpr.new(tokens[0].position, args)
@@ -303,6 +316,10 @@ module Loxxy
303
316
  def reduce_function(_production, _range, _tokens, theChildren)
304
317
  first_child = theChildren.first
305
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
306
323
  LoxFunStmt.new(pos, first_child.token.lexeme, theChildren[2], theChildren[4])
307
324
  end
308
325
 
@@ -120,7 +120,7 @@ module Loxxy
120
120
  def visit_assign_expr(anAssignExpr)
121
121
  broadcast(:before_assign_expr, anAssignExpr)
122
122
  traverse_subnodes(anAssignExpr)
123
- broadcast(:after_assign_expr, anAssignExpr)
123
+ broadcast(:after_assign_expr, anAssignExpr, self)
124
124
  end
125
125
 
126
126
  # Visit event. The visitor is about to visit a logical expression.
@@ -194,7 +194,7 @@ module Loxxy
194
194
  # Visit event. The visitor is about to visit a function statement node.
195
195
  # @param aFunStmt [AST::LoxFunStmt] function declaration to visit
196
196
  def visit_fun_stmt(aFunStmt)
197
- broadcast(:before_fun_stmt, aFunStmt)
197
+ broadcast(:before_fun_stmt, aFunStmt, self)
198
198
  traverse_subnodes(aFunStmt)
199
199
  broadcast(:after_fun_stmt, aFunStmt, self)
200
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
@@ -8,7 +8,8 @@ module Loxxy
8
8
  # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
9
9
  # @param anExpression [Ast::LoxNode] expression to return
10
10
  def initialize(aPosition, anExpression)
11
- super(aPosition, [anExpression])
11
+ expr = anExpression || Datatype::Nil.instance
12
+ super(aPosition, [expr])
12
13
  end
13
14
 
14
15
  # Part of the 'visitee' role in Visitor design pattern.
@@ -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
@@ -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
@@ -121,14 +137,13 @@ module Loxxy
121
137
  symbol_table.leave_environment
122
138
  end
123
139
 
124
- def after_assign_expr(anAssignExpr)
140
+ def after_assign_expr(anAssignExpr, _visitor)
125
141
  var_name = anAssignExpr.name
126
- variable = symbol_table.lookup(var_name)
142
+ variable = variable_lookup(anAssignExpr)
127
143
  raise StandardError, "Unknown variable #{var_name}" unless variable
128
144
 
129
- value = stack.pop
145
+ value = stack.last # ToS remains since an assignment produces a value
130
146
  variable.assign(value)
131
- stack.push value # An expression produces a value
132
147
  end
133
148
 
134
149
  def after_logical_expr(aLogicalExpr, visitor)
@@ -199,39 +214,28 @@ module Loxxy
199
214
  case callee
200
215
  when NativeFunction
201
216
  stack.push callee.call # Pass arguments
202
- when Function
203
- new_env = Environment.new(symbol_table.current_env)
204
- symbol_table.enter_environment(new_env)
205
- callee.parameters&.each do |param_name|
206
- local = Variable.new(param_name, stack.pop)
207
- symbol_table.insert(local)
208
- end
209
- catch(:return) do
210
- callee.call(aVisitor)
211
- throw(:return)
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
212
222
  end
213
-
214
- symbol_table.leave_environment
223
+ callee.call(self, aVisitor)
215
224
  else
216
225
  raise Loxxy::RuntimeError, 'Can only call functions and classes.'
217
226
  end
218
227
  end
219
228
 
220
- def complete_call
221
- callee = ret_stack.pop
222
- symbol_table.leave_environment if callee.kind_of?(Function)
223
- end
224
-
225
229
  def after_grouping_expr(_groupingExpr)
226
230
  # Do nothing: work was already done by visiting /evaluating the subexpression
227
231
  end
228
232
 
229
233
  def after_variable_expr(aVarExpr, aVisitor)
230
234
  var_name = aVarExpr.name
231
- var = symbol_table.lookup(var_name)
235
+ var = variable_lookup(aVarExpr)
232
236
  raise StandardError, "Unknown variable #{var_name}" unless var
233
237
 
234
- var.value.accept(aVisitor) # Evaluate the variable value
238
+ var.value.accept(aVisitor) # Evaluate variable value then push on stack
235
239
  end
236
240
 
237
241
  # @param literalExpr [Ast::LoxLiteralExpr]
@@ -245,13 +249,26 @@ module Loxxy
245
249
  end
246
250
 
247
251
  def after_fun_stmt(aFunStmt, _visitor)
248
- function = Function.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, stack)
252
+ function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
249
253
  new_var = Variable.new(aFunStmt.name, function)
250
254
  symbol_table.insert(new_var)
251
255
  end
252
256
 
253
257
  private
254
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
+
255
272
  NativeFunction = Struct.new(:callable, :interp) do
256
273
  def accept(_visitor)
257
274
  interp.stack.push self
@@ -271,7 +288,7 @@ module Loxxy
271
288
  unary_operators[:-@] = negate_op
272
289
 
273
290
  negation_op = UnaryOperator.new('!', [Datatype::BuiltinDatatype,
274
- BackEnd::Function])
291
+ BackEnd::LoxFunction])
275
292
  unary_operators[:!] = negation_op
276
293
  end
277
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,182 @@
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 before_return_stmt(_returnStmt)
68
+ if scopes.size < 2
69
+ msg = "Error at 'return': Can't return from top-level code."
70
+ raise StandardError, msg
71
+ end
72
+ end
73
+
74
+ def after_while_stmt(aWhileStmt, aVisitor)
75
+ aWhileStmt.body.accept(aVisitor)
76
+ aWhileStmt.condition.accept(aVisitor)
77
+ end
78
+
79
+ # A variable declaration adds a new variable to current scope
80
+ def before_var_stmt(aVarStmt)
81
+ declare(aVarStmt.name)
82
+ end
83
+
84
+ def after_var_stmt(aVarStmt)
85
+ define(aVarStmt.name)
86
+ end
87
+
88
+ # Assignment expressions require their variables resolved
89
+ def after_assign_expr(anAssignExpr, aVisitor)
90
+ resolve_local(anAssignExpr, aVisitor)
91
+ end
92
+
93
+ # Variable expressions require their variables resolved
94
+ def before_variable_expr(aVarExpr)
95
+ var_name = aVarExpr.name
96
+ if !scopes.empty? && (scopes.last[var_name] == false)
97
+ raise StandardError, "Can't read variable #{var_name} in its own initializer"
98
+ end
99
+ end
100
+
101
+ def after_variable_expr(aVarExpr, aVisitor)
102
+ resolve_local(aVarExpr, aVisitor)
103
+ end
104
+
105
+ def after_call_expr(aCallExpr, aVisitor)
106
+ # Evaluate callee part
107
+ aCallExpr.callee.accept(aVisitor)
108
+ aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
109
+ end
110
+
111
+ # function declaration creates a new scope for its body & binds its parameters for that scope
112
+ def before_fun_stmt(aFunStmt, aVisitor)
113
+ declare(aFunStmt.name)
114
+ define(aFunStmt.name)
115
+ resolve_function(aFunStmt, aVisitor)
116
+ end
117
+
118
+ private
119
+
120
+ def begin_scope
121
+ scopes.push({})
122
+ end
123
+
124
+ def end_scope
125
+ scopes.pop
126
+ end
127
+
128
+ def declare(aVarName)
129
+ return if scopes.empty?
130
+
131
+ curr_scope = scopes.last
132
+ if curr_scope.include?(aVarName)
133
+ msg = "Error at '#{aVarName}': Already variable with this name in this scope."
134
+ raise StandardError, msg
135
+ end
136
+
137
+ # The initializer is not yet processed.
138
+ # Mark the variable as 'not yet ready' = exists but may not be referenced yet
139
+ curr_scope[aVarName] = false
140
+ end
141
+
142
+ def define(aVarName)
143
+ return if scopes.empty?
144
+
145
+ curr_scope = scopes.last
146
+
147
+ # The initializer (if any) was processed.
148
+ # Mark the variable as alive (= can be referenced in an expression)
149
+ curr_scope[aVarName] = true
150
+ end
151
+
152
+ def resolve_local(aVarExpr, _visitor)
153
+ max_i = i = scopes.size - 1
154
+ scopes.reverse_each do |scp|
155
+ if scp.include?(aVarExpr.name)
156
+ # Keep track of the difference of nesting levels between current scope
157
+ # and the scope where the variable is declared
158
+ @locals[aVarExpr] = max_i - i
159
+ break
160
+ end
161
+ i -= 1
162
+ end
163
+ end
164
+
165
+ def resolve_function(aFunStmt, aVisitor)
166
+ begin_scope
167
+
168
+ aFunStmt.params&.each do |param_name|
169
+ declare(param_name)
170
+ define(param_name)
171
+ end
172
+
173
+ body = aFunStmt.body
174
+ unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
175
+ body.subnodes.first&.accept(aVisitor)
176
+ end
177
+
178
+ end_scope
179
+ end
180
+ end # class
181
+ end # mmodule
182
+ 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
@@ -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')
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.04'
4
+ VERSION = '0.1.09'
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
@@ -438,18 +429,35 @@ LOX_END
438
429
  expect(result).to eq(3)
439
430
  end
440
431
 
441
- it 'should print the hello world message' do
442
- expect { subject.evaluate(hello_world) }.not_to raise_error
443
- expect(sample_cfg[:ostream].string).to eq('Hello, world!')
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')
444
451
  end
445
- end # context
452
+ # rubocop: enable Style/StringConcatenation
446
453
 
447
- context 'Test suite:' do
448
- it "should complain if one argument isn't a number" do
449
- source = '1 + nil;'
450
- err = Loxxy::RuntimeError
451
- err_msg = 'Operands must be two numbers or two strings.'
452
- expect { subject.evaluate(source) }.to raise_error(err, err_msg)
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!')
453
461
  end
454
462
  end # context
455
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.04
4
+ version: 0.1.09
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-28 00:00:00.000000000 Z
11
+ date: 2021-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -113,7 +113,8 @@ files:
113
113
  - lib/loxxy/back_end/engine.rb
114
114
  - lib/loxxy/back_end/entry.rb
115
115
  - lib/loxxy/back_end/environment.rb
116
- - lib/loxxy/back_end/function.rb
116
+ - lib/loxxy/back_end/lox_function.rb
117
+ - lib/loxxy/back_end/resolver.rb
117
118
  - lib/loxxy/back_end/symbol_table.rb
118
119
  - lib/loxxy/back_end/unary_operator.rb
119
120
  - lib/loxxy/back_end/variable.rb
@@ -1,51 +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
- # Logical negation.
38
- # As a function is a truthy thing, its negation is thus false.
39
- # @return [Datatype::False]
40
- def !
41
- Datatype::False.instance
42
- end
43
-
44
- # Text representation of a Lox function
45
- def to_str
46
- "<fn #{name}>"
47
- end
48
- end # class
49
- # rubocop: enable Style/AccessorGrouping
50
- end # module
51
- end # module