loxxy 0.2.01 → 0.2.06

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/README.md +25 -11
  4. data/bin/loxxy +9 -5
  5. data/lib/loxxy/ast/all_lox_nodes.rb +0 -1
  6. data/lib/loxxy/ast/ast_builder.rb +27 -4
  7. data/lib/loxxy/ast/ast_visitee.rb +53 -0
  8. data/lib/loxxy/ast/ast_visitor.rb +2 -10
  9. data/lib/loxxy/ast/lox_assign_expr.rb +1 -5
  10. data/lib/loxxy/ast/lox_binary_expr.rb +1 -6
  11. data/lib/loxxy/ast/lox_block_stmt.rb +1 -5
  12. data/lib/loxxy/ast/lox_call_expr.rb +1 -5
  13. data/lib/loxxy/ast/lox_class_stmt.rb +1 -5
  14. data/lib/loxxy/ast/lox_fun_stmt.rb +1 -5
  15. data/lib/loxxy/ast/lox_get_expr.rb +1 -6
  16. data/lib/loxxy/ast/lox_grouping_expr.rb +1 -5
  17. data/lib/loxxy/ast/lox_if_stmt.rb +2 -6
  18. data/lib/loxxy/ast/lox_literal_expr.rb +1 -5
  19. data/lib/loxxy/ast/lox_logical_expr.rb +1 -6
  20. data/lib/loxxy/ast/lox_node.rb +9 -1
  21. data/lib/loxxy/ast/lox_print_stmt.rb +1 -5
  22. data/lib/loxxy/ast/lox_return_stmt.rb +1 -5
  23. data/lib/loxxy/ast/lox_seq_decl.rb +1 -5
  24. data/lib/loxxy/ast/lox_set_expr.rb +1 -5
  25. data/lib/loxxy/ast/lox_super_expr.rb +2 -6
  26. data/lib/loxxy/ast/lox_this_expr.rb +1 -5
  27. data/lib/loxxy/ast/lox_unary_expr.rb +1 -6
  28. data/lib/loxxy/ast/lox_var_stmt.rb +1 -5
  29. data/lib/loxxy/ast/lox_variable_expr.rb +1 -5
  30. data/lib/loxxy/ast/lox_while_stmt.rb +2 -6
  31. data/lib/loxxy/back_end/engine.rb +28 -26
  32. data/lib/loxxy/back_end/lox_instance.rb +1 -1
  33. data/lib/loxxy/back_end/resolver.rb +16 -22
  34. data/lib/loxxy/datatype/number.rb +19 -4
  35. data/lib/loxxy/error.rb +3 -0
  36. data/lib/loxxy/front_end/parser.rb +1 -1
  37. data/lib/loxxy/front_end/scanner.rb +43 -17
  38. data/lib/loxxy/version.rb +1 -1
  39. data/spec/front_end/scanner_spec.rb +69 -7
  40. data/spec/interpreter_spec.rb +36 -0
  41. metadata +3 -3
  42. data/lib/loxxy/ast/lox_for_stmt.rb +0 -41
@@ -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]
data/lib/loxxy/error.rb CHANGED
@@ -7,6 +7,9 @@ module Loxxy
7
7
  # Error occurring while Loxxy executes some invalid Lox code.
8
8
  class RuntimeError < Error; end
9
9
 
10
+ # Error occurring while Loxxy scans invalid input.
11
+ class ScanError < Error; end
12
+
10
13
  # Error occurring while Loxxy parses some invalid Lox code.
11
14
  class SyntaxError < Error; end
12
15
  end
@@ -46,7 +46,7 @@ module Loxxy
46
46
  # Stop if the parse failed...
47
47
  line1 = "Parsing failed\n"
48
48
  line2 = "Reason: #{result.failure_reason.message}"
49
- raise StandardError, line1 + line2
49
+ raise SyntaxError, line1 + line2
50
50
  end
51
51
 
52
52
  return engine.convert(result) # engine.to_ptree(result)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'strscan'
4
4
  require 'rley'
5
+ require_relative '../error'
5
6
  require_relative '../datatype/all_datatypes'
6
7
  require_relative 'literal'
7
8
 
@@ -53,15 +54,13 @@ module Loxxy
53
54
  '<=' => 'LESS_EQUAL'
54
55
  }.freeze
55
56
 
56
- # Here are all the implemented Lox keywords (in uppercase)
57
+ # Here are all the implemented Lox keywords
57
58
  # These are enumerated in section 4.2.1 Token type
