loxxy 0.1.13 → 0.2.00

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: 172ec5df8e553332b497968f7b910383cb38314aabbdb84fdb42fba6986841ea
4
- data.tar.gz: 3d72fd4f13f507f88e9e735e1ebdd0d63fd603b771f9cfb552b9aa38b5b3ca6a
3
+ metadata.gz: 6a071e03ebadb94c255a118d3bebd75227a2d36c535495a79ad2525e4fc2bf7c
4
+ data.tar.gz: 35aa1fc822287f7b86628acac39e5c3ca1c12c478cbb330d302705597ff2f13b
5
5
  SHA512:
6
- metadata.gz: 61073737ef6c069e6e1177fa79cdac543253e06ea63d2ca1b00517c08a06f46948fe7d51257308935d92579152793b4fe60bc0f5b901a1c931e5eafab9f35ecd
7
- data.tar.gz: 57c87936451c3015511740d7d44f01d8f032a06e597bdfd1d133119413c227a61df11847f86a4be3a2b3440cc1f614a43b1144cceef05a58ade56ca1fa3c598b
6
+ metadata.gz: 51b1a4622ee11c20582fa9ffa72f499a64b280c0092b82a8f07d922309c800f787409e8c273a5316d09de594d82026af0d979e2ad7f52ee6d3eb02233549ebf5
7
+ data.tar.gz: 2649d33c23f73351f9ecb3b9b4b9899541398c94fa4effa8ca4f4c766589df575b1f68685a039f595ca1efb83a515d0a3620afa3721a0604e187b321449b9716
data/CHANGELOG.md CHANGED
@@ -1,7 +1,56 @@
1
+ ## [0.1.17] - 2021-04-11
2
+ - `Loxxy` now support custom initializer.
3
+
4
+ ### Changed
5
+ - Method `BackEnd::Class#call` updated for custom initializer.
6
+ - Class `BackEnd::LoxFunction` added an attribute `is_initializer`
7
+ - Class `BackEnd::Resolver#before_return_stmt` added a check that return in initializer may not return a value
8
+
9
+ ### Fixed
10
+ - Method `BackEnd::Engine#after_call_expr` now does arity checking also for initalizer.
11
+ - Method `BackEnd::LoxInstance#set` removed the check of field existence that prevented the creation of ... fields
12
+
13
+ ## [0.1.16] - 2021-04-10
14
+ - Fixed an issue in name lookup. All the `this` test suite is passing.
15
+
16
+ ### Changed
17
+ - Method `BackEnd::Engine#after_var_stmt` now it creates the variable and pouts it in the symbol table
18
+
19
+ ### Removed
20
+ - Method `BackEnd::Engine#before_var_stmt` it generated bug when assigning a value to a var, when that var name occurred elsewhere
21
+
22
+ ## [0.1.15] - 2021-04-08
23
+ - Fixed the `dangling else`by tweaking the grammar rules
24
+
25
+ ### Changed
26
+ - Method `Ast::ASTBuilder#reduce_if__else_stmt` parse action specific for if with else branch
27
+
28
+ ### Fixed
29
+ - File `grammar.rb` changed rules to cope with `dangling else` issue
30
+
31
+ ### Changed
32
+ - Method `Ast::ASTBuilder#reduce_if_stmt` parse action for if without else branch
33
+ - File `README.md` removed the section about the `dangling else` issue.
34
+
35
+
36
+ ## [0.1.14] - 2021-04-05
37
+ - `Loxxy` now implements the 'this' keyword
38
+
39
+ ### New
40
+ - Class `Ast::LoxThisExpr` a syntax node that represents an occurrence of the `this` keyword
41
+ - Method `Ast::ASTBuilder#reduce_this_expr` parse action for this keyword
42
+ - Method `Ast::Visitor#visit_this_expr` visit of an `Ast::LoxThisExpr` node
43
+ - Method `BackEnd::Engine#after_this_expr` runtime action for this keyword (i.e. refers to current instance)
44
+ - Method `BackEnd::LoxFunction#bind` implementation of bound method
45
+
46
+ ### Changed
47
+ - Class `BackEnd::Resolver` implementing semantic actions for `this` keyword
48
+ - File `grammar.rb` added name to a syntax rule
49
+
1
50
  ## [0.1.13] - 2021-04-05
2
51
  - `Loxxy` now implements method calls
3
52
 
4
- ## New
53
+ ### New
5
54
  - Class `Ast::LoxGetExpr` a syntax node that represents a read access to an object property
6
55
  - Class `Ast::LoxSetExpr` a syntax node that represents a write access to an object property
7
56
  - Method `Ast::ASTBuilder#reduce_set_expr` parse action for write access to an object property
@@ -11,11 +60,11 @@
11
60
  - Method `BackEnd::Engine#after_set_expr` runtime action for property setting
12
61
  - Method `BackEnd::Engine#after_get_expr` runtime action for property getting
13
62
  - Method `BackEnd::LoxInstance#set` implementation of write accessor
14
- - Method `BackEnd::LoxInstance#getr` implementation of read accessor
63
+ - Method `BackEnd::LoxInstance#get` implementation of read accessor
15
64
  - Method `BackEnd::Resolver#after_set_expr` resolve action for property setting
16
65
  - Method `BackEnd::Resolver#after_get_expr` resolve action for property getting
17
66
 
18
- ## Changed
67
+ ### Changed
19
68
  - Method `Ast::ASTBuilder#reduce_assign_expr` expanded to support write access to an object property
