loxxy 0.0.16 → 0.0.21
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +60 -1
- data/README.md +155 -52
- data/lib/loxxy/ast/all_lox_nodes.rb +7 -1
- data/lib/loxxy/ast/ast_builder.rb +57 -3
- data/lib/loxxy/ast/ast_visitor.rb +62 -2
- data/lib/loxxy/ast/lox_binary_expr.rb +1 -1
- data/lib/loxxy/ast/lox_grouping_expr.rb +23 -0
- data/lib/loxxy/ast/lox_if_stmt.rb +35 -0
- data/lib/loxxy/ast/lox_logical_expr.rb +28 -0
- data/lib/loxxy/ast/lox_unary_expr.rb +27 -0
- data/lib/loxxy/ast/lox_var_stmt.rb +28 -0
- data/lib/loxxy/back_end/engine.rb +71 -4
- data/lib/loxxy/back_end/entry.rb +41 -0
- data/lib/loxxy/back_end/environment.rb +66 -0
- data/lib/loxxy/back_end/symbol_table.rb +135 -0
- data/lib/loxxy/back_end/variable.rb +25 -0
- data/lib/loxxy/datatype/builtin_datatype.rb +55 -0
- data/lib/loxxy/datatype/false.rb +14 -8
- data/lib/loxxy/datatype/lx_string.rb +0 -14
- data/lib/loxxy/datatype/nil.rb +12 -6
- data/lib/loxxy/datatype/number.rb +44 -6
- data/lib/loxxy/datatype/true.rb +1 -9
- data/lib/loxxy/front_end/grammar.rb +7 -7
- data/lib/loxxy/interpreter.rb +1 -1
- data/lib/loxxy/version.rb +1 -1
- data/spec/back_end/engine_spec.rb +8 -0
- data/spec/back_end/environment_spec.rb +74 -0
- data/spec/back_end/symbol_table_spec.rb +142 -0
- data/spec/back_end/variable_spec.rb +79 -0
- data/spec/front_end/parser_spec.rb +4 -4
- data/spec/interpreter_spec.rb +152 -0
- metadata +17 -2
data/lib/loxxy/interpreter.rb
CHANGED
@@ -31,7 +31,7 @@ module Loxxy
|
|
31
31
|
ast_tree = parser.parse(lox_input)
|
32
32
|
visitor = Ast::ASTVisitor.new(ast_tree)
|
33
33
|
|
34
|
-
# Back-end launches the tree walking &
|
34
|
+
# Back-end launches the tree walking & responds to visit events
|
35
35
|
# by executing the code determined by the visited AST node.
|
36
36
|
engine = BackEnd::Engine.new(config)
|
37
37
|
engine.execute(visitor)
|
data/lib/loxxy/version.rb
CHANGED
@@ -31,8 +31,16 @@ module Loxxy
|
|
31
31
|
context 'Listening to visitor events:' do
|
32
32
|
let(:greeting) { Datatype::LXString.new('Hello, world') }
|
33
33
|
let(:sample_pos) { double('fake-position') }
|
34
|
+
let(:var_decl) { Ast::LoxVarStmt.new(sample_pos, 'greeting', greeting) }
|
34
35
|
let(:lit_expr) { Ast::LoxLiteralExpr.new(sample_pos, greeting) }
|
35
36
|
|
37
|
+
it "should react to 'after_var_stmt' event" do
|
38
|
+
expect { subject.after_var_stmt(var_decl) }.not_to raise_error
|
39
|
+
current_env = subject.symbol_table.current_env
|
40
|
+
expect(current_env.defns['greeting']).to be_kind_of(Variable)
|
41
|
+
expect(current_env.defns['greeting'].value).to eq(greeting)
|
42
|
+
end
|
43
|
+
|
36
44
|
it "should react to 'before_literal_expr' event" do
|
37
45
|
expect { subject.before_literal_expr(lit_expr) }.not_to raise_error
|
38
46
|
expect(subject.stack.pop).to eq(greeting)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
|
5
|
+
# Load the class under test
|
6
|
+
require_relative '../../lib/loxxy/back_end/environment'
|
7
|
+
|
8
|
+
module Loxxy
|
9
|
+
module BackEnd
|
10
|
+
describe Environment do
|
11
|
+
let(:foo) { Datatype::LXString.new('foo') }
|
12
|
+
let(:bar) { Datatype::LXString.new('bar') }
|
13
|
+
let(:mother) { Environment.new }
|
14
|
+
subject { Environment.new(mother) }
|
15
|
+
|
16
|
+
# Shortand factory method.
|
17
|
+
def var(aName, aValue)
|
18
|
+
Variable.new(aName, aValue)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'Initialization:' do
|
22
|
+
it 'could be initialized without argument' do
|
23
|
+
expect { Environment.new }.not_to raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'could be initialized with a parent environment' do
|
27
|
+
expect { Environment.new(mother) }.not_to raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "shouldn't have definitions by default" do
|
31
|
+
expect(subject.defns).to be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should know its parent (if any)' do
|
35
|
+
expect(subject.parent).to eq(mother)
|
36
|
+
end
|
37
|
+
end # context
|
38
|
+
|
39
|
+
context 'Provided services:' do
|
40
|
+
it 'should accept the addition of a variable' do
|
41
|
+
subject.insert(var('a', foo))
|
42
|
+
expect(subject.defns).not_to be_empty
|
43
|
+
var_a = subject.defns['a']
|
44
|
+
expect(var_a).to be_kind_of(Variable)
|
45
|
+
expect(var_a.name).to eq('a')
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should accept the addition of multiple variables' do
|
49
|
+
subject.insert(var('a', foo))
|
50
|
+
expect(subject.defns).not_to be_empty
|
51
|
+
|
52
|
+
subject.insert(var('b', bar))
|
53
|
+
var_b = subject.defns['b']
|
54
|
+
expect(var_b).to be_kind_of(Variable)
|
55
|
+
expect(var_b.name).to eq('b')
|
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
|
+
end # context
|
72
|
+
end # describe
|
73
|
+
end # module
|
74
|
+
end # module
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
require_relative '../../lib/loxxy/back_end/variable'
|
5
|
+
|
6
|
+
# Load the class under test
|
7
|
+
require_relative '../../lib/loxxy/back_end/symbol_table'
|
8
|
+
|
9
|
+
module Loxxy
|
10
|
+
module BackEnd
|
11
|
+
describe SymbolTable do
|
12
|
+
subject { SymbolTable.new }
|
13
|
+
|
14
|
+
context 'Initialization:' do
|
15
|
+
it 'should be initialized without argument' do
|
16
|
+
expect { SymbolTable.new }.not_to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have a root BackEnd' do
|
20
|
+
expect(subject.root).not_to be_nil
|
21
|
+
expect(subject.current_env).to eq(subject.root)
|
22
|
+
expect(subject.root).to be_kind_of(BackEnd::Environment)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "shouldn't have names at initialization" do
|
26
|
+
expect(subject.name2envs).to be_empty
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should be empty at initialization' do
|
30
|
+
expect(subject).to be_empty
|
31
|
+
end
|
32
|
+
end # context
|
33
|
+
|
34
|
+
context 'Provided services:' do
|
35
|
+
def var(aName)
|
36
|
+
Variable.new(aName)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should allow the addition of a variable' do
|
40
|
+
expect { subject.insert(var('q')) }.not_to raise_error
|
41
|
+
expect(subject).not_to be_empty
|
42
|
+
expect(subject.name2envs['q']).to be_kind_of(Array)
|
43
|
+
expect(subject.name2envs['q'].size).to eq(1)
|
44
|
+
expect(subject.name2envs['q'].first).to eq(subject.current_env)
|
45
|
+
expect(subject.current_env.defns['q']).to be_kind_of(BackEnd::Variable)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should allow the addition of several labels for same env' do
|
49
|
+
i_name = subject.insert(var('q'))
|
50
|
+
#expect(i_name).to match(/^q_[0-9a-z]*$/)
|
51
|
+
|
52
|
+
expect { subject.insert(var('x')) }.not_to raise_error
|
53
|
+
expect(subject.name2envs['x']).to be_kind_of(Array)
|
54
|
+
expect(subject.name2envs['x'].first).to eq(subject.current_env)
|
55
|
+
expect(subject.current_env.defns['x']).to be_kind_of(BackEnd::Variable)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should allow the entry into a new scope' do
|
59
|
+
subject.insert(var('q'))
|
60
|
+
new_env = BackEnd::Environment.new
|
61
|
+
expect { subject.enter_environment(new_env) }.not_to raise_error
|
62
|
+
expect(subject.current_env).to eq(new_env)
|
63
|
+
expect(subject.current_env.parent).to eq(subject.root)
|
64
|
+
expect(subject.name2envs['q']).to eq([subject.root])
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should allow the addition of same name in different scopes' do
|
68
|
+
subject.insert(var('q'))
|
69
|
+
subject.enter_environment(BackEnd::Environment.new)
|
70
|
+
subject.insert(var('q'))
|
71
|
+
expect(subject.name2envs['q']).to be_kind_of(Array)
|
72
|
+
expect(subject.name2envs['q'].size).to eq(2)
|
73
|
+
expect(subject.name2envs['q'].first).to eq(subject.root)
|
74
|
+
expect(subject.name2envs['q'].last).to eq(subject.current_env)
|
75
|
+
expect(subject.current_env.defns['q']).to be_kind_of(BackEnd::Variable)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should allow the removal of a scope' do
|
79
|
+
subject.insert(var('q'))
|
80
|
+
new_env = BackEnd::Environment.new
|
81
|
+
subject.enter_environment(new_env)
|
82
|
+
subject.insert(var('q'))
|
83
|
+
expect(subject.name2envs['q'].size).to eq(2)
|
84
|
+
|
85
|
+
expect { subject.leave_environment }.not_to raise_error
|
86
|
+
expect(subject.current_env).to eq(subject.root)
|
87
|
+
expect(subject.name2envs['q'].size).to eq(1)
|
88
|
+
expect(subject.name2envs['q']).to eq([subject.root])
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should allow the search of an entry based on its name' do
|
92
|
+
subject.insert(var('q'))
|
93
|
+
subject.insert(var('x'))
|
94
|
+
subject.enter_environment(BackEnd::Environment.new)
|
95
|
+
subject.insert(var('q'))
|
96
|
+
subject.insert(var('y'))
|
97
|
+
|
98
|
+
# Search for unknown name
|
99
|
+
expect(subject.lookup('z')).to be_nil
|
100
|
+
|
101
|
+
# Search for existing unique names
|
102
|
+
expect(subject.lookup('y')).to eq(subject.current_env.defns['y'])
|
103
|
+
expect(subject.lookup('x')).to eq(subject.root.defns['x'])
|
104
|
+
|
105
|
+
# Search for redefined name
|
106
|
+
expect(subject.lookup('q')).to eq(subject.current_env.defns['q'])
|
107
|
+
end
|
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
|
+
it 'should list all the variables defined in all the szcope chain' do
|
129
|
+
subject.insert(var('q'))
|
130
|
+
subject.enter_environment(BackEnd::Environment.new)
|
131
|
+
subject.insert(var('x'))
|
132
|
+
subject.enter_environment(BackEnd::Environment.new)
|
133
|
+
subject.insert(var('y'))
|
134
|
+
subject.insert(var('x'))
|
135
|
+
|
136
|
+
vars = subject.all_variables
|
137
|
+
expect(vars.map(&:name)).to eq(%w[q x y x])
|
138
|
+
end
|
139
|
+
end # context
|
140
|
+
end # describe
|
141
|
+
end # module
|
142
|
+
end # module
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
|
5
|
+
# Load the class under test
|
6
|
+
require_relative '../../lib/loxxy/back_end/variable'
|
7
|
+
|
8
|
+
|
9
|
+
module Loxxy
|
10
|
+
module BackEnd
|
11
|
+
describe Variable do
|
12
|
+
let(:sample_name) { 'iAmAVariable' }
|
13
|
+
let(:sample_value) { 'here is my value' }
|
14
|
+
subject { Variable.new(sample_name, sample_value) }
|
15
|
+
|
16
|
+
context 'Initialization:' do
|
17
|
+
it 'should be initialized with a name and a value, or...' do
|
18
|
+
expect { Variable.new(sample_name, sample_value) }.not_to raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be initialized with just a name' do
|
22
|
+
expect { Variable.new(sample_name) }.not_to raise_error
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should know its name' do
|
26
|
+
expect(subject.name).to eq(sample_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should have a frozen name' do
|
30
|
+
expect(subject.name).to be_frozen
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should know its value (if provided)' do
|
34
|
+
expect(subject.value).to eq(sample_value)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should have a nil value otherwise' do
|
38
|
+
instance = Variable.new(sample_name)
|
39
|
+
expect(instance.value).to eq(Datatype::Nil.instance)
|
40
|
+
end
|
41
|
+
|
42
|
+
# it 'should know its default internal name' do
|
43
|
+
# # By default: internal name == label
|
44
|
+
# expect(subject.i_name).to eq(subject.label)
|
45
|
+
# end
|
46
|
+
|
47
|
+
# it 'should have a nil suffix' do
|
48
|
+
# expect(subject.suffix).to be_nil
|
49
|
+
# end
|
50
|
+
end # context
|
51
|
+
|
52
|
+
context 'Provided service:' do
|
53
|
+
let(:sample_suffix) { 'sample-suffix' }
|
54
|
+
it 'should have a label equal to its user-defined name' do
|
55
|
+
# expect(subject.label).to eq(subject.name)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should accept a suffix' do
|
59
|
+
# expect { subject.suffix = sample_suffix }.not_to raise_error
|
60
|
+
# expect(subject.suffix).to eq(sample_suffix)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should calculate its internal name' do
|
64
|
+
# # Rule: empty suffix => internal name == label
|
65
|
+
# subject.suffix = ''
|
66
|
+
# expect(subject.i_name).to eq(subject.label)
|
67
|
+
|
68
|
+
# # Rule: suffix starting underscore: internal name = label + suffix
|
69
|
+
# subject.suffix = '_10'
|
70
|
+
# expect(subject.i_name).to eq(subject.label + subject.suffix)
|
71
|
+
|
72
|
+
# # Rule: ... otherwise: internal name == suffix
|
73
|
+
# subject.suffix = sample_suffix
|
74
|
+
# expect(subject.i_name).to eq(subject.suffix)
|
75
|
+
end
|
76
|
+
end # context
|
77
|
+
end # describe
|
78
|
+
end # module
|
79
|
+
end # module
|
@@ -262,12 +262,12 @@ LOX_END
|
|
262
262
|
end # context
|
263
263
|
|
264
264
|
context 'Parsing logical expressions' do
|
265
|
-
it 'should parse the logical operations
|
265
|
+
it 'should parse the logical operations between two sub-expression' do
|
266
266
|
%w[or and].each do |connector|
|
267
267
|
input = "5 > 2 #{connector} 3 <= 4;"
|
268
268
|
ptree = subject.parse(input)
|
269
269
|
expr = ptree.root
|
270
|
-
expect(expr).to be_kind_of(Ast::
|
270
|
+
expect(expr).to be_kind_of(Ast::LoxLogicalExpr)
|
271
271
|
expect(expr.operator).to eq(connector.to_sym)
|
272
272
|
expect(expr.operands[0]).to be_kind_of(Ast::LoxBinaryExpr)
|
273
273
|
expect(expr.operands[0].operator).to eq(:>)
|
@@ -284,9 +284,9 @@ LOX_END
|
|
284
284
|
input = '4 > 3 and 1 < 2 or 4 >= 5;'
|
285
285
|
ptree = subject.parse(input)
|
286
286
|
expr = ptree.root
|
287
|
-
expect(expr).to be_kind_of(Ast::
|
287
|
+
expect(expr).to be_kind_of(Ast::LoxLogicalExpr)
|
288
288
|
expect(expr.operator).to eq(:or) # or has lower precedence than and
|
289
|
-
expect(expr.operands[0]).to be_kind_of(Ast::
|
289
|
+
expect(expr.operands[0]).to be_kind_of(Ast::LoxLogicalExpr)
|
290
290
|
expect(expr.operands[0].operator).to eq(:and)
|
291
291
|
conjuncts = expr.operands[0].operands
|
292
292
|
expect(conjuncts[0]).to be_kind_of(Ast::LoxBinaryExpr)
|
data/spec/interpreter_spec.rb
CHANGED
@@ -107,6 +107,158 @@ module Loxxy
|
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
|
+
it 'should evaluate a comparison of two numbers' do
|
111
|
+
[
|
112
|
+
['1 < 2;', true],
|
113
|
+
['2 < 2;', false],
|
114
|
+
['2 < 1;', false],
|
115
|
+
['1 <= 2;', true],
|
116
|
+
['2 <= 2;', true],
|
117
|
+
['2 <= 1;', false],
|
118
|
+
['1 > 2;', false],
|
119
|
+
['2 > 2;', false],
|
120
|
+
['2 > 1;', true],
|
121
|
+
['1 >= 2;', false],
|
122
|
+
['2 >= 2;', true],
|
123
|
+
['2 >= 1;', true],
|
124
|
+
['0 < -0;', false],
|
125
|
+
['-0 < 0;', false],
|
126
|
+
['0 > -0;', false],
|
127
|
+
['-0 > 0;', false],
|
128
|
+
['0 <= -0;', true],
|
129
|
+
['-0 <= 0;', true],
|
130
|
+
['0 >= -0;', true],
|
131
|
+
['-0 >= 0;', true]
|
132
|
+
].each do |(source, predicted)|
|
133
|
+
lox = Loxxy::Interpreter.new
|
134
|
+
result = lox.evaluate(source)
|
135
|
+
expect(result.value == predicted).to be_truthy
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should evaluate the change sign of a number' do
|
140
|
+
[
|
141
|
+
['- 3;', -3],
|
142
|
+
['- - 3;', 3],
|
143
|
+
['- - - 3;', -3]
|
144
|
+
].each do |(source, predicted)|
|
145
|
+
lox = Loxxy::Interpreter.new
|
146
|
+
result = lox.evaluate(source)
|
147
|
+
expect(result.value == predicted).to be_truthy
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should evaluate the negation of an object' do
|
152
|
+
[
|
153
|
+
['!true;', false],
|
154
|
+
['!false;', true],
|
155
|
+
['!!true;', true],
|
156
|
+
['!123;', false],
|
157
|
+
['!0;', false],
|
158
|
+
['!nil;', true],
|
159
|
+
['!"";', false]
|
160
|
+
].each do |(source, predicted)|
|
161
|
+
lox = Loxxy::Interpreter.new
|
162
|
+
result = lox.evaluate(source)
|
163
|
+
expect(result.value == predicted).to be_truthy
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'should evaluate the "conjunction" of two values' do
|
168
|
+
[
|
169
|
+
# Return the first falsey argument
|
170
|
+
['false and 1;', false],
|
171
|
+
['nil and 1;', nil],
|
172
|
+
['true and 1;', 1],
|
173
|
+
['1 and 2 and false;', false],
|
174
|
+
['1 and 2 and nil;', nil],
|
175
|
+
|
176
|
+
# Return the last argument if all are truthy
|
177
|
+
['1 and true;', true],
|
178
|
+
['0 and true;', true],
|
179
|
+
['"false" and 0;', 0],
|
180
|
+
['1 and 2 and 3;', 3]
|
181
|
+
|
182
|
+
# TODO test short-circuit at first false argument
|
183
|
+
].each do |(source, predicted)|
|
184
|
+
lox = Loxxy::Interpreter.new
|
185
|
+
result = lox.evaluate(source)
|
186
|
+
expect(result.value == predicted).to be_truthy
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'should evaluate the "disjunction" of two values' do
|
191
|
+
[
|
192
|
+
# Return the first truthy argument
|
193
|
+
['1 or true;', 1],
|
194
|
+
['false or 1;', 1],
|
195
|
+
['nil or 1;', 1],
|
196
|
+
['false or false or true;', true],
|
197
|
+
['1 and 2 and nil;', nil],
|
198
|
+
|
199
|
+
# Return the last argument if all are falsey
|
200
|
+
['false or false;', false],
|
201
|
+
['nil or false;', false],
|
202
|
+
['false or nil;', nil],
|
203
|
+
['false or false or false;', false],
|
204
|
+
['false or false or nil;', nil]
|
205
|
+
|
206
|
+
# TODO test short-circuit at first false argument
|
207
|
+
].each do |(source, predicted)|
|
208
|
+
lox = Loxxy::Interpreter.new
|
209
|
+
result = lox.evaluate(source)
|
210
|
+
expect(result.value == predicted).to be_truthy
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'should support expressions between parentheses' do
|
215
|
+
[
|
216
|
+
['3 + 4 * 5;', 23],
|
217
|
+
['(3 + 4) * 5;', 35],
|
218
|
+
['(5 - (3 - 1)) + -(1);', 2]
|
219
|
+
].each do |(source, predicted)|
|
220
|
+
lox = Loxxy::Interpreter.new
|
221
|
+
result = lox.evaluate(source)
|
222
|
+
expect(result.value == predicted).to be_truthy
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should evaluate an if statement' do
|
227
|
+
[
|
228
|
+
# Evaluate the 'then' expression if the condition is true.
|
229
|
+
['if (true) print "then-branch";', 'then-branch'],
|
230
|
+
['if (false) print "ignored";', ''],
|
231
|
+
# TODO: test with then block body
|
232
|
+
# TODO: test with assignment in if condition
|
233
|
+
|
234
|
+
# Evaluate the 'else' expression if the condition is false.
|
235
|
+
['if (true) print "then-branch"; else print "else-branch";', 'then-branch'],
|
236
|
+
['if (false) print "then-branch"; else print "else-branch";', 'else-branch'],
|
237
|
+
['if (0) print "then-branch"; else print "else-branch";', 'then-branch'],
|
238
|
+
['if (nil) print "then-branch"; else print "else-branch";', 'else-branch'],
|
239
|
+
# TODO: test with else block body
|
240
|
+
|
241
|
+
# TODO: A dangling else binds to the right-most if.
|
242
|
+
# ['if (true) if (false) print "bad"; else print "good";', 'good'],
|
243
|
+
# ['if (false) if (true) print "bad"; else print "worse";', 'bad']
|
244
|
+
].each do |(source, predicted)|
|
245
|
+
io = StringIO.new
|
246
|
+
cfg = { ostream: io }
|
247
|
+
lox = Loxxy::Interpreter.new(cfg)
|
248
|
+
result = lox.evaluate(source)
|
249
|
+
expect(io.string).to eq(predicted)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'should accept variable declarations' do
|
254
|
+
# Variable with initialization value
|
255
|
+
var_decl = 'var iAmAVariable = "here is my value";'
|
256
|
+
expect { subject.evaluate(var_decl) }.not_to raise_error
|
257
|
+
|
258
|
+
# Variable without initialization value
|
259
|
+
expect { subject.evaluate('var iAmNil;') }.not_to raise_error
|
260
|
+
end
|
261
|
+
|
110
262
|
it 'should print the hello world message' do
|
111
263
|
expect { subject.evaluate(hello_world) }.not_to raise_error
|
112
264
|
expect(sample_cfg[:ostream].string).to eq('Hello, world!')
|