loxxy 0.1.11 → 0.1.16

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: aee9fb6c5101bc39682ab401ffc0434aea9e6c4115f74a4f641d0df2d68a03ad
4
- data.tar.gz: ccc11c428ef206db9129fabbb97526df3477e03c5dce0b1b5a2f758425c8f9cb
3
+ metadata.gz: 60e57ef9264b4239571378ddcbefe044ff63192534ce94fc99b9a3b1c1813f02
4
+ data.tar.gz: 94c26f57455d0e267354f68655e86288dd00f5c322df3e4d1b6f5478cad0eb17
5
5
  SHA512:
6
- metadata.gz: dbb1a28eb85868636b304aa3ec3c2fb71f3f37f5c573740bd7d577ed4e88374ce1fea5446e39429168228e68bedbddfccd554bc6c88ec7996621956e11a42a3c
7
- data.tar.gz: 3b4c1d6b0996a351cfd7dae5d8f80683fcbdd1eeecc17c3a7932b0675285700dfecdabecc31f505243ffaf2f1bbc1bd8babb604c154f53dad0019c4ad96b007a
6
+ metadata.gz: 8e5d6f864c1a340aecc7a362d8f2c311dc553869fc72b40d287736df2b62715eb208b2b299f1106dba5645dc4bb859f1d96fbfa79441c83c0e5faa223dd6642d
7
+ data.tar.gz: 452f9815e8812d9313c228697410a4c7433ec12c58269da305999f1de2822c89ad0b9a0f4e72865c333ddbee725b0ff102b4bc14ab4c51dcf7dba2784a08074a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,79 @@
1
+ ## [0.1.16] - 2021-04-10
2
+ - Fixed an issue in name lookup. All the `this` test suite is passing.
3
+
4
+ ### Changed
5
+ - Method `BackEnd::Engine#after_var_stmt` now it creates the variable and pouts it in the symbol table
6
+
7
+ ### Removed
8
+ - Method `BackEnd::Engine#before_var_stmt` it generated bug when assigning a value to a var, when that var name occurred elsewhere
9
+
10
+ ## [0.1.15] - 2021-04-08
11
+ - Fixed the `dangling else`by tweaking the grammar rules
12
+
13
+ ### Changed
14
+ - Method `Ast::ASTBuilder#reduce_if__else_stmt` parse action specific for if with else branch
15
+
16
+ ### Fixed
17
+ - File `grammar.rb` changed rules to cope with `dangling else` issue
18
+
19
+ ### Changed
20
+ - Method `Ast::ASTBuilder#reduce_if_stmt` parse action for if without else branch
21
+ - File `README.md` removed the section about the `dangling else` issue.
22
+
23
+
24
+ ## [0.1.14] - 2021-04-05
25
+ - `Loxxy` now implements the 'this' keyword
26
+
27
+ ### New
28
+ - Class `Ast::LoxThisExpr` a syntax node that represents an occurrence of the `this` keyword
29
+ - Method `Ast::ASTBuilder#reduce_this_expr` parse action for this keyword
30
+ - Method `Ast::Visitor#visit_this_expr` visit of an `Ast::LoxThisExpr` node
31
+ - Method `BackEnd::Engine#after_this_expr` runtime action for this keyword (i.e. refers to current instance)
32
+ - Method `BackEnd::LoxFunction#bind` implementation of bound method
33
+
34
+ ### Changed
35
+ - Class `BackEnd::Resolver` implementing semantic actions for `this` keyword
36
+ - File `grammar.rb` added name to a syntax rule
37
+
38
+ ## [0.1.13] - 2021-04-05
39
+ - `Loxxy` now implements method calls
40
+
41
+ ### New
42
+ - Class `Ast::LoxGetExpr` a syntax node that represents a read access to an object property
43
+ - Class `Ast::LoxSetExpr` a syntax node that represents a write access to an object property
44
+ - Method `Ast::ASTBuilder#reduce_set_expr` parse action for write access to an object property
45
+ - Method `Ast::ASTBuilder#reduce_get_expr` parse action for read access to an object property
46
+ - Method `Ast::Visitor#visit_set_expr` visit of an `Ast::LoxSetExpr` node
47
+ - Method `Ast::Visitor#visit_get_expr` visit of an `Ast::LoxGetExpr` node
48
+ - Method `BackEnd::Engine#after_set_expr` runtime action for property setting
49
+ - Method `BackEnd::Engine#after_get_expr` runtime action for property getting
50
+ - Method `BackEnd::LoxInstance#set` implementation of write accessor
51
+ - Method `BackEnd::LoxInstance#get` implementation of read accessor
52
+ - Method `BackEnd::Resolver#after_set_expr` resolve action for property setting
53
+ - Method `BackEnd::Resolver#after_get_expr` resolve action for property getting
54
+
55
+ ### Changed
56
+ - Method `Ast::ASTBuilder#reduce_assign_expr` expanded to support write access to an object property
57
+ - Class `LoxClassStmt`: methods are now aggregate under the `body` attribute
58
+ - Class `LoxFunStmt`: has a new attribute `is_method` and inherits from `Ast::LoxNode`
59
+ - Method `BackEnd::Engine#after_class_stmt` methods are aggregated into the classes
60
+ - Method `BackEnd::Engine#after_fun_stmt` extension for method
61
+ - File `grammar.rb` added names to two syntax rules
62
+
63
+
64
+ ## [0.1.12] - 2021-04-03
65
+ - Intermediate version: `Loxxy` does instance creation (default constructor)
66
+
67
+ ### New
68
+ - Method `BackEnd::LoxClass#call` made class callable (for invoking the constructor)
69
+ - Class `BackEnd::LoxInstance` runtime representation of a Lox instance (object).
70
+
71
+ ### Changed
72
+ - Method `BackEnd::Engine#after_call_expr` Added Lox class as callable thing.
73
+
74
+ ### Fixed
75
+ - Method `Ast::ASTBuilder#reduce_class_body` couldn't handle properly empty classes
76
+
1
77
  ## [0.1.11] - 2021-04-03
