loxxy 0.1.15 → 0.2.02

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.
data/bin/loxxy CHANGED
@@ -3,9 +3,57 @@
3
3
 
4
4
  require 'loxxy'
5
5
 
6
- if ARGV[0]
7
- lox = Loxxy::Interpreter.new
8
- File.open(ARGV[0], 'r') do |f|
9
- lox.evaluate(f.read)
10
- end
11
- end
6
+ class LoxxyRunner
7
+ DefaultLoxExtension = 'lox'
8
+ attr_reader(:cli_options)
9
+
10
+ def initialize(prog_name, args)
11
+ my_version = Loxxy::VERSION
12
+ cli = Loxxy::CLIParser.new(prog_name, my_version)
13
+ @cli_options = cli.parse!(args)
14
+ end
15
+
16
+ def run!(file_names)
17
+ return if file_names.nil? || file_names.empty?
18
+
19
+ lox = Loxxy::Interpreter.new
20
+ begin
21
+ file_names.each do |lox_file|
22
+ fname = validate_filename(lox_file)
23
+ next unless file_exist?(fname)
24
+
25
+ File.open(fname, 'r') { |f| lox.evaluate(f.read) }
26
+ end
27
+ rescue Loxxy::ScanError => e
28
+ $stderr.puts e.message
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def validate_filename(raw_fname)
35
+ # When necessary add extension to file name
36
+ fname = raw_fname.dup
37
+ basename = File.basename(fname)
38
+ has_extension = basename =~ /(?<=[^.])\.[^.]+$/
39
+ fname << '.' << DefaultLoxExtension unless has_extension
40
+
41
+ fname
42
+ end
43
+
44
+ def file_exist?(fname)
45
+ exists = File.exist?(fname)
46
+ $stderr.puts "No such file '#{fname}'" unless exists
47
+
48
+ exists
49
+ end
50
+ end # class
51
+
52
+ ########################################
53
+ # ENTRY POINT
54
+ ########################################
55
+ program = LoxxyRunner.new(File.basename(__FILE__), ARGV)
56
+
57
+ # All options from CLI gobbled from ARGV, remains only file name
58
+ program.run!(ARGV)
59
+ # End of file
data/lib/loxxy.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'loxxy/version'
4
+ require_relative 'loxxy/cli_parser'
4
5
  require_relative 'loxxy/interpreter'
5
6
  require_relative 'loxxy/front_end/raw_parser'
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'lox_fun_stmt'
4
+ require_relative 'lox_super_expr'
4
5
  require_relative 'lox_this_expr'
5
6
  require_relative 'lox_variable_expr'
6
7
  require_relative 'lox_literal_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]
@@ -368,6 +382,11 @@ module Loxxy
368
382
  LoxThisExpr.new(tokens[0].position)
369
383
  end
370
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
+
371
390
  # rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
372
391
  def reduce_function(_production, _range, _tokens, theChildren)
373
392
  first_child = theChildren.first
@@ -212,6 +212,13 @@ module Loxxy
212
212
  broadcast(:after_this_expr, aThisExpr, self)
213
213
  end
214
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
+
215
222
  # Visit event. The visitor is about to visit the given terminal datatype value.
216
223
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
217
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
@@ -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
@@ -286,6 +300,20 @@ module Loxxy
286
300
  var.value.accept(aVisitor) # Evaluate this value then push on stack
287
301
  end
288
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
+
289
317
  # @param aValue [Ast::BuiltinDattype] the built-in datatype value
290
318
  def before_visit_builtin(aValue)
291
319
  stack.push(aValue)
@@ -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,6 +50,10 @@ 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
@@ -47,10 +47,6 @@ module Loxxy
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
@@ -67,12 +67,24 @@ module Loxxy
67
67
  previous_class = current_class
68
68
  @current_class = :class
69
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
70
80
  begin_scope
71
81
  define('this')
72
82
  aClassStmt.body.each do |fun_stmt|
73
- resolve_function(fun_stmt, :method, aVisitor)
83
+ mth_type = fun_stmt.name == 'init' ? :initializer : :method
84
+ resolve_function(fun_stmt, mth_type, aVisitor)
74
85
  end
75
86
  end_scope
87
+ end_scope if aClassStmt.superclass
76
88
  @current_class = previous_class
77
89
  end
78
90
 
@@ -92,7 +104,7 @@ module Loxxy
92
104
  anIfStmt.else_stmt&.accept(aVisitor)
93
105
  end
94
106
 
95
- def before_return_stmt(_returnStmt)
107
+ def before_return_stmt(returnStmt)
96
108
  if scopes.size < 2
97
109
  msg = "Error at 'return': Can't return from top-level code."
98
110
  raise StandardError, msg
@@ -102,6 +114,11 @@ module Loxxy
102
114
  msg = "Error at 'return': Can't return from outside a function."
103
115
  raise StandardError, msg
104
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
105
122
  end
106
123
 
107
124
  def after_while_stmt(aWhileStmt, aVisitor)
@@ -163,6 +180,25 @@ module Loxxy
163
180
  resolve_local(aThisExpr, aVisitor)
164
181
  end
165
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
+
166
202
  # function declaration creates a new scope for its body & binds its parameters for that scope
167
203
  def before_fun_stmt(aFunStmt, aVisitor)
168
204
  declare(aFunStmt.name)