loxxy 0.1.03 → 0.1.08

Sign up to get free protection for your applications and to get access to all the features.
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