loxxy 0.2.05 → 0.3.03

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
@@ -96,17 +96,17 @@ module Loxxy
96
96
  def before_return_stmt(returnStmt)
97
97
  if scopes.size < 2
98
98
  msg = "Error at 'return': Can't return from top-level code."
99
- raise StandardError, msg
99
+ raise Loxxy::RuntimeError, msg
100
100
  end
101
101
 
102
102
  if current_function == :none
103
103
  msg = "Error at 'return': Can't return from outside a function."
104
- raise StandardError, msg
104
+ raise Loxxy::RuntimeError, msg
105
105
  end
106
106
 
107
107
  if current_function == :initializer
108
108
  msg = "Error at 'return': Can't return a value from an initializer."
109
- raise StandardError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
109
+ raise Loxxy::RuntimeError, msg unless returnStmt.subnodes[0].kind_of?(Datatype::Nil)
110
110
  end
111
111
  end
112
112
 
@@ -117,6 +117,9 @@ module Loxxy
117
117
 
118
118
  # A variable declaration adds a new variable to current scope
119
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
+
120
123
  declare(aVarStmt.name)
121
124
  end
122
125
 
@@ -130,6 +133,7 @@ module Loxxy
130
133
  end
131
134
 
132
135
  def after_set_expr(aSetExpr, aVisitor)
136
+ aSetExpr.value.accept(aVisitor)
133
137
  # Evaluate object part
134
138
  aSetExpr.object.accept(aVisitor)
135
139
  end
@@ -138,7 +142,7 @@ module Loxxy
138
142
  def before_variable_expr(aVarExpr)
139
143
  var_name = aVarExpr.name
140
144
  if !scopes.empty? && (scopes.last[var_name] == false)
141
- 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"
142
146
  end
143
147
  end
144
148
 
@@ -160,7 +164,7 @@ module Loxxy
160
164
  def before_this_expr(_thisExpr)
161
165
  if current_class == :none
162
166
  msg = "Error at 'this': Can't use 'this' outside of a class."
163
- raise StandardError, msg
167
+ raise Loxxy::RuntimeError, msg
164
168
  end
165
169
  end
166
170
 
@@ -175,11 +179,11 @@ module Loxxy
175
179
  msg_prefix = "Error at 'super': Can't use 'super' "
176
180
  if current_class == :none
177
181
  err_msg = msg_prefix + 'outside of a class.'
178
- raise StandardError, err_msg
182
+ raise Loxxy::RuntimeError, err_msg
179
183
 
180
184
  elsif current_class == :class
181
185
  err_msg = msg_prefix + 'in a class without superclass.'
182
- raise StandardError, err_msg
186
+ raise Loxxy::RuntimeError, err_msg
183
187
 
184
188
  end
185
189
  # 'super' behaves closely to a local variable
@@ -209,9 +213,13 @@ module Loxxy
209
213
  return if scopes.empty?
210
214
 
211
215
  curr_scope = scopes.last
216
+ # Oddly enough, Lox allows variable re-declaration at top-level
212
217
  if curr_scope.include?(aVarName)
213
218
  msg = "Error at '#{aVarName}': Already variable with this name in this scope."
214
- 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
215
223
  end
216
224
 
217
225
  # The initializer is not yet processed.
@@ -47,11 +47,10 @@ module Loxxy
47
47
 
48
48
  def validated_value(aValue)
49
49
  unless aValue.is_a?(String)
50
- raise StandardError, "Invalid number value #{aValue}"
50
+ raise StandardError, "Invalid string value #{aValue}"
51
51
  end
52
52
 
53
- # Remove double quotes delimiter
54
- aValue.gsub(/(^")|("$)/, '')
53
+ aValue
55
54
  end
56
55
  end # class
57
56
  end # module
@@ -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]
@@ -61,6 +61,20 @@ module Loxxy
61
61
  print return super this true var while
62
62
  ].map { |x| [x, x] }.to_h
63
63
 
64
+ # Single character that have a special meaning when escaped
65
+ # @return [{Char => String}]
66
+ @@escape_chars = {
67
+ ?a => "\a",
68
+ ?b => "\b",
69
+ ?e => "\e",
70
+ ?f => "\f",
71
+ ?n => "\n",
72
+ ?r => "\r",
73
+ ?s => "\s",
74
+ ?t => "\t",
75
+ ?v => "\v"
76
+ }.freeze
77
+
64
78
  # Constructor. Initialize a tokenizer for Lox input.
65
79
  # @param source [String] Lox text to tokenize.
66
80
  def initialize(source = nil)
@@ -84,14 +98,13 @@ module Loxxy
84
98
  token = _next_token
85
99
  tok_sequence << token unless token.nil?
86
100
  end
87
- tok_sequence << build_token('EOF', '')
101
+ tok_sequence << build_token('EOF', nil)
88
102
 
89
103
  return tok_sequence
90
104
  end
91
105
 
