loxxy 0.1.06 → 0.1.11

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: 293bf5f6fb0eb5a1daf2723484a2867909c03cc99b20270e562d288d334f0bdc
4
- data.tar.gz: 9e26854c6d4334f9869d0cd420ebd4365c0f978666e7ede8310d25e600cf5834
3
+ metadata.gz: aee9fb6c5101bc39682ab401ffc0434aea9e6c4115f74a4f641d0df2d68a03ad
4
+ data.tar.gz: ccc11c428ef206db9129fabbb97526df3477e03c5dce0b1b5a2f758425c8f9cb
5
5
  SHA512:
6
- metadata.gz: 2ac692f16a41b162001fee869d426abf634f1a2bb82bf17eac26d8c35939a8ca63234246a6ff88cc04db3833057b445e2650773652ce102a0dcc4f3aa90182e8
7
- data.tar.gz: 2764cb3d1703fdc948180cb2060f93a0ef72148f2877f5b711ecacd17d2ba52a9d3b9f7736eb53130850635cb0d127f2a5161a8361cb4a2268fc6275d15a6b1c
6
+ metadata.gz: dbb1a28eb85868636b304aa3ec3c2fb71f3f37f5c573740bd7d577ed4e88374ce1fea5446e39429168228e68bedbddfccd554bc6c88ec7996621956e11a42a3c
7
+ data.tar.gz: 3b4c1d6b0996a351cfd7dae5d8f80683fcbdd1eeecc17c3a7932b0675285700dfecdabecc31f505243ffaf2f1bbc1bd8babb604c154f53dad0019c4ad96b007a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,64 @@
1
+ ## [0.1.11] - 2021-04-03
2
+ - Intermediate version: `Loxxy` does class declarations
3
+
4
+ ### New
5
+ - Class `Ast::LoxClassStmt` a syntax node that represents a class declaration
6
+ - Method `Ast::ASTBuilder#reduce_class_decl` creates a `LoxClassStmt` instance
7
+ - Method `Ast::ASTBuilder#reduce_class_name`
8
+ - Method `Ast::ASTBuilder#reduce_reduce_class_body` collect the methods of the class
9
+ - Method `Ast::ASTBuilder#reduce_method_plus_more` for dealing with methods
10
+ - Method `Ast::ASTBuilder#reduce_method_plus_end`
11
+ - Method `Ast::ASTVisitor#visit_class_stmt` for visiting an `Ast::LoxClassStmt` node
12
+ - Method `Ast::LoxBlockStmt#empty?` returns true if the code block is empty
13
+ - Method `BackEnd::Engine#after_class_stmt`
14
+ - Method `BackEnd::Resolver#after_class_stmt`
15
+ - Method `BackEnd::Resolver#before_class_stmt`
16
+ - Class `BackEnd::LoxClass` implementation of a Lox class.
17
+
18
+ ### CHANGED
19
+ - File `grammar.rb` refactoring of class declaration syntax rules
20
+
21
+ ## [0.1.10] - 2021-03-31
22
+ - Flag return statements occurring outside functions as an error
23
+
24
+ ### Changed
25
+ - Class `BackEnd::Resolver` Added attribute `current_function` to know whether the visited parse node is located inside a function
26
+
27
+
28
+ ## [0.1.09] - 2021-03-28
29
+ - Fix and test suite for return statements
30
+
31
+ ### Changed
32
+ - `Loxxy` reports an error when a return statement occurs in top-level scope
33
+
34
+ ### Fixed
35
+ - A return without explicit value genrated an exception in some cases.
36
+
37
+ ## [0.1.08] - 2021-03-27
38
+ - `Loxxy` implements variable resolving and binding as described in Chapter 11 of "Crafting Interpreters" book.
39
+
40
+ ### New
41
+ - Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
42
+
43
+ ### Changed
44
+ - Class `Ast::Visitor` changes in some method signatures
45
+ - Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
46
+ - Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
47
+
48
+ ## [0.1.07] - 2021-03-14
49
+ - `Loxxy` now supports nested functions and closures
50
+
51
+ ### Changed
52
+ - Method `Ast::AstBuilder#reduce_call_expr` now supports nested call expressions (e.g. `getCallback()();` )
53
+ - Class `BackEnd::Environment`: added the attributes `predecessor` and `embedding` to support closures.
54
+ - Class `BackeEnd::LoxFunction`: added the attribute `closure` that is equal to the environment where the function is declared.
55
+ - Constructor `BackEnd::LoxFunction#new` now takes a `BackEnd::Engine`as its fourth parameter
56
+ - Methods `BackEnd::SymbolTable#enter_environment`, `BackEnd::SymbolTable#leave_environment` take into account closures.
57
+
58
+ ### Fixed
59
+ - Method `Ast::AstBuilder#after_var_stmt` now takes into account the value from the top of stack
60
+
61
+
1
62
  ## [0.1.06] - 2021-03-06
