loxxy 0.1.12 → 0.1.17

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: 665b2f201c4e369bb46b4b62cd8398f8d1f6f563b073fb6e1595b767f306c627
4
- data.tar.gz: 7bd3ca2024f2f05150d3fd8af9cdaee28d6dc4d2bbe48ebfe6ae9fd78166fc7f
3
+ metadata.gz: 2c7d720e2638407882bfd3f34384748a872086e1b1f7a3849f0e029e8672fc1c
4
+ data.tar.gz: c3aa7ebe4a625ef1b0a257cc750430b9a0aa404bb39dab900b409b2987e09dea
5
5
  SHA512:
6
- metadata.gz: 0cffb636754ac0140aa7873955c75d8e6a048a0806cacf22484de91bacecb4fdb7d55127f7ff6e2f5602ff02f1eb9f24de788a6be0053768c68f12b9cdd0caab
7
- data.tar.gz: 331fb47a8a46a9ea6a086556814b95aba7d7a4d7a3c600f82acacfd38dd567c657ca79f6ce683e17e64e8104167692a2b49e7628bac9d8536a5bd0625ceab347
6
+ metadata.gz: 1eb8bb6575dd39834c55ce3ad359fbdb9a2129f91d25bc39ea093c5a4ca2aa38300ab28e673b39991678c304aea64ffda473707f9d317980b976c8f98fc3a55f
7
+ data.tar.gz: d3dd5baae665ab27a60a13b7392e9d57ef32ff72a37892d8f08e314888a1bfcc3db9ceeb071a28c00dda44f4bce7afed6aadf6c33c411a93d561a30fef833edd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,72 @@
1
+ ## [0.1.17] - 2021-04-11
2
+ - TODO
3
+
4
+ ### Fixed
5
+ - TODO
6
+
7
+ ## [0.1.16] - 2021-04-10
8
+ - Fixed an issue in name lookup. All the `this` test suite is passing.
9
+
10
+ ### Changed
11
+ - Method `BackEnd::Engine#after_var_stmt` now it creates the variable and pouts it in the symbol table
12
+
13
+ ### Removed
14
+ - Method `BackEnd::Engine#before_var_stmt` it generated bug when assigning a value to a var, when that var name occurred elsewhere
15
+
16
+ ## [0.1.15] - 2021-04-08
17
+ - Fixed the `dangling else`by tweaking the grammar rules
18
+
19
+ ### Changed
20
+ - Method `Ast::ASTBuilder#reduce_if__else_stmt` parse action specific for if with else branch
21
+
22
+ ### Fixed
23
+ - File `grammar.rb` changed rules to cope with `dangling else` issue
24
+
25
+ ### Changed
26
+ - Method `Ast::ASTBuilder#reduce_if_stmt` parse action for if without else branch
27
+ - File `README.md` removed the section about the `dangling else` issue.
28
+
29
+
30
+ ## [0.1.14] - 2021-04-05
31
+ - `Loxxy` now implements the 'this' keyword
32
+
33
+ ### New
34
+ - Class `Ast::LoxThisExpr` a syntax node that represents an occurrence of the `this` keyword
35
+ - Method `Ast::ASTBuilder#reduce_this_expr` parse action for this keyword
36
+ - Method `Ast::Visitor#visit_this_expr` visit of an `Ast::LoxThisExpr` node
37
+ - Method `BackEnd::Engine#after_this_expr` runtime action for this keyword (i.e. refers to current instance)
38
+ - Method `BackEnd::LoxFunction#bind` implementation of bound method
39
+
40
+ ### Changed
41
+ - Class `BackEnd::Resolver` implementing semantic actions for `this` keyword
42
+ - File `grammar.rb` added name to a syntax rule
43
+
44
+ ## [0.1.13] - 2021-04-05
45
+ - `Loxxy` now implements method calls
46
+
47
+ ### New
48
+ - Class `Ast::LoxGetExpr` a syntax node that represents a read access to an object property
49
+ - Class `Ast::LoxSetExpr` a syntax node that represents a write access to an object property
50
+ - Method `Ast::ASTBuilder#reduce_set_expr` parse action for write access to an object property
51
+ - Method `Ast::ASTBuilder#reduce_get_expr` parse action for read access to an object property
52
+ - Method `Ast::Visitor#visit_set_expr` visit of an `Ast::LoxSetExpr` node
53
+ - Method `Ast::Visitor#visit_get_expr` visit of an `Ast::LoxGetExpr` node
54
+ - Method `BackEnd::Engine#after_set_expr` runtime action for property setting
55
+ - Method `BackEnd::Engine#after_get_expr` runtime action for property getting
56
+ - Method `BackEnd::LoxInstance#set` implementation of write accessor
57
+ - Method `BackEnd::LoxInstance#get` implementation of read accessor
58
+ - Method `BackEnd::Resolver#after_set_expr` resolve action for property setting
59
+ - Method `BackEnd::Resolver#after_get_expr` resolve action for property getting
60
+
61
+ ### Changed
62
+ - Method `Ast::ASTBuilder#reduce_assign_expr` expanded to support write access to an object property
63
+ - Class `LoxClassStmt`: methods are now aggregate under the `body` attribute
64
+ - Class `LoxFunStmt`: has a new attribute `is_method` and inherits from `Ast::LoxNode`
65
+ - Method `BackEnd::Engine#after_class_stmt` methods are aggregated into the classes
66
+ - Method `BackEnd::Engine#after_fun_stmt` extension for method
67
+ - File `grammar.rb` added names to two syntax rules
68
+
69
+
1
70
  ## [0.1.12] - 2021-04-03