2
78
  - Intermediate version: `Loxxy` does class declarations
3
79
 
@@ -13,9 +89,9 @@
13
89
  - Method `BackEnd::Engine#after_class_stmt`
14
90
  - Method `BackEnd::Resolver#after_class_stmt`
15
91
  - Method `BackEnd::Resolver#before_class_stmt`
16
- - Class `BackEnd::LoxClass` implementation of a Lox class.
92
+ - Class `BackEnd::LoxClass` runtime representation of a Lox class.
17
93
 
18
- ### CHANGED
94
+ ### Changed
19
95
  - File `grammar.rb` refactoring of class declaration syntax rules
20
96
 
21
97
  ## [0.1.10] - 2021-03-31
@@ -28,7 +104,7 @@
28
104
  ## [0.1.09] - 2021-03-28
29
105
  - Fix and test suite for return statements
30
106
 
31
- ### Changed
107
+ ### CHANGED
32
108
  - `Loxxy` reports an error when a return statement occurs in top-level scope
33
109
 
34
110
  ### Fixed
@@ -40,7 +116,7 @@
40
116
  ### New
41
117
  - Class `BackEnd::Resolver` implements the variable resolution (whenever a variable is in use, locate the declaration of that variable)
42
118
 
43
- ### Changed
119
+ ### CHANGED
44
120
  - Class `Ast::Visitor` changes in some method signatures
45
121
  - Class `BackEnd::Engine` new attribute `resolver` that points to a `BackEnd::Resolver` instance
46
122
  - Class `BackEnd::Engine` several methods dealing with variables have been adapted to take the resolver into account.
@@ -132,7 +208,7 @@
132
208
  - Method `BackEnd::Engine#after_fun_stmt`
133
209
  - Method `BackEnd::NativeFunction#call`
134
210
  - Method `BackEnd::NativeFunction#to_str`
135
- - Method `BackEnd::LoxFunction` implementation of a function object.
211
+ - Method `BackEnd::LoxFunction` runtime representation of a Lox function.
136
212
 
137
213
  ### Changed
138
214
  - Method `BackEnd::Engine#after_call_expr`
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'
@@ -175,7 +175,7 @@ module Loxxy
175
175
 
176
176
  # rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE')
177
177
  def reduce_class_body(_production, _range, _tokens, theChildren)
178
- theChildren[1]
178
+ theChildren[1].nil? ? [] : theChildren[1]
179
179
  end
180
180
 
181
181
  # rule('method_plus' => 'method_plus function')
@@ -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
289
+ end
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])
274
294
  end
275
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,25 @@ 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
+ stack.pop
78
+ end
79
+
80
+ klass = LoxClass.new(aClassStmt.name, meths, self)
74
81
  new_var = Variable.new(aClassStmt.name, klass)
75
82
  symbol_table.insert(new_var)
76
83
  end
77
84
 
78
- def before_var_stmt(aVarStmt)
85
+ def after_var_stmt(aVarStmt)
79
86
  new_var = Variable.new(aVarStmt.name, Datatype::Nil.instance)
80
87
  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
88
 
88
89
  value = stack.pop
89
- variable.assign(value)
90
+ new_var.assign(value)
90
91
  end
91
92
 
92
93
  def before_for_stmt(aForStmt)
@@ -153,6 +154,18 @@ module Loxxy
153
154
  variable.assign(value)
154
155
  end
155
156
 