2
63
  - Parameters/arguments checks in function declaration and call
3
64
 
@@ -69,9 +130,9 @@
69
130
  - Method `Ast::ASTVisitor#visit_fun_stmt` for visiting an `Ast::LoxFunStmt` node
70
131
  - Method `Ast::LoxBlockStmt#empty?` returns true if the code block is empty
71
132
  - Method `BackEnd::Engine#after_fun_stmt`
72
- - Method `Backend::NativeFunction#call`
73
- - Method `Backend::NativeFunction#to_str`
74
- - Method `Backend::Function` implementation of a function object.
133
+ - Method `BackEnd::NativeFunction#call`
134
+ - Method `BackEnd::NativeFunction#to_str`
135
+ - Method `BackEnd::LoxFunction` implementation of a function object.
75
136
 
76
137
  ### Changed
77
138
  - Method `BackEnd::Engine#after_call_expr`
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?
@@ -17,4 +17,5 @@ require_relative 'lox_print_stmt'
17
17
  require_relative 'lox_if_stmt'
18
18
  require_relative 'lox_for_stmt'
19
19
  require_relative 'lox_var_stmt'
20
+ require_relative 'lox_class_stmt'
20
21
  require_relative 'lox_seq_decl'
@@ -163,6 +163,31 @@ module Loxxy
163
163
  [theChildren[0]]
164
164
  end
165
165
 
166
+ # rule('classDecl' => 'CLASS classNaming class_body')
167
+ def reduce_class_decl(_production, _range, _tokens, theChildren)
168
+ Ast::LoxClassStmt.new(tokens[1].position, theChildren[1], theChildren[2])
169
+ end
170
+
171
+ # rule('classNaming' => 'IDENTIFIER')
172
+ def reduce_class_name(_production, _range, _tokens, theChildren)
173
+ theChildren[0].token.lexeme
174
+ end
175
+
176
+ # rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
177
+ def reduce_class_body(_production, _range, _tokens, theChildren)
178
+ theChildren[1]
179
+ end
180
+
181
+ # rule('method_plus' => 'method_plus function')
182
+ def reduce_method_plus_more(_production, _range, _tokens, theChildren)
183
+ theChildren[0] << theChildren[1]
184
+ end
185
+
186
+ # rule('method_plus' => 'function')
187
+ def reduce_method_plus_end(_production, _range, _tokens, theChildren)
188
+ theChildren
189
+ end
190
+
166
191
  # rule('funDecl' => 'FUN function')
167
192
  def reduce_fun_decl(_production, _range, _tokens, theChildren)
168
193
  theChildren[1]
@@ -260,13 +285,26 @@ module Loxxy
260
285
 
261
286
  # rule('call' => 'primary refinement_plus').as 'call_expr'
262
287
  def reduce_call_expr(_production, _range, _tokens, theChildren)
263
- theChildren[1].callee = theChildren[0]
264
- theChildren[1]
288
+ members = theChildren.flatten
289
+ call_expr = nil
290
+ loop do
291
+ (callee, call_expr) = members.shift(2)
292
+ call_expr.callee = callee
293
+ members.unshift(call_expr)
294
+ break if members.size == 1
295
+ end
296
+
297
+ call_expr
298
+ end
299
+
300
+ # rule('refinement_plus' => 'refinement_plus refinement')
301
+ def reduce_refinement_plus_more(_production, _range, _tokens, theChildren)
302
+ theChildren[0] << theChildren[1]
265
303
  end
266
304
 