58
59
  @@keywords = %w[
59
- AND CLASS ELSE FALSE FUN FOR IF NIL OR
60
- 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
61
62
  ].map { |x| [x, x] }.to_h
62
63
 
63
- class ScanError < StandardError; end
64
-
65
64
  # Constructor. Initialize a tokenizer for Lox input.
66
65
  # @param source [String] Lox text to tokenize.
67
66
  def initialize(source = nil)
@@ -85,7 +84,7 @@ module Loxxy
85
84
  token = _next_token
86
85
  tok_sequence << token unless token.nil?
87
86
  end
88
- tok_sequence << build_token('EOF', '')
87
+ tok_sequence << build_token('EOF', nil)
89
88
 
90
89
  return tok_sequence
91
90
  end
@@ -100,28 +99,31 @@ module Loxxy
100
99
 
101
100
  token = nil
102
101
 
103
- if '(){},.;/*'.include? curr_ch
102
+ if '(){},.;-/*'.include? curr_ch
104
103
  # Single delimiter or separator character
105
104
  token = build_token(@@lexeme2name[curr_ch], scanner.getch)
106
- elsif (lexeme = scanner.scan(/[+\-](?!\d)/))
105
+ elsif (lexeme = scanner.scan(/\+(?!\d)/))
107
106
  # Minus or plus character not preceding a digit
108
107
  token = build_token(@@lexeme2name[lexeme], lexeme)
109
108
  elsif (lexeme = scanner.scan(/[!=><]=?/))
110
109
  # One or two special character tokens
111
110
  token = build_token(@@lexeme2name[lexeme], lexeme)
112
- elsif (lexeme = scanner.scan(/-?\d+(?:\.\d+)?/))
111
+ elsif (lexeme = scanner.scan(/\d+(?:\.\d+)?/))
113
112
  token = build_token('NUMBER', lexeme)
114
113
  elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
115
114
  token = build_token('STRING', lexeme)
116
115
  elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
117
- keyw = @@keywords[lexeme.upcase]
118
- tok_type = keyw || 'IDENTIFIER'
116
+ keyw = @@keywords[lexeme]
117
+ tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
119
118
  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."
120
123
  else # Unknown token
121
- erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
122
- sequel = scanner.scan(/.{1,20}/)
123
- erroneous += sequel unless sequel.nil?
124
- raise ScanError, "Unknown token #{erroneous} on line #{lineno}"
124
+ col = scanner.pos - @line_start + 1
125
+ _erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
126
+ raise ScanError, "Error: [line #{lineno}:#{col}]: Unexpected character."
125
127
  end
126
128
 
127
129
  return token
@@ -131,7 +133,8 @@ module Loxxy
131
133
  def build_token(aSymbolName, aLexeme)
132
134
  begin
133
135
  (value, symb) = convert_to(aLexeme, aSymbolName)
134
- col = scanner.pos - aLexeme.size - @line_start + 1
136
+ lex_length = aLexeme ? aLexeme.size : 0
137
+ col = scanner.pos - lex_length - @line_start + 1
135
138
  pos = Rley::Lexical::Position.new(@lineno, col)
136
139
  if value
137
140
  token = Literal.new(value, aLexeme.dup, symb, pos)
@@ -156,7 +159,7 @@ module Loxxy
156
159
  when 'NUMBER'
157
160
  value = Datatype::Number.new(aLexeme)
158
161
  when 'STRING'
159
- value = Datatype::LXString.new(aLexeme)
162
+ value = Datatype::LXString.new(unescape_string(aLexeme))
160
163
  when 'TRUE'
161
164
  value = Datatype::True.instance
162
165
  else
@@ -166,6 +169,29 @@ module Loxxy
166
169
  return [value, symb]
167
170
  end
168
171
 
172
+ # Replace any sequence sequence by their "real" value.
173
+ def unescape_string(aText)
174
+ result = +''
175
+ previous = nil
176
+
177
+ aText.each_char do |ch|
178
+ if previous
179
+ if ch == ?n
180
+ result << "\n"
181
+ else
182
+ result << ch
183
+ end
184
+ previous = nil
185
+ elsif ch == '\\'
186
+ previous = ?\
187
+ else
188
+ result << ch
189
+ end
190
+ end
191
+
192
+ result
193
+ end
194
+
169
195
  # Skip non-significant whitespaces and comments.
170
196
  # Advance the scanner until something significant is found.
171
197
  def skip_intertoken_spaces
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.01'
4
+ VERSION = '0.2.06'
5
5
  end
