loxxy 0.2.04 → 0.3.02

Sign up to get free protection for your applications and to get access to all the features.
@@ -25,11 +25,15 @@ module Loxxy
25
25
  # @return [Hash{String => Variable}] Pairs of the kind
26
26
  attr_reader :defns
27
27
 
28
+ # @return [Array<LoxNode>] stack of values needed in evaluating an expression
29
+ attr_reader :expr_stack
30
+
28
31
  # Construct a environment instance.
29
32
  # @param aParent [Environment, NilClass] Parent environment to this one.
30
33
  def initialize(aParent = nil)
31
34
  @enclosing = aParent
32
35
  @defns = {}
36
+ @expr_stack = []
33
37
  end
34
38
 
35
39
  # Add a new variable to the environment.
@@ -7,17 +7,15 @@ module Loxxy
7
7
  module BackEnd
8
8
  # Runtime representation of a Lox class.
9
9
  class LoxClass
10
- # rubocop: disable Style/AccessorGrouping
11
-
12
10
  # @return [String] The name of the class
13
11
  attr_reader :name
14
12
  attr_reader :superclass
15
13
 
16
14
  # @return [Hash{String => LoxFunction}] the list of methods
17
15
  attr_reader :meths
18
- attr_reader :stack
19
16
 
20
- # rubocop: enable Style/AccessorGrouping
17
+ # @return [Loxxy::BackEnd::Engine]
18
+ attr_reader :engine
21
19
 
22
20
  # Create a class with given name
23
21
  # @param aName [String] The name of the class
@@ -28,11 +26,11 @@ module Loxxy
28
26
  theMethods.each do |func|
29
27
  meths[func.name] = func
30
28
  end
31
- @stack = anEngine.stack
29
+ @engine = anEngine
32
30
  end
33
31
 
34
32
  def accept(_visitor)
35
- stack.push self
33
+ engine.expr_stack.push self
36
34
  end
37
35
 
38
36
  def arity
@@ -46,9 +44,9 @@ module Loxxy
46
44
  if initializer
47
45
  constructor = initializer.bind(instance)
48
46
  constructor.call(engine, visitor)
47
+ else
48
+ engine.expr_stack.push(instance)
49
49
  end
50
-
51
- engine.stack.push(instance)
52
50
  end
53
51
 
54
52
  # @param aName [String] the method name to search for
@@ -13,7 +13,7 @@ module Loxxy
13
13
  # @return [Array<>] the parameters
14
14
  attr_reader :parameters
15
15
  attr_reader :body
16
- attr_reader :stack
16
+ attr_reader :engine
17
17
  attr_reader :closure
18
18
  attr_accessor :is_initializer
19
19
 
@@ -23,7 +23,7 @@ module Loxxy
23
23
  @name = aName.dup
24
24
  @parameters = parameterList
25
25
  @body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
26
- @stack = anEngine.stack
26
+ @engine = anEngine
27
27
  @closure = anEngine.symbol_table.current_env
28
28
  @is_initializer = false
29
29
  anEngine.symbol_table.current_env.embedding = true
@@ -34,28 +34,34 @@ module Loxxy
34
34
  end
35
35
 
36
36
  def accept(_visitor)
37
- stack.push self
37
+ engine.expr_stack.push self
38
38
  end
39
39
 
40
- def call(engine, aVisitor)
40
+ def call(_engine, aVisitor)
41
41
  new_env = Environment.new(closure)
42
42
  engine.symbol_table.enter_environment(new_env)
43
43
 
44
44
  parameters&.each do |param_name|
45
- local = Variable.new(param_name, stack.pop)
45
+ local = Variable.new(param_name, engine.stack.pop)
46
46
  engine.symbol_table.insert(local)
47
47
  end
48
48
 
49
49
  catch(:return) do