92
106
  private
93
107
 
94
- # rubocop: disable Lint/DuplicateBranch
95
108
  def _next_token
96
109
  skip_intertoken_spaces
97
110
  curr_ch = scanner.peek(1)
@@ -99,27 +112,20 @@ module Loxxy
99
112
 
100
113
  token = nil
101
114
 
102
- if '(){},.;/*'.include? curr_ch
115
+ if '(){},.;+-/*'.include? curr_ch
103
116
  # Single delimiter or separator character
104
117
  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
118
  elsif (lexeme = scanner.scan(/[!=><]=?/))
109
119
  # One or two special character tokens
110
120
  token = build_token(@@lexeme2name[lexeme], lexeme)
111
- elsif (lexeme = scanner.scan(/-?\d+(?:\.\d+)?/))
121
+ elsif scanner.scan(/"/) # Start of string detected...
122
+ token = build_string_token
123
+ elsif (lexeme = scanner.scan(/\d+(?:\.\d+)?/))
112
124
  token = build_token('NUMBER', lexeme)
113
- elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
114
- token = build_token('STRING', lexeme)
115
125
  elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
116
126
  keyw = @@keywords[lexeme]
117
127
  tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
118
128
  token = build_token(tok_type, lexeme)
119
- elsif scanner.scan(/"(?:\\"|[^"])*\z/)
120
- # Error: unterminated string...
121
- col = scanner.pos - @line_start + 1
122
- raise ScanError, "Error: [line #{lineno}:#{col}]: Unterminated string."
123
129
  else # Unknown token
124
130
  col = scanner.pos - @line_start + 1
125
131
  _erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
@@ -128,12 +134,12 @@ module Loxxy
128
134
 
129
135
  return token
130
136
  end
131
- # rubocop: enable Lint/DuplicateBranch
132
137
 
133
138
  def build_token(aSymbolName, aLexeme)
134
139
  begin
135
140
  (value, symb) = convert_to(aLexeme, aSymbolName)
136
- col = scanner.pos - aLexeme.size - @line_start + 1
141
+ lex_length = aLexeme ? aLexeme.size : 0
142
+ col = scanner.pos - lex_length - @line_start + 1
137
143
  pos = Rley::Lexical::Position.new(@lineno, col)
138
144
  if value
139
145
  token = Literal.new(value, aLexeme.dup, symb, pos)
@@ -157,8 +163,6 @@ module Loxxy
157
163
  value = Datatype::Nil.instance
158
164
  when 'NUMBER'
159
165
  value = Datatype::Number.new(aLexeme)
160
- when 'STRING'
161
- value = Datatype::LXString.new(unescape_string(aLexeme))
162
166
  when 'TRUE'
163
167
  value = Datatype::True.instance
164
168
  else
@@ -168,27 +172,47 @@ module Loxxy
168
172
  return [value, symb]
169
173
  end
170
174
 
171
- # Replace any sequence sequence by their "real" value.
172
- def unescape_string(aText)
173
- result = +''
174
- previous = nil
175
-
176
- aText.each_char do |ch|
177
- if previous
178
- if ch == ?n
179
- result << "\n"
180
- else
181
- result << ch
182
- end
183
- previous = nil
184
- elsif ch == '\\'
185
- previous = ?\
175
+ # precondition: current position at leading quote
176
+ def build_string_token
177
+ scan_pos = scanner.pos
178
+ line = @lineno
179
+ column_start = scan_pos - @line_start
180
+ literal = +''
181
+ loop do
182
+ substr = scanner.scan(/[^"\\\r\n]*/)
183
+ if scanner.eos?
184
+ pos_start = "line #{line}:#{column_start}"
185
+ raise ScanError, "Error: [#{pos_start}]: Unterminated string."
186
186
  else
187
- result << ch
187
+ literal << substr
188
+ special = scanner.scan(/["\\\r\n]/)
189
+ case special
190
+ when '"' # Terminating quote found
191
+ break
192
+ when "\r"
193
+ next_line
194
+ special << scanner.scan(/./) if scanner.match?(/\n/)
195
+ literal << special
196
+ when "\n"
197
+ next_line
198
+ literal << special
199
+ when '\\'
200
+ ch = scanner.scan(/./)
201
+ next unless ch
202
+
203
+ escaped = @@escape_chars[ch]
204
+ if escaped
205
+ literal << escaped
206
+ else
207
+ literal << ch
208
+ end
209
+ end
188
210
  end
189
211
  end
190
-
191
- result
212
+ pos = Rley::Lexical::Position.new(line, column_start)
213
+ lox_string = Datatype::LXString.new(literal)
214
+ lexeme = scanner.string[scan_pos - 1..scanner.pos - 1]
215
+ Literal.new(lox_string, lexeme, 'STRING', pos)
192
216
  end
193
217
 
194
218
  # Skip non-significant whitespaces and comments.
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.05'
4
+ VERSION = '0.3.03'
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