2
71
  - Intermediate version: `Loxxy` does instance creation (default constructor)
3
72
 
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,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'lox_fun_stmt'
4
+ require_relative 'lox_this_expr'
4
5
  require_relative 'lox_variable_expr'
5
6
  require_relative 'lox_literal_expr'
6
7
  require_relative 'lox_noop_expr'
8
+ require_relative 'lox_get_expr'
7
9
  require_relative 'lox_call_expr'
8
10
  require_relative 'lox_grouping_expr'
9
11
  require_relative 'lox_unary_expr'
10
12
  require_relative 'lox_binary_expr'
11
13
  require_relative 'lox_logical_expr'
14
+ require_relative 'lox_set_expr'
12
15
  require_relative 'lox_assign_expr'
13
16
  require_relative 'lox_block_stmt'
14
17
  require_relative 'lox_while_stmt'
@@ -233,11 +233,20 @@ module Loxxy
233
233
  return_first_child(range, tokens, theChildren)
234
234
  end
235
235
 
236
- # rule('ifStmt' => 'IF ifCondition statement elsePart_opt')
236
+ # rule('ifStmt' => 'IF ifCondition statement ELSE statement')
237
+ # rule('unbalancedStmt' => 'IF ifCondition statement ELSE unbalancedStmt')
238
+ def reduce_if_else_stmt(_production, _range, tokens, theChildren)
239
+ condition = theChildren[1]
240
+ then_stmt = theChildren[2]
241
+ else_stmt = theChildren[4]
242
+ LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
243
+ end
244
+
245
+ # rule('unbalancedStmt' => 'IF ifCondition stmt').as ''
237
246
  def reduce_if_stmt(_production, _range, tokens, theChildren)
238
247
  condition = theChildren[1]
239
248
  then_stmt = theChildren[2]
240
- else_stmt = theChildren[3]
249
+ else_stmt = nil
241
250
  LoxIfStmt.new(tokens[0].position, condition, then_stmt, else_stmt)
242
251
  end
243
252
 
@@ -269,10 +278,22 @@ module Loxxy
269
278
 
270
279
  # rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment')
271
280
  def reduce_assign_expr(_production, _range, tokens, theChildren)
272
- var_name = theChildren[1].token.lexeme.dup
273
- Ast::LoxAssignExpr.new(tokens[1].position, var_name, theChildren[3])
281
+ name_assignee = theChildren[1].token.lexeme.dup
282
+ if theChildren[0].kind_of?(Ast::LoxSetExpr)
283
+ theChildren[0].property = name_assignee
284
+ theChildren[0].subnodes << theChildren[3]
285
+ theChildren[0]
286
+ else
287
+ Ast::LoxAssignExpr.new(tokens[1].position, name_assignee, theChildren[3])
288
+ end
274
289
  end
275
290
 
291
+ # rule('owner_opt' => 'call DOT')
292
+ def reduce_set_expr(_production, _range, tokens, theChildren)
293
+ Ast::LoxSetExpr.new(tokens[1].position, theChildren[0])
294
+ end
295
+
296
+
276
297
  # rule('comparisonTest_plus' => 'comparisonTest_plus comparisonTest term').as 'comparison_t_plus_more'
277
298
  # TODO: is it meaningful to implement this rule?
278
299
 
@@ -317,6 +338,11 @@ module Loxxy
317
338
  LoxCallExpr.new(tokens[0].position, args)
318
339
  end
319
340
 
341
+ # rule('refinement' => 'DOT IDENTIFIER').as 'get_expr'
342
+ def reduce_get_expr(_production, _range, tokens, theChildren)
343
+ LoxGetExpr.new(tokens[0].position, theChildren[1].token.lexeme)
344
+ end
345
+
320
346
  # rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN')
321
347
  def reduce_grouping_expr(_production, _range, tokens, theChildren)
322
348
  subexpr = theChildren[1]
@@ -337,6 +363,11 @@ module Loxxy
337
363
  LoxVariableExpr.new(tokens[0].position, var_name)
338
364
  end
339
365
 
366
+ # rule('primary' => 'THIS')
367
+ def reduce_this_expr(_production, _range, tokens, _children)
368
+ LoxThisExpr.new(tokens[0].position)
369
+ end
370
+
340
371
  # rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
341
372
  def reduce_function(_production, _range, _tokens, theChildren)
342
373
  first_child = theChildren.first
@@ -68,7 +68,7 @@ module Loxxy
68
68
  end
69
69
 
70
70
  # Visit event. The visitor is about to visit a class declaration.
71
- # @param aXlassStmt [AST::LOXClassStmt] the for statement node to visit
71
+ # @param aClassStmt [AST::LOXClassStmt] the for statement node to visit
72
72
  def visit_class_stmt(aClassStmt)