267
305
  # rule('refinement_plus' => 'refinement').
268
306
  def reduce_refinement_plus_end(_production, _range, _tokens, theChildren)
269
- theChildren[0]
307
+ theChildren
270
308
  end
271
309
 
272
310
  # rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
@@ -67,6 +67,14 @@ module Loxxy
67
67
  broadcast(:after_var_stmt, aVarStmt)
68
68
  end
69
69
 
70
+ # Visit event. The visitor is about to visit a class declaration.
71
+ # @param aXlassStmt [AST::LOXClassStmt] the for statement node to visit
72
+ def visit_class_stmt(aClassStmt)
73
+ broadcast(:before_class_stmt, aClassStmt)
74
+ traverse_subnodes(aClassStmt) # The methods are visited here...
75
+ broadcast(:after_class_stmt, aClassStmt, self)
76
+ end
77
+
70
78
  # Visit event. The visitor is about to visit a for statement.
71
79
  # @param aForStmt [AST::LOXForStmt] the for statement node to visit
72
80
  def visit_for_stmt(aForStmt)
@@ -120,7 +128,7 @@ module Loxxy
120
128
  def visit_assign_expr(anAssignExpr)
121
129
  broadcast(:before_assign_expr, anAssignExpr)
122
130
  traverse_subnodes(anAssignExpr)
123
- broadcast(:after_assign_expr, anAssignExpr)
131
+ broadcast(:after_assign_expr, anAssignExpr, self)
124
132
  end
125
133
 
126
134
  # Visit event. The visitor is about to visit a logical expression.
@@ -194,7 +202,7 @@ module Loxxy
194
202
  # Visit event. The visitor is about to visit a function statement node.
195
203
  # @param aFunStmt [AST::LoxFunStmt] function declaration to visit
196
204
  def visit_fun_stmt(aFunStmt)
197
- broadcast(:before_fun_stmt, aFunStmt)
205
+ broadcast(:before_fun_stmt, aFunStmt, self)
198
206
  traverse_subnodes(aFunStmt)
199
207
  broadcast(:after_fun_stmt, aFunStmt, self)
200
208
  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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_compound_expr'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ class LoxClassStmt < LoxCompoundExpr
8
+ attr_reader :name
9
+
10
+ # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
11
+ # @param condExpr [Loxxy::Ast::LoxNode] iteration condition
12
+ # @param theBody [Loxxy::Ast::LoxNode]
13
+ def initialize(aPosition, aName, theMethods)
14
+ super(aPosition, theMethods)
15
+ @name = aName.dup
16
+ end
17
+
18
+ # Part of the 'visitee' role in Visitor design pattern.
19
+ # @param visitor [Ast::ASTVisitor] the visitor
20
+ def accept(visitor)
21
+ visitor.visit_class_stmt(self)
22
+ end
23
+
24
+ alias body subnodes
25
+ end # class
26
+ end # module
27
+ end # module
@@ -16,7 +16,7 @@ module Loxxy
16
16
  # @param body [Ast::LoxBlockStmt]
17
17
  def initialize(aPosition, aName, paramList, aBody)
18
18
  super(aPosition, [])
19
- @name = aName
19
+ @name = aName.dup
20
20
  @params = paramList
21
21
  @body = aBody
22
22
  end
@@ -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,9 @@
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 'lox_class'
6
7
  require_relative 'lox_function'
8
+ require_relative 'resolver'
7
9
  require_relative 'symbol_table'
8
10
  require_relative 'unary_operator'
9
11
 
@@ -11,7 +13,6 @@ module Loxxy
11
13
  module BackEnd
12
14
  # An instance of this class executes the statements as when they
13
15
  # occur during the abstract syntax tree walking.
14
- # @note WIP: very crude implementation.
15
16
  class Engine
16
17
  # @return [Hash] A set of configuration options
17
18
  attr_reader :config
@@ -28,6 +29,9 @@ module Loxxy
28
29
  # @return [Hash { Symbol => BinaryOperator}]
29
30
  attr_reader :binary_operators
30
31
 
32
+ # @return [BackEnd::Resolver]
33
+ attr_reader :resolver
34
+
31
35
  # @param theOptions [Hash]
32
36
  def initialize(theOptions)
