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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +25 -11
- data/bin/loxxy +9 -5
- data/lib/loxxy/ast/all_lox_nodes.rb +0 -1
- data/lib/loxxy/ast/ast_builder.rb +27 -4
- data/lib/loxxy/ast/ast_visitee.rb +53 -0
- data/lib/loxxy/ast/ast_visitor.rb +2 -10
- data/lib/loxxy/ast/lox_assign_expr.rb +1 -5
- data/lib/loxxy/ast/lox_binary_expr.rb +1 -6
- data/lib/loxxy/ast/lox_block_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_call_expr.rb +1 -5
- data/lib/loxxy/ast/lox_class_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_fun_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_get_expr.rb +1 -6
- data/lib/loxxy/ast/lox_grouping_expr.rb +1 -5
- data/lib/loxxy/ast/lox_if_stmt.rb +2 -6
- data/lib/loxxy/ast/lox_literal_expr.rb +1 -5
- data/lib/loxxy/ast/lox_logical_expr.rb +1 -6
- data/lib/loxxy/ast/lox_node.rb +9 -1
- data/lib/loxxy/ast/lox_print_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_return_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_seq_decl.rb +1 -5
- data/lib/loxxy/ast/lox_set_expr.rb +1 -5
- data/lib/loxxy/ast/lox_super_expr.rb +2 -6
- data/lib/loxxy/ast/lox_this_expr.rb +1 -5
- data/lib/loxxy/ast/lox_unary_expr.rb +1 -6
- data/lib/loxxy/ast/lox_var_stmt.rb +1 -5
- data/lib/loxxy/ast/lox_variable_expr.rb +1 -5
- data/lib/loxxy/ast/lox_while_stmt.rb +2 -6
- data/lib/loxxy/back_end/engine.rb +28 -26
- data/lib/loxxy/back_end/lox_instance.rb +1 -1
- data/lib/loxxy/back_end/resolver.rb +16 -22
- data/lib/loxxy/datatype/number.rb +19 -4
- data/lib/loxxy/error.rb +3 -0
- data/lib/loxxy/front_end/parser.rb +1 -1
- data/lib/loxxy/front_end/scanner.rb +43 -17
- data/lib/loxxy/version.rb +1 -1
- data/spec/front_end/scanner_spec.rb +69 -7
- data/spec/interpreter_spec.rb +36 -0
- metadata +3 -3
- 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
|
-
|
66
|
-
|
67
|
-
|
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
|
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
|
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
|
-
|
60
|
-
|
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 '(){}
|
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(
|
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(
|
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
|
118
|
-
tok_type = keyw
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
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
@@ -124,18 +124,15 @@ LOX_END
|
|
124
124
|
|
125
125
|
it 'should recognize number values' do
|
126
126
|
input = <<-LOX_END
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
['
|
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
|
data/spec/interpreter_spec.rb
CHANGED
@@ -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.
|
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
|
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
|