73
73
  broadcast(:before_class_stmt, aClassStmt)
74
74
  traverse_subnodes(aClassStmt) # The methods are visited here...
@@ -131,6 +131,13 @@ module Loxxy
131
131
  broadcast(:after_assign_expr, anAssignExpr, self)
132
132
  end
133
133
 
134
+ # @param aSetExpr [AST::LOXGetExpr] the get expression node to visit
135
+ def visit_set_expr(aSetExpr)
136
+ broadcast(:before_set_expr, aSetExpr)
137
+ traverse_subnodes(aSetExpr)
138
+ broadcast(:after_set_expr, aSetExpr, self)
139
+ end
140
+
134
141
  # Visit event. The visitor is about to visit a logical expression.
135
142
  # Since logical expressions may take shorcuts by not evaluating all their
136
143
  # sub-expressiosns, they are responsible for visiting or not their children.
@@ -169,6 +176,12 @@ module Loxxy
169
176
  broadcast(:after_call_expr, aCallExpr, self)
170
177
  end
171
178
 
179
+ # @param aGetExpr [AST::LOXGetExpr] the get expression node to visit
180
+ def visit_get_expr(aGetExpr)
181
+ broadcast(:before_get_expr, aGetExpr)
182
+ broadcast(:after_get_expr, aGetExpr, self)
183
+ end
184
+
172
185
  # Visit event. The visitor is about to visit a grouping expression.
173
186
  # @param aGroupingExpr [AST::LoxGroupingExpr] grouping expression to visit
174
187
  def visit_grouping_expr(aGroupingExpr)
@@ -192,6 +205,13 @@ module Loxxy
192
205
  broadcast(:after_variable_expr, aVariableExpr, self)
193
206
  end
194
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
+
195
215
  # Visit event. The visitor is about to visit the given terminal datatype value.
196
216
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
197
217
  def visit_builtin(aValue)
@@ -203,7 +223,6 @@ module Loxxy
203
223
  # @param aFunStmt [AST::LoxFunStmt] function declaration to visit
204
224
  def visit_fun_stmt(aFunStmt)
205
225
  broadcast(:before_fun_stmt, aFunStmt, self)
206
- traverse_subnodes(aFunStmt)
207
226
  broadcast(:after_fun_stmt, aFunStmt, self)
208
227
  end
209
228
 
@@ -5,14 +5,19 @@ require_relative 'lox_compound_expr'
5
5
  module Loxxy
6
6
  module Ast
7
7
  class LoxClassStmt < LoxCompoundExpr
8
+ # @return [String] the class name
8
9
  attr_reader :name
9
10
 
11
+ # @return [Array<Ast::LoxFunStmt>] the methods
12
+ attr_reader :body
13
+
10
14
  # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
11
15
  # @param condExpr [Loxxy::Ast::LoxNode] iteration condition
12
- # @param theBody [Loxxy::Ast::LoxNode]
16
+ # @param theBody [Array<Loxxy::Ast::LoxNode>]
13
17
  def initialize(aPosition, aName, theMethods)
14
- super(aPosition, theMethods)
18
+ super(aPosition, [])
15
19
  @name = aName.dup
20
+ @body = theMethods
16
21
  end
17
22
 
18
23
  # Part of the 'visitee' role in Visitor design pattern.
@@ -20,8 +25,6 @@ module Loxxy
20
25
  def accept(visitor)
21
26
  visitor.visit_class_stmt(self)
22
27
  end
23
-
24
- alias body subnodes
25
28
  end # class
26
29
  end # module
27
30
  end # module
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lox_compound_expr'
3
+ require_relative 'lox_node'
4
4
 
5
5
  module Loxxy
6
6
  module Ast
7
7
  # rubocop: disable Style/AccessorGrouping
8
- class LoxFunStmt < LoxCompoundExpr
8
+ class LoxFunStmt < LoxNode
9
9
  attr_reader :name
10
10
  attr_reader :params
11
11
  attr_reader :body
12
+ attr_accessor :is_method
12
13
 
13
14
  # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
14
15
  # @param aName [String]
15
16
  # @param arguments [Array<String>]
16
17
  # @param body [Ast::LoxBlockStmt]
17
18
  def initialize(aPosition, aName, paramList, aBody)
18
- super(aPosition, [])
19
+ super(aPosition)
19
20
  @name = aName.dup
20
21
  @params = paramList
21
22
  @body = aBody
23
+ @is_method = false
22
24
  end
23
25
 
24
26
  # Part of the 'visitee' role in Visitor design pattern.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_node'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ class LoxGetExpr < LoxNode