33
37
  @config = theOptions
@@ -47,6 +51,10 @@ module Loxxy
47
51
  # @param aVisitor [AST::ASTVisitor]
48
52
  # @return [Loxxy::Datatype::BuiltinDatatype]
49
53
  def execute(aVisitor)
54
+ # Do variable resolution pass first
55
+ @resolver = BackEnd::Resolver.new
56
+ resolver.analyze(aVisitor)
57
+
50
58
  aVisitor.subscribe(self)
51
59
  aVisitor.start
52
60
  aVisitor.unsubscribe(self)
@@ -61,11 +69,26 @@ module Loxxy
61
69
  # Do nothing, subnodes were already evaluated
62
70
  end
63
71
 
64
- def after_var_stmt(aVarStmt)
65
- new_var = Variable.new(aVarStmt.name, aVarStmt.subnodes[0])
72
+ def after_class_stmt(aClassStmt, _visitor)
73
+ klass = LoxClass.new(aClassStmt.name, aClassStmt.methods, self)
74
+ new_var = Variable.new(aClassStmt.name, klass)
75
+ symbol_table.insert(new_var)
76
+ end
77
+
78
+ def before_var_stmt(aVarStmt)
79
+ new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
66
80
  symbol_table.insert(new_var)
67
81
  end
68
82
 
83
+ def after_var_stmt(aVarStmt)
84
+ var_name = aVarStmt.name
85
+ variable = symbol_table.lookup(var_name)
86
+ raise StandardError, "Unknown variable #{var_name}" unless variable
87
+
88
+ value = stack.pop
89
+ variable.assign(value)
90
+ end
91
+
69
92
  def before_for_stmt(aForStmt)
70
93
  before_block_stmt(aForStmt)
71
94
  end
@@ -121,14 +144,13 @@ module Loxxy
121
144
  symbol_table.leave_environment
122
145
  end
123
146
 
124
- def after_assign_expr(anAssignExpr)
147
+ def after_assign_expr(anAssignExpr, _visitor)
125
148
  var_name = anAssignExpr.name
126
- variable = symbol_table.lookup(var_name)
149
+ variable = variable_lookup(anAssignExpr)
127
150
  raise StandardError, "Unknown variable #{var_name}" unless variable
128
151
 
129
- value = stack.pop
152
+ value = stack.last # ToS remains since an assignment produces a value
130
153
  variable.assign(value)
131
- stack.push value # An expression produces a value
132
154
  end
133
155
 
134
156
  def after_logical_expr(aLogicalExpr, visitor)
@@ -217,10 +239,10 @@ module Loxxy
217
239
 
218
240
  def after_variable_expr(aVarExpr, aVisitor)
219
241
  var_name = aVarExpr.name
220
- var = symbol_table.lookup(var_name)
242
+ var = variable_lookup(aVarExpr)
221
243
  raise StandardError, "Unknown variable #{var_name}" unless var
222
244
 
223
- var.value.accept(aVisitor) # Evaluate the variable value
245
+ var.value.accept(aVisitor) # Evaluate variable value then push on stack
224
246
  end
225
247
 
226
248
  # @param literalExpr [Ast::LoxLiteralExpr]
@@ -234,13 +256,26 @@ module Loxxy
234
256
  end
235
257
 
236
258
  def after_fun_stmt(aFunStmt, _visitor)
237
- function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, stack)
259
+ function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
238
260
  new_var = Variable.new(aFunStmt.name, function)
239
261
  symbol_table.insert(new_var)
240
262
  end
241
263
 
242
264
  private
243
265
 
266
+ def variable_lookup(aVarNode)
267
+ env = nil
268
+ offset = resolver.locals[aVarNode]
269
+ if offset.nil?
270
+ env = symbol_table.root
271
+ else
272
+ env = symbol_table.current_env
273
+ offset.times { env = env.enclosing }
274
+ end
275
+
276
+ env.defns[aVarNode.name]
277
+ end
278
+
244
279
  NativeFunction = Struct.new(:callable, :interp) do
245
280
  def accept(_visitor)
246
281
  interp.stack.push self