157
+ def after_set_expr(aSetExpr, aVisitor)
158
+ value = stack.pop
159
+ # Evaluate object part
160
+ aSetExpr.object.accept(aVisitor)
161
+ assignee = stack.pop
162
+ unless assignee.kind_of?(LoxInstance)
163
+ raise StandardError, 'Only instances have fields.'
164
+ end
165
+
166
+ assignee.set(aSetExpr.property, value)
167
+ end
168
+
156
169
  def after_logical_expr(aLogicalExpr, visitor)
157
170
  op = aLogicalExpr.operator
158
171
  operand1 = stack.pop # only first operand was evaluated
@@ -228,11 +241,23 @@ module Loxxy
228
241
  raise Loxxy::RuntimeError, msg
229
242
  end
230
243
  callee.call(self, aVisitor)
244
+ when LoxClass
245
+ callee.call(self, aVisitor)
231
246
  else
232
247
  raise Loxxy::RuntimeError, 'Can only call functions and classes.'
233
248
  end
234
249
  end
235
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
+
236
261
  def after_grouping_expr(_groupingExpr)
237
262
  # Do nothing: work was already done by visiting /evaluating the subexpression
238
263
  end
@@ -240,7 +265,7 @@ module Loxxy
240
265
  def after_variable_expr(aVarExpr, aVisitor)
241
266
  var_name = aVarExpr.name
242
267
  var = variable_lookup(aVarExpr)
243
- raise StandardError, "Unknown variable #{var_name}" unless var
268
+ raise StandardError, "Undefined variable '#{var_name}'." unless var
244
269
 
245
270
  var.value.accept(aVisitor) # Evaluate variable value then push on stack
246
271
  end
@@ -250,6 +275,11 @@ module Loxxy
250
275
  stack.push(literalExpr.literal)
251
276
  end
252
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
+
253
283
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
254
284
  def before_visit_builtin(aValue)
255
285
  stack.push(aValue)
@@ -257,8 +287,12 @@ module Loxxy
257
287
 
258
288
  def after_fun_stmt(aFunStmt, _visitor)
259
289
  function = LoxFunction.new(aFunStmt.name, aFunStmt.params, aFunStmt.body, self)
260
- new_var = Variable.new(aFunStmt.name, function)
261
- 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
262
296
  end
263
297
 
264
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
@@ -1,23 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../datatype/all_datatypes'
4
+ require_relative 'lox_instance'
4
5
 
5
6
  module Loxxy
6
7
  module BackEnd
7
- # Representation of a Lox class.
8
+ # Runtime representation of a Lox class.
8
9
  class LoxClass
9
10
  # @return [String] The name of the class
10
11
  attr_reader :name
11
12
 
12
- # @return [Array<>] the list of methods
13
- attr_reader :methods
13
+ # @return [Hash{String => LoxFunction}] the list of methods
14
+ attr_reader :meths
14
15
  attr_reader :stack
15
16
 
16
17
  # Create a class with given name
17
18
  # @param aName [String] The name of the class
18
19
  def initialize(aName, theMethods, anEngine)
19
20
  @name = aName.dup
20
- @methods = theMethods
21
+ @meths = {}
22
+ theMethods.each do |func|
23
+ meths[func.name] = func
24
+ end
21
25
  @stack = anEngine.stack
22
26
  end
23
27
 
@@ -25,6 +29,20 @@ module Loxxy
25
29
  stack.push self
26
30
  end
27
31
 
32
+ def arity
33
+ 0
34
+ end
35
+
36
+ def call(engine, _visitor)
37
+ instance = LoxInstance.new(self, engine)
38
+ engine.stack.push(instance)
39
+ end
40
+
41
+ # @param aName [String] the method name to search for
42
+ def find_method(aName)
43
+ meths[aName]
44
+ end
45
+
28
46
  # Logical negation.
29
47
  # As a function is a truthy thing, its negation is thus false.
30
48
  # @return [Datatype::False]
@@ -32,7 +50,7 @@ module Loxxy
32
50
  Datatype::False.instance
33
51
  end
34
52
 
35
- # Text representation of a Lox function
53
+ # Text representation of a Lox class
36
54
  def to_str
37
55
  name
38
56
  end
@@ -36,7 +36,6 @@ module Loxxy
36
36
  end
37
37
 
38
38
  def call(engine, aVisitor)
39
- # new_env = Environment.new(engine.symbol_table.current_env)
40
39
  new_env = Environment.new(closure)
41
40
  engine.symbol_table.enter_environment(new_env)
42
41
 
@@ -53,6 +52,16 @@ module Loxxy
53
52
  engine.symbol_table.leave_environment
54
53
  end
55
54
 