8
+ # @return [Ast::LoxNode] the object to which the property belongs to
9
+ attr_accessor :object
10
+
11
+ # @return [String] Name of an object property
12
+ attr_reader :property
13
+
14
+ # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
15
+ # @param aPropertyName [String] Name of an object property
16
+ def initialize(aPosition, aPropertyName)
17
+ super(aPosition)
18
+ @property = aPropertyName
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_get_expr(self)
25
+ end
26
+
27
+ alias callee= object=
28
+ end # class
29
+ end # module
30
+ end # module
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lox_compound_expr'
4
+
5
+ module Loxxy
6
+ module Ast
7
+ class LoxSetExpr < LoxCompoundExpr
8
+ # @return [Ast::LoxNode] the object to which the property belongs to
9
+ attr_reader :object
10
+
11
+ # @return [String] Name of an object property
12
+ attr_accessor :property
13
+
14
+ # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
15
+ # @param anObject [Ast::LoxNode] The object which the given property is being set
16
+ def initialize(aPosition, anObject)
17
+ super(aPosition, [])
18
+ @object = anObject
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_set_expr(self)
25
+ end
26
+ end # class
27
+ end # module
28
+ 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
@@ -69,24 +69,27 @@ module Loxxy
69
69
  # Do nothing, subnodes were already evaluated
70
70
  end
71
71
 
72
- def after_class_stmt(aClassStmt, _visitor)
73
- klass = LoxClass.new(aClassStmt.name, aClassStmt.methods, self)
72
+ def after_class_stmt(aClassStmt, aVisitor)
73
+ # Convert LoxFunStmt into LoxFunction
74
+ meths = aClassStmt.body.map do |func_node|
75
+ func_node.is_method = true
76
+ func_node.accept(aVisitor)
77
+ mth = stack.pop
78
+ mth.is_initializer = true if mth.name == 'init'
79
+ mth
80
+ end
81
+
82
+ klass = LoxClass.new(aClassStmt.name, meths, self)
74
83
  new_var = Variable.new(aClassStmt.name, klass)
75
84
  symbol_table.insert(new_var)
76
85
  end
77
86
 
78
- def before_var_stmt(aVarStmt)
87
+ def after_var_stmt(aVarStmt)
79
88
  new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
80
89
  symbol_table.insert(new_var)
81
- end
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
90
 
88
91
  value = stack.pop
89
- variable.assign(value)
92
+ new_var.assign(value)
90
93
  end
91
94
 
92
95
  def before_for_stmt(aForStmt)
@@ -153,6 +156,18 @@ module Loxxy
153
156
  variable.assign(value)
154
157
  end
155
158
 
159
+ def after_set_expr(aSetExpr, aVisitor)
160
+ value = stack.pop
161
+ # Evaluate object part
162
+ aSetExpr.object.accept(aVisitor)
163
+ assignee = stack.pop
164
+ unless assignee.kind_of?(LoxInstance)
165
+ raise StandardError, 'Only instances have fields.'
166
+ end
167
+
168
+ assignee.set(aSetExpr.property, value)
169
+ end
170
+
156
171
  def after_logical_expr(aLogicalExpr, visitor)
157
172
  op = aLogicalExpr.operator
158
173
  operand1 = stack.pop # only first operand was evaluated
@@ -221,20 +236,28 @@ module Loxxy
221
236
  case callee
222
237
  when NativeFunction
223
238
  stack.push callee.call # Pass arguments
224
- when LoxFunction
239
+ when LoxFunction, LoxClass
225
240
  arg_count = aCallExpr.arguments.size
226
241
  if arg_count != callee.arity
227
242
  msg = "Expected #{callee.arity} arguments but got #{arg_count}."
228
243
  raise Loxxy::RuntimeError, msg
229
244
  end
230
245
  callee.call(self, aVisitor)
231
- when LoxClass
232
- callee.call(self, aVisitor)
233
246
  else
234
247
  raise Loxxy::RuntimeError, 'Can only call functions and classes.'
235
248
  end
236
249
  end
237
250
 
251
+ def after_get_expr(aGetExpr, aVisitor)
252
+ aGetExpr.object.accept(aVisitor)
253
+ instance = stack.pop
254
+ unless instance.kind_of?(LoxInstance)
255
+ raise StandardError, 'Only instances have properties.'
256
+ end
257
+
258
+ stack.push instance.get(aGetExpr.property)
259
+ end
260
+
238
261
  def after_grouping_expr(_groupingExpr)
239
262
  # Do nothing: work was already done by visiting /evaluating the subexpression
240
263
  end
@@ -242,7 +265,7 @@ module Loxxy
242
265
  def after_variable_expr(aVarExpr, aVisitor)
243
266
  var_name = aVarExpr.name
244
267
  var = variable_lookup(aVarExpr)
245
- raise StandardError, "Unknown variable #{var_name}" unless var
268
+ raise StandardError, "Undefined variable '#{var_name}'." unless var
246
269
 
247
270
  var.value.accept(aVisitor) # Evaluate variable value then push on stack
248
271
  end
@@ -252,6 +275,11 @@ module Loxxy
252
275
  stack.push(literalExpr.literal)
253
276
  end
254
277
 
278
+ def after_this_expr(aThisExpr, aVisitor)
279
+ var = variable_lookup(aThisExpr)
280
+ var.value.accept(aVisitor) # Evaluate this value then push on stack
281
+ end
282
+
255
283
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
256
284
  def before_visit_builtin(aValue)
257
285
  stack.push(aValue)
@@ -259,8 +287,12 @@ module Loxxy
259
287
 
260
288
  def after_fun_stmt(aFunStmt, _visitor)
261
289
  function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