50
- (body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
51
- throw(:return)
50
+ body.accept(aVisitor) unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
51
+ # implicit return at end of function...
52
+ engine.stack.push(Datatype::Nil.instance) unless is_initializer
52
53
  end
54
+ # Compensate for deeply nested return
55
+ engine.symbol_table.leave_environment while engine.current_env != new_env
56
+
53
57
  if is_initializer
54
58
  enclosing_env = engine.symbol_table.current_env.enclosing
55
59
  engine.stack.push(enclosing_env.defns['this'].value)
56
60
  end
57
61
 
58
62
  engine.symbol_table.leave_environment
63
+ # engine.expr_stack.clear
64
+ engine.expr_stack.push(engine.stack.pop) unless engine.stack.empty?
59
65
  end
60
66
 
61
67
  def bind(anInstance)
@@ -23,7 +23,7 @@ module Loxxy
23
23
  end
24
24
 
25
25
  def accept(_visitor)
26
- engine.stack.push self
26
+ engine.expr_stack.push self
27
27
  end
28
28
 
29
29
  # Text representation of a Lox instance
@@ -69,7 +69,7 @@ module Loxxy
69
69
  define(aClassStmt.name)
70
70
  if aClassStmt.superclass
71
71
  if aClassStmt.name == aClassStmt.superclass.name
72
- raise StandardError, "'A class can't inherit from itself."
72
+ raise Loxxy::RuntimeError, "'A class can't inherit from itself."
73
73
  end
74
74
 
75
75
  @current_class = :subclass
@@ -88,17 +88,6 @@ module Loxxy
88
88
  @current_class = previous_class
89
89
  end
90
90
 
91
- def before_for_stmt(aForStmt)
92
- before_block_stmt(aForStmt)
93
- end
94
-
95
- def after_for_stmt(aForStmt, aVisitor)
96
- aForStmt.test_expr&.accept(aVisitor)
97
- aForStmt.body_stmt.accept(aVisitor)
98
- aForStmt.update_expr&.accept(aVisitor)
99
- after_block_stmt(aForStmt)
100
- end
101
-
102
91
  def after_if_stmt(anIfStmt, aVisitor)
103
92
  anIfStmt.then_stmt.accept(aVisitor)
104
93
  anIfStmt.else_stmt&.accept(aVisitor)
@@ -107,27 +96,30 @@ module Loxxy
107
96
  def before_return_stmt(returnStmt)
108
97
  if scopes.size < 2
109
98
  msg = "Error at 'return': Can't return from top-level code."
110
- raise StandardError, msg
99
+ raise Loxxy::RuntimeError, msg
111
100
  end
112
101
 
113
102
  if current_function == :none
114
103
  msg = "Error at 'return': Can't return from outside a function."
115
- raise StandardError, msg
104
+ raise Loxxy::RuntimeError, msg
116
105
  end
117
106
 
118
107
  if current_function == :initializer
119
108
  msg = "Error at 'return': Can't return a value from an initializer."
120
- raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
109
+ raise Loxxy::RuntimeError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
121
110
  end
122
111
  end
123
112
 
124
113
  def after_while_stmt(aWhileStmt, aVisitor)
125
114
  aWhileStmt.body.accept(aVisitor)
126
- aWhileStmt.condition.accept(aVisitor)
115
+ aWhileStmt.condition&.accept(aVisitor)
127
116
  end
128
117
 
129
118
  # A variable declaration adds a new variable to current scope
130
119
  def before_var_stmt(aVarStmt)
120
+ # Oddly enough, Lox allows the re-definition of a variable at top-level scope
121
+ return if scopes.size == 1 && scopes.last[aVarStmt.name]
122
+
131
123
  declare(aVarStmt.name)
132
124
  end
133
125
 
@@ -141,6 +133,7 @@ module Loxxy
141
133
  end
142
134
 
143
135
  def after_set_expr(aSetExpr, aVisitor)
136
+ aSetExpr.value.accept(aVisitor)
144
137
  # Evaluate object part
145
138
  aSetExpr.object.accept(aVisitor)
146
139
  end
@@ -149,7 +142,7 @@ module Loxxy
149
142
  def before_variable_expr(aVarExpr)
150
143
  var_name = aVarExpr.name
151
144
  if !scopes.empty? && (scopes.last[var_name] == false)
152
- raise StandardError, "Can't read variable #{var_name} in its own initializer"
145
+ raise Loxxy::RuntimeError, "Can't read variable #{var_name} in its own initializer"
153
146
  end
154
147
  end
155
148
 
@@ -171,7 +164,7 @@ module Loxxy
171
164
  def before_this_expr(_thisExpr)
172
165
  if current_class == :none
173
166
  msg = "Error at 'this': Can't use 'this' outside of a class."
174
- raise StandardError, msg
167
+ raise Loxxy::RuntimeError, msg
175
168
  end
176
169
  end
177
170
 
@@ -186,11 +179,11 @@ module Loxxy
186
179
  msg_prefix = "Error at 'super': Can't use 'super' "
187
180
  if current_class == :none
188
181
  err_msg = msg_prefix + 'outside of a class.'
189
- raise StandardError, err_msg
182
+ raise Loxxy::RuntimeError, err_msg
190
183
 
191
184
  elsif current_class == :class
192
185
  err_msg = msg_prefix + 'in a class without superclass.'
193
- raise StandardError, err_msg
186
+ raise Loxxy::RuntimeError, err_msg
194
187
 
195
188
  end
196
189
  # 'super' behaves closely to a local variable
@@ -220,9 +213,13 @@ module Loxxy
220
213
  return if scopes.empty?
221
214
 
222
215
  curr_scope = scopes.last
216
+ # Oddly enough, Lox allows variable re-declaration at top-level
223
217
  if curr_scope.include?(aVarName)
224
218
  msg = "Error at '#{aVarName}': Already variable with this name in this scope."
225
- raise StandardError, msg
219
+ raise Loxxy::RuntimeError, msg
220
+ elsif curr_scope.size == 255 && current_function != :none
221
+ msg = "Error at '#{aVarName}': Too many local variables in function."
222
+ raise Loxxy::RuntimeError, msg
226
223
  end
227
224
 
228
225
  # The initializer is not yet processed.
@@ -7,6 +7,10 @@ module Loxxy
7
7
  module Datatype
8
8
  # Class for representing a Lox numeric value.
9
9
  class Number < BuiltinDatatype
10
+ def zero?
11
+ value.zero?
12
+ end
13
+
10
14
  # Perform the addition of two Lox numbers or
11
15
  # one Lox number and a Ruby Numeric
12
16
  # @param other [Loxxy::Datatype::Number, Numeric]
@@ -59,17 +63,28 @@ module Loxxy
59
63
  # one Lox number and a Ruby Numeric
60
64
  # @param other [Loxxy::Datatype::Number, Numeric]
61
65
  # @return [Loxxy::Datatype::Number]
66
+ # rubocop: disable Lint/BinaryOperatorWithIdenticalOperands
62
67
  def /(other)
63
68
  case other
64
- when Number
65
- self.class.new(value / other.value)
66
- when Numeric
67
- self.class.new(value / other)
69
+ when Number, Numeric
70
+ if other.zero?
71
+ if zero?
72
+ # NaN case detected
73
+ self.class.new(0.0 / 0.0)
74
+ else
75
+ raise ZeroDivisionError
76
+ end
77
+ elsif other.kind_of?(Number)
78
+ self.class.new(value / other.value)
79
+ else
80
+ self.class.new(value / other)
81
+ end
68
82
  else
69
83
  err_msg = "'/': Operands must be numbers."
70
84
  raise TypeError, err_msg
71
85
  end
72
86
  end
87
+ # rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
73
88
 
74
89
  # Unary minus (return value with changed sign)
75
90
  # @return [Loxxy::Datatype::Number]
@@ -54,11 +54,11 @@ module Loxxy
54
54
  '<=' => 'LESS_EQUAL'
55
55
  }.freeze
