loxxy 0.1.15 → 0.2.02

Sign up to get free protection for your applications and to get access to all the features.
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)