262
- new_var = Variable.new(aFunStmt.name, function)
263
- symbol_table.insert(new_var)
290
+ if aFunStmt.is_method
291
+ stack.push function
292
+ else
293
+ new_var = Variable.new(aFunStmt.name, function)
294
+ symbol_table.insert(new_var)
295
+ end
264
296
  end
265
297
 
266
298
  private
@@ -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
@@ -10,15 +10,18 @@ module Loxxy
10
10
  # @return [String] The name of the class
11
11
  attr_reader :name
12
12
 
13
- # @return [Array<>] the list of methods
14
- attr_reader :methods
13
+ # @return [Hash{String => LoxFunction}] the list of methods
14
+ attr_reader :meths
15
15
  attr_reader :stack
16
16
 
17
17
  # Create a class with given name
18
18
  # @param aName [String] The name of the class
19
19
  def initialize(aName, theMethods, anEngine)
20
20
  @name = aName.dup
21
- @methods = theMethods
21
+ @meths = {}
22
+ theMethods.each do |func|
23
+ meths[func.name] = func
24
+ end
22
25
  @stack = anEngine.stack
23
26
  end
24
27
 
@@ -27,14 +30,26 @@ module Loxxy
27
30
  end
28
31
 
29
32
  def arity
30
- 0
33
+ initializer = find_method('init')
34
+ initializer ? initializer.arity : 0
31
35
  end
32
36
 
33
- def call(engine, _visitor)
37
+ def call(engine, visitor)
34
38
  instance = LoxInstance.new(self, engine)
39
+ initializer = find_method('init')
40
+ if initializer
41
+ constructor = initializer.bind(instance)
42
+ constructor.call(engine, visitor)
43
+ end
44
+
35
45
  engine.stack.push(instance)
36
46
  end
37
47
 
48
+ # @param aName [String] the method name to search for
49
+ def find_method(aName)
50
+ meths[aName]
51
+ end
52
+
38
53
  # Logical negation.
39
54
  # As a function is a truthy thing, its negation is thus false.
40
55
  # @return [Datatype::False]
@@ -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]
@@ -6,26 +6,49 @@ module Loxxy
6
6
  module BackEnd
7
7
  # Runtime representation of a Lox object (instance).
8
8
  class LoxInstance
9
- # @return BackEnd::LoxClass] the class this this object belong
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
+
14
+ # @return [Hash{String => BuiltinDatatype | LoxFunction | LoxInstance }]
15
+ attr_reader :fields
13
16
 
14
17
  # Create an instance from given class
15
18
  # @param aClass [BackEnd::LoxClass] the class this this object belong
16
19
  def initialize(aClass, anEngine)
17
20
  @klass = aClass
18
- @stack = anEngine.stack
21
+ @engine = anEngine
22
+ @fields = {}
19
23
  end
20
24
 
21
25
  def accept(_visitor)
22
- stack.push self
26
+ engine.stack.push self
23
27
  end
24
28
 
25
29
  # Text representation of a Lox instance
26
30
  def to_str
27
31
  "#{klass.to_str} instance"
28
32
  end
33
+
34
+ # Look up the value of property with given name
35
+ # aName [String] name of object property
36
+ def get(aName)
37
+ return fields[aName] if fields.include? aName
38
+
39
+ method = klass.find_method(aName)
40
+ unless method
41
+ raise StandardError, "Undefined property '#{aName}'."
42
+ end
43
+
44
+ method.bind(self)
45
+ end
46
+
47
+ # Set the value of property with given name
48
+ # aName [String] name of object property
49
+ def set(aName, aValue)
50
+ fields[aName] = aValue
51
+ end
29
52
  end # class
30
53
  end # module
31
54
  end # module
@@ -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
@@ -58,8 +63,18 @@ module Loxxy
58
63
  declare(aClassStmt.name)
59
64
  end
60
65
 
61
- def after_class_stmt(aClassStmt, _visitor)
66
+ def after_class_stmt(aClassStmt, aVisitor)
67
+ previous_class = current_class
68
+ @current_class = :class
62
69
  define(aClassStmt.name)
70
+ begin_scope
71
+ define('this')
72
+ aClassStmt.body.each do |fun_stmt|
73
+ mth_type = fun_stmt.name == 'init' ? :initializer : :method
74
+ resolve_function(fun_stmt, mth_type, aVisitor)
75
+ end
76
+ end_scope
77
+ @current_class = previous_class
63
78
  end
64
79
 
65
80
  def before_for_stmt(aForStmt)
@@ -78,7 +93,7 @@ module Loxxy
78
93
  anIfStmt.else_stmt&.accept(aVisitor)
79
94
  end
80
95
 
81
- def before_return_stmt(_returnStmt)
96
+ def before_return_stmt(returnStmt)
82
97
  if scopes.size < 2
83
98
  msg = "Error at 'return': Can't return from top-level code."
84
99
  raise StandardError, msg
@@ -88,6 +103,11 @@ module Loxxy
88
103
  msg = "Error at 'return': Can't return from outside a function."
89
104
  raise StandardError, msg
90
105
  end
106
+
107
+ if current_function == :initializer
108
+ msg = "Error at 'return': Can't return a value from an initializer."
109
+ raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
110
+ end
91
111
  end
92
112
 
