loxxy 0.2.01 → 0.2.06

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.
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