@@ -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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../datatype/all_datatypes'
4
+
5
+ module Loxxy
6
+ module BackEnd
7
+ # Representation of a Lox class.
8
+ class LoxClass
9
+ # @return [String] The name of the class
10
+ attr_reader :name
11
+
12
+ # @return [Array<>] the list of methods
13
+ attr_reader :methods
14
+ attr_reader :stack
15
+
16
+ # Create a class with given name
17
+ # @param aName [String] The name of the class
18
+ def initialize(aName, theMethods, anEngine)
19
+ @name = aName.dup
20
+ @methods = theMethods
21
+ @stack = anEngine.stack
22
+ end
23
+
24
+ def accept(_visitor)
25
+ stack.push self
26
+ end
27
+
28
+ # Logical negation.
29
+ # As a function is a truthy thing, its negation is thus false.
30
+ # @return [Datatype::False]
31
+ def !
32
+ Datatype::False.instance
33
+ end
34
+
35
+ # Text representation of a Lox function
36
+ def to_str
37
+ name
38
+ end
39
+ end # class
40
+ end # module
41
+ end # module
@@ -6,24 +6,25 @@ module Loxxy
6
6
  module BackEnd
7
7
  # rubocop: disable Style/AccessorGrouping
8
8
  # Representation of a Lox function.
9
- # It is a named slot that can be associated with a value at the time.
10
9
  class LoxFunction
11
- # @return [String]
10
+ # @return [String] The name of the function (if any)
12
11
  attr_reader :name
13
12
 
14
13
  # @return [Array<>] the parameters
15
14
  attr_reader :parameters
16
15
  attr_reader :body
17
16
  attr_reader :stack
17
+ attr_reader :closure
18
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)
19
+ # Create a function with given name
20
+ # @param aName [String] The name of the function
21
+ def initialize(aName, parameterList, aBody, anEngine)
23
22
  @name = aName.dup
24
23
  @parameters = parameterList
25
24
  @body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
26
- @stack = aStack
25
+ @stack = anEngine.stack
26
+ @closure = anEngine.symbol_table.current_env
27
+ anEngine.symbol_table.current_env.embedding = true
27
28
  end
28
29
 
29
30
  def arity
@@ -35,7 +36,8 @@ module Loxxy
35
36
  end
36
37
 
37
38
  def call(engine, aVisitor)
38
- new_env = Environment.new(engine.symbol_table.current_env)
39
+ # new_env = Environment.new(engine.symbol_table.current_env)
40
+ new_env = Environment.new(closure)
39
41
  engine.symbol_table.enter_environment(new_env)
40
42
 
41
43
  parameters&.each do |param_name|