93
113
  def after_while_stmt(aWhileStmt, aVisitor)
@@ -109,6 +129,11 @@ module Loxxy
109
129
  resolve_local(anAssignExpr, aVisitor)
110
130
  end
111
131
 
132
+ def after_set_expr(aSetExpr, aVisitor)
133
+ # Evaluate object part
134
+ aSetExpr.object.accept(aVisitor)
135
+ end
136
+
112
137
  # Variable expressions require their variables resolved
113
138
  def before_variable_expr(aVarExpr)
114
139
  var_name = aVarExpr.name
@@ -127,6 +152,23 @@ module Loxxy
127
152
  aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
128
153
  end
129
154
 
155
+ def after_get_expr(aGetExpr, aVisitor)
156
+ # Evaluate object part
157
+ aGetExpr.object.accept(aVisitor)
158
+ end
159
+
160
+ def before_this_expr(_thisExpr)
161
+ if current_class == :none
162
+ msg = "Error at 'this': Can't use 'this' outside of a class."
163
+ raise StandardError, msg
164
+ end
165
+ end
166
+
167
+ def after_this_expr(aThisExpr, aVisitor)
168
+ # 'this' behaves closely to a local variable
169
+ resolve_local(aThisExpr, aVisitor)
170
+ end
171
+
130
172
  # function declaration creates a new scope for its body & binds its parameters for that scope
131
173
  def before_fun_stmt(aFunStmt, aVisitor)
132
174
  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,7 +35,7 @@ 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
41
  rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
@@ -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'
@@ -87,7 +89,7 @@ module Loxxy
87
89
  rule('expression' => 'assignment')
88
90
  rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment').as 'assign_expr'
89
91
  rule('assignment' => 'logic_or')
90
- rule('owner_opt' => 'call DOT')
92
+ rule('owner_opt' => 'call DOT').as 'set_expr'
91
93
  rule('owner_opt' => [])
92
94
  rule('logic_or' => 'logic_and')
93
95
  rule('logic_or' => 'logic_and disjunct_plus').as 'logical_expr'
@@ -132,11 +134,11 @@ module Loxxy
132
134
  rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
133
135
  rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
134
136
  rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
135
- rule('refinement' => 'DOT IDENTIFIER')
137
+ rule('refinement' => 'DOT IDENTIFIER').as 'get_expr'
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'
@@ -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.12'
4
+ VERSION = '0.1.17'
5
5
  end
@@ -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
 
@@ -305,6 +305,83 @@ LOX_END
305
305
  expect(expr.operands[1].operands[1].literal.value).to eq(5)
306
306
  end
307
307
  end # context
308
+
309
+ context 'Object orientation:' do
310
+ it 'should parse object property get access' do
311
+ input = 'print someObject.someProperty;'
312
+ ptree = subject.parse(input)
313
+ expr = ptree.root.subnodes[0]
314
+ expect(expr).to be_kind_of(Ast::LoxPrintStmt)
315
+ get_expr = expr.subnodes[0]
316
+ expect(get_expr).to be_kind_of(Ast::LoxGetExpr)
317
+ expect(get_expr.object.name).to eq('someObject')
318
+ expect(get_expr.property).to eq('someProperty')
319
+ end
320
+
321
+ it 'should parse nested call expressions' do
322
+ input = 'print egg.scramble(3).with(cheddar);'
323
+ # From section 12.3.1, one expects something like:
324
+ # LoxCallExpr
325
+ # +- arguments = ['cheddar']
326
+ # +- callee = LoxGetExpr
327
+ # +- property = 'with'
328
+ # +- object = LoxCallExpr
329
+ # +- arguments = [3]
330
+ # +- callee = LoxGetExpr
331
+ # +- property = 'scramble'
332
+ # +- object = variable 'egg'
333
+ ptree = subject.parse(input)
334
+ print_stmt = ptree.root.subnodes[0]
335
+ expect(print_stmt).to be_kind_of(Ast::LoxPrintStmt)
336
+ outer_call = print_stmt.subnodes[0]
337
+ expect(outer_call).to be_kind_of(Ast::LoxCallExpr)
338
+ expect(outer_call.arguments[0].name).to eq('cheddar')
339
+ expect(outer_call.callee).to be_kind_of(Ast::LoxGetExpr)
340
+ expect(outer_call.callee.property).to eq('with')
341
+ inner_call = outer_call.callee.object
342
+ expect(inner_call).to be_kind_of(Ast::LoxCallExpr)
343
+ expect(inner_call.arguments[0].literal).to eq(3)
344
+ expect(inner_call.callee).to be_kind_of(Ast::LoxGetExpr)
345
+ expect(inner_call.callee.property).to eq('scramble')
346
+ expect(inner_call.callee.object.name).to eq('egg')
347
+ end
348
+
349
+ it 'should parse object property set access' do
350
+ input = 'someObject.someProperty = value;'
351
+ ptree = subject.parse(input)
352
+ expr = ptree.root.subnodes[0]
353
+ expect(expr).to be_kind_of(Ast::LoxSetExpr)
354
+ expect(expr.object.name).to eq('someObject')
355
+ expect(expr.property).to eq('someProperty')
356
+ expect(expr.subnodes[0]).to be_kind_of(Ast::LoxVariableExpr)
357
+ expect(expr.subnodes[0].name).to eq('value')
358
+ end
359
+
360
+ it 'should parse complex set access' do
361
+ input = 'breakfast.omelette.filling.meat = ham;'
362
+ # From section 12.3.2, one expects something like:
363
+ # LoxSetExpr
364
+ # +- property = 'meat'
365
+ # +- subnodes[0] = LoxVariableExpr 'ham'
366
+ # +- object = LoxGetExpr
367
+ # +- property = 'filling'
368
+ # +- object = LoxGetExpr
369
+ # +- property = 'omelette'
370
+ # +- object = LoxVariableExpr 'breakfast'
371
+ ptree = subject.parse(input)
372
+ expr = ptree.root.subnodes[0]
373
+ expect(expr).to be_kind_of(Ast::LoxSetExpr)
374
+ expect(expr.property).to eq('meat')
375
+ expect(expr.subnodes[0]).to be_kind_of(Ast::LoxVariableExpr)
376
+ expect(expr.subnodes[0].name).to eq('ham')
377
+ expect(expr.object).to be_kind_of(Ast::LoxGetExpr)
378
+ expect(expr.object.property).to eq('filling')
379
+ expect(expr.object.object).to be_kind_of(Ast::LoxGetExpr)
380
+ expect(expr.object.object.property).to eq('omelette')
381
+ expect(expr.object.object.object).to be_kind_of(Ast::LoxVariableExpr)
382
+ expect(expr.object.object.object.name).to eq('breakfast')
383
+ end
384
+ end # context
308
385
  end # describe