56
56
 
57
- # Here are all the implemented Lox keywords (in uppercase)
57
+ # Here are all the implemented Lox keywords
58
58
  # These are enumerated in section 4.2.1 Token type
59
59
  @@keywords = %w[
60
- AND CLASS ELSE FALSE FUN FOR IF NIL OR
61
- PRINT RETURN SUPER THIS TRUE VAR WHILE
60
+ and class else false fun for if nil or
61
+ print return super this true var while
62
62
  ].map { |x| [x, x] }.to_h
63
63
 
64
64
  # Constructor. Initialize a tokenizer for Lox input.
@@ -84,14 +84,13 @@ module Loxxy
84
84
  token = _next_token
85
85
  tok_sequence << token unless token.nil?
86
86
  end
87
- tok_sequence << build_token('EOF', '')
87
+ tok_sequence << build_token('EOF', nil)
88
88
 
89
89
  return tok_sequence
90
90
  end
91
91
 
92
92
  private
93
93
 
94
- # rubocop: disable Lint/DuplicateBranch
95
94
  def _next_token
96
95
  skip_intertoken_spaces
97
96
  curr_ch = scanner.peek(1)
@@ -99,22 +98,19 @@ module Loxxy
99
98
 
100
99
  token = nil
101
100
 
102
- if '(){},.;/*'.include? curr_ch
101
+ if '(){},.;+-/*'.include? curr_ch
103
102
  # Single delimiter or separator character
104
103
  token = build_token(@@lexeme2name[curr_ch], scanner.getch)
105
- elsif (lexeme = scanner.scan(/[+\-](?!\d)/))
106
- # Minus or plus character not preceding a digit
107
- token = build_token(@@lexeme2name[lexeme], lexeme)
108
104
  elsif (lexeme = scanner.scan(/[!=><]=?/))
109
105
  # One or two special character tokens
110
106
  token = build_token(@@lexeme2name[lexeme], lexeme)
111
- elsif (lexeme = scanner.scan(/-?\d+(?:\.\d+)?/))
107
+ elsif (lexeme = scanner.scan(/\d+(?:\.\d+)?/))
112
108
  token = build_token('NUMBER', lexeme)
113
109
  elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
114
110
  token = build_token('STRING', lexeme)
115
111
  elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