20
69
  - Class `LoxClassStmt`: methods are now aggregate under the `body` attribute
21
70
  - Class `LoxFunStmt`: has a new attribute `is_method` and inherits from `Ast::LoxNode`
data/README.md CHANGED
@@ -177,8 +177,8 @@ Loxxy supports single line C-style comments.
177
177
  ### Keywords
178
178
  Loxxy implements the following __Lox__ reserved keywords:
179
179
  ```lang-none
180
- and, else, false, for, fun, if,
181
- nil, or, print, true, var, while
180
+ and, class, else, false, for, fun, if,
181
+ nil, or, print, return, this, true, var, while
182
182
  ```
183
183
 
184
184
  ### Datatypes
@@ -331,30 +331,6 @@ print "else-branch";
331
331
  ```
332
332
 
333
333
  As for other languages, the `else` part is optional.
334
- ##### Warning: nested `if`...`else`
335
- Call it a bug ... Nested `if` `else` control flow structure aren't yet supported by __Loxxy__.
336
- The culprit has a name: [the dangling else](https://en.wikipedia.org/wiki/Dangling_else).
337
-
338
- The problem in a nutshell: in a nested if ... else ... statement like this:
339
- ``` javascript
340
- 'if (true) if (false) print "bad"; else print "good";
341
- ```
342
- ... there is an ambiguity.
343
- Indeed, according to the __Lox__ grammar, the `else` could be bound
344
- either to the first `if` or to the second one.
345
- This ambiguity is usually lifted by applying an ad-hoc rule: an `else` is aways bound to the most
346
- recent (rightmost) `if`.
347
- Being a generic parsing library, `Rley` doesn't apply any of these supplemental rules.
348
- As a consequence,it complains about the found ambiguity and stops the parsing...
349
- Although `Rley` can cope with ambiguities, this requires the use of an advanced data structure
350
- called `Shared Packed Parse Forest (SPPF)`.
351
- SPPF are much more complex to handle than the "common" parse trees present in most compiler or interpreter books.
352
- Therefore, a future version of `Rley` will incorporate the capability to define disambuiguation rules.
353
-
354
- In the meantime, the `Loxxy` will progress on other __Lox__ features like:
355
- - Block structures...
356
- - Iteration structures (`for` and `while` loops)
357
-
358
334
 
359
335
  #### Print Statement
360
336
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'lox_fun_stmt'
4
+ require_relative 'lox_super_expr'
5
+ require_relative 'lox_this_expr'
4
6
  require_relative 'lox_variable_expr'
5
7
  require_relative 'lox_literal_expr'
6
8
  require_relative 'lox_noop_expr'
@@ -165,7 +165,14 @@ module Loxxy
165
165
 
166
166
  # rule('classDecl' => 'CLASS classNaming class_body')
167
167
  def reduce_class_decl(_production, _range, _tokens, theChildren)
168
- Ast::LoxClassStmt.new(tokens[1].position, theChildren[1], theChildren[2])
168
+ if theChildren[1].kind_of?(Array)
169
+ name = theChildren[1].first
170
+ parent = theChildren[1].last
171
+ else
172
+ name = theChildren[1]
173
+ parent = nil
174
+ end
175
+ Ast::LoxClassStmt.new(tokens[1].position, name, parent, theChildren[2])
169
176
  end
170
177
 
171
178
  # rule('classNaming' => 'IDENTIFIER')
@@ -173,6 +180,13 @@ module Loxxy
173
180
  theChildren[0].token.lexeme
174
181
  end
175
182
 
183
+ # rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
184
+ def reduce_class_subclassing(_production, _range, _tokens, theChildren)
185
+ super_token = theChildren[2].token
186
+ super_var = LoxVariableExpr.new(super_token.position, super_token.lexeme)
187
+ [theChildren[0].token.lexeme, super_var]
188
+ end
189
+
176
190
  # rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
177
191
  def reduce_class_body(_production, _range, _tokens, theChildren)
178
192
  theChildren[1].nil? ? [] : theChildren[1]
@@ -233,11 +247,20 @@ module Loxxy
233
247
  return_first_child(range, tokens, theChildren)
234
248
  end
235
249
 
236
- # rule('ifStmt' => 'IF ifCondition statement elsePart_opt')
250
+ # rule('ifStmt' => 'IF ifCondition statement ELSE statement')
251
+ # rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt')
252
+ def reduce_if_else_stmt(_production, _range, tokens, theChildren)
253
+ condition = theChildren[1]
254
+ then_stmt = theChildren[2]
255
+ else_stmt = theChildren[4]
256
+ LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
257
+ end
258
+
259
+ # rule('unbalancedStmt' => 'IF ifCondition stmt').as ''
237
260
  def reduce_if_stmt(_production, _range, tokens, theChildren)
238
261
  condition = theChildren[1]
239
262
  then_stmt = theChildren[2]
240
- else_stmt = theChildren[3]
263
+ else_stmt = nil
241
264
  LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
242
265
  end
243
266
 
@@ -354,6 +377,16 @@ module Loxxy
354
377
  LoxVariableExpr.new(tokens[0].position, var_name)
355
378
  end
356
379
 
380
+ # rule('primary' => 'THIS')
381
+ def reduce_this_expr(_production, _range, tokens, _children)
382
+ LoxThisExpr.new(tokens[0].position)
383
+ end
384
+
385
+ # rule('primary' => 'SUPER DOT IDENTIFIER')
386
+ def reduce_super_expr(_production, _range, _tokens, theChildren)
387
+ LoxSuperExpr.new(theChildren[0].token.position, theChildren[2].token.lexeme)
388
+ end
389
+
357
390
  # rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
358
391
  def reduce_function(_production, _range, _tokens, theChildren)
359
392
  first_child = theChildren.first
@@ -205,6 +205,20 @@ module Loxxy
205
205
  broadcast(:after_variable_expr, aVariableExpr, self)
206
206
  end
207
207
 
208
+ # Visit event. The visitor is about to visit the this keyword.
209
+ # @param aThisExpr [Ast::LoxThisExpr] this expression
210
+ def visit_this_expr(aThisExpr)
211
+ broadcast(:before_this_expr, aThisExpr)
212
+ broadcast(:after_this_expr, aThisExpr, self)
213
+ end
214
+
215
+ # Visit event. The visitor is about to visit the super keyword.
216
+ # @param aSuperExpr [Ast::LoxSuperExpr] super expression
217
+ def visit_super_expr(aSuperExpr)
218
+ broadcast(:before_super_expr, aSuperExpr)
219
+ broadcast(:after_super_expr, aSuperExpr, self)
220
+ end
221
+
208
222
  # Visit event. The visitor is about to visit the given terminal datatype value.
209
223
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
210
224
  def visit_builtin(aValue)
@@ -8,15 +8,19 @@ module Loxxy
8
8
  # @return [String] the class name
9
9
  attr_reader :name
10
10
 
11
+ # @return [Ast::LoxVariableExpr] variable referencing the superclass (if any)
12
+ attr_reader :superclass
13
+
11
14
  # @return [Array<Ast::LoxFunStmt>] the methods
12
15
  attr_reader :body
13
16
 
14
17
  # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
15
18
  # @param condExpr [Loxxy::Ast::LoxNode] iteration condition
16
19
  # @param theBody [Array<Loxxy::Ast::LoxNode>]
17
- def initialize(aPosition, aName, theMethods)
20
+ def initialize(aPosition, aName, aSuperclassName, theMethods)
18
21
  super(aPosition, [])
19
22
  @name = aName.dup
23
+ @superclass = aSuperclassName
20
24
  @body = theMethods
21
25
  end
22
26
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_node'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ class LoxSuperExpr < LoxNode
8
+ # @return [Ast::LoxNode] the object to which the property belongs to
9
+ attr_accessor :object
10
+
11
+ # @return [String] Name of a method name
12
+ attr_reader :property
13
+
14
+ # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
15
+ # @param aMethodName [String] Name of a method
16
+ def initialize(aPosition, aMethodName)
17
+ super(aPosition)
18
+ @property = aMethodName
19
+ end
20
+
21
+ # Part of the 'visitee' role in Visitor design pattern.
22
+ # @param visitor [ASTVisitor] the visitor
23
+ def accept(visitor)
24
+ visitor.visit_super_expr(self)
25
+ end
26
+
27
+ # Quack like a LoxVariableExpr
28
+ # @return [String] the `super` keyword
29
+ def name
30
+ 'super'
31
+ end
32
+ alias callee= object=
33
+ end # class
34
+ end # module
35
+ end # module
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_node'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ # A node in a parse tree that represents the occurrence of 'this' keyword.
8
+ class LoxThisExpr < LoxNode
9
+ # Duck-typing: behaves like a LoxVarExpr
10
+ # @return [String] return the this keyword
11
+ def name
12
+ 'this'
13
+ end
14
+
15
+ # Part of the 'visitee' role in Visitor design pattern.
16
+ # @param _visitor [LoxxyTreeVisitor] the visitor
17
+ def accept(aVisitor)
18
+ aVisitor.visit_this_expr(self)
19
+ end
20
+ end # class
21
+ end # module
22
+ end # module
@@ -70,30 +70,46 @@ module Loxxy
70
70
  end
71
71
 
72
72
  def after_class_stmt(aClassStmt, aVisitor)
73
+ if aClassStmt.superclass
74
+ aClassStmt.superclass.accept(aVisitor)
75
+ parent = stack.pop
76
+ unless parent.kind_of?(LoxClass)
77
+ raise StandardError, 'Superclass must be a class.'
78
+ end
79
+ else
80
+ parent = nil
81
+ end
82
+
83
+ if parent # Create an environment specific for 'super'
84
+ super_env = Environment.new(symbol_table.current_env)
85
+ symbol_table.enter_environment(super_env)
86
+ end
87
+
73
88
  # Convert LoxFunStmt into LoxFunction
74
89
  meths = aClassStmt.body.map do |func_node|
75
90
  func_node.is_method = true
76
91
  func_node.accept(aVisitor)
77
- stack.pop
92
+ mth = stack.pop
93
+ mth.is_initializer = true if mth.name == 'init'
94
+ mth
78
95
  end
79
96
 
80
- klass = LoxClass.new(aClassStmt.name, meths, self)
97
+ klass = LoxClass.new(aClassStmt.name, parent, meths, self)
98
+ if parent
99
+ super_var = Variable.new('super', klass)
100
+ symbol_table.insert(super_var)
101
+ symbol_table.leave_environment
102
+ end
81
103
  new_var = Variable.new(aClassStmt.name, klass)
82
104
  symbol_table.insert(new_var)
83
105
  end
84
106
 
85
- def before_var_stmt(aVarStmt)
107
+ def after_var_stmt(aVarStmt)
86
108
  new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
87
109
  symbol_table.insert(new_var)
88
- end
89
-
90
- def after_var_stmt(aVarStmt)
91
- var_name = aVarStmt.name
92
- variable = symbol_table.lookup(var_name)
93
- raise StandardError, "Unknown variable #{var_name}" unless variable
94
110
 
95
111
  value = stack.pop
96
- variable.assign(value)
112
+ new_var.assign(value)
97
113
  end
98
114
 
99
115
  def before_for_stmt(aForStmt)
@@ -240,15 +256,13 @@ module Loxxy
240
256
  case callee
241
257
  when NativeFunction
242
258
  stack.push callee.call # Pass arguments
243
- when LoxFunction
259
+ when LoxFunction, LoxClass
244
260
  arg_count = aCallExpr.arguments.size
245
261
  if arg_count != callee.arity
246
262
  msg = "Expected #{callee.arity} arguments but got #{arg_count}."
247
263
  raise Loxxy::RuntimeError, msg
248
264
  end
249
265
  callee.call(self, aVisitor)
250
- when LoxClass
251
- callee.call(self, aVisitor)
252
266
  else
253
267
  raise Loxxy::RuntimeError, 'Can only call functions and classes.'
254
268
  end
@@ -281,6 +295,25 @@ module Loxxy
281
295
  stack.push(literalExpr.literal)
282
296
  end
283
297
 
298
+ def after_this_expr(aThisExpr, aVisitor)
299
+ var = variable_lookup(aThisExpr)
300
+ var.value.accept(aVisitor) # Evaluate this value then push on stack
301
+ end
302
+
303
+ def after_super_expr(aSuperExpr, aVisitor)
304
+ offset = resolver.locals[aSuperExpr]
305
+ env = symbol_table.current_env
306
+ (offset - 1).times { env = env.enclosing }
307
+ instance = env.defns['this'].value.accept(aVisitor)[0]
308
+ superklass = variable_lookup(aSuperExpr).value.superclass
309
+ method = superklass.find_method(aSuperExpr.property)
310
+ unless method
311
+ raise StandardError, "Undefined property '#{aSuperExpr.property}'."
312
+ end
313
+
314
+ stack.push method.bind(instance)
315
+ end
316
+
284
317
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
285
318
  def before_visit_builtin(aValue)
286
319
  stack.push(aValue)
@@ -37,7 +37,6 @@ module Loxxy
37
37
  # @return [BackEnd::Variable] the variable
38
38
  def insert(anEntry)
39
39
  e = validated_entry(anEntry)
40
- # e.suffix = default_suffix if e.kind_of?(BackEnd::Variable)
41
40
  defns[e.name] = e
42
41
 
43
42
  e
@@ -7,17 +7,23 @@ module Loxxy
7
7
  module BackEnd
8
8
  # Runtime representation of a Lox class.
9
9
  class LoxClass
10
+ # rubocop: disable Style/AccessorGrouping
11
+
10
12
  # @return [String] The name of the class
11
13
  attr_reader :name
14
+ attr_reader :superclass
12
15
 
13
16
  # @return [Hash{String => LoxFunction}] the list of methods
14
17
  attr_reader :meths
15
18
  attr_reader :stack
16
19
 
20
+ # rubocop: enable Style/AccessorGrouping
21
+
17
22
  # Create a class with given name
18
23
  # @param aName [String] The name of the class
19
- def initialize(aName, theMethods, anEngine)
24
+ def initialize(aName, aSuperclass, theMethods, anEngine)
20
25
  @name = aName.dup
26
+ @superclass = aSuperclass
21
27
  @meths = {}
22
28
  theMethods.each do |func|
23
29
  meths[func.name] = func
@@ -30,17 +36,29 @@ module Loxxy
30
36
  end
31
37
 
32
38
  def arity
33
- 0
39
+ initializer = find_method('init')
40
+ initializer ? initializer.arity : 0
34
41
  end
35
42
 
36
- def call(engine, _visitor)
43
+ def call(engine, visitor)
37
44
  instance = LoxInstance.new(self, engine)
45
+ initializer = find_method('init')
46
+ if initializer
47
+ constructor = initializer.bind(instance)
48
+ constructor.call(engine, visitor)
49
+ end
50
+
38
51
  engine.stack.push(instance)
39
52
  end
40
53
 
41
54
  # @param aName [String] the method name to search for
42
55
  def find_method(aName)
43
- meths[aName]
56
+ found = meths[aName]
57
+ unless found || superclass.nil?
58
+ found = superclass.find_method(aName)
59
+ end
60
+
61
+ found
44
62
  end
45
63
 
46
64
  # Logical negation.
@@ -15,6 +15,7 @@ module Loxxy
15
15
  attr_reader :body
16
16
  attr_reader :stack
17
17
  attr_reader :closure
18
+ attr_accessor :is_initializer
18
19
 
19
20
  # Create a function with given name
20
21
  # @param aName [String] The name of the function
@@ -24,6 +25,7 @@ module Loxxy
24
25
  @body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
25
26
  @stack = anEngine.stack
26
27
  @closure = anEngine.symbol_table.current_env
28
+ @is_initializer = false
27
29
  anEngine.symbol_table.current_env.embedding = true
28
30
  end
29
31
 
@@ -48,10 +50,24 @@ module Loxxy
48
50
  (body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
49
51
  throw(:return)
50
52
  end
53
+ if is_initializer
54
+ enclosing_env = engine.symbol_table.current_env.enclosing
55
+ engine.stack.push(enclosing_env.defns['this'].value)
56
+ end
51
57
 
52
58
  engine.symbol_table.leave_environment
53
59
  end
54
60
 
61
+ def bind(anInstance)
62
+ new_env = Environment.new(closure)
63
+ this = Variable.new('this', anInstance)
64
+ new_env.insert(this)
65
+ bound_method = dup
66
+ bound_method.instance_variable_set(:@closure, new_env)
67
+
68
+ bound_method
69
+ end
70
+
55
71
  # Logical negation.
56
72
  # As a function is a truthy thing, its negation is thus false.
57
73
  # @return [Datatype::False]
@@ -9,7 +9,7 @@ module Loxxy
9
9
  # @return BackEnd::LoxClass] the class that this object is an instance of
10
10
  attr_reader :klass
11
11
 
12
- attr_reader :stack
12
+ attr_reader :engine
13
13
 
14
14
  # @return [Hash{String => BuiltinDatatype | LoxFunction | LoxInstance }]
15
15
  attr_reader :fields
@@ -18,12 +18,12 @@ module Loxxy
18
18
  # @param aClass [BackEnd::LoxClass] the class this this object belong
19
19
  def initialize(aClass, anEngine)
20
20
  @klass = aClass
21
- @stack = anEngine.stack
21
+ @engine = anEngine
22
22
  @fields = {}
23
23
  end
24
24
 
25
25
  def accept(_visitor)
26
- stack.push self
26
+ engine.stack.push self
27
27
  end
28
28
 
29
29
  # Text representation of a Lox instance
@@ -41,16 +41,12 @@ module Loxxy
41
41
  raise StandardError, "Undefined property '#{aName}'."
42
42
  end
43
43
 
44
- method
44
+ method.bind(self)
45
45
  end
46
46
 
47
47
  # Set the value of property with given name
48
48
  # aName [String] name of object property
49
49
  def set(aName, aValue)
50
- unless fields.include? aName
51
- raise StandardError, "Undefined property '#{aName}'."
52
- end
53
-
54
50
  fields[aName] = aValue
55
51
  end
56
52
  end # class
@@ -26,10 +26,15 @@ module Loxxy
26
26
  # @return [Symbol] must be one of: :none, :function
27
27
  attr_reader :current_function
28
28
 
29
+ # An indicator that tells we're in the middle of a class declaration
30
+ # @return [Symbol] must be one of: :none, :class
31
+ attr_reader :current_class
32
+
29
33
  def initialize
30
34
  @scopes = []
31
35
  @locals = {}
32
36
  @current_function = :none
37
+ @current_class = :none
33
38
  end
34
39
 
35
40
  # Given an abstract syntax parse tree visitor, launch the visit
@@ -59,10 +64,28 @@ module Loxxy
59
64
  end
60
65
 
61
66
  def after_class_stmt(aClassStmt, aVisitor)
67
+ previous_class = current_class
68
+ @current_class = :class
62
69
  define(aClassStmt.name)
70
+ if aClassStmt.superclass
71
+ if aClassStmt.name == aClassStmt.superclass.name
72
+ raise StandardError, "'A class can't inherit from itself."
73
+ end
74
+
75
+ @current_class = :subclass
76
+ aClassStmt.superclass.accept(aVisitor)
77
+ begin_scope
78
+ define('super')
79
+ end
80
+ begin_scope
81
+ define('this')
63
82
  aClassStmt.body.each do |fun_stmt|
64
- resolve_function(fun_stmt, :method, aVisitor)
83
+ mth_type = fun_stmt.name == 'init' ? :initializer : :method
84
+ resolve_function(fun_stmt, mth_type, aVisitor)
65
85
  end
86
+ end_scope
87
+ end_scope if aClassStmt.superclass
88
+ @current_class = previous_class
66
89
  end
67
90
 
68
91
  def before_for_stmt(aForStmt)
@@ -81,7 +104,7 @@ module Loxxy
81
104
  anIfStmt.else_stmt&.accept(aVisitor)
82
105
  end
83
106
 
84
- def before_return_stmt(_returnStmt)
107
+ def before_return_stmt(returnStmt)
85
108
  if scopes.size < 2
86
109
  msg = "Error at 'return': Can't return from top-level code."
87
110
  raise StandardError, msg
@@ -91,6 +114,11 @@ module Loxxy
91
114
  msg = "Error at 'return': Can't return from outside a function."
92
115
  raise StandardError, msg
93
116
  end
117
+
118
+ if current_function == :initializer
119
+ msg = "Error at 'return': Can't return a value from an initializer."
120
+ raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
121
+ end
94
122
  end
95
123
 
96
124
  def after_while_stmt(aWhileStmt, aVisitor)
@@ -140,6 +168,37 @@ module Loxxy
140
168
  aGetExpr.object.accept(aVisitor)
141
169
  end
142
170
 
171
+ def before_this_expr(_thisExpr)
172
+ if current_class == :none
173
+ msg = "Error at 'this': Can't use 'this' outside of a class."
174
+ raise StandardError, msg
175
+ end
176
+ end
177
+
178
+ def after_this_expr(aThisExpr, aVisitor)
179
+ # 'this' behaves closely to a local variable
180
+ resolve_local(aThisExpr, aVisitor)
181
+ end
182
+
183
+ # rubocop: disable Style/CaseLikeIf
184
+ # rubocop: disable Style/StringConcatenation
185
+ def after_super_expr(aSuperExpr, aVisitor)
186
+ msg_prefix = "Error at 'super': Can't use 'super' "
187
+ if current_class == :none
188
+ err_msg = msg_prefix + 'outside of a class.'
189
+ raise StandardError, err_msg
190
+
191
+ elsif current_class == :class
192
+ err_msg = msg_prefix + 'in a class without superclass.'
193
+ raise StandardError, err_msg
194
+
195
+ end
196
+ # 'super' behaves closely to a local variable
197
+ resolve_local(aSuperExpr, aVisitor)
198
+ end
199
+ # rubocop: enable Style/StringConcatenation
200
+ # rubocop: enable Style/CaseLikeIf
201
+
143
202
  # function declaration creates a new scope for its body & binds its parameters for that scope
144
203
  def before_fun_stmt(aFunStmt, aVisitor)
145
204
  declare(aFunStmt.name)
@@ -85,7 +85,7 @@ module Loxxy
85
85
  name2envs[name] = [current_env]
86
86
  end
87
87
 
88
- anEntry.name # anEntry.i_name
88
+ anEntry.name
89
89
  end
90
90
 
91
91
  # Search for the object with the given name
@@ -99,23 +99,6 @@ module Loxxy
99
99
  sc.defns[aName]
100
100
  end
101
101
 
102
- # Search for the object with the given i_name
103
- # @param anIName [String]
104
- # @return [BackEnd::Variable]
105
- # def lookup_i_name(anIName)
106
- # found = nil
107
- # environment = current_env
108
-
109
- # begin
110
- # found = environment.defns.values.find { |e| e.i_name == anIName }
111
- # break if found
112
-
113
- # environment = environment.parent
114
- # end while environment
115
-
116
- # found
117
- # end
118
-
119
102
  # Return all variables defined in the current .. root chain.
120
103
  # Variables are sorted top-down and left-to-right.
121
104
  def all_variables
@@ -35,10 +35,10 @@ module Loxxy
35
35
  rule('declaration' => 'classDecl')
36
36
  rule('declaration' => 'funDecl')
37
37
  rule('declaration' => 'varDecl')
38
- rule('declaration' => 'statement')
38
+ rule('declaration' => 'stmt')
39
39
 
40
40
  rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
41
- rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
41
+ rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER').as 'class_subclassing'
42
42
  rule('classNaming' => 'IDENTIFIER').as 'class_name'
43
43
  rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
44
44
  rule('methods_opt' => 'method_plus')
@@ -52,6 +52,8 @@ module Loxxy
52
52
  rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON').as 'var_initialization'
53
53
 
54
54
  # Statements: produce side effects, but don't introduce bindings
55
+ rule('stmt' => 'statement')
56
+ rule('stmt' => 'unbalancedStmt') # Tweak to cope with "dangling else" problem
55
57
  rule('statement' => 'exprStmt')
56
58
  rule('statement' => 'forStmt')
57
59
  rule('statement' => 'ifStmt')
@@ -70,10 +72,10 @@ module Loxxy
70
72
  rule('forTest' => 'expression_opt SEMICOLON').as 'for_test'
71
73
  rule('forUpdate' => 'expression_opt')
72
74
 
73
- rule('ifStmt' => 'IF ifCondition statement elsePart_opt').as 'if_stmt'
75
+ rule('ifStmt' => 'IF ifCondition statement ELSE statement').as 'if_else_stmt'
76
+ rule('unbalancedStmt' => 'IF ifCondition stmt').as 'if_stmt'
77
+ rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt').as 'if_else_stmt'
74
78
  rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN').as 'keep_symbol2'
75
- rule('elsePart_opt' => 'ELSE statement').as 'keep_symbol2'
76
- rule('elsePart_opt' => [])
77
79
 
78
80
  rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
79
81
  rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
@@ -136,12 +138,12 @@ module Loxxy
136
138
  rule('primary' => 'TRUE').as 'literal_expr'
137
139
  rule('primary' => 'FALSE').as 'literal_expr'
138
140
  rule('primary' => 'NIL').as 'literal_expr'
139
- rule('primary' => 'THIS')
141
+ rule('primary' => 'THIS').as 'this_expr'
140
142
  rule('primary' => 'NUMBER').as 'literal_expr'
141
143
  rule('primary' => 'STRING').as 'literal_expr'
142
144
  rule('primary' => 'IDENTIFIER').as 'variable_expr'
143
145
  rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN').as 'grouping_expr'
144
- rule('primary' => 'SUPER DOT IDENTIFIER')
146
+ rule('primary' => 'SUPER DOT IDENTIFIER').as 'super_expr'
145
147
 
146
148
  # Utility rules
147
149
  rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
@@ -24,6 +24,15 @@ module Loxxy
24
24
  # @param lox_input [String] Lox program to evaluate
25
25
  # @return [Loxxy::Datatype::BuiltinDatatype]
26
26
  def evaluate(lox_input)
27
+ raw_evaluate(lox_input).first
28
+ end
29
+
30
+ # Evaluate the given Lox program.
31
+ # Return the pair [result, a BackEnd::Engine instance]
32
+ # where result is the value of the last executed expression (if any)
33
+ # @param lox_input [String] Lox program to evaluate
34
+ # @return Loxxy::Datatype::BuiltinDatatype, Loxxy::BackEnd::Engine]
35
+ def raw_evaluate(lox_input)
27
36
  # Front-end scans, parses the input and blurps an AST...
28
37
  parser = FrontEnd::Parser.new
29
38
 
@@ -34,7 +43,9 @@ module Loxxy
34
43
  # Back-end launches the tree walking & responds to visit events
35
44
  # by executing the code determined by the visited AST node.
36
45
  engine = BackEnd::Engine.new(config)
37
- engine.execute(visitor)
46
+ result = engine.execute(visitor)
47
+
48
+ [result, engine]
38
49
  end
39
50
  end # class
40
51
  end # module
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.13'
4
+ VERSION = '0.2.00'
5
5
  end
data/loxxy.gemspec CHANGED
@@ -40,8 +40,8 @@ Gem::Specification.new do |spec|
40
40
  spec.version = Loxxy::VERSION
41
41
  spec.authors = ['Dimitri Geshef']
42
42
  spec.email = ['famished.tiger@yahoo.com']
43
- spec.summary = 'An implementation of the Lox programming language. WIP'
44
- spec.description = 'An implementation of the Lox programming language. WIP'
43
+ spec.summary = 'An implementation of the Lox programming language.'
44
+ spec.description = 'An implementation of the Lox programming language.'
45
45
  spec.homepage = 'https://github.com/famished-tiger/loxxy'
46
46
  spec.license = 'MIT'
47
47
  spec.required_ruby_version = '~> 2.4'
@@ -34,15 +34,7 @@ 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
42
-
43
37
  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
38
  # Precondition: value to assign is on top of stack
47
39
  subject.stack.push(greeting)
48
40
 
@@ -76,8 +76,11 @@ LOX_END
76
76
  expect(decls.symbol.name).to eq('declaration_plus')
77
77
  stmt = decls.subnodes[0].subnodes[0]
78
78
  expect(stmt).to be_kind_of(Rley::PTree::NonTerminalNode)
79
- expect(stmt.symbol.name).to eq('statement')
80
- prnt_stmt = stmt.subnodes[0]
79
+ expect(stmt.symbol.name).to eq('stmt')
80
+ statement = stmt.subnodes[0]
81
+ expect(statement).to be_kind_of(Rley::PTree::NonTerminalNode)
82
+ expect(statement.symbol.name).to eq('statement')
83
+ prnt_stmt = statement.subnodes[0]
81
84
  expect(prnt_stmt).to be_kind_of(Rley::PTree::NonTerminalNode)
82
85
  expect(prnt_stmt.subnodes.size).to eq(3)
83
86
  expect(prnt_stmt.subnodes[0]).to be_kind_of(Rley::PTree::TerminalNode)
@@ -6,6 +6,7 @@ require 'stringio'
6
6
  # Load the class under test
7
7
  require_relative '../lib/loxxy/interpreter'
8
8
 
9
+ # rubocop: disable Metrics/ModuleLength
9
10
  module Loxxy
10
11
  # This spec contains the bare bones test for the Interpreter class.
11
12
  # The execution of Lox code is tested elsewhere.
@@ -227,19 +228,20 @@ module Loxxy
227
228
  # Evaluate the 'then' expression if the condition is true.
228
229
  ['if (true) print "then-branch";', 'then-branch'],
229
230
  ['if (false) print "ignored";', ''],
230
- # TODO: test with then block body
231
- # TODO: test with assignment in if condition
231
+ ['if (nil) print "ignored";', ''],
232
+ ['if (true) { print "block"; }', 'block'],
233
+ ['var a = false; if (a = true) print a;', 'true'],
232
234
 
233
235
  # Evaluate the 'else' expression if the condition is false.
234
236
  ['if (true) print "then-branch"; else print "else-branch";', 'then-branch'],
235
237
  ['if (false) print "then-branch"; else print "else-branch";', 'else-branch'],
236
238
  ['if (0) print "then-branch"; else print "else-branch";', 'then-branch'],
237
- ['if (nil) print "then-branch"; else print "else-branch";', 'else-branch']
238
- # TODO: test with else block body
239
+ ['if (nil) print "then-branch"; else print "else-branch";', 'else-branch'],
240
+ ['if (false) nil; else { print "else-branch"; }', 'else-branch'],
239
241
 
240
- # TODO: A dangling else binds to the right-most if.
241
- # ['if (true) if (false) print "bad"; else print "good";', 'good'],
242
- # ['if (false) if (true) print "bad"; else print "worse";', 'bad']
242
+ # A dangling else binds to the right-most if.
243
+ ['if (true) if (false) print "bad"; else print "good";', 'good'],
244
+ ['if (false) if (true) print "bad"; else print "worse";', '']
243
245
  ].each do |(source, predicted)|
244
246
  io = StringIO.new
245
247
  cfg = { ostream: io }
@@ -466,7 +468,7 @@ LOX_END
466
468
  snippet = <<-LOX_END
467
469
  class Duck {
468
470
  noise() {
469
- quack();
471
+ this.quack();
470
472
  }
471
473
 
472
474
  quack() {
@@ -509,7 +511,115 @@ LOX_END
509
511
  expect { subject.evaluate(program) }.not_to raise_error
510
512
  expect(sample_cfg[:ostream].string).to eq('quack')
511
513
  end
514
+
515
+ it "should support the 'this' keyword" do
516
+ program = <<-LOX_END
517
+ class Egotist {
518
+ speak() {
519
+ print this;
520
+ }
521
+ }
522
+
523
+ var method = Egotist().speak;
524
+ method(); // Output: Egotist instance
525
+ LOX_END
526
+ expect { subject.evaluate(program) }.not_to raise_error
527
+ expect(sample_cfg[:ostream].string).to eq('Egotist instance')
528
+ end
529
+
530
+ it 'should support a closure nested in a method' do
531
+ lox_snippet = <<-LOX_END
532
+ class Foo {
533
+ getClosure() {
534
+ fun closure() {
535
+ return this.toString();
536
+ }
537
+ return closure;
538
+ }
539
+
540
+ toString() { return "foo"; }
541
+ }
542
+
543
+ var closure = Foo().getClosure();
544
+ closure;
545
+ LOX_END
546
+ # Expected result: Backend::LoxFunction('closure')
547
+ # Expected function's closure (environment layout):
548
+ # Environment('global')
549
+ # defns
550
+ # +- ['clock'] => BackEnd::Engine::NativeFunction
551
+ # *- ['Foo'] => BackEnd::LoxClass
552
+ # Environment
553
+ # defns
554
+ # ['this'] => BackEnd::LoxInstance
555
+ # Environment
556
+ # defns
557
+ # +- ['closure'] => Backend::LoxFunction
558
+ result = subject.evaluate(lox_snippet)
559
+ expect(result).to be_kind_of(BackEnd::LoxFunction)
560
+ expect(result.name).to eq('closure')
561
+ closure = result.closure
562
+ expect(closure).to be_kind_of(Loxxy::BackEnd::Environment)
563
+ expect(closure.defns['closure'].value).to eq(result)
564
+ expect(closure.enclosing).to be_kind_of(Loxxy::BackEnd::Environment)
565
+ expect(closure.enclosing.defns['this'].value).to be_kind_of(Loxxy::BackEnd::LoxInstance)
566
+ global_env = closure.enclosing.enclosing
567
+ expect(global_env).to be_kind_of(Loxxy::BackEnd::Environment)
568
+ expect(global_env.defns['clock'].value).to be_kind_of(BackEnd::Engine::NativeFunction)
569
+ expect(global_env.defns['Foo'].value).to be_kind_of(BackEnd::LoxClass)
570
+ end
571
+
572
+ it 'should support custom initializer' do
573
+ lox_snippet = <<-LOX_END
574
+ // From section 3.9.5
575
+ class Breakfast {
576
+ init(meat, bread) {
577
+ this.meat = meat;
578
+ this.bread = bread;
579
+ }
580
+
581
+ serve(who) {
582
+ print "Enjoy your " + this.meat + " and " +
583
+ this.bread + ", " + who + ".";
584
+ }
585
+ }
586
+
587
+ var baconAndToast = Breakfast("bacon", "toast");
588
+ baconAndToast.serve("Dear Reader");
589
+ // Output: "Enjoy your bacon and toast, Dear Reader."
590
+ LOX_END
591
+ expect { subject.evaluate(lox_snippet) }.not_to raise_error
592
+ predicted = 'Enjoy your bacon and toast, Dear Reader.'
593
+ expect(sample_cfg[:ostream].string).to eq(predicted)
594
+ end
595
+
596
+ it 'should support class inheritance and super keyword' do
597
+ lox_snippet = <<-LOX_END
598
+ class A {
599
+ method() {
600
+ print "A method";
601
+ }
602
+ }
603
+
604
+ class B < A {
605
+ method() {
606
+ print "B method";
607
+ }
608
+
609
+ test() {
610
+ super.method();
611
+ }
612
+ }
613
+
614
+ class C < B {}
615
+
616
+ C().test();
617
+ LOX_END
618
+ expect { subject.evaluate(lox_snippet) }.not_to raise_error
619
+ expect(sample_cfg[:ostream].string).to eq('A method')
620
+ end
512
621
  end # context
513
622
  end # describe
514
623
  # rubocop: enable Metrics/BlockLength
515
624
  end # module
625
+ # rubocop: enable Metrics/ModuleLength
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.13
4
+ version: 0.2.00
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-04-05 00:00:00.000000000 Z
11
+ date: 2021-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -66,7 +66,7 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
- description: An implementation of the Lox programming language. WIP
69
+ description: An implementation of the Lox programming language.
70
70
  email:
71
71
  - famished.tiger@yahoo.com
72
72
  executables:
@@ -108,6 +108,8 @@ files:
108
108
  - lib/loxxy/ast/lox_return_stmt.rb
109
109
  - lib/loxxy/ast/lox_seq_decl.rb
110
110
  - lib/loxxy/ast/lox_set_expr.rb
111
+ - lib/loxxy/ast/lox_super_expr.rb
112
+ - lib/loxxy/ast/lox_this_expr.rb
111
113
  - lib/loxxy/ast/lox_unary_expr.rb
112
114
  - lib/loxxy/ast/lox_var_stmt.rb
113
115
  - lib/loxxy/ast/lox_variable_expr.rb
@@ -177,7 +179,7 @@ requirements: []
177
179
  rubygems_version: 3.1.4
178
180
  signing_key:
179
181
  specification_version: 4
180
- summary: An implementation of the Lox programming language. WIP
182
+ summary: An implementation of the Lox programming language.
181
183
  test_files:
182
184
  - spec/back_end/engine_spec.rb
183
185
  - spec/back_end/environment_spec.rb