309
386
  end # module
310
387
  end # module
@@ -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)
@@ -227,19 +227,20 @@ module Loxxy
227
227
  # Evaluate the 'then' expression if the condition is true.
228
228
  ['if (true) print "then-branch";', 'then-branch'],
229
229
  ['if (false) print "ignored";', ''],
230
- # TODO: test with then block body
231
- # TODO: test with assignment in if condition
230
+ ['if (nil) print "ignored";', ''],
231
+ ['if (true) { print "block"; }', 'block'],
232
+ ['var a = false; if (a = true) print a;', 'true'],
232
233
 
233
234
  # Evaluate the 'else' expression if the condition is false.
234
235
  ['if (true) print "then-branch"; else print "else-branch";', 'then-branch'],
235
236
  ['if (false) print "then-branch"; else print "else-branch";', 'else-branch'],
236
237
  ['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
238
+ ['if (nil) print "then-branch"; else print "else-branch";', 'else-branch'],
239
+ ['if (false) nil; else { print "else-branch"; }', 'else-branch'],
239
240
 
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']
241
+ # A dangling else binds to the right-most if.
242
+ ['if (true) if (false) print "bad"; else print "good";', 'good'],
243
+ ['if (false) if (true) print "bad"; else print "worse";', '']
243
244
  ].each do |(source, predicted)|
244
245
  io = StringIO.new
245
246
  cfg = { ostream: io }
@@ -451,44 +452,144 @@ LOX_END
451
452
  end
452
453
  # rubocop: enable Style/StringConcatenation
453
454
 
454
- it 'should support class declaration' do
455
+ it 'should print the hello world message' do
455
456
  program = <<-LOX_END
457
+ var greeting = "Hello"; // Declaring a variable
458
+ print greeting + ", " + "world!"; // ... Playing with concatenation
459
+ LOX_END
460
+ expect { subject.evaluate(program) }.not_to raise_error
461
+ expect(sample_cfg[:ostream].string).to eq('Hello, world!')
462
+ end
463
+ end # context
464
+
465
+ context 'Object orientation:' do
466
+ let(:duck_class) do
467
+ snippet = <<-LOX_END
456
468
  class Duck {
457
469
  noise() {
458
- quack();
470
+ this.quack();
459
471
  }
460
472
 
461
473
  quack() {
462
474
  print "quack";
463
475
  }
464
476
  }
465
- print Duck;
477
+ LOX_END
478
+
479
+ snippet
480
+ end
481
+
482
+ it 'should support class declaration' do
483
+ program = <<-LOX_END
484
+ #{duck_class}
485
+
486
+ print Duck; // Class names can appear in statements
466
487
  LOX_END
467
488
  expect { subject.evaluate(program) }.not_to raise_error
468
489
  expect(sample_cfg[:ostream].string).to eq('Duck')
469
490
  end
470
491
 
471
- it 'should support instance creation' do
492
+ it 'should support default instance creation' do
472
493
  program = <<-LOX_END
473
- class Duck {
474
- quack() {
475
- print "quack";
476
- }
477
- }
478
- var daffy = Duck();
494
+ #{duck_class}
495
+
496
+ var daffy = Duck(); // Default constructor
479
497
  print daffy;
480
498
  LOX_END
481
499
  expect { subject.evaluate(program) }.not_to raise_error
482
500
  expect(sample_cfg[:ostream].string).to eq('Duck instance')
483
501
  end
484
502
 
485
- it 'should print the hello world message' do
503
+ it 'should support calls to method' do
486
504
  program = <<-LOX_END