@@ -124,18 +124,15 @@ LOX_END
124
124
 
125
125
  it 'should recognize number values' do
126
126
  input = <<-LOX_END
127
- 123 987654
128
- 0 -0
129
- 123.456 -0.001
130
- LOX_END
127
+ 123 987654
128
+ 0 123.456
129
+ LOX_END
131
130
 
132
131
  expectations = [
133
132
  ['123', 123],
134
133
  ['987654', 987654],
135
134
  ['0', 0],
136
- ['-0', 0],
137
- ['123.456', 123.456],
138
- ['-0.001', -0.001]
135
+ ['123.456', 123.456]
139
136
  ]
140
137
 
141
138
  subject.start_with(input)
@@ -149,6 +146,30 @@ LOX_END
149
146
  end
150
147
  end
151
148
 
149
+ it 'should recognize negative number values' do
150
+ input = <<-LOX_END
151
+ -0
152
+ -0.001
153
+ LOX_END
154
+
155
+ expectations = [
156
+ ['-', '0'],
157
+ ['-', '0.001']
158
+ ].flatten
159
+
160
+ subject.start_with(input)
161
+ tokens = subject.tokens
162
+ tokens.pop
163
+ i = 0
164
+ tokens.each_slice(2) do |(sign, lit)|
165
+ expect(sign.terminal).to eq('MINUS')
166
+ expect(sign.lexeme).to eq(expectations[i])
167
+ expect(lit.terminal).to eq('NUMBER')
168
+ expect(lit.lexeme).to eq(expectations[i + 1])
169
+ i += 2
170
+ end
171
+ end
172
+
152
173
  it 'should recognize leading and trailing dots as distinct tokens' do
153
174
  input = '.456 123.'
154
175
 
@@ -189,6 +210,26 @@ LOX_END
189
210
  end
190
211
  end
191
212
 
213
+ it 'should recognize escaped quotes' do
214
+ embedded_quotes = %q{she said: \"Hello\"}
215
+ result = subject.send(:unescape_string, embedded_quotes)
216
+ expect(result).to eq('she said: "Hello"')
217
+ end
218
+
219
+ it 'should recognize escaped backslash' do
220
+ embedded_backslash = 'backslash>\\\\'
221
+ result = subject.send(:unescape_string, embedded_backslash)
222
+ expect(result).to eq('backslash>\\')
223
+ end
224
+
225
+ # rubocop: disable Style/StringConcatenation
226
+ it 'should recognize newline escape sequence' do
227
+ embedded_newline = 'line1\\nline2'
228
+ result = subject.send(:unescape_string, embedded_newline)
229
+ expect(result).to eq('line1' + "\n" + 'line2')
230
+ end
231
+ # rubocop: enable Style/StringConcatenation
232
+
192
233
  it 'should recognize a nil token' do
193
234
  subject.start_with('nil')
194
235
  token_nil = subject.tokens[0]
@@ -197,6 +238,13 @@ LOX_END
197
238
  expect(token_nil.lexeme).to eq('nil')
198
239
  expect(token_nil.value).to be_kind_of(Datatype::Nil)
199
240
  end
241
+
242
+ it 'should differentiate nil from variable spelled same' do
243
+ subject.start_with('Nil')
244
+ similar = subject.tokens[0]
245
+ expect(similar.terminal).to eq('IDENTIFIER')
246
+ expect(similar.lexeme).to eq('Nil')
247
+ end
200
248
  end # context
201
249
 
202
250
  context 'Handling comments:' do
@@ -237,6 +285,20 @@ LOX_END
237
285
  ]
238
286
  match_expectations(subject, expectations)
239
287
  end
288
+
289
+ it 'should complain if it finds an unterminated string' do
290
+ subject.start_with('var a = "Unfinished;')
291
+ err = Loxxy::ScanError
292
+ err_msg = 'Error: [line 1:21]: Unterminated string.'
293
+ expect { subject.tokens }.to raise_error(err, err_msg)
294
+ end
295
+
296
+ it 'should complain if it finds an unexpected character' do
297
+ subject.start_with('var a = ?1?;')
298
+ err = Loxxy::ScanError
299
+ err_msg = 'Error: [line 1:9]: Unexpected character.'
300
+ expect { subject.tokens }.to raise_error(err, err_msg)
301
+ end
240
302
  end # context
241
303
  end # describe
242
304
  end # module
@@ -148,6 +148,19 @@ module Loxxy
148
148
  end
149
149
  end
150
150
 