55
+ def bind(anInstance)
56
+ new_env = Environment.new(closure)
57
+ this = Variable.new('this', anInstance)
58
+ new_env.insert(this)
59
+ bound_method = dup
60
+ bound_method.instance_variable_set(:@closure, new_env)
61
+
62
+ bound_method
63
+ end
64
+
56
65
  # Logical negation.
57
66
  # As a function is a truthy thing, its negation is thus false.
58
67
  # @return [Datatype::False]
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../datatype/all_datatypes'
4
+
5
+ module Loxxy
6
+ module BackEnd
7
+ # Runtime representation of a Lox object (instance).
8
+ class LoxInstance
9
+ # @return BackEnd::LoxClass] the class that this object is an instance of
10
+ attr_reader :klass
11
+
12
+ attr_reader :engine
13
+
14
+ # @return [Hash{String => BuiltinDatatype | LoxFunction | LoxInstance }]
15
+ attr_reader :fields
16
+
17
+ # Create an instance from given class
18
+ # @param aClass [BackEnd::LoxClass] the class this this object belong
19
+ def initialize(aClass, anEngine)
20
+ @klass = aClass
21
+ @engine = anEngine
22
+ @fields = {}
23
+ end
24
+
25
+ def accept(_visitor)
26
+ engine.stack.push self
27
+ end
28
+
29
+ # Text representation of a Lox instance
30
+ def to_str
31
+ "#{klass.to_str} instance"
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
+ unless fields.include? aName
51
+ raise StandardError, "Undefined property '#{aName}'."
52
+ end
53
+
54
+ fields[aName] = aValue
55
+ end
56
+ end # class
57
+ end # module
58
+ 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,17 @@ 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
+ resolve_function(fun_stmt, :method, aVisitor)
74
+ end
75
+ end_scope
76
+ @current_class = previous_class
63
77
  end
64
78
 
65
79
  def before_for_stmt(aForStmt)
@@ -109,6 +123,11 @@ module Loxxy
109
123
  resolve_local(anAssignExpr, aVisitor)
110
124
  end
111
125
 
126
+ def after_set_expr(aSetExpr, aVisitor)
127
+ # Evaluate object part
128
+ aSetExpr.object.accept(aVisitor)
129
+ end
130
+
112
131
  # Variable expressions require their variables resolved
113
132
  def before_variable_expr(aVarExpr)
114
133
  var_name = aVarExpr.name
@@ -127,6 +146,23 @@ module Loxxy
127
146
  aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) }
128
147
  end
129
148
 
149
+ def after_get_expr(aGetExpr, aVisitor)
150
+ # Evaluate object part
151
+ aGetExpr.object.accept(aVisitor)
152
+ end
153
+
154
+ def before_this_expr(_thisExpr)
155
+ if current_class == :none
156
+ msg = "Error at 'this': Can't use 'this' outside of a class."
157
+ raise StandardError, msg
158
+ end
159
+ end
160
+
161
+ def after_this_expr(aThisExpr, aVisitor)
162
+ # 'this' behaves closely to a local variable
163
+ resolve_local(aThisExpr, aVisitor)
164
+ end
165
+
130
166
  # function declaration creates a new scope for its body & binds its parameters for that scope
131
167
  def before_fun_stmt(aFunStmt, aVisitor)
132
168
  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.11'
4
+ VERSION = '0.1.16'
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,30 +452,120 @@ 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 print the hello world message' do
492
+ it 'should support default instance creation' do
472
493
  program = <<-LOX_END
473
- var greeting = "Hello"; // Declaring a variable
474
- print greeting + ", " + "world!"; // ... Playing with concatenation
494
+ #{duck_class}
495
+
496
+ var daffy = Duck(); // Default constructor
497
+ print daffy;
475
498
  LOX_END
476
499
  expect { subject.evaluate(program) }.not_to raise_error
477
- expect(sample_cfg[:ostream].string).to eq('Hello, world!')
500
+ expect(sample_cfg[:ostream].string).to eq('Duck instance')
501
+ end
502
+
503
+ it 'should support calls to method' do
504
+ program = <<-LOX_END
505
+ #{duck_class}
506
+
507
+ var daffy = Duck(); // Default constructor
508
+ daffy.quack();
509
+ LOX_END
510
+ expect { subject.evaluate(program) }.not_to raise_error
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)
478
569
  end
479
570
  end # context
480
571
  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.11
4
+ version: 0.1.16
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-10 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
@@ -116,6 +119,7 @@ files:
116
119
  - lib/loxxy/back_end/environment.rb
117
120
  - lib/loxxy/back_end/lox_class.rb
118
121
  - lib/loxxy/back_end/lox_function.rb
122
+ - lib/loxxy/back_end/lox_instance.rb
119
123
  - lib/loxxy/back_end/resolver.rb
120
124
  - lib/loxxy/back_end/symbol_table.rb
121
125
  - lib/loxxy/back_end/unary_operator.rb