487
- var greeting = "Hello"; // Declaring a variable
488
- print greeting + ", " + "world!"; // ... Playing with concatenation
505
+ #{duck_class}
506
+
507
+ var daffy = Duck(); // Default constructor
508
+ daffy.quack();
489
509
  LOX_END
490
510
  expect { subject.evaluate(program) }.not_to raise_error
491
- expect(sample_cfg[:ostream].string).to eq('Hello, world!')
511
+ expect(sample_cfg[:ostream].string).to eq('quack')
512
+ end
513
+
514
+ it "should support the 'this' keyword" do
515
+ program = <<-LOX_END
516
+ class Egotist {
517
+ speak() {
518
+ print this;
519
+ }
520
+ }
521
+
522
+ var method = Egotist().speak;
523
+ method(); // Output: Egotist instance
524
+ LOX_END
525
+ expect { subject.evaluate(program) }.not_to raise_error
526
+ expect(sample_cfg[:ostream].string).to eq('Egotist instance')
527
+ end
528
+
529
+ it 'should support a closure nested in a method' do
530
+ lox_snippet = <<-LOX_END
531
+ class Foo {
532
+ getClosure() {
533
+ fun closure() {
534
+ return this.toString();
535
+ }
536
+ return closure;
537
+ }
538
+
539
+ toString() { return "foo"; }
540
+ }
541
+
542
+ var closure = Foo().getClosure();
543
+ closure;
544
+ LOX_END
545
+ # Expected result: Backend::LoxFunction('closure')
546
+ # Expected function's closure (environment layout):
547
+ # Environment('global')
548
+ # defns
549
+ # +- ['clock'] => BackEnd::Engine::NativeFunction
550
+ # *- ['Foo'] => BackEnd::LoxClass
551
+ # Environment
552
+ # defns
553
+ # ['this'] => BackEnd::LoxInstance
554
+ # Environment
555
+ # defns
556
+ # +- ['closure'] => Backend::LoxFunction
557
+ result = subject.evaluate(lox_snippet)
558
+ expect(result).to be_kind_of(BackEnd::LoxFunction)
559
+ expect(result.name).to eq('closure')
560
+ closure = result.closure
561
+ expect(closure).to be_kind_of(Loxxy::BackEnd::Environment)
562
+ expect(closure.defns['closure'].value).to eq(result)
563
+ expect(closure.enclosing).to be_kind_of(Loxxy::BackEnd::Environment)
564
+ expect(closure.enclosing.defns['this'].value).to be_kind_of(Loxxy::BackEnd::LoxInstance)
565
+ global_env = closure.enclosing.enclosing
566
+ expect(global_env).to be_kind_of(Loxxy::BackEnd::Environment)
567
+ expect(global_env.defns['clock'].value).to be_kind_of(BackEnd::Engine::NativeFunction)
568
+ expect(global_env.defns['Foo'].value).to be_kind_of(BackEnd::LoxClass)
569
+ end
570
+
571
+ it 'should support custom initializer' do
572
+ lox_snippet = <<-LOX_END
573
+ // From section 3.9.5
574
+ class Breakfast {
575
+ init(meat, bread) {
576
+ this.meat = meat;
577
+ this.bread = bread;
578
+ }
579
+
580
+ serve(who) {
581
+ print "Enjoy your " + this.meat + " and " +
582
+ this.bread + ", " + who + ".";
583
+ }
584
+ }
585
+
586
+ var baconAndToast = Breakfast("bacon", "toast");
587
+ baconAndToast.serve("Dear Reader");
588
+ // Output: "Enjoy your bacon and toast, Dear Reader."
589
+ LOX_END
590
+ expect { subject.evaluate(lox_snippet) }.not_to raise_error
591
+ predicted = 'Enjoy your bacon and toast, Dear Reader.'
592
+ expect(sample_cfg[:ostream].string).to eq(predicted)
492
593
  end
493
594
  end # context
494
595
  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.12
4
+ version: 0.1.17
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-03 00:00:00.000000000 Z
11
+ date: 2021-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -97,6 +97,7 @@ files:
97
97
  - lib/loxxy/ast/lox_compound_expr.rb
98
98
  - lib/loxxy/ast/lox_for_stmt.rb
99
99
  - lib/loxxy/ast/lox_fun_stmt.rb
100
+ - lib/loxxy/ast/lox_get_expr.rb
100
101
  - lib/loxxy/ast/lox_grouping_expr.rb
101
102
  - lib/loxxy/ast/lox_if_stmt.rb
102
103
  - lib/loxxy/ast/lox_literal_expr.rb
@@ -106,6 +107,8 @@ files:
106
107
  - lib/loxxy/ast/lox_print_stmt.rb
107
108
  - lib/loxxy/ast/lox_return_stmt.rb
108
109
  - lib/loxxy/ast/lox_seq_decl.rb
110
+ - lib/loxxy/ast/lox_set_expr.rb
111
+ - lib/loxxy/ast/lox_this_expr.rb
109
112
  - lib/loxxy/ast/lox_unary_expr.rb
110
113
  - lib/loxxy/ast/lox_var_stmt.rb
111
114
  - lib/loxxy/ast/lox_variable_expr.rb