loxxy 0.1.13 → 0.2.00

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