layo 1.0.0
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.
- data/LICENSE +26 -0
- data/README.mkd +103 -0
- data/Rakefile +21 -0
- data/UnicodeData.txt +23697 -0
- data/bin/layo +22 -0
- data/layo.gemspec +23 -0
- data/lib/layo.rb +11 -0
- data/lib/layo/ast.rb +5 -0
- data/lib/layo/ast/block.rb +13 -0
- data/lib/layo/ast/expression.rb +14 -0
- data/lib/layo/ast/node.rb +6 -0
- data/lib/layo/ast/program.rb +9 -0
- data/lib/layo/ast/statement.rb +10 -0
- data/lib/layo/interpreter.rb +360 -0
- data/lib/layo/lexer.rb +162 -0
- data/lib/layo/parser.rb +371 -0
- data/lib/layo/peekable.rb +31 -0
- data/lib/layo/runtime_error.rb +9 -0
- data/lib/layo/syntax_error.rb +14 -0
- data/lib/layo/tokenizer.rb +119 -0
- data/lib/layo/unexpected_token_error.rb +13 -0
- data/lib/layo/unicode.rb +23614 -0
- data/lib/layo/unknown_token_error.rb +7 -0
- data/spec/interpreter_spec.rb +52 -0
- data/spec/lexer_spec.rb +176 -0
- data/spec/parser_spec.rb +373 -0
- data/spec/source/basic/comments.lol +16 -0
- data/spec/source/basic/comments.out +2 -0
- data/spec/source/basic/line-continuation.lol +8 -0
- data/spec/source/basic/line-continuation.out +2 -0
- data/spec/source/basic/line-endings.lol +5 -0
- data/spec/source/basic/line-endings.out +3 -0
- data/spec/source/basic/minimal.lol +2 -0
- data/spec/source/casting/boolean.lol +8 -0
- data/spec/source/casting/boolean.out +5 -0
- data/spec/source/casting/float.lol +10 -0
- data/spec/source/casting/float.out +5 -0
- data/spec/source/casting/int.lol +9 -0
- data/spec/source/casting/int.out +4 -0
- data/spec/source/casting/nil.lol +9 -0
- data/spec/source/casting/nil.out +4 -0
- data/spec/source/casting/string.lol +5 -0
- data/spec/source/casting/string.out +2 -0
- data/spec/source/expressions/boolean.lol +30 -0
- data/spec/source/expressions/boolean.out +17 -0
- data/spec/source/expressions/cast.lol +28 -0
- data/spec/source/expressions/cast.out +20 -0
- data/spec/source/expressions/function.lol +24 -0
- data/spec/source/expressions/function.out +4 -0
- data/spec/source/expressions/math.lol +9 -0
- data/spec/source/expressions/math.out +7 -0
- data/spec/source/expressions/string.lol +20 -0
- data/spec/source/expressions/string.out +7 -0
- data/spec/source/statements/assignment.lol +8 -0
- data/spec/source/statements/assignment.out +3 -0
- data/spec/source/statements/cast.lol +11 -0
- data/spec/source/statements/cast.out +3 -0
- data/spec/source/statements/declaration.lol +9 -0
- data/spec/source/statements/declaration.out +2 -0
- data/spec/source/statements/expression.lol +10 -0
- data/spec/source/statements/expression.out +2 -0
- data/spec/source/statements/if_then_else.lol +42 -0
- data/spec/source/statements/if_then_else.out +3 -0
- data/spec/source/statements/input.in +1 -0
- data/spec/source/statements/input.lol +4 -0
- data/spec/source/statements/input.out +1 -0
- data/spec/source/statements/loop.lol +50 -0
- data/spec/source/statements/loop.out +20 -0
- data/spec/source/statements/print.lol +7 -0
- data/spec/source/statements/print.out +2 -0
- data/spec/source/statements/switch.lol +95 -0
- data/spec/source/statements/switch.out +12 -0
- data/spec/tokenizer_spec.rb +105 -0
- metadata +135 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'layo'
|
3
|
+
|
4
|
+
include Layo
|
5
|
+
|
6
|
+
describe Interpreter do
|
7
|
+
before do
|
8
|
+
@interpreter = Interpreter.new
|
9
|
+
@interpreter.output = StringIO.new
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should correctly interpret test programs" do
|
13
|
+
mask = File.join(File.dirname(__FILE__), 'source', '**', '*.lol')
|
14
|
+
Dir.glob(mask).each do |source_filename|
|
15
|
+
lexer = Lexer.new(File.new(source_filename))
|
16
|
+
parser = Parser.new(Tokenizer.new(lexer))
|
17
|
+
@interpreter.output.string = ''
|
18
|
+
|
19
|
+
# Supply input stream if provided
|
20
|
+
if File.exist?(infile = source_filename[0..-4] + 'in')
|
21
|
+
infile = File.new(infile)
|
22
|
+
@interpreter.input = infile
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get contents of output file (if provided) to assert later
|
26
|
+
if File.exist?(outfile = source_filename[0..-4] + 'out')
|
27
|
+
expected_output = File.open(outfile) do |file|
|
28
|
+
file.read
|
29
|
+
end
|
30
|
+
else
|
31
|
+
expected_output = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Execute the program
|
35
|
+
begin
|
36
|
+
@interpreter.interpret(parser.parse)
|
37
|
+
rescue RuntimeError, SyntaxError => e
|
38
|
+
puts "Error interpreting #{source_filename}"
|
39
|
+
puts e.message
|
40
|
+
end
|
41
|
+
|
42
|
+
# Assertions
|
43
|
+
if expected_output
|
44
|
+
assert_equal expected_output, @interpreter.output.string, "File: #{source_filename}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Cleanup
|
48
|
+
lexer.input.close
|
49
|
+
infile.close if infile.instance_of?(File)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/lexer_spec.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'layo'
|
6
|
+
|
7
|
+
include Layo
|
8
|
+
|
9
|
+
describe Lexer do
|
10
|
+
before do
|
11
|
+
@lexer = Lexer.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should transform all line-ending characters to \n' do
|
15
|
+
str = "\n \r\n \r "
|
16
|
+
@lexer.input = StringIO.new(str)
|
17
|
+
@lexer.next.must_equal ["\n", 1, 1]
|
18
|
+
@lexer.next.must_equal ["\n", 2, 2]
|
19
|
+
@lexer.next.must_equal ["\n", 3, 2]
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should recognize lexemes separated by whitespaces' do
|
23
|
+
@lexer.input = StringIO.new("abc def \t\tghi")
|
24
|
+
@lexer.next.must_equal ['abc', 1, 1]
|
25
|
+
@lexer.next.must_equal ['def', 1, 6]
|
26
|
+
@lexer.next.must_equal ['ghi', 1, 14]
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should recognize lexemes separated by newlines' do
|
30
|
+
@lexer.input = StringIO.new("abc\rdef\nghi")
|
31
|
+
@lexer.next.must_equal ['abc', 1, 1]
|
32
|
+
@lexer.next.must_equal ["\n", 1, 4]
|
33
|
+
@lexer.next.must_equal ['def', 2, 1]
|
34
|
+
@lexer.next.must_equal ["\n", 2, 4]
|
35
|
+
@lexer.next.must_equal ['ghi', 3, 1]
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should recognize special character lexemes' do
|
39
|
+
@lexer.input = StringIO.new("abc! ,def ,")
|
40
|
+
@lexer.next.must_equal ['abc', 1, 1]
|
41
|
+
@lexer.next.must_equal ['!', 1, 4]
|
42
|
+
# Comma acts as a virtual newline or a soft-command-break
|
43
|
+
@lexer.next.must_equal ["\n", 1, 6]
|
44
|
+
@lexer.next.must_equal ['def', 1, 7]
|
45
|
+
@lexer.next.must_equal ["\n", 1, 11]
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'when sees line ending with triple dots' do
|
49
|
+
it 'should join subsequent non-empty line' do
|
50
|
+
@lexer.input = StringIO.new("abc...\ndef…\nghi")
|
51
|
+
@lexer.next.must_equal ['abc', 1, 1]
|
52
|
+
@lexer.next.must_equal ['def', 2, 1]
|
53
|
+
@lexer.next.must_equal ['ghi', 3, 1]
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should raise a syntax error when the subsequent line is empty' do
|
57
|
+
@lexer.input = StringIO.new("abc...\n \n")
|
58
|
+
@lexer.next
|
59
|
+
lambda { @lexer.next }.must_raise Layo::SyntaxError
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should raise a syntax error when there is not subsequent line' do
|
63
|
+
@lexer.input = StringIO.new("abc...\n")
|
64
|
+
@lexer.next
|
65
|
+
lambda { @lexer.next }.must_raise Layo::SyntaxError
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'when sees BTW' do
|
70
|
+
it 'should treat everything till the end of line as a comment' do
|
71
|
+
@lexer.input = StringIO.new("abc BTW it's comment")
|
72
|
+
@lexer.next.must_equal ['abc', 1, 1]
|
73
|
+
# Newline should be added
|
74
|
+
@lexer.next[0].must_equal "\n"
|
75
|
+
@lexer.next[0].must_be_nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'when sees OBTW' do
|
80
|
+
it 'should treat all lines until TLDR as a comment' do
|
81
|
+
@lexer.input = StringIO.new("ABC
|
82
|
+
OBTW this is a long comment block
|
83
|
+
see, i have more comments here
|
84
|
+
and here
|
85
|
+
TLDR
|
86
|
+
DEF")
|
87
|
+
@lexer.next.must_equal ['ABC', 1, 1]
|
88
|
+
@lexer.next.must_equal ["\n", 1, 4]
|
89
|
+
@lexer.next.must_equal ["\n", 5, 5]
|
90
|
+
@lexer.next.must_equal ['DEF', 6, 1]
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should recognize commands before OBTW and after TLDR' do
|
94
|
+
@lexer.input = StringIO.new("ABC, OBTW
|
95
|
+
this is comment
|
96
|
+
valid comment
|
97
|
+
TLDR, DEF")
|
98
|
+
@lexer.next.must_equal ['ABC', 1, 1]
|
99
|
+
@lexer.next.must_equal ["\n", 1, 4]
|
100
|
+
@lexer.next.must_equal ['DEF', 4, 7]
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should raise a syntax error when there is not TLDR' do
|
104
|
+
@lexer.input = StringIO.new('OBTW
|
105
|
+
comment. no tldr
|
106
|
+
')
|
107
|
+
lambda { @lexer.next }.must_raise Layo::SyntaxError
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'when sees double quotation marks (")' do
|
112
|
+
it 'should treat everything till another " character as a string lexeme' do
|
113
|
+
@lexer.input = StringIO.new('"hello world"')
|
114
|
+
@lexer.next.must_equal ['"hello world"', 1, 1]
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should handle empty string' do
|
118
|
+
@lexer.input = StringIO.new('""')
|
119
|
+
@lexer.next.must_equal ['""', 1, 1]
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should handle escape characters" do
|
123
|
+
@lexer.input = StringIO.new('"Special :) :::" :> :o chars ::"')
|
124
|
+
@lexer.next[0].must_equal %["Special \n :" \t \a chars :"]
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should raise a syntax error if string is unterminated' do
|
128
|
+
@lexer.input = StringIO.new(' "bla bla bla ')
|
129
|
+
lambda { @lexer.next }.must_raise Layo::SyntaxError
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should raise a syntax error if string terminator is not followed by allowed delimiter' do
|
133
|
+
@lexer.input = StringIO.new('"a","b" "c"!"d"...
|
134
|
+
"e"…
|
135
|
+
"f"
|
136
|
+
"g"bla')
|
137
|
+
# OK, since "a" is followed by a ','
|
138
|
+
@lexer.next.must_equal ['"a"', 1, 1]
|
139
|
+
@lexer.next.must_equal ["\n", 1, 4]
|
140
|
+
# OK, since "b" is followed by a space
|
141
|
+
@lexer.next.must_equal ['"b"', 1, 5]
|
142
|
+
# OK, since "c" is followed by a '!'
|
143
|
+
@lexer.next.must_equal ['"c"', 1, 9]
|
144
|
+
@lexer.next.must_equal ['!', 1, 12]
|
145
|
+
# OK, since "d" is followed by a '...'
|
146
|
+
@lexer.next.must_equal ['"d"', 1, 13]
|
147
|
+
# OK, since "e" is followed by a '…'
|
148
|
+
@lexer.next.must_equal ['"e"', 2, 1]
|
149
|
+
# OK, since "f" is followed by a newline
|
150
|
+
@lexer.next.must_equal ['"f"', 3, 1]
|
151
|
+
@lexer.next.must_equal ["\n", 3, 4]
|
152
|
+
# Error, since "g" is not followed by any allowed delimiter
|
153
|
+
lambda { @lexer.next }.must_raise Layo::SyntaxError
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe '#escape_string' do
|
158
|
+
it "should handle escape characters" do
|
159
|
+
str = 'Example :) usage :> of :" special :o chars ::'
|
160
|
+
result = @lexer.escape_string(str)
|
161
|
+
result.must_equal %[Example \n usage \t of " special \a chars :]
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should resolve :(<hex>) into corresponding Unicode code point" do
|
165
|
+
str = 'Kazakh symbol :(049b), pound sign :(0A3)'
|
166
|
+
result = @lexer.escape_string(str)
|
167
|
+
result.must_equal 'Kazakh symbol қ, pound sign £'
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should resolve :[<char name>] into corresponding Unicode normative name" do
|
171
|
+
str = ':[CYRILLIC SMALL LETTER KA WITH DESCENDER] and :[COMMERCIAL AT]'
|
172
|
+
result = @lexer.escape_string(str)
|
173
|
+
result.must_equal 'қ and @'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mocha'
|
3
|
+
require 'layo'
|
4
|
+
|
5
|
+
include Layo
|
6
|
+
include Layo::Ast
|
7
|
+
|
8
|
+
describe Parser do
|
9
|
+
before do
|
10
|
+
@tokenizer = Tokenizer.new(Lexer.new)
|
11
|
+
@parser = Parser.new(@tokenizer)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should restore peek index after statement lookaheads" do
|
15
|
+
@tokenizer.stubs(:next_item).returns(
|
16
|
+
{:type => :identifier, :data => 'abc'},
|
17
|
+
{:type => :is_now_a}, {:type => :newline}
|
18
|
+
)
|
19
|
+
@parser.next_statement.must_equal 'cast'
|
20
|
+
@tokenizer.peek.must_equal :type => :identifier, :data => 'abc'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should restore peek index after expression lookaheads" do
|
24
|
+
@tokenizer.stubs(:next_item).returns({:type => :string, :data => 'abc'})
|
25
|
+
@parser.next_expression.must_equal 'constant'
|
26
|
+
@tokenizer.peek.must_equal :type => :string, :data => 'abc'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should parse program' do
|
30
|
+
@tokenizer.stubs(:next_item).returns(
|
31
|
+
{:type => :hai}, {:type => :float, :data => 1.2},
|
32
|
+
{:type => :newline}, {:type => :kthxbye}, {:type => :eof}
|
33
|
+
)
|
34
|
+
block = mock
|
35
|
+
@parser.expects(:parse_block).returns(block)
|
36
|
+
node = @parser.parse_program
|
37
|
+
node.must_be_instance_of Program
|
38
|
+
node.version.must_equal 1.2
|
39
|
+
node.block.must_be_same_as block
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should parse block" do
|
43
|
+
statements = [mock]
|
44
|
+
@parser.stubs(:skip_newlines)
|
45
|
+
@parser.stubs(:parse_statement).returns(*statements)
|
46
|
+
@parser.stubs(:next_statement).returns('print', nil)
|
47
|
+
node = @parser.parse_block
|
48
|
+
node.must_be_instance_of Block
|
49
|
+
node.statement_list.must_equal statements
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should parse cast statement" do
|
53
|
+
@tokenizer.stubs(:next_item).returns(
|
54
|
+
{ type: :identifier, data: 'abc' },
|
55
|
+
{ type: :is_now_a}, { type: :troof }
|
56
|
+
)
|
57
|
+
node = @parser.parse_cast_statement
|
58
|
+
node.type.must_equal 'cast'
|
59
|
+
node.identifier.must_equal 'abc'
|
60
|
+
node.to.must_equal :troof
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#parse_print_statement" do
|
64
|
+
it "should parse print statement with exclamation mark" do
|
65
|
+
@tokenizer.stubs(:next_item).returns(
|
66
|
+
{:type => :visible}, {:type => :exclamation}
|
67
|
+
)
|
68
|
+
expr = mock
|
69
|
+
@parser.expects(:parse_expression).returns(expr)
|
70
|
+
@parser.stubs(:next_expression).returns(nil)
|
71
|
+
node = @parser.parse_print_statement
|
72
|
+
node.type.must_equal 'print'
|
73
|
+
node.expressions.size.must_equal 1
|
74
|
+
node.expressions.first.must_be_same_as expr
|
75
|
+
node.suppress.must_equal true
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should parse print statement without exclamation mark" do
|
79
|
+
@tokenizer.stubs(:next_item).returns(
|
80
|
+
{:type => :visible}
|
81
|
+
)
|
82
|
+
exprs = [mock, mock]
|
83
|
+
@parser.stubs(:parse_expression).returns(*exprs)
|
84
|
+
@parser.stubs(:next_expression).returns('constant', nil)
|
85
|
+
node = @parser.parse_print_statement
|
86
|
+
node.type.must_equal 'print'
|
87
|
+
node.expressions.must_equal exprs
|
88
|
+
node.suppress.must_equal false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should parse input statement" do
|
93
|
+
@tokenizer.stubs(:next_item).returns(
|
94
|
+
{:type => :gimmeh}, {:type => :identifier, :data => 'var'},
|
95
|
+
)
|
96
|
+
node = @parser.parse_input_statement
|
97
|
+
node.type.must_equal 'input'
|
98
|
+
node.identifier.must_equal 'var'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should parse assignment statement" do
|
102
|
+
@tokenizer.stubs(:next_item).returns(
|
103
|
+
{:type => :identifier, :data => 'abc'}, {:type => :r}
|
104
|
+
)
|
105
|
+
expr = mock
|
106
|
+
@parser.expects(:parse_expression).returns(expr)
|
107
|
+
node = @parser.parse_assignment_statement
|
108
|
+
node.type.must_equal 'assignment'
|
109
|
+
node.identifier.must_equal 'abc'
|
110
|
+
node.expression.must_be_same_as expr
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#parse_declaration_statement" do
|
114
|
+
it "should parse declaration statement without initialization" do
|
115
|
+
@tokenizer.stubs(:next_item).returns(
|
116
|
+
{:type => :i_has_a}, {:type => :identifier, :data => 'abc'}
|
117
|
+
)
|
118
|
+
node = @parser.parse_declaration_statement
|
119
|
+
node.type.must_equal 'declaration'
|
120
|
+
node.identifier.must_equal 'abc'
|
121
|
+
node.initialization.must_be_nil
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should parse declaration statement with initialization" do
|
125
|
+
@tokenizer.stubs(:next_item).returns(
|
126
|
+
{:type => :i_has_a}, {:type => :identifier, :data => 'abc'},
|
127
|
+
{:type => :itz}
|
128
|
+
)
|
129
|
+
init = mock
|
130
|
+
@parser.expects(:parse_expression).returns(init)
|
131
|
+
node = @parser.parse_declaration_statement
|
132
|
+
node.type.must_equal 'declaration'
|
133
|
+
node.identifier.must_equal 'abc'
|
134
|
+
node.initialization.must_be_same_as init
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#parse_condition_statement" do
|
139
|
+
before do
|
140
|
+
@tokenizer_expectation = @tokenizer.stubs(:next_item).returns(
|
141
|
+
{:type => :o_rly?}, {:type => :newline},
|
142
|
+
{:type => :ya_rly}, {:type => :newline}
|
143
|
+
)
|
144
|
+
@block = mock
|
145
|
+
@parser_expectation = @parser.stubs(:parse_block).returns(@block)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should parse conditional statement without else's" do
|
149
|
+
@tokenizer_expectation.then.returns({:type => :oic})
|
150
|
+
node = @parser.parse_condition_statement
|
151
|
+
node.type.must_equal 'condition'
|
152
|
+
node.then.must_be_same_as @block
|
153
|
+
node.elseif.must_be_empty
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should parse conditional statement with else and elseif's" do
|
157
|
+
@tokenizer_expectation.then.returns(
|
158
|
+
{ type: :mebbe }, { type: :newline },
|
159
|
+
{:type => :no_wai}, {:type => :newline},
|
160
|
+
{:type => :oic}
|
161
|
+
)
|
162
|
+
elseif_condition = mock
|
163
|
+
elseif_block = mock
|
164
|
+
else_block = mock
|
165
|
+
@parser.stubs(:parse_expression).returns(elseif_condition)
|
166
|
+
@parser_expectation.then.returns(elseif_block, else_block)
|
167
|
+
|
168
|
+
node = @parser.parse_condition_statement
|
169
|
+
|
170
|
+
node.type.must_equal 'condition'
|
171
|
+
node.then.must_be_same_as @block
|
172
|
+
node.elseif.first[:condition].must_be_same_as elseif_condition
|
173
|
+
node.elseif.first[:block].must_be_same_as elseif_block
|
174
|
+
node.else.must_be_same_as else_block
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should parse switch statement" do
|
179
|
+
@tokenizer.stubs(:next_item).returns(
|
180
|
+
{:type => :wtf?}, {:type => :newline},
|
181
|
+
# One case
|
182
|
+
{ type: :omg }, { type: :newline },
|
183
|
+
# Default case
|
184
|
+
{:type => :omgwtf}, {:type => :newline},
|
185
|
+
{:type => :oic}
|
186
|
+
)
|
187
|
+
kase_expr, kase, default = mock, mock, mock
|
188
|
+
@parser.expects(:parse_expression).returns(kase_expr)
|
189
|
+
@parser.stubs(:parse_block).returns(kase, default)
|
190
|
+
|
191
|
+
node = @parser.parse_switch_statement
|
192
|
+
|
193
|
+
node.type.must_equal 'switch'
|
194
|
+
node.cases.first[:expression].must_be_same_as kase_expr
|
195
|
+
node.cases.first[:block].must_be_same_as kase
|
196
|
+
node.default.must_be_same_as default
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should parse break statement" do
|
200
|
+
@tokenizer.stubs(:next_item).returns({:type => :gtfo})
|
201
|
+
node = @parser.parse_break_statement
|
202
|
+
node.type.must_equal 'break'
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should parse return statement" do
|
206
|
+
@tokenizer.stubs(:next_item).returns({:type => :found_yr})
|
207
|
+
expr = mock
|
208
|
+
@parser.expects(:parse_expression).returns(expr)
|
209
|
+
node = @parser.parse_return_statement
|
210
|
+
node.type.must_equal 'return'
|
211
|
+
node.expression.must_be_same_as expr
|
212
|
+
end
|
213
|
+
|
214
|
+
describe "#parse_loop_statement" do
|
215
|
+
it "should parse loop statement" do
|
216
|
+
@tokenizer.stubs(:next_item).returns(
|
217
|
+
{:type => :im_in_yr}, {:type => :identifier, :data => 'abc'},
|
218
|
+
# Loop operation
|
219
|
+
{ type: :uppin }, { type: :yr }, { type: :identifier, data: 'i' },
|
220
|
+
# Loop condition
|
221
|
+
{ type: :wile },
|
222
|
+
{:type => :im_outta_yr}, {:type => :identifier, :data => 'abc'}
|
223
|
+
)
|
224
|
+
expr, block = mock, mock
|
225
|
+
@parser.expects(:parse_expression).returns(expr)
|
226
|
+
@parser.expects(:parse_block).returns(block)
|
227
|
+
|
228
|
+
node = @parser.parse_loop_statement
|
229
|
+
|
230
|
+
node.type.must_equal 'loop'
|
231
|
+
node.label.must_equal 'abc'
|
232
|
+
node.op.must_equal :uppin
|
233
|
+
node.counter.must_equal 'i'
|
234
|
+
node.guard[:type].must_equal :wile
|
235
|
+
node.guard[:expression].must_be_same_as expr
|
236
|
+
node.block.must_be_same_as block
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should raise exception if labels are not equal" do
|
240
|
+
@tokenizer.stubs(:next_item).returns(
|
241
|
+
{:type => :im_in_yr, :line => 1, :pos => 1},
|
242
|
+
{:type => :identifier, :data => 'foo'},
|
243
|
+
{:type => :newline}, {:type => :im_outta_yr},
|
244
|
+
{:type => :identifier, :data => 'bar'}
|
245
|
+
)
|
246
|
+
lambda { @parser.parse_loop_statement }.must_raise Layo::SyntaxError
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
it "should parse function statement" do
|
251
|
+
@tokenizer.stubs(:next_item).returns(
|
252
|
+
{:type => :how_duz_i}, {:type => :identifier, :data => 'hello'},
|
253
|
+
{:type => :newline}, {:type => :if_u_say_so}
|
254
|
+
)
|
255
|
+
block = mock
|
256
|
+
@parser.expects(:parse_block).returns(block)
|
257
|
+
node = @parser.parse_function_statement
|
258
|
+
node.type.must_equal 'function'
|
259
|
+
node.name.must_equal 'hello'
|
260
|
+
node.block.must_be_same_as block
|
261
|
+
node.args.must_equal []
|
262
|
+
end
|
263
|
+
|
264
|
+
it "should parse expression statement" do
|
265
|
+
expr = mock
|
266
|
+
@parser.expects(:parse_expression).returns(expr)
|
267
|
+
node = @parser.parse_expression_statement
|
268
|
+
node.type.must_equal 'expression'
|
269
|
+
node.expression.must_be_same_as expr
|
270
|
+
end
|
271
|
+
|
272
|
+
it "should parse cast expression" do
|
273
|
+
@tokenizer.stubs(:next_item).returns(
|
274
|
+
{:type => :maek}, {:type => :a}, {:type => :troof}
|
275
|
+
)
|
276
|
+
expr = mock
|
277
|
+
@parser.expects(:parse_expression).returns(expr)
|
278
|
+
node = @parser.parse_cast_expression
|
279
|
+
node.type.must_equal 'cast'
|
280
|
+
node.being_casted.must_be_same_as expr
|
281
|
+
node.to.must_equal :troof
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "#parse_constant_expressionession" do
|
285
|
+
it "should parse boolean values" do
|
286
|
+
@tokenizer.stubs(:next_item).returns(
|
287
|
+
{:type => :boolean, :data => true},
|
288
|
+
{:type => :boolean, :data => false}
|
289
|
+
)
|
290
|
+
node = @parser.parse_constant_expression
|
291
|
+
node.type.must_equal 'constant'
|
292
|
+
node.vtype.must_equal :boolean
|
293
|
+
node.value.must_equal true
|
294
|
+
|
295
|
+
node = @parser.parse_constant_expression
|
296
|
+
node.value.must_equal false
|
297
|
+
end
|
298
|
+
|
299
|
+
it "should parse integer values" do
|
300
|
+
@tokenizer.stubs(:next_item).returns(
|
301
|
+
{:type => :integer, :data => 567}
|
302
|
+
)
|
303
|
+
node = @parser.parse_constant_expression
|
304
|
+
node.value.must_equal 567
|
305
|
+
end
|
306
|
+
|
307
|
+
it "should parse float values" do
|
308
|
+
@tokenizer.stubs(:next_item).returns(
|
309
|
+
{:type => :float, :data => -5.234}
|
310
|
+
)
|
311
|
+
node = @parser.parse_constant_expression
|
312
|
+
node.value.must_equal -5.234
|
313
|
+
end
|
314
|
+
|
315
|
+
it "should parse string values" do
|
316
|
+
@tokenizer.stubs(:next_item).returns :type => :string, :data => 'some value'
|
317
|
+
node = @parser.parse_constant_expression
|
318
|
+
node.value.must_equal 'some value'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe '#parse_identifier_expression' do
|
323
|
+
it "should parse variable expression" do
|
324
|
+
@tokenizer.stubs(:next_item).returns({:type => :identifier, :data => 'var'})
|
325
|
+
node = @parser.parse_identifier_expression
|
326
|
+
node.type.must_equal 'variable'
|
327
|
+
node.name.must_equal 'var'
|
328
|
+
end
|
329
|
+
|
330
|
+
it "should parse function expression" do
|
331
|
+
@tokenizer.stubs(:next_item).returns({:type => :identifier, :data => 'foo'})
|
332
|
+
exprs = [mock, mock]
|
333
|
+
@parser.stubs(:functions).returns({'foo' => ['arg1', 'arg2']})
|
334
|
+
@parser.stubs(:next_expression).returns('cast', 'constant')
|
335
|
+
@parser.stubs(:parse_expression).returns(*exprs)
|
336
|
+
node = @parser.parse_identifier_expression
|
337
|
+
node.type.must_equal 'function'
|
338
|
+
node.name.must_equal 'foo'
|
339
|
+
node.parameters.must_equal exprs
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
it "should parse unary expr" do
|
344
|
+
@tokenizer.stubs(:next_item).returns({:type => :not})
|
345
|
+
expr = mock
|
346
|
+
@parser.expects(:parse_expression).returns(expr)
|
347
|
+
node = @parser.parse_unary_expression
|
348
|
+
node.type.must_equal 'unary'
|
349
|
+
node.expression.must_be_same_as expr
|
350
|
+
end
|
351
|
+
|
352
|
+
it "should parse binary expr" do
|
353
|
+
@tokenizer.stubs(:next_item).returns({:type => :both_of})
|
354
|
+
expr1, expr2 = mock, mock
|
355
|
+
@parser.stubs(:parse_expression).returns(expr1, expr2)
|
356
|
+
node = @parser.parse_binary_expression
|
357
|
+
node.type.must_equal 'binary'
|
358
|
+
node.operator.must_equal :both_of
|
359
|
+
node.left.must_be_same_as expr1
|
360
|
+
node.right.must_be_same_as expr2
|
361
|
+
end
|
362
|
+
|
363
|
+
it "should parse nary expr" do
|
364
|
+
@tokenizer.stubs(:next_item).returns({:type => :any_of}, {:type => :mkay})
|
365
|
+
expressions = [mock, mock, mock]
|
366
|
+
@parser.stubs(:parse_expression).returns(*expressions)
|
367
|
+
@parser.stubs(:next_expression).returns('constant', 'constant', nil)
|
368
|
+
node = @parser.parse_nary_expression
|
369
|
+
node.type.must_equal 'nary'
|
370
|
+
node.operator.must_equal :any_of
|
371
|
+
node.expressions.must_equal expressions
|
372
|
+
end
|
373
|
+
end
|