@@ -0,0 +1,204 @@
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
+ # An indicator that tells we're in the middle of a function declaration
26
+ # @return [Symbol] must be one of: :none, :function
27
+ attr_reader :current_function
28
+
29
+ def initialize
30
+ @scopes = []
31
+ @locals = {}
32
+ @current_function = :none
33
+ end
34
+
35
+ # Given an abstract syntax parse tree visitor, launch the visit
36
+ # and execute the visit events in the output stream.
37
+ # @param aVisitor [AST::ASTVisitor]
38
+ # @return [Loxxy::Datatype::BuiltinDatatype]
39
+ def analyze(aVisitor)
40
+ begin_scope
41
+ aVisitor.subscribe(self)
42
+ aVisitor.start
43
+ aVisitor.unsubscribe(self)
44
+ end_scope
45
+ end
46
+
47
+ # block statement introduces a new scope
48
+ def before_block_stmt(_aBlockStmt)
49
+ begin_scope
50
+ end
51
+
52
+ def after_block_stmt(_aBlockStmt)
53
+ end_scope
54
+ end
55
+
56
+ # A class declaration adds a new variable to current scope
57
+ def before_class_stmt(aClassStmt)
58
+ declare(aClassStmt.name)
59
+ end
60
+
61
+ def after_class_stmt(aClassStmt, _visitor)
62
+ define(aClassStmt.name)
63
+ end
64
+
65
+ def before_for_stmt(aForStmt)
66
+ before_block_stmt(aForStmt)
67
+ end
68
+
69
+ def after_for_stmt(aForStmt, aVisitor)
70
+ aForStmt.test_expr.accept(aVisitor)
71
+ aForStmt.body_stmt.accept(aVisitor)
72
+ aForStmt.update_expr&.accept(aVisitor)
73
+ after_block_stmt(aForStmt)
74
+ end
75
+
76
+ def after_if_stmt(anIfStmt, aVisitor)
77
+ anIfStmt.then_stmt.accept(aVisitor)
78
+ anIfStmt.else_stmt&.accept(aVisitor)
79
+ end
80
+
81
+ def before_return_stmt(_returnStmt)
82
+ if scopes.size < 2
83
+ msg = "Error at 'return': Can't return from top-level code."
84
+ raise StandardError, msg
85
+ end
86
+
87
+ if current_function == :none
88
+ msg = "Error at 'return': Can't return from outside a function."
89
+ raise StandardError, msg
90
+ end
91
+ end
92
+
93
+ def after_while_stmt(aWhileStmt, aVisitor)
94
+ aWhileStmt.body.accept(aVisitor)
95
+ aWhileStmt.condition.accept(aVisitor)
96
+ end
97
+
98
+ # A variable declaration adds a new variable to current scope
99
+ def before_var_stmt(aVarStmt)
100
+ declare(aVarStmt.name)
101
+ end
102
+
103
+ def after_var_stmt(aVarStmt)
104
+ define(aVarStmt.name)
105
+ end
106
+
107
+ # Assignment expressions require their variables resolved
108
+ def after_assign_expr(anAssignExpr, aVisitor)
109
+ resolve_local(anAssignExpr, aVisitor)
110
+ end
111
+
112
+ # Variable expressions require their variables resolved
113
+ def before_variable_expr(aVarExpr)
114
+ var_name = aVarExpr.name
115
+ if !scopes.empty? && (scopes.last[var_name] == false)
116
+ raise StandardError, "Can't read variable #{var_name} in its own initializer"
117
+ end
118
+ end
119
+
120
+ def after_variable_expr(aVarExpr, aVisitor)
121
+ resolve_local(aVarExpr, aVisitor)
122
+ end
123
+
124
+ def after_call_expr(aCallExpr, aVisitor)
125
+ # Evaluate callee part
126
+ aCallExpr.callee.accept(aVisitor)
127
+ aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
128
+ end
129
+
130
+ # function declaration creates a new scope for its body & binds its parameters for that scope
131
+ def before_fun_stmt(aFunStmt, aVisitor)
132
+ declare(aFunStmt.name)
133
+ define(aFunStmt.name)
134
+ resolve_function(aFunStmt, :function, aVisitor)
135
+ end
136
+
137
+ private
138
+
139
+ def begin_scope
140
+ scopes.push({})
141
+ end
142
+
143
+ def end_scope
144
+ scopes.pop
145
+ end
146
+
147
+ def declare(aVarName)
148
+ return if scopes.empty?
149
+
150
+ curr_scope = scopes.last
151
+ if curr_scope.include?(aVarName)
152
+ msg = "Error at '#{aVarName}': Already variable with this name in this scope."
153
+ raise StandardError, msg
154
+ end
155
+
156
+ # The initializer is not yet processed.
157
+ # Mark the variable as 'not yet ready' = exists but may not be referenced yet
158
+ curr_scope[aVarName] = false
159
+ end
160
+
161
+ def define(aVarName)
162
+ return if scopes.empty?
163
+
164
+ curr_scope = scopes.last
165
+
166
+ # The initializer (if any) was processed.
167
+ # Mark the variable as alive (= can be referenced in an expression)
168
+ curr_scope[aVarName] = true
169
+ end
170
+
171
+ def resolve_local(aVarExpr, _visitor)
172
+ max_i = i = scopes.size - 1
173
+ scopes.reverse_each do |scp|
174
+ if scp.include?(aVarExpr.name)
175
+ # Keep track of the difference of nesting levels between current scope
176
+ # and the scope where the variable is declared
177
+ @locals[aVarExpr] = max_i - i
178
+ break
179
+ end
180
+ i -= 1
181
+ end
182
+ end
183
+
184
+ def resolve_function(aFunStmt, funVisitState, aVisitor)
185
+ enclosing_function = current_function
186
+ @current_function = funVisitState
187
+ begin_scope
188
+
189
+ aFunStmt.params&.each do |param_name|
190
+ declare(param_name)
191
+ define(param_name)
192
+ end
193
+
194
+ body = aFunStmt.body
195
+ unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
196
+ body.subnodes.first&.accept(aVisitor)
197
+ end
198
+
199
+ end_scope
200
+ @current_function = enclosing_function
201
+ end
202
+ end # class
203
+ end # mmodule
204
+ 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.
@@ -37,12 +37,14 @@ module Loxxy
37
37
  rule('declaration' => 'varDecl')
