llip 0.1.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/History.txt +4 -0
- data/MIT-LICENSE +21 -0
- data/Manifest.txt +45 -0
- data/README.txt +148 -0
- data/Rakefile +66 -0
- data/examples/ariteval/ariteval.rb +132 -0
- data/examples/ariteval/evaluator.rb +61 -0
- data/examples/ariteval/exp.rb +104 -0
- data/lib/llip.rb +6 -0
- data/lib/llip/abstract_parser.rb +170 -0
- data/lib/llip/abstract_scanner.rb +83 -0
- data/lib/llip/buffer.rb +35 -0
- data/lib/llip/llip_error.rb +43 -0
- data/lib/llip/parser.rb +93 -0
- data/lib/llip/production_compiler.rb +168 -0
- data/lib/llip/production_specification.rb +79 -0
- data/lib/llip/recursive_production_compiler.rb +35 -0
- data/lib/llip/regexp_abstract_scanner.rb +116 -0
- data/lib/llip/regexp_parser.rb +197 -0
- data/lib/llip/regexp_scanner.rb +33 -0
- data/lib/llip/regexp_specification.rb +210 -0
- data/lib/llip/token.rb +47 -0
- data/lib/llip/visitable.rb +37 -0
- data/spec/ariteval/ariteval_spec.rb +111 -0
- data/spec/ariteval/evaluator_spec.rb +106 -0
- data/spec/ariteval/exp_spec.rb +232 -0
- data/spec/llip/abstract_parser_spec.rb +273 -0
- data/spec/llip/abstract_scanner_spec.rb +152 -0
- data/spec/llip/buffer_spec.rb +60 -0
- data/spec/llip/llip_error_spec.rb +77 -0
- data/spec/llip/parser_spec.rb +163 -0
- data/spec/llip/production_compiler_spec.rb +271 -0
- data/spec/llip/production_specification_spec.rb +75 -0
- data/spec/llip/recursive_production_compiler_spec.rb +86 -0
- data/spec/llip/regexp_abstract_scanner_spec.rb +320 -0
- data/spec/llip/regexp_parser_spec.rb +265 -0
- data/spec/llip/regexp_scanner_spec.rb +40 -0
- data/spec/llip/regexp_specification_spec.rb +734 -0
- data/spec/llip/token_spec.rb +70 -0
- data/spec/llip/visitable_spec.rb +38 -0
- data/spec/spec_helper.rb +10 -0
- metadata +110 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'production_compiler'
|
3
|
+
|
4
|
+
describe "A ProductionCompiler" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@compiler = ProductionCompiler.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have a code method which returns a String" do
|
11
|
+
@compiler.should respond_to(:code)
|
12
|
+
@compiler.code.should == ""
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have a start method which accept a name and build the definition of a method" do
|
16
|
+
@compiler.should respond_to(:start)
|
17
|
+
@compiler.start(:new_production)
|
18
|
+
@compiler.code.strip.should =~ /^def parse_new_production$/
|
19
|
+
@compiler.code.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].default.call\(@scanner,self\)\n*$/
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should have an end method which close the definition of a method" do
|
23
|
+
@compiler.should respond_to(:end)
|
24
|
+
@compiler.start(:new_production)
|
25
|
+
@compiler.end
|
26
|
+
@compiler.code.strip.should =~ /^def parse_new_production$/
|
27
|
+
@compiler.code.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].default.call\(@scanner,self\)\n*$/
|
28
|
+
@compiler.code.should =~ /^( |\t|\n)*return result\n*$/
|
29
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*end$/
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should build an 'if'..'elsif' block for a sequence of tokens" do
|
33
|
+
|
34
|
+
@compiler.start(:new_production)
|
35
|
+
@compiler.token(:first_token)
|
36
|
+
@compiler.token(:second_token)
|
37
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == :"first_token"$/
|
38
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[:"first_token"\].call\(result,@scanner,self\)$/
|
39
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*elsif @scanner.current == :"second_token"$/
|
40
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[:"second_token"\].call\(result,@scanner,self\)$/
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should build an 'if'..'elsif' block for a sequence of tokens" do
|
44
|
+
@compiler.start(:new_production)
|
45
|
+
@compiler.token("token")
|
46
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == 'token'$/
|
47
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\['token'\].call\(result,@scanner,self\)$/
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should close the 'if' block with an appropriate raising 'else' clause as a default" do
|
51
|
+
@compiler.start(:new_production)
|
52
|
+
@compiler.token(:first_token)
|
53
|
+
|
54
|
+
lambda { @compiler.end }.should_not raise_error
|
55
|
+
|
56
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == :"first_token"$/
|
57
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[:"first_token"\].call\(result,@scanner,self\)$/
|
58
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*else$/
|
59
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*raise NotAllowedTokenError.new\(@scanner.current,:"new_production"\)( |\t|\n)*end$/
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should close the 'if' block without an 'else' clause if it's specified so" do
|
63
|
+
@compiler.start(:new_production)
|
64
|
+
@compiler.token(:first_token)
|
65
|
+
|
66
|
+
lambda { @compiler.end(false) }.should_not raise_error
|
67
|
+
|
68
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == :"first_token"$/
|
69
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[:"first_token"\].call\(result,@scanner,self\)( |\t|\n)*end$/
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should produce an 'if'..'elsif' block containing lookaheads" do
|
73
|
+
@compiler.start(:new_production)
|
74
|
+
@compiler.token(["token","first","second"])
|
75
|
+
@compiler.token(["token","first"])
|
76
|
+
@compiler.token(["token","second"])
|
77
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == 'token' and @scanner.lookahead\(1\) == 'first' and @scanner.lookahead\(2\) == 'second'$/
|
78
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[\['token','first','second'\]\].call\(result,@scanner,self\)$/
|
79
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*elsif @scanner.current == 'token' and @scanner.lookahead\(1\) == 'first'$/
|
80
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[\['token','first'\]\].call\(result,@scanner,self\)$/
|
81
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*elsif @scanner.current == 'token' and @scanner.lookahead\(1\) == 'second'$/
|
82
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[\['token','second'\]\].call\(result,@scanner,self\)$/
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should not modify the lookaheads tokens" do
|
86
|
+
@compiler.start(:new_production)
|
87
|
+
lookahead = ["token","first","second"]
|
88
|
+
@compiler.token(lookahead)
|
89
|
+
lookahead.should == ["token","first","second"]
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should produce code that doesn't raise Exceptions if evaluated" do
|
93
|
+
@compiler.start(:fake)
|
94
|
+
@compiler.token(:first)
|
95
|
+
@compiler.token(:second)
|
96
|
+
@compiler.token(:third)
|
97
|
+
@compiler.token([:fourth,:lk])
|
98
|
+
@compiler.token(:fourth)
|
99
|
+
@compiler.end
|
100
|
+
|
101
|
+
@class = Class.new
|
102
|
+
lambda { @class.class_eval(@compiler.code) }.should_not raise_error
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should have a reset method which clean the code" do
|
106
|
+
@compiler.should respond_to(:reset)
|
107
|
+
@compiler.code.should == ""
|
108
|
+
@compiler.start(:fake)
|
109
|
+
@compiler.token(:first)
|
110
|
+
@compiler.token(:second)
|
111
|
+
@compiler.token(:third)
|
112
|
+
@compiler.end
|
113
|
+
@compiler.code.should_not == ""
|
114
|
+
@compiler.reset
|
115
|
+
@compiler.code.should == ""
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should have a compile method which accept a ProductionSpecification and create the code" do
|
119
|
+
@compiler.should respond_to(:compile)
|
120
|
+
|
121
|
+
@production = mock "ProductionSpecification"
|
122
|
+
@production.should_receive(:name).and_return(:fake)
|
123
|
+
@production.should_receive(:tokens).and_return({ :first => nil, :second => nil, :third => nil})
|
124
|
+
@production.should_receive(:raise_on_error).and_return(false)
|
125
|
+
|
126
|
+
@compiler.should_receive(:start).with(:fake)
|
127
|
+
@compiler.should_receive(:token).with(:first)
|
128
|
+
@compiler.should_receive(:token).with(:second)
|
129
|
+
@compiler.should_receive(:token).with(:third)
|
130
|
+
@compiler.should_receive(:end).with(false)
|
131
|
+
|
132
|
+
@compiler.compile(@production)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should compile a lookahead production correctly" do
|
136
|
+
@compiler.should respond_to(:compile)
|
137
|
+
|
138
|
+
@production = mock "Production"
|
139
|
+
@production.should_receive(:name).and_return(:fake)
|
140
|
+
@production.should_receive(:tokens).and_return({ [:look,"3"] => nil, [:look,"1"] => nil, :look => nil})
|
141
|
+
@production.should_receive(:raise_on_error).and_return(true)
|
142
|
+
|
143
|
+
@compiler.should_receive(:start).with(:fake)
|
144
|
+
@compiler.should_receive(:token).with([:look,"3"])
|
145
|
+
@compiler.should_receive(:token).with([:look,"1"])
|
146
|
+
@compiler.should_receive(:token).with(:look)
|
147
|
+
@compiler.should_receive(:end).with(true)
|
148
|
+
|
149
|
+
@compiler.compile(@production)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should have a method to sort the productions correctly" do
|
153
|
+
@compiler.should respond_to(:sort_production)
|
154
|
+
|
155
|
+
@production = mock "Production"
|
156
|
+
@production.should_receive(:tokens).and_return({ [:look,"1"] => nil, [:look,"3","2"] => nil, :everything => nil, :look => nil})
|
157
|
+
|
158
|
+
@compiler.sort_production(@production).should == [[:look,"3","2"],[:look,"1"], :look, :everything]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
describe "The code produced by a ProductionCompiler without lookaheads" do
|
164
|
+
|
165
|
+
before(:each) do
|
166
|
+
@compiler = ProductionCompiler.new
|
167
|
+
@compiler.start(:fake)
|
168
|
+
@compiler.token(:first)
|
169
|
+
@compiler.token(:second)
|
170
|
+
@compiler.token(:third)
|
171
|
+
@compiler.token("+")
|
172
|
+
@compiler.end
|
173
|
+
|
174
|
+
@class = Class.new
|
175
|
+
lambda { @class.class_eval(@compiler.code) }.should_not raise_error
|
176
|
+
@class.class_eval <<-CODE
|
177
|
+
attr_accessor :productions
|
178
|
+
attr_accessor :scanner
|
179
|
+
CODE
|
180
|
+
@instance = @class.new
|
181
|
+
@instance.productions = {
|
182
|
+
:fake => mock("ProductionSpecification")
|
183
|
+
}
|
184
|
+
@tokens = {
|
185
|
+
:first => mock('first'),
|
186
|
+
:second => mock('second'),
|
187
|
+
:third => mock('third'),
|
188
|
+
"+" => mock('+')
|
189
|
+
}
|
190
|
+
@instance.productions[:fake].should_receive(:tokens).any_number_of_times.and_return(@tokens)
|
191
|
+
@instance.scanner = mock 'scanner'
|
192
|
+
@instance.scanner.should_receive(:next).any_number_of_times
|
193
|
+
|
194
|
+
@default = mock "default"
|
195
|
+
@instance.productions[:fake].should_receive(:default).any_number_of_times.and_return(@default)
|
196
|
+
@default.should_receive(:call).and_return(nil)
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should call the specified block when received the correct token" do
|
200
|
+
@instance.scanner.should_receive(:current).and_return(:first)
|
201
|
+
@tokens[:first].should_receive(:call).with(nil,@instance.scanner,@instance)
|
202
|
+
@instance.parse_fake
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should call the specified block even if the token is a string" do
|
206
|
+
@instance.scanner.should_receive(:current).exactly(4).and_return("+")
|
207
|
+
@tokens["+"].should_receive(:call).with(nil,@instance.scanner,@instance)
|
208
|
+
@instance.parse_fake
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should call the specified block and return the correct value" do
|
212
|
+
@tokens[:second] = lambda { |result,scanner,parser| :ok }
|
213
|
+
@instance.scanner.should_receive(:current).twice.and_return(:second)
|
214
|
+
@instance.parse_fake.should == :ok
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should raise an exception if the token isn't recognized" do
|
218
|
+
token = mock "Token"
|
219
|
+
token.should_receive(:value).and_return("value")
|
220
|
+
token.should_receive(:name).and_return(:a_name)
|
221
|
+
@instance.scanner.should_receive(:current).exactly(@tokens.size + 1).times.and_return(token)
|
222
|
+
lambda { @instance.parse_fake }.should raise_error(NotAllowedTokenError)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "The code produced by a ProductionCompiler with lookaheads" do
|
227
|
+
|
228
|
+
before(:each) do
|
229
|
+
@compiler = ProductionCompiler.new
|
230
|
+
@compiler.start(:fake)
|
231
|
+
@compiler.token([:token,:first,:third])
|
232
|
+
@compiler.token([:token,:second])
|
233
|
+
@compiler.token([:token,:third])
|
234
|
+
@compiler.token(:token)
|
235
|
+
@compiler.end
|
236
|
+
|
237
|
+
@class = Class.new
|
238
|
+
lambda { @class.class_eval(@compiler.code) }.should_not raise_error
|
239
|
+
@class.class_eval <<-CODE
|
240
|
+
attr_accessor :productions
|
241
|
+
attr_accessor :scanner
|
242
|
+
CODE
|
243
|
+
@instance = @class.new
|
244
|
+
@instance.productions = {
|
245
|
+
:fake => mock("ProductionSpecification")
|
246
|
+
}
|
247
|
+
@tokens = {
|
248
|
+
[:token,:first,:third] => mock('first'),
|
249
|
+
[:token,:second] => mock('second'),
|
250
|
+
[:token,:third] => mock('third'),
|
251
|
+
:token => mock('token')
|
252
|
+
}
|
253
|
+
@instance.productions[:fake].should_receive(:tokens).any_number_of_times.and_return(@tokens)
|
254
|
+
@instance.scanner = mock 'scanner'
|
255
|
+
@scanner = @instance.scanner
|
256
|
+
|
257
|
+
@default = mock "default"
|
258
|
+
@instance.productions[:fake].should_receive(:default).any_number_of_times.and_return(@default)
|
259
|
+
@default.should_receive(:call).and_return(nil)
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should recognize the sequence [:token,:first,:third]" do
|
263
|
+
@scanner.should_receive(:current).once.and_return(:token)
|
264
|
+
@scanner.should_receive(:lookahead).with(1).once.and_return(:first)
|
265
|
+
@scanner.should_receive(:lookahead).with(2).once.and_return(:third)
|
266
|
+
|
267
|
+
@tokens[[:token,:first,:third]].should_receive(:call).with(nil,@scanner,@instance)
|
268
|
+
|
269
|
+
@instance.parse_fake
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'production_specification'
|
3
|
+
|
4
|
+
describe "A ProductionSpecification" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@production = ProductionSpecification.new(:test_production)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have a name" do
|
11
|
+
@production.should respond_to(:name)
|
12
|
+
@production.name.should == :test_production
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should allow to specify a recognized token with a block parameter" do
|
16
|
+
@production.should respond_to(:token)
|
17
|
+
lambda { @production.token(:token_name) { "hello" } }.should_not raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have some tokens" do
|
21
|
+
@production.should respond_to(:tokens)
|
22
|
+
@production.tokens.should == {}
|
23
|
+
|
24
|
+
first_block = lambda { :first_block }
|
25
|
+
second_block = lambda { :second_block }
|
26
|
+
@production.token(:first_token,&first_block)
|
27
|
+
@production.token(:second_token,second_block)
|
28
|
+
|
29
|
+
@production.tokens[:first_token].should == first_block
|
30
|
+
@production.tokens[:second_token].should == second_block
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have a 'mode' attribute with a default (:single)" do
|
34
|
+
@production.should respond_to(:mode)
|
35
|
+
@production.mode.should == :single
|
36
|
+
|
37
|
+
@production.should respond_to(:mode=)
|
38
|
+
@production.mode= :recursive
|
39
|
+
@production.mode.should == :recursive
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have a default result" do
|
43
|
+
@production.should respond_to(:default)
|
44
|
+
@production.default.should respond_to(:call)
|
45
|
+
@production.default.call.should be_nil
|
46
|
+
|
47
|
+
called = false
|
48
|
+
@production.default { called = true }
|
49
|
+
@production.default.call
|
50
|
+
called.should == true
|
51
|
+
|
52
|
+
block = mock "block"
|
53
|
+
block.should_receive(:call)
|
54
|
+
@production.default(block)
|
55
|
+
@production.default.call
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should accept as a token an array" do
|
59
|
+
@production.token([:first,:second])
|
60
|
+
@production.tokens.has_key?([:first,:second]).should be_true
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should assume that a lot of symbols as arguments are an array" do
|
64
|
+
@production.token :first, :second
|
65
|
+
@production.tokens.has_key?([:first,:second]).should be_true
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should have a :raise_on_error attribute with true as a default" do
|
69
|
+
@production.should respond_to(:raise_on_error)
|
70
|
+
@production.should respond_to(:raise_on_error=)
|
71
|
+
@production.raise_on_error.should == true
|
72
|
+
@production.raise_on_error=false
|
73
|
+
@production.raise_on_error.should == false
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'recursive_production_compiler'
|
3
|
+
|
4
|
+
describe "A RecursiveProductionCompiler" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@compiler = RecursiveProductionCompiler.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should add to the 'start' and 'end' method a behaviour that add a while cycle to the code" do
|
11
|
+
@compiler.should respond_to(:start)
|
12
|
+
@compiler.start(:new_production)
|
13
|
+
@compiler.end
|
14
|
+
|
15
|
+
@compiler.code.strip.should =~ /^def parse_new_production$/
|
16
|
+
@compiler.code.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].default.call\(@scanner,self\)\n*$/
|
17
|
+
@compiler.code.should =~ /^( |\t|\n)*while not @scanner\.current\.nil\?\n(.|\t|\n)*end$/
|
18
|
+
@compiler.code.should =~ /^( |\t|\n)*return result\n*$/
|
19
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*end$/
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should modify the 'else' behaviour to break, instead of raise" do
|
23
|
+
@compiler.start(:new_production)
|
24
|
+
@compiler.token(:first_token)
|
25
|
+
|
26
|
+
lambda { @compiler.send(:build_else) }.should_not raise_error
|
27
|
+
|
28
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*if @scanner.current == :"first_token"$/
|
29
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*result = productions\[:"new_production"\].tokens\[:"first_token"\].call\(result,@scanner,self\)$/
|
30
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*else$/
|
31
|
+
@compiler.code.strip.should =~ /^( |\t|\n)*break\n( |\t|\n)*end$/
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should produce code that doesn't raise Exceptions if evaluated" do
|
35
|
+
@compiler.start(:fake)
|
36
|
+
@compiler.token(:first)
|
37
|
+
@compiler.token(:second)
|
38
|
+
@compiler.token(:third)
|
39
|
+
@compiler.end
|
40
|
+
|
41
|
+
@class = Class.new
|
42
|
+
lambda { @class.class_eval(@compiler.code) }.should_not raise_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "The code produced by RecursiveProductionCompiler" do
|
47
|
+
|
48
|
+
before(:each) do
|
49
|
+
@compiler = RecursiveProductionCompiler.new
|
50
|
+
@compiler.start(:fake)
|
51
|
+
@compiler.token(:first)
|
52
|
+
@compiler.token(:second)
|
53
|
+
@compiler.token(:third)
|
54
|
+
@compiler.end
|
55
|
+
|
56
|
+
@class = Class.new
|
57
|
+
lambda { @class.class_eval(@compiler.code) }.should_not raise_error
|
58
|
+
@class.class_eval <<-CODE
|
59
|
+
attr_accessor :productions
|
60
|
+
attr_accessor :scanner
|
61
|
+
CODE
|
62
|
+
@instance = @class.new
|
63
|
+
@instance.productions = {
|
64
|
+
:fake => mock("ProductionSpecification")
|
65
|
+
}
|
66
|
+
@tokens = {
|
67
|
+
:first => mock('first'),
|
68
|
+
:second => mock('second'),
|
69
|
+
:third => mock('third')
|
70
|
+
}
|
71
|
+
@instance.productions[:fake].should_receive(:tokens).any_number_of_times.and_return(@tokens)
|
72
|
+
@instance.scanner = mock 'scanner'
|
73
|
+
@instance.scanner.should_receive(:next).any_number_of_times
|
74
|
+
|
75
|
+
@default = mock "default"
|
76
|
+
@instance.productions[:fake].should_receive(:default).any_number_of_times.and_return(@default)
|
77
|
+
@default.should_receive(:call).and_return(nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should call the specified block the number of times the token is given" do
|
81
|
+
@instance.scanner.should_receive(:current).exactly(7).times.and_return(*([:first]*6 + [nil]))
|
82
|
+
@tokens[:first].should_receive(:call).exactly(3).times.and_return("hello")
|
83
|
+
lambda { @instance.parse_fake.should == "hello" }.should_not raise_error
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
require 'regexp_abstract_scanner'
|
3
|
+
|
4
|
+
describe "A class descending from RegexpAbstractScanner" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@class = Class.new(RegexpAbstractScanner)
|
8
|
+
@scanner = @class.new
|
9
|
+
@class.should_receive(:build).any_number_of_times
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should respond to next and return Token.new if it isn't scanning a string" do
|
13
|
+
@scanner.should respond_to(:next)
|
14
|
+
@scanner.next.should be_kind_of(Token)
|
15
|
+
@scanner.next.should be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should allow to add some regexp adding them to its scanning table" do
|
19
|
+
@class.should respond_to(:add_regexp)
|
20
|
+
r = mock "regexp"
|
21
|
+
r.should_receive(:starting_chars).and_return(["a","b"])
|
22
|
+
lambda { @class.add_regexp(r) }.should_not raise_error
|
23
|
+
@class.scanning_table['a'].should == r
|
24
|
+
@class.scanning_table['b'].should == r
|
25
|
+
|
26
|
+
r2 = mock "regexp2"
|
27
|
+
r2.should_receive(:starting_chars).and_return(["c","d"])
|
28
|
+
@class.add_regexp(r2)
|
29
|
+
@class.scanning_table['c'].should == r2
|
30
|
+
@class.scanning_table['d'].should == r2
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should handle correctly if two regexp ('ab', 'ac') have some starting chars in common" do
|
34
|
+
r1 = mock "regexp1"
|
35
|
+
s1 = mock "state1"
|
36
|
+
s2 = mock "state2"
|
37
|
+
s3 = mock "state3"
|
38
|
+
r1.should_receive(:starting_chars).any_number_of_times.and_return(["a"])
|
39
|
+
r1.should_receive(:name).any_number_of_times.and_return("r1")
|
40
|
+
r1.should_receive(:init).any_number_of_times.and_return(s1)
|
41
|
+
s1.should_receive(:keys).any_number_of_times.and_return(["a"])
|
42
|
+
s1.should_receive(:[]).any_number_of_times.with("a").and_return(s2)
|
43
|
+
s1.should_receive(:final?).any_number_of_times.and_return(false)
|
44
|
+
s2.should_receive(:keys).any_number_of_times.and_return(['b'])
|
45
|
+
s2.should_receive(:[]).with('b').and_return(s3)
|
46
|
+
s2.should_receive(:final?).any_number_of_times.and_return(false)
|
47
|
+
s3.should_receive(:keys).any_number_of_times.and_return([])
|
48
|
+
s3.should_receive(:final?).any_number_of_times.and_return(true)
|
49
|
+
s1.should_receive(:error).any_number_of_times.and_return(:error)
|
50
|
+
s2.should_receive(:error).any_number_of_times.and_return(:error)
|
51
|
+
s3.should_receive(:error).any_number_of_times.and_return(:error)
|
52
|
+
s1.should_receive(:regexp).any_number_of_times.and_return(r1)
|
53
|
+
s2.should_receive(:regexp).any_number_of_times.and_return(r1)
|
54
|
+
s3.should_receive(:regexp).any_number_of_times.and_return(r1)
|
55
|
+
|
56
|
+
r2 = mock "regexp2"
|
57
|
+
s4 = mock "state4"
|
58
|
+
s5 = mock "state5"
|
59
|
+
s6 = mock "state6"
|
60
|
+
r2.should_receive(:starting_chars).any_number_of_times.and_return(["a"])
|
61
|
+
r2.should_receive(:name).any_number_of_times.and_return("r2")
|
62
|
+
r2.should_receive(:init).any_number_of_times.and_return(s4)
|
63
|
+
s4.should_receive(:keys).any_number_of_times.and_return(["a"])
|
64
|
+
s4.should_receive(:[]).any_number_of_times.with("a").and_return(s5)
|
65
|
+
s4.should_receive(:final?).any_number_of_times.and_return(false)
|
66
|
+
s5.should_receive(:keys).any_number_of_times.and_return(['c'])
|
67
|
+
s5.should_receive(:[]).with('c').and_return(s6)
|
68
|
+
s5.should_receive(:final?).any_number_of_times.and_return(false)
|
69
|
+
s6.should_receive(:keys).any_number_of_times.and_return([])
|
70
|
+
s6.should_receive(:final?).any_number_of_times.and_return(true)
|
71
|
+
s4.should_receive(:error).any_number_of_times.and_return(:error)
|
72
|
+
s5.should_receive(:error).any_number_of_times.and_return(:error)
|
73
|
+
s6.should_receive(:error).any_number_of_times.and_return(:error)
|
74
|
+
s4.should_receive(:regexp).any_number_of_times.and_return(r2)
|
75
|
+
s5.should_receive(:regexp).any_number_of_times.and_return(r2)
|
76
|
+
s6.should_receive(:regexp).any_number_of_times.and_return(r2)
|
77
|
+
|
78
|
+
@class.add_regexp(r1)
|
79
|
+
lambda { @class.add_regexp(r2) }.should_not raise_error
|
80
|
+
@class.scanning_table['a'].should be_kind_of(RegexpSpecification)
|
81
|
+
init = @class.scanning_table['a'].init
|
82
|
+
second = init['a']
|
83
|
+
second.keys.should == ['b','c']
|
84
|
+
second['b'].should == s3
|
85
|
+
second['c'].should == s6
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should set the default value of the scanning table to the regexp that have starting_chars == :everything" do
|
89
|
+
r = mock "regexp"
|
90
|
+
r.should_receive(:starting_chars).and_return(:everything)
|
91
|
+
lambda { @class.add_regexp(r) }.should_not raise_error
|
92
|
+
lambda { @class.scanning_table['a'].should == r }.should_not raise_error
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should expose its scanning table" do
|
96
|
+
@class.should respond_to(:scanning_table)
|
97
|
+
@class.scanning_table.should be_kind_of(Hash)
|
98
|
+
lambda { @class.scanning_table['a'] }.should_not raise_error
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should be able to scan a simple regexp" do
|
102
|
+
r = mock "regexp"
|
103
|
+
s1 = mock "state1"
|
104
|
+
s2 = mock "state"
|
105
|
+
r.should_receive(:init).and_return(s1)
|
106
|
+
r.should_receive(:starting_chars).and_return(["a"])
|
107
|
+
r.should_receive(:name).and_return(:a_name)
|
108
|
+
s1.should_receive(:[]).twice.with('a').and_return(s2)
|
109
|
+
s2.should_receive(:[]).and_return(:error)
|
110
|
+
s2.should_receive(:final?).and_return(true)
|
111
|
+
s2.should_receive(:regexp).and_return(r)
|
112
|
+
|
113
|
+
@class.add_regexp(r)
|
114
|
+
@scanner.scan("a")
|
115
|
+
token = @scanner.next
|
116
|
+
token.should be_kind_of(Token)
|
117
|
+
token.should == "a"
|
118
|
+
token.should == :a_name
|
119
|
+
token.line.should == 0
|
120
|
+
token.char.should == 0
|
121
|
+
|
122
|
+
@scanner.scan("b")
|
123
|
+
lambda { @scanner.next }.should raise_error(LLIPError)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should be able to scan '.*b' and 'eb' " do
|
127
|
+
r1 = mock "first"
|
128
|
+
s1 = mock "state1"
|
129
|
+
s2 = mock "state2"
|
130
|
+
r1.should_receive(:init).any_number_of_times.and_return(s1)
|
131
|
+
r1.should_receive(:starting_chars).and_return(:everything)
|
132
|
+
r1.should_receive(:name).any_number_of_times.and_return(:first_name)
|
133
|
+
s1.should_receive(:[]).any_number_of_times.with('b').and_return(s2)
|
134
|
+
s1.should_receive(:[]).any_number_of_times.and_return(s1)
|
135
|
+
s1.should_receive(:final?).and_return(false)
|
136
|
+
s2.should_receive(:[]).any_number_of_times.and_return(:error)
|
137
|
+
s2.should_receive(:final?).any_number_of_times.and_return(true)
|
138
|
+
s1.should_receive(:regexp).any_number_of_times.and_return(r1)
|
139
|
+
s2.should_receive(:regexp).any_number_of_times.and_return(r1)
|
140
|
+
|
141
|
+
r2 = mock "second"
|
142
|
+
s3 = mock "state3"
|
143
|
+
s4 = mock "state4"
|
144
|
+
s5 = mock "state5"
|
145
|
+
r2.should_receive(:init).any_number_of_times.and_return(s3)
|
146
|
+
r2.should_receive(:starting_chars).and_return(["e"])
|
147
|
+
r2.should_receive(:name).any_number_of_times.and_return(:second_name)
|
148
|
+
s3.should_receive(:[]).any_number_of_times.with('e').and_return(s4)
|
149
|
+
s3.should_receive(:final?).any_number_of_times.and_return(false)
|
150
|
+
s4.should_receive(:[]).any_number_of_times.with('b').and_return(s5)
|
151
|
+
s5.should_receive(:[]).any_number_of_times.and_return(:error)
|
152
|
+
s5.should_receive(:final?).any_number_of_times.and_return(true)
|
153
|
+
s3.should_receive(:regexp).any_number_of_times.and_return(r2)
|
154
|
+
s4.should_receive(:regexp).any_number_of_times.and_return(r2)
|
155
|
+
s5.should_receive(:regexp).any_number_of_times.and_return(r2)
|
156
|
+
|
157
|
+
@class.add_regexp(r1)
|
158
|
+
@class.add_regexp(r2)
|
159
|
+
|
160
|
+
@scanner.scan("acdb \nb abd")
|
161
|
+
@scanner.next.should == "acdb"
|
162
|
+
@scanner.current.char == 0
|
163
|
+
@scanner.current.line == 0
|
164
|
+
@scanner.current.name == :first_name
|
165
|
+
|
166
|
+
@scanner.current.should == "acdb"
|
167
|
+
@scanner.next.should == " \nb"
|
168
|
+
@scanner.current.char == 4
|
169
|
+
@scanner.current.line == 0
|
170
|
+
@scanner.next.should == " ab"
|
171
|
+
@scanner.current.line == 1
|
172
|
+
@scanner.current.char == 0
|
173
|
+
lambda { @scanner.next }.should raise_error(UnvalidTokenError)
|
174
|
+
|
175
|
+
@scanner.scan("b")
|
176
|
+
@scanner.next
|
177
|
+
token = @scanner.next
|
178
|
+
token.should be_nil
|
179
|
+
token.line.should == 2
|
180
|
+
|
181
|
+
@scanner.scan("eb")
|
182
|
+
token = @scanner.next
|
183
|
+
token.should == "eb"
|
184
|
+
token.name.should == :second_name
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
it "should be able to scan '\+ *'" do
|
189
|
+
r = mock "regexp"
|
190
|
+
s1 = mock "state1"
|
191
|
+
s2 = mock "state"
|
192
|
+
r.should_receive(:init).any_number_of_times.and_return(s1)
|
193
|
+
r.should_receive(:starting_chars).and_return(['+'])
|
194
|
+
r.should_receive(:name).any_number_of_times.and_return(:a_name)
|
195
|
+
s1.should_receive(:[]).any_number_of_times.with('+').and_return(s2)
|
196
|
+
s2.should_receive(:[]).any_number_of_times.with(' ').and_return(s2)
|
197
|
+
s2.should_receive(:[]).any_number_of_times.and_return(:error)
|
198
|
+
s2.should_receive(:final?).any_number_of_times.and_return(true)
|
199
|
+
s1.should_receive(:regexp).any_number_of_times.and_return(r)
|
200
|
+
s2.should_receive(:regexp).any_number_of_times.and_return(r)
|
201
|
+
|
202
|
+
@class.add_regexp(r)
|
203
|
+
@scanner.scan("+")
|
204
|
+
@scanner.next.should == "+"
|
205
|
+
@scanner.current.should == "+"
|
206
|
+
@scanner.next.should be_nil
|
207
|
+
|
208
|
+
@scanner.scan("+ ")
|
209
|
+
@scanner.next.should == "+ "
|
210
|
+
@scanner.current.should == "+ "
|
211
|
+
@scanner.next.should be_nil
|
212
|
+
|
213
|
+
@scanner.scan("+ ")
|
214
|
+
@scanner.next.should == "+ "
|
215
|
+
@scanner.current.should == "+ "
|
216
|
+
@scanner.next.should be_nil
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "A class descending from RegexpAbstractScanner with the build method exposed" do
|
222
|
+
|
223
|
+
before(:each) do
|
224
|
+
@class = Class.new(RegexpAbstractScanner)
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should be able to scan '.+' and 'eb' " do
|
228
|
+
r1 = mock "first"
|
229
|
+
s1 = mock "state1"
|
230
|
+
r1.should_receive(:init).any_number_of_times.and_return(s1)
|
231
|
+
r1.should_receive(:starting_chars).and_return(:everything)
|
232
|
+
r1.should_receive(:name).any_number_of_times.and_return(:first_name)
|
233
|
+
r1.should_receive(:last).any_number_of_times.and_return([s1])
|
234
|
+
s1.should_receive(:[]=).with("e",:error)
|
235
|
+
s1.should_receive(:[]).with("e").and_return(:error)
|
236
|
+
s1.should_receive(:[]).any_number_of_times.and_return(s1)
|
237
|
+
s1.should_receive(:final?).any_number_of_times.and_return(true)
|
238
|
+
s1.should_receive(:error).any_number_of_times.and_return(s1)
|
239
|
+
s1.should_receive(:regexp).any_number_of_times.and_return(r1)
|
240
|
+
|
241
|
+
r2 = mock "second"
|
242
|
+
s3 = mock "state3"
|
243
|
+
s4 = mock "state4"
|
244
|
+
s5 = mock "state5"
|
245
|
+
r2.should_receive(:init).any_number_of_times.and_return(s3)
|
246
|
+
r2.should_receive(:starting_chars).and_return(["e"])
|
247
|
+
r2.should_receive(:name).any_number_of_times.and_return(:second_name)
|
248
|
+
r2.should_receive(:last).any_number_of_times.and_return([s5])
|
249
|
+
s3.should_receive(:[]).any_number_of_times.with('e').and_return(s4)
|
250
|
+
s3.should_receive(:final?).any_number_of_times.and_return(false)
|
251
|
+
s4.should_receive(:[]).any_number_of_times.with('b').and_return(s5)
|
252
|
+
s5.should_receive(:[]).any_number_of_times.and_return(:error)
|
253
|
+
s5.should_receive(:final?).any_number_of_times.and_return(true)
|
254
|
+
s5.should_receive(:error).any_number_of_times.and_return(nil)
|
255
|
+
s3.should_receive(:regexp).any_number_of_times.and_return(r2)
|
256
|
+
s4.should_receive(:regexp).any_number_of_times.and_return(r2)
|
257
|
+
s5.should_receive(:regexp).any_number_of_times.and_return(r2)
|
258
|
+
|
259
|
+
@class.add_regexp(r1)
|
260
|
+
@class.add_regexp(r2)
|
261
|
+
|
262
|
+
@scanner = @class.new
|
263
|
+
|
264
|
+
@scanner.scan("acdb \nb abd")
|
265
|
+
token = @scanner.next
|
266
|
+
token.should == "acdb \nb abd"
|
267
|
+
|
268
|
+
@scanner.scan("aeb")
|
269
|
+
token = @scanner.next
|
270
|
+
token.should == "a"
|
271
|
+
token.name.should == :first_name
|
272
|
+
token = @scanner.next
|
273
|
+
token.should == "eb"
|
274
|
+
token.name.should == :second_name
|
275
|
+
|
276
|
+
@scanner.scan("eb")
|
277
|
+
token = @scanner.next
|
278
|
+
token.should == "eb"
|
279
|
+
token.name.should == :second_name
|
280
|
+
end
|
281
|
+
|
282
|
+
it "should recall the build method if a new regexp is added and the scanner has already been built" do
|
283
|
+
@class.built?.should == false
|
284
|
+
@scanner = @class.new
|
285
|
+
@class.built?.should == true
|
286
|
+
@class.should_receive(:build)
|
287
|
+
r = mock "regexp"
|
288
|
+
r.should_receive(:starting_chars).and_return(["a"])
|
289
|
+
@class.add_regexp(r)
|
290
|
+
end
|
291
|
+
|
292
|
+
it "should have a build method which sets :error for every final state which ends with '.*' or '.+'" do
|
293
|
+
@class.should respond_to(:build)
|
294
|
+
@class.should respond_to(:built?)
|
295
|
+
|
296
|
+
r1 = mock "normal"
|
297
|
+
s1 = mock "state1"
|
298
|
+
s2 = mock "state2"
|
299
|
+
r1.should_receive(:init).any_number_of_times.and_return(s1)
|
300
|
+
r1.should_receive(:starting_chars).and_return(["a"])
|
301
|
+
r1.should_receive(:last).and_return([s2])
|
302
|
+
s2.should_receive(:error).any_number_of_times.and_return(:error)
|
303
|
+
|
304
|
+
r2 = mock "everything"
|
305
|
+
s3 = mock "state3"
|
306
|
+
r2.should_receive(:init).any_number_of_times.and_return(s3)
|
307
|
+
r2.should_receive(:starting_chars).and_return(:everything)
|
308
|
+
r2.should_receive(:last).and_return([s3])
|
309
|
+
s3.should_receive(:error).any_number_of_times.and_return(s3)
|
310
|
+
s3.should_receive(:[]=).with("a",:error)
|
311
|
+
|
312
|
+
@class.add_regexp(r1)
|
313
|
+
@class.add_regexp(r2)
|
314
|
+
|
315
|
+
@class.built?.should == false
|
316
|
+
|
317
|
+
@class.build.should == @class
|
318
|
+
@class.built?.should == true
|
319
|
+
end
|
320
|
+
end
|