151
+ it 'should ignore spaces surrounding minus in subtraction of two numbers' do
152
+ [
153
+ ['1 - 1;', 0],
154
+ ['1 -1;', 0],
155
+ ['1- 1;', 0],
156
+ ['1-1;', 0]
157
+ ].each do |(source, predicted)|
158
+ lox = Loxxy::Interpreter.new
159
+ result = lox.evaluate(source)
160
+ expect(result.value == predicted).to be_truthy
161
+ end
162
+ end
163
+
151
164
  it 'should evaluate the negation of an object' do
152
165
  [
153
166
  ['!true;', false],
@@ -431,6 +444,17 @@ LOX_END
431
444
  expect(result).to eq(3)
432
445
  end
433
446
 
447
+ it 'should support return within statements inside a function' do
448
+ program = <<-LOX_END
449
+ fun foo() {
450
+ for (;;) return "done";
451
+ }
452
+ print foo(); // output: done
453
+ LOX_END
454
+ expect { subject.evaluate(program) }.not_to raise_error
455
+ expect(sample_cfg[:ostream].string).to eq('done')
456
+ end
457
+
434
458
  # rubocop: disable Style/StringConcatenation
435
459
  it 'should support local functions and closures' do
436
460
  program = <<-LOX_END
@@ -480,6 +504,18 @@ LOX_END
480
504
  snippet
481
505
  end
482
506
 
507
+ it 'should support field assignment expression' do
508
+ program = <<-LOX_END
509
+ class Foo {}
510
+
511
+ var foo = Foo();
512
+
513
+ print foo.bar = "bar value"; // expect: bar value
514
+ LOX_END
515
+ expect { subject.evaluate(program) }.not_to raise_error
516
+ expect(sample_cfg[:ostream].string).to eq('bar value')
517
+ end
518
+
483
519
  it 'should support class declaration' do
484
520
  program = <<-LOX_END
485
521
  #{duck_class}
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.2.01
4
+ version: 0.2.06
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-18 00:00:00.000000000 Z
11
+ date: 2021-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rley
@@ -91,6 +91,7 @@ files:
91
91
  - lib/loxxy.rb
92
92
  - lib/loxxy/ast/all_lox_nodes.rb
93
93
  - lib/loxxy/ast/ast_builder.rb
94
+ - lib/loxxy/ast/ast_visitee.rb
94
95
  - lib/loxxy/ast/ast_visitor.rb
95
96
  - lib/loxxy/ast/lox_assign_expr.rb
96
97
  - lib/loxxy/ast/lox_binary_expr.rb
@@ -98,7 +99,6 @@ files:
98
99
  - lib/loxxy/ast/lox_call_expr.rb
99
100
  - lib/loxxy/ast/lox_class_stmt.rb
100
101
  - lib/loxxy/ast/lox_compound_expr.rb
101
- - lib/loxxy/ast/lox_for_stmt.rb
102
102
  - lib/loxxy/ast/lox_fun_stmt.rb
103
103
  - lib/loxxy/ast/lox_get_expr.rb
104
104
  - lib/loxxy/ast/lox_grouping_expr.rb
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lox_compound_expr'
4
-
5
- module Loxxy
6
- module Ast
7
- class LoxForStmt < LoxCompoundExpr
8
- # @return [LoxNode] test expression
9
- attr_reader :test_expr
10
-
11
- # @return [LoxNode] update expression
12
- attr_reader :update_expr
13
-
14
- # @return [LoxNode] body statement
15
- attr_accessor :body_stmt
16
-
17
- # @param aPosition [Rley::Lexical::Position] Position of the entry in the input stream.
18
- # @param initialization [Loxxy::Ast::LoxNode]
19
- # @param testExpr [Loxxy::Ast::LoxNode]
20
- # @param updateExpr [Loxxy::Ast::LoxNode]
21
- def initialize(aPosition, initialization, testExpr, updateExpr)
22
- child = initialization ? [initialization] : []
23
- super(aPosition, child)
24
- @test_expr = testExpr
25
- @update_expr = updateExpr
26
- end
27
-
28
- # Part of the 'visitee' role in Visitor design pattern.
29
- # @param visitor [Ast::ASTVisitor] the visitor
30
- def accept(visitor)
31
- visitor.visit_for_stmt(self)
32
- end
33
-
34
- # Accessor to the condition expression
35
- # @return [LoxNode]
36
- def condition
37
- subnodes[0]
38
- end
39
- end # class
40
- end # module
41
- end # module