116
- keyw = @@keywords[lexeme.upcase]
117
- tok_type = keyw || 'IDENTIFIER'
112
+ keyw = @@keywords[lexeme]
113
+ tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
118
114
  token = build_token(tok_type, lexeme)
119
115
  elsif scanner.scan(/"(?:\\"|[^"])*\z/)
120
116
  # Error: unterminated string...
@@ -128,12 +124,12 @@ module Loxxy
128
124
 
129
125
  return token
130
126
  end
131
- # rubocop: enable Lint/DuplicateBranch
132
127
 
133
128
  def build_token(aSymbolName, aLexeme)
134
129
  begin
135
130
  (value, symb) = convert_to(aLexeme, aSymbolName)
136
- col = scanner.pos - aLexeme.size - @line_start + 1
131
+ lex_length = aLexeme ? aLexeme.size : 0
132
+ col = scanner.pos - lex_length - @line_start + 1
137
133
  pos = Rley::Lexical::Position.new(@lineno, col)
138
134
  if value
139
135
  token = Literal.new(value, aLexeme.dup, symb, pos)
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.2.04'
4
+ VERSION = '0.3.02'
5
5
  end
@@ -35,8 +35,8 @@ module Loxxy
35
35
  let(:lit_expr) { Ast::LoxLiteralExpr.new(sample_pos, greeting) }
36
36
 
37
37
  it "should react to 'after_var_stmt' event" do
38
- # Precondition: value to assign is on top of stack
39
- subject.stack.push(greeting)
38
+ # Precondition: value to assign is on top of expr stack
39
+ subject.expr_stack.push(greeting)
40
40
 
41
41
  expect { subject.after_var_stmt(var_decl) }.not_to raise_error
42
42
  current_env = subject.symbol_table.current_env
@@ -46,7 +46,17 @@ module Loxxy
46
46
 
47
47
  it "should react to 'before_literal_expr' event" do
48
48
  expect { subject.before_literal_expr(lit_expr) }.not_to raise_error
49
- expect(subject.stack.pop).to eq(greeting)
49
+ expect(subject.expr_stack.pop).to eq(greeting)
50
+ end
51
+ end
52
+
53
+ context 'Built-in functions:' do
54
+ it 'should provide built-in functions' do
55
+ symb_table = subject.symbol_table
56
+ %w[clock getc chr exit print_error].each do |name|
57
+ fun_var = symb_table.current_env.defns[name]
58
+ expect(fun_var.value).to be_kind_of(BackEnd::Engine::NativeFunction)
59
+ end
50
60
  end
51
61
  end
52
62
  end # describe
@@ -54,20 +54,6 @@ module Loxxy
54
54
  expect(var_b).to be_kind_of(Variable)
55
55
  expect(var_b.name).to eq('b')
56
56
  end
57
-
58
- # it 'should set the suffix of just created variable' do
59
- # subject.insert(var('a'))
60
- # var_a = subject.defns['a']
61
- # expect(var_a.suffix).to eq("_#{subject.object_id.to_s(16)}")
62
- # end
63
-
64
- # it 'should complain when variable names collide' do
65
- # subject.insert(var('c'))
66
- # expect(subject.defns['c']).to be_kind_of(Datatype::Variable)
67
- # err = StandardError
68
- # err_msg = "Variable with name 'c' already exists."
69
- # expect { subject.insert(var('c')) }.to raise_error(err, err_msg)
70
- # end
71
57
  end # context
72
58
  end # describe
73
59
  end # module
@@ -106,25 +106,6 @@ module Loxxy
106
106
  expect(subject.lookup('q')).to eq(subject.current_env.defns['q'])
107
107
  end
108
108
 
109
- # it 'should allow the search of an entry based on its i_name' do
110
- # subject.insert(var('q'))
111
- # i_name_x = subject.insert(var('x'))
112
- # subject.enter_environment(BackEnd::Environment.new)
113
- # i_name_q2 = subject.insert(var('q'))
114
- # i_name_y = subject.insert(var('y'))
115
-
116
- # # Search for unknown i_name
117
- # expect(subject.lookup_i_name('dummy')).to be_nil
118
-
119
- # curr_scope = subject.current_env
120
- # # # Search for existing unique names
121
- # expect(subject.lookup_i_name(i_name_y)).to eq(curr_scope.defns['y'])
122
- # expect(subject.lookup_i_name(i_name_x)).to eq(subject.root.defns['x'])
123
-
124
- # # Search for redefined name
125
- # expect(subject.lookup_i_name(i_name_q2)).to eq(curr_scope.defns['q'])
126
- # end
127
-
128
109
  it 'should list all the variables defined in all the szcope chain' do
129
110
  subject.insert(var('q'))
130
111
  subject.enter_environment(BackEnd::Environment.new)