38
38
  rule('declaration' => 'statement')
39
39
 
40
- rule('classDecl' => 'CLASS classNaming class_body')
40
+ rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
41
41
  rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
42
- rule('classNaming' => 'IDENTIFIER')
43
- rule('class_body' => 'LEFT_BRACE function_star RIGHT_BRACE')
44
- rule('function_star' => 'function_star function')
45
- rule('function_star' => [])
42
+ rule('classNaming' => 'IDENTIFIER').as 'class_name'
43
+ rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
44
+ rule('methods_opt' => 'method_plus')
45
+ rule('methods_opt' => [])
46
+ rule('method_plus' => 'method_plus function').as 'method_plus_more'
47
+ rule('method_plus' => 'function').as 'method_plus_end'
46
48
 
47
49
  rule('funDecl' => 'FUN function').as 'fun_decl'
48
50
 
@@ -127,7 +129,7 @@ module Loxxy
127
129
  rule('unaryOp' => 'MINUS')
128
130
  rule('call' => 'primary')
129
131
  rule('call' => 'primary refinement_plus').as 'call_expr'
130
- rule('refinement_plus' => 'refinement_plus refinement') # .as 'refinement_plus_more'
132
+ rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
131
133
  rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
132
134
  rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
133
135
  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.06'
4
+ VERSION = '0.1.11'
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,52 @@ 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')
451
+ end
452
+ # rubocop: enable Style/StringConcatenation
453
+
454
+ it 'should support class declaration' do
455
+ program = <<-LOX_END
456
+ class Duck {
457
+ noise() {
458
+ quack();
459
+ }
460
+
461
+ quack() {
462
+ print "quack";
463
+ }
464
+ }
465
+ print Duck;
466
+ LOX_END
467
+ expect { subject.evaluate(program) }.not_to raise_error
468
+ expect(sample_cfg[:ostream].string).to eq('Duck')
444
469
  end
445
- end # context
446
470
 
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)
471
+ it 'should print the hello world message' do
472
+ program = <<-LOX_END
473
+ var greeting = "Hello"; // Declaring a variable
474
+ print greeting + ", " + "world!"; // ... Playing with concatenation
475
+ LOX_END
476
+ expect { subject.evaluate(program) }.not_to raise_error
477
+ expect(sample_cfg[:ostream].string).to eq('Hello, world!')
453
478
  end
454
479
  end # context
455
480
  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.06
4
+ version: 0.1.11
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-03-06 00:00:00.000000000 Z
11
+ date: 2021-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -93,6 +93,7 @@ files:
93
93
  - lib/loxxy/ast/lox_binary_expr.rb
94
94
  - lib/loxxy/ast/lox_block_stmt.rb
95
95
  - lib/loxxy/ast/lox_call_expr.rb
96
+ - lib/loxxy/ast/lox_class_stmt.rb
96
97
  - lib/loxxy/ast/lox_compound_expr.rb
97
98
  - lib/loxxy/ast/lox_for_stmt.rb
98
99
  - lib/loxxy/ast/lox_fun_stmt.rb
@@ -113,7 +114,9 @@ files:
113
114
  - lib/loxxy/back_end/engine.rb
114
115
  - lib/loxxy/back_end/entry.rb
115
116
  - lib/loxxy/back_end/environment.rb
117
+ - lib/loxxy/back_end/lox_class.rb
116
118
  - lib/loxxy/back_end/lox_function.rb
119
+ - lib/loxxy/back_end/resolver.rb
117
120
  - lib/loxxy/back_end/symbol_table.rb
118
121
  - lib/loxxy/back_end/unary_operator.rb
119
122
  - lib/loxxy/back_end/variable.rb