grammar 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/grammar.rb +692 -0
- data/samples/fact.tcl +12 -0
- data/samples/infix2postfix.rb +114 -0
- data/samples/tcl.rb +163 -0
- data/samples/test.infix +4 -0
- data/test/test_grammar.rb +274 -0
- metadata +54 -0
data/lib/grammar.rb
ADDED
@@ -0,0 +1,692 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
# = grammar.rb - specify BNF-like grammar directly in Ruby
|
3
|
+
# $Id: grammar.rb,v 1.1 2005/10/13 23:58:45 eric_mahurin Exp $
|
4
|
+
# Author:: Eric Mahurin (Eric under Mahurin at yahoo dot com)
|
5
|
+
# License:: Ruby license
|
6
|
+
# Home:: http://rubyforge.org/projects/grammar
|
7
|
+
|
8
|
+
# This base class defines common operators to the derived Grammar classes to
|
9
|
+
# make specifying the Grammar look similar to BNF. This base class also serves
|
10
|
+
# the purpose of handling recursion in the Grammar.
|
11
|
+
class Grammar
|
12
|
+
class << self
|
13
|
+
alias_method(:[],:new)
|
14
|
+
# With several interlocking recursive grammars, this can be used.
|
15
|
+
# For each argument that the block needs, an empty Grammar is
|
16
|
+
# given. The result of the block should be an Array of the final
|
17
|
+
# grammars for those arguments.
|
18
|
+
def multiple(&block) # :yield: *recursive_grammars
|
19
|
+
grammars = (1..block.arity).map { self.new }
|
20
|
+
grammars.zip(yield(*grammars)) { |g,g1| g << g1 }
|
21
|
+
grammars
|
22
|
+
end
|
23
|
+
end
|
24
|
+
# Creates a Grammar from another +grammar+. If +grammar+ is not given
|
25
|
+
# and a block is instead, the block is passed +self+ (to handle recursion)
|
26
|
+
# and the resulting grammar from this block will be used.
|
27
|
+
def initialize(grammar=nil,&block) # :yield: +self+
|
28
|
+
@grammar = grammar || block && yield(self)
|
29
|
+
end
|
30
|
+
# Reinitialize with another Grammar. This will be needed for recursion
|
31
|
+
# unless the block form of new is used.
|
32
|
+
def << (*args)
|
33
|
+
initialize(*args)
|
34
|
+
end
|
35
|
+
# Match to elements at a Cursor while advancing. When matched, a parse
|
36
|
+
# buffer is returned. Instead of an empty Array, the seed to this parse buffer
|
37
|
+
# can be given by +buffer+ which should respond to #concat and #<< like Array.
|
38
|
+
# When a mismatch occurs several possibilities exist. If +lookahead+ and
|
39
|
+
# the Grammar is within its lookahead (defaults one element/token - can be
|
40
|
+
# controlled by #lookahead), the cursor is moved back to where it started and
|
41
|
+
# +false+ is returned. Otherwise an exception describing the mismatch is
|
42
|
+
# raised.
|
43
|
+
def scan(cursor,buffer=[],lookahead=false)
|
44
|
+
@grammar.scan(cursor,buffer,lookahead)
|
45
|
+
end
|
46
|
+
# Same as #scan except the +cursor+ is held in place
|
47
|
+
def check(cursor,buffer=[],lookahead=false)
|
48
|
+
cursor.pos { (@grammar||self).scan(cursor,buffer,lookahead) }
|
49
|
+
end
|
50
|
+
def scanner(me,cursor,buffer,lookahead,hold) # :nodoc:
|
51
|
+
hold ?
|
52
|
+
"#{me}.check(#{cursor},#{buffer},#{lookahead})" :
|
53
|
+
"#{me}.scan(#{cursor},#{buffer},#{lookahead})"
|
54
|
+
end
|
55
|
+
def leaves # :nodoc:
|
56
|
+
[@grammar||self]
|
57
|
+
end
|
58
|
+
# Creates a new Grammar that matches +self+ or +other+ if that fails.
|
59
|
+
def |(other)
|
60
|
+
Inline.new(self,other) { |us,them,cursor,buffer,lookahead,hold|
|
61
|
+
"(#{us[cursor,buffer,true,hold]} ||
|
62
|
+
#{them[cursor,buffer,lookahead,hold]})"
|
63
|
+
}
|
64
|
+
end
|
65
|
+
# Creates a new Grammar that matches +self+ followed by +other+.
|
66
|
+
# The resulting match list is a concatenation from the match lists
|
67
|
+
# from +self+ and +other+.
|
68
|
+
def +(other)
|
69
|
+
Inline.new(self,other) { |us,them,cursor,buffer,lookahead|
|
70
|
+
"(#{us[cursor,buffer,lookahead,false]} &&
|
71
|
+
#{them[cursor,buffer,false,false]})"
|
72
|
+
}
|
73
|
+
end
|
74
|
+
# Generates a Grammar that matches when +self+ (in-place) and +other+.
|
75
|
+
def &(other)
|
76
|
+
Inline.new(self,other) { |us,them,cursor,buffer,lookahead,hold|
|
77
|
+
"(#{us[cursor,buffer,lookahead,true]} &&
|
78
|
+
#{them[cursor,buffer,lookahead,hold]})"
|
79
|
+
}
|
80
|
+
end
|
81
|
+
# Creates a new Grammar that matches +self+ replicated +multiplier+ times.
|
82
|
+
# +multiplier+ can be a Range to specify a variable multiplier. The
|
83
|
+
# +multiplier+ just needs to responds to #=== to determine the min and
|
84
|
+
# max iterations.
|
85
|
+
def *(multiplier)
|
86
|
+
Inline.new(self,nil,multiplier) { |us,multiplier,cursor,buffer,lookahead|
|
87
|
+
Inline.var { |n,ret,look| "(
|
88
|
+
#{n} = -1
|
89
|
+
#{ret} = false
|
90
|
+
#{look} = #{lookahead}
|
91
|
+
while true
|
92
|
+
if #{multiplier}===(#{n}+=1)
|
93
|
+
if !#{ret}
|
94
|
+
#{ret} = #{buffer}
|
95
|
+
#{look} = true
|
96
|
+
end
|
97
|
+
else
|
98
|
+
break(#{ret}) if #{ret}
|
99
|
+
end
|
100
|
+
#{us[cursor,buffer,look,false]} or break(#{ret})
|
101
|
+
#{look} = false if !#{ret}
|
102
|
+
end
|
103
|
+
)" }
|
104
|
+
}
|
105
|
+
end
|
106
|
+
# Creates a new zero-width Grammar that matches +self+.
|
107
|
+
def +@
|
108
|
+
Inline.new(self) { |us,cursor,buffer,lookahead,hold|
|
109
|
+
"(#{us[cursor,'DISCARD',lookahead,true]} && #{buffer})"
|
110
|
+
}
|
111
|
+
end
|
112
|
+
# Creates a new zero-width Grammar that matches anything but +self+.
|
113
|
+
def -@
|
114
|
+
Inline.new(self) { |us,cursor,buffer,lookahead,hold|
|
115
|
+
"(!#{us[cursor,'DISCARD',true,true]} ? #{buffer} :
|
116
|
+
!#{lookahead}&&raise(Error.new(cursor,'a negative syntatic predicate')))"
|
117
|
+
}
|
118
|
+
end
|
119
|
+
# Returns a Grammar that as long as what follows doesn't match +self+, it
|
120
|
+
# matches to the next element. Most useful for a single element Grammar.
|
121
|
+
def ~
|
122
|
+
(-self)&ANY
|
123
|
+
end
|
124
|
+
# Creates a new Grammar that optionally matches +self+.
|
125
|
+
def optional
|
126
|
+
self|NULL
|
127
|
+
end
|
128
|
+
# Matches a list of +self+ (plus possibly other stuff) one or more times.
|
129
|
+
# The arguments are an alternating list of optional terminators and
|
130
|
+
# separators. Along with #list0 you should be able to describe any
|
131
|
+
# tail recursive grammar. This is equivalent to this recursive Grammar:
|
132
|
+
#
|
133
|
+
# Grammar.new { |g| a+(z|b+(y|...g)) }
|
134
|
+
#
|
135
|
+
# where a, b, ... are +self+ and the separators and z, y, ... are the
|
136
|
+
# terminators.
|
137
|
+
#
|
138
|
+
# When a terminator is +nil+, the next item is treated
|
139
|
+
# as optional (i.e. instead of a+(nil|g), a+(g|) is used).
|
140
|
+
#
|
141
|
+
# When there is a missing terminator at the end of +term_sep+ (and it is
|
142
|
+
# non-empty), the list is not allowed to stop at that point.
|
143
|
+
def list1(*term_sep)
|
144
|
+
term_sep.push(nil) if term_sep.empty?
|
145
|
+
term_sep.unshift(self)
|
146
|
+
Inline.new(*term_sep.compact) { |*args|
|
147
|
+
cursor,buffer,lookahead = args.slice!(-3,3)
|
148
|
+
Inline.var { |look,ret|
|
149
|
+
terminated = (term_sep.size&1).nonzero? || term_sep[-1]
|
150
|
+
code = "(
|
151
|
+
#{look} = #{lookahead}
|
152
|
+
#{terminated ? (ret=false;'') : "#{ret} = false"}
|
153
|
+
while true
|
154
|
+
#{args[j=0][cursor,buffer,look,false]} or break(#{ret})
|
155
|
+
#{look} = #{terminated ? false : true}
|
156
|
+
#{terminated ? '' : "#{ret} = #{buffer}"}"
|
157
|
+
1.step(term_sep.size-1,2) { |i|
|
158
|
+
if term_sep[i]
|
159
|
+
code << "
|
160
|
+
#{args[j+=1][cursor,buffer,true,false]} and break(#{buffer})"
|
161
|
+
if i+1<term_sep.size
|
162
|
+
code << "
|
163
|
+
#{args[j+=1][cursor,buffer,false,false]} or break(false)"
|
164
|
+
end
|
165
|
+
elsif i+1<term_sep.size
|
166
|
+
code << "
|
167
|
+
#{args[j+=1][cursor,buffer,true,false]} or break(#{buffer})"
|
168
|
+
end
|
169
|
+
}
|
170
|
+
code << "
|
171
|
+
end
|
172
|
+
)"
|
173
|
+
}
|
174
|
+
}
|
175
|
+
end
|
176
|
+
# Matches a list of +self+ (plus possibly other stuff) zero or more times.
|
177
|
+
# The arguments are an alternating list of optional terminators and
|
178
|
+
# separators. Along with #list1 you should be able to describe any
|
179
|
+
# tail recursive grammar. This is equivalent to this recursive Grammar:
|
180
|
+
#
|
181
|
+
# Grammar.new { |g| x|(a+(z|b+(y|...g))) }
|
182
|
+
#
|
183
|
+
# where a, b, ... are +self+ and the separators and z, y, ..., x are the
|
184
|
+
# terminators.
|
185
|
+
#
|
186
|
+
# When a terminator is +nil+/missing, the next item is treated
|
187
|
+
# as optional.
|
188
|
+
def list0(*term_sep)
|
189
|
+
term_sep.push(nil) if (term_sep.size&1).zero?
|
190
|
+
term_sep.unshift(self)
|
191
|
+
Inline.new(*term_sep.compact) { |*args|
|
192
|
+
cursor,buffer,lookahead = args.slice!(-3,3)
|
193
|
+
Inline.var { |look,ret|
|
194
|
+
code = "("
|
195
|
+
code << "
|
196
|
+
#{look} = #{lookahead}" if term_sep[-1]
|
197
|
+
code << "
|
198
|
+
while true"
|
199
|
+
j = -2
|
200
|
+
-1.step(term_sep.size-3,2) { |i|
|
201
|
+
if term_sep[i]
|
202
|
+
code << "
|
203
|
+
#{args[j+=1][cursor,buffer,true,false]} and break(#{buffer})"
|
204
|
+
if j.zero?
|
205
|
+
code << "
|
206
|
+
#{args[j+=1][cursor,buffer,look,false]} or break(false)
|
207
|
+
#{look} = false"
|
208
|
+
else
|
209
|
+
code << "
|
210
|
+
#{args[j+=1][cursor,buffer,false,false]} or break(false)"
|
211
|
+
end
|
212
|
+
else
|
213
|
+
j += 1 if j==2
|
214
|
+
code << "
|
215
|
+
#{args[j+=1][cursor,buffer,true,false]} or break(#{buffer})"
|
216
|
+
end
|
217
|
+
}
|
218
|
+
code << "
|
219
|
+
end)"
|
220
|
+
}
|
221
|
+
}
|
222
|
+
end
|
223
|
+
# Creates a new Grammar where the entire grammar is considered a
|
224
|
+
# part of the lookahead (instead of just the first element).
|
225
|
+
def lookahead
|
226
|
+
Inline.new(self) { |us,cursor,buffer,lookahead|
|
227
|
+
Inline.var { |branch| "(
|
228
|
+
#{branch} = #{buffer}.class.new
|
229
|
+
#{cursor}.pos? { begin
|
230
|
+
#{us[cursor,branch,false]}
|
231
|
+
rescue Error => err
|
232
|
+
raise(err) if !#{lookahead}
|
233
|
+
end } && #{buffer}.concat(#{branch})
|
234
|
+
)" }
|
235
|
+
}
|
236
|
+
end
|
237
|
+
# Creates a new Grammar where the match list of +self+ is filtered by
|
238
|
+
# some code.
|
239
|
+
# When a +klass+ is given, +klass+.new is used as the buffer to hold what
|
240
|
+
# will be passed to the code. Otherwise this temporary buffer will come
|
241
|
+
# from buffer.class.new.
|
242
|
+
# If the block needs 1 argument, this temporary buffer will be passed
|
243
|
+
# and the block should return something that will be given to buffer.concat.
|
244
|
+
# If the block needs 2 arguments, the second argument will be the buffer
|
245
|
+
# and the block should do the concatenation.
|
246
|
+
# If there is no block, the temporary buffer is passed to buffer.concat
|
247
|
+
# directly. Use this to get some isolation.
|
248
|
+
def filter(klass=nil,&code) # :yield: branch[, buffer]
|
249
|
+
if !code
|
250
|
+
if klass
|
251
|
+
Inline.new(self,nil,klass) { |us,klass,cursor,buffer,lookahead,hold|
|
252
|
+
Inline.var { |branch| "(
|
253
|
+
#{branch}=#{klass}.new
|
254
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
255
|
+
#{buffer}.concat(#{branch})
|
256
|
+
)"}
|
257
|
+
}
|
258
|
+
else
|
259
|
+
Inline.new(self) { |us,cursor,buffer,lookahead,hold|
|
260
|
+
Inline.var { |branch| "(
|
261
|
+
#{branch}=#{buffer}.class.new
|
262
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
263
|
+
#{buffer}.concat(#{branch})
|
264
|
+
)"}
|
265
|
+
}
|
266
|
+
end
|
267
|
+
elsif code.arity>=2
|
268
|
+
if klass
|
269
|
+
Inline.new(self,nil,klass,code) { |us,klass,code,cursor,buffer,lookahead,hold|
|
270
|
+
Inline.var { |branch| "(
|
271
|
+
#{branch}=#{klass}.new
|
272
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
273
|
+
(#{code}[#{branch},#{buffer}]||
|
274
|
+
raise(Error.new(cursor,'a filtered '+#{branch}.inspect)))
|
275
|
+
)"}
|
276
|
+
}
|
277
|
+
else
|
278
|
+
Inline.new(self,nil,code) { |us,code,cursor,buffer,lookahead,hold|
|
279
|
+
Inline.var { |branch| "(
|
280
|
+
#{branch}=#{buffer}.class.new
|
281
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
282
|
+
(#{code}[#{branch},#{buffer}]||
|
283
|
+
raise(Error.new(cursor,'a filtered '+#{branch}.inspect)))
|
284
|
+
)"}
|
285
|
+
}
|
286
|
+
end
|
287
|
+
else
|
288
|
+
if klass
|
289
|
+
Inline.new(self,nil,klass,code) { |us,klass,code,cursor,buffer,lookahead,hold|
|
290
|
+
Inline.var { |branch| "(
|
291
|
+
#{branch}=#{klass}.new
|
292
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
293
|
+
#{buffer}.concat(#{code}[#{branch}]||
|
294
|
+
raise(Error.new(cursor,'a filtered '+#{branch}.inspect)))
|
295
|
+
)"}
|
296
|
+
}
|
297
|
+
else
|
298
|
+
Inline.new(self,nil,code) { |us,code,cursor,buffer,lookahead,hold|
|
299
|
+
Inline.var { |branch| "(
|
300
|
+
#{branch}=#{buffer}.class.new
|
301
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
302
|
+
#{buffer}.concat(#{code}[#{branch}]||
|
303
|
+
raise(Error.new(cursor,'a filtered '+#{branch}.inspect)))
|
304
|
+
)"}
|
305
|
+
}
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
# Returns a Grammar that discards the match list from +self+
|
310
|
+
def discard
|
311
|
+
Inline.new(self) { |us,cursor,buffer,lookahead,hold|
|
312
|
+
"(#{us[cursor,'DISCARD',lookahead,hold]}&&#{buffer})"
|
313
|
+
}
|
314
|
+
end
|
315
|
+
# Returns a Grammar that groups the match list from +self+. A temporary
|
316
|
+
# buffer is formed just list #filter, but buffer.<< is used instead of
|
317
|
+
# buffer.concat.
|
318
|
+
def group(klass=nil)
|
319
|
+
if klass
|
320
|
+
Inline.new(self,nil,klass) { |us,klass,cursor,buffer,lookahead,hold|
|
321
|
+
Inline.var { |branch| "(
|
322
|
+
#{branch}=#{klass}.new
|
323
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
324
|
+
#{buffer}<<#{branch}
|
325
|
+
)"}
|
326
|
+
}
|
327
|
+
else
|
328
|
+
Inline.new(self) { |us,cursor,buffer,lookahead,hold|
|
329
|
+
Inline.var { |branch| "(
|
330
|
+
#{branch}=#{buffer}.class.new
|
331
|
+
#{us[cursor,branch,lookahead,hold]} &&
|
332
|
+
#{buffer}<<#{branch}
|
333
|
+
)"}
|
334
|
+
}
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
# A Grammar that can flatten itself (with code strings) to reduce the
|
340
|
+
# amount of method calls needed while parsing. This is tricky stuff.
|
341
|
+
# Will explain later.
|
342
|
+
class Inline < Grammar
|
343
|
+
def initialize(*objects,&block) # :yield: cursor,buffer,lookahead[,hold]
|
344
|
+
@objects = objects
|
345
|
+
@block = block
|
346
|
+
end
|
347
|
+
Arg_names = %w(cursor buffer lookahead)
|
348
|
+
def scan(cursor,buffer=[],lookahead=false) # :nodoc:
|
349
|
+
(class << self;self;end).class_eval(
|
350
|
+
"def scan(cursor,buffer=[],lookahead=false)\n"+
|
351
|
+
scanner(*(_leaf_names+Arg_names+[false]))+
|
352
|
+
"\nend"
|
353
|
+
)
|
354
|
+
scan(cursor,buffer,lookahead)
|
355
|
+
end
|
356
|
+
def check(cursor,buffer=[],lookahead=false) # :nodoc:
|
357
|
+
(class << self;self;end).class_eval(
|
358
|
+
"def check(cursor,buffer=[],lookahead=false)\n"+
|
359
|
+
scanner(*(_leaf_names+Arg_names+[true]))+
|
360
|
+
"\nend"
|
361
|
+
)
|
362
|
+
check(cursor,buffer,lookahead)
|
363
|
+
end
|
364
|
+
def scanner(*leaves_args) # :nodoc:
|
365
|
+
objects = _extractors.map { |e| e[leaves_args] }
|
366
|
+
args = objects+leaves_args
|
367
|
+
if @block.arity<args.size and args.slice!(-1)
|
368
|
+
"#{leaves_args[0]}.pos{#{@block.call(*args)}}"
|
369
|
+
else
|
370
|
+
@block.call(*args)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
def leaves # :nodoc:
|
374
|
+
@_ or begin
|
375
|
+
@_ = []
|
376
|
+
@extractors = []
|
377
|
+
@objects.inject(false) { |leaf,object|
|
378
|
+
if leaf
|
379
|
+
@_ << object
|
380
|
+
@extractors << lambda { |leaves_args|
|
381
|
+
leaves_args.slice!(0)
|
382
|
+
}
|
383
|
+
true
|
384
|
+
elsif !object
|
385
|
+
true
|
386
|
+
elsif false
|
387
|
+
# enable this code to disable code flattening
|
388
|
+
@_ << object
|
389
|
+
@extractors << lambda { |leaves_args|
|
390
|
+
g = leaves_args.slice!(0)
|
391
|
+
lambda { |*args|
|
392
|
+
"#{g}.#{args.slice!(-1) ? 'check' : 'scan'}(#{args.join(',')})"
|
393
|
+
}
|
394
|
+
}
|
395
|
+
false
|
396
|
+
else
|
397
|
+
leaves = object.leaves
|
398
|
+
@_.concat(leaves)
|
399
|
+
n = leaves.size
|
400
|
+
@extractors << lambda { |leaves_args|
|
401
|
+
leaf_names = leaves_args.slice!(0,n)
|
402
|
+
lambda { |*args| object.scanner(*(leaf_names+args)) }
|
403
|
+
}
|
404
|
+
false
|
405
|
+
end
|
406
|
+
}
|
407
|
+
remove_instance_variable(:@objects)
|
408
|
+
@_
|
409
|
+
end
|
410
|
+
end
|
411
|
+
def _extractors # :nodoc:
|
412
|
+
@extractors or (leaves;@extractors)
|
413
|
+
end
|
414
|
+
def _leaf_names # :nodoc:
|
415
|
+
(0...leaves.size).map { |i| "@_[#{i}]" }
|
416
|
+
end
|
417
|
+
def inspect # :nodoc:
|
418
|
+
to_s[0..-2].concat(" #{scanner(*(leaves+Arg_names+[false]))}>")
|
419
|
+
end
|
420
|
+
@@symbol = "_0".to_sym
|
421
|
+
# used for generating "local" variable names
|
422
|
+
def self.var(&block)
|
423
|
+
critical0 = Thread.critical
|
424
|
+
Thread.critical = true
|
425
|
+
if block
|
426
|
+
begin
|
427
|
+
symbol = @@symbol
|
428
|
+
symbols = []
|
429
|
+
block.arity.times {
|
430
|
+
symbols << @@symbol
|
431
|
+
@@symbol = @@symbol.to_s.succ.to_sym
|
432
|
+
}
|
433
|
+
# this better not need other threads - critical section
|
434
|
+
yield(*symbols)
|
435
|
+
ensure
|
436
|
+
@@symbol = symbol
|
437
|
+
end
|
438
|
+
else
|
439
|
+
begin
|
440
|
+
@@symbol
|
441
|
+
ensure
|
442
|
+
@@symbol = @@symbol.to_s.succ.to_sym
|
443
|
+
end
|
444
|
+
end
|
445
|
+
ensure
|
446
|
+
Thread.critical = critical0
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# A Grammar that matches using arbitrary code
|
451
|
+
class Code < Inline
|
452
|
+
def initialize(&code) # :yield: cursor,buffer,lookahead
|
453
|
+
if code.arity<4
|
454
|
+
super(nil,code) { |code,cursor,buffer,lookahead|
|
455
|
+
"#{code}[#{cursor},#{buffer},#{lookahead}]"
|
456
|
+
}
|
457
|
+
else
|
458
|
+
super(nil,code) { |code,cursor,buffer,lookahead,hold|
|
459
|
+
"#{code}[#{cursor},#{buffer},#{lookahead},#{hold}]"
|
460
|
+
}
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# Lookup grammar from next token. Need to doc.
|
466
|
+
class Lookup < Grammar
|
467
|
+
def initialize(lookup)
|
468
|
+
@lookup = lookup
|
469
|
+
end
|
470
|
+
def scan(cursor,buffer=[],lookahead=false) # :nodoc:
|
471
|
+
v = cursor.read1next
|
472
|
+
if grammar = @lookup[v]
|
473
|
+
buffer << v
|
474
|
+
grammar.scan(cursor,buffer,false)
|
475
|
+
else
|
476
|
+
raise(Error.new(cursor,"no grammar for #{v} found in #{@lookup}"))
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
class LookupAhead < Grammar
|
481
|
+
def initialize(lookup)
|
482
|
+
@lookup = lookup
|
483
|
+
end
|
484
|
+
def scan(cursor,buffer=[],lookahead=false) # :nodoc:
|
485
|
+
v = cursor.read1after
|
486
|
+
if grammar = @lookup[v]
|
487
|
+
grammar.scan(cursor,buffer,false)
|
488
|
+
elsif lookahead
|
489
|
+
false
|
490
|
+
else
|
491
|
+
raise(Error.new(cursor,"no grammar for #{v} found in #{@lookup}"))
|
492
|
+
end
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
# Grammar that matches to a sequence. An object responding to #[index]
|
498
|
+
# (i.e. String/Array) is used to represent this sequence. Each element
|
499
|
+
# returned by #[] should respond to #== to compare each element in the
|
500
|
+
# sequence.
|
501
|
+
class Sequence < Grammar
|
502
|
+
def initialize(value,partial=false)
|
503
|
+
@value = value
|
504
|
+
@partial = partial
|
505
|
+
end
|
506
|
+
def scan(cursor,buffer=[],lookahead=false) # :nodoc:
|
507
|
+
i = cursor.scan(@value,false,false,buffer)
|
508
|
+
if !i
|
509
|
+
if lookahead
|
510
|
+
false
|
511
|
+
else
|
512
|
+
raise(Error.new(cursor,@value[0]))
|
513
|
+
end
|
514
|
+
elsif !@partial and i<0
|
515
|
+
raise(Error.new(cursor,@value[-i]))
|
516
|
+
else
|
517
|
+
buffer
|
518
|
+
end
|
519
|
+
end
|
520
|
+
def inspect
|
521
|
+
"#{self.class}.new(#{@value.inspect},#{@partial.inspect})"
|
522
|
+
end
|
523
|
+
def to_s
|
524
|
+
inspect
|
525
|
+
end
|
526
|
+
end
|
527
|
+
# Grammar that matches elements until it finds a specific sequence.
|
528
|
+
# Compare to IO#gets.
|
529
|
+
class SequenceUntil < Grammar
|
530
|
+
def initialize(value,allow_eof=false)
|
531
|
+
@value = value
|
532
|
+
@allow_eof = allow_eof
|
533
|
+
end
|
534
|
+
def scan(cursor,buffer=[],lookahead=false) # :nodoc:
|
535
|
+
len,i = cursor.scan_until(@value,false,false,buffer)
|
536
|
+
if !len
|
537
|
+
if lookahead
|
538
|
+
false
|
539
|
+
else
|
540
|
+
raise(Error.new(cursor,@value[0]))
|
541
|
+
end
|
542
|
+
elsif !@allow_eof and len.nonzero? and i<=0
|
543
|
+
raise(Error.new(cursor,@value[-i]))
|
544
|
+
else
|
545
|
+
buffer
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
# Grammar that matches to a single element. An object responding to #==
|
550
|
+
# is used to do the matching.
|
551
|
+
class Element < Inline
|
552
|
+
def initialize(value)
|
553
|
+
super(nil,value) { |value,cursor,buffer,lookahead,hold|
|
554
|
+
condition = hold ?
|
555
|
+
"#{value}==(v=#{cursor}.read1after)" :
|
556
|
+
"(v=#{cursor}.scan1next(#{value}))"
|
557
|
+
"(#{condition} ? " +
|
558
|
+
"#{buffer} << v : " +
|
559
|
+
"!#{lookahead}&&raise(Error.new(#{cursor},#{value})))"
|
560
|
+
}
|
561
|
+
end
|
562
|
+
end
|
563
|
+
# Grammar that always fails (with a +message+)
|
564
|
+
class Fail < Inline
|
565
|
+
def initialize(message)
|
566
|
+
super { |cursor,buffer,lookahead|
|
567
|
+
"!#{lookahead}&&raise(Error.new(cursor,#{message.inspect}))"
|
568
|
+
}
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
# Grammar that matches any single element
|
573
|
+
ANY = Inline.new { |cursor,buffer,lookahead,hold|
|
574
|
+
"((v=#{cursor}.read1#{hold ? 'after' : 'next'}) ? " +
|
575
|
+
"#{buffer} << v : " +
|
576
|
+
"!#{lookahead}&&raise(Error.new(#{cursor},'any element')))"
|
577
|
+
}
|
578
|
+
# Grammar that always passes and matches nothing
|
579
|
+
NULL = Inline.new { |_,buffer,_,_| "#{buffer}" }
|
580
|
+
# Grammar that matches the end-of-file (or end-of-cursor)
|
581
|
+
EOF = Inline.new { |cursor,buffer,_,_|
|
582
|
+
"(!#{cursor}.skip1after&&#{buffer})"
|
583
|
+
}
|
584
|
+
|
585
|
+
# Exception class for handling Grammar errors
|
586
|
+
class Error < RuntimeError
|
587
|
+
attr_accessor(:cursor,:expected,:found)
|
588
|
+
def initialize(cursor=nil,expected=nil,found=nil)
|
589
|
+
@cursor = cursor
|
590
|
+
@expected = expected
|
591
|
+
@found = found
|
592
|
+
end
|
593
|
+
def to_s
|
594
|
+
err = [super]
|
595
|
+
err << "expected #{@expected.inspect}" if @expected
|
596
|
+
err << "found #{@found.inspect}" if @found
|
597
|
+
begin
|
598
|
+
#err << @cursor.to_s if @cursor
|
599
|
+
rescue
|
600
|
+
end
|
601
|
+
err * ", "
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
# :stopdoc:
|
606
|
+
# Parse buffer that throws out everything
|
607
|
+
DISCARD = Class.new {
|
608
|
+
def concat(v);self;end
|
609
|
+
def << (v);self;end
|
610
|
+
define_method(:class) do;self;end # using "def class" messed up rdoc
|
611
|
+
def new;self;end
|
612
|
+
}.new
|
613
|
+
# :startdoc:
|
614
|
+
|
615
|
+
end
|
616
|
+
|
617
|
+
|
618
|
+
class Cursor
|
619
|
+
# A Cursor that gets its data from a producer Thread. This Thread is
|
620
|
+
# generated from the block given (passed +self+). The code in this
|
621
|
+
# block is expected to apply the << and concat methods to the argument
|
622
|
+
# given. The current Thread is the consumer.
|
623
|
+
#
|
624
|
+
# Unfortunately, this Cursor isn't full-featured (yet). It is not
|
625
|
+
# reversable. This will one day be reversable #pos*.
|
626
|
+
class Producer < Cursor
|
627
|
+
def initialize(max_size=16,&producer)
|
628
|
+
@buffer = []
|
629
|
+
@size = 0
|
630
|
+
@max_size = max_size
|
631
|
+
@consumer = Thread.current
|
632
|
+
@producer = Thread.new { producer[self] }
|
633
|
+
end
|
634
|
+
def new_data
|
635
|
+
[]
|
636
|
+
end
|
637
|
+
def read1next
|
638
|
+
while (Thread.critical=true;@buffer.empty?&&@producer.alive?)
|
639
|
+
Thread.critical = false
|
640
|
+
@producer.run
|
641
|
+
end
|
642
|
+
v = @buffer.shift
|
643
|
+
@size -= 1
|
644
|
+
v
|
645
|
+
ensure
|
646
|
+
Thread.critical = false
|
647
|
+
end
|
648
|
+
def read1after
|
649
|
+
v = read1next
|
650
|
+
unless v.nil?;begin
|
651
|
+
Thread.critical = true
|
652
|
+
@buffer.unshift(v)
|
653
|
+
ensure
|
654
|
+
Thread.critical = false
|
655
|
+
end;end
|
656
|
+
v
|
657
|
+
end
|
658
|
+
def skip1after
|
659
|
+
read1after.nil? ? nil : true
|
660
|
+
end
|
661
|
+
def scan1next(v)
|
662
|
+
v0 = read1next
|
663
|
+
(v0.nil? || v==v0) ? v0 : begin
|
664
|
+
Thread.critical = true
|
665
|
+
@buffer.unshift(v0)
|
666
|
+
nil
|
667
|
+
ensure
|
668
|
+
Thread.critical = false
|
669
|
+
end
|
670
|
+
end
|
671
|
+
def << (v)
|
672
|
+
while (Thread.critical=true;@size>=@max_size&&@consumer.alive?)
|
673
|
+
Thread.critical = false
|
674
|
+
@consumer.run
|
675
|
+
end
|
676
|
+
@buffer << v
|
677
|
+
@size += 1
|
678
|
+
self
|
679
|
+
ensure
|
680
|
+
Thread.critical = false
|
681
|
+
end
|
682
|
+
def concat(value)
|
683
|
+
i = 0
|
684
|
+
until (v = value[i]).nil?
|
685
|
+
self << v
|
686
|
+
end
|
687
|
+
self
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
|
data/samples/fact.tcl
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'cursor/io'
|
5
|
+
require 'cursor/indexed'
|
6
|
+
require 'grammar'
|
7
|
+
require 'duck'
|
8
|
+
require 'set'
|
9
|
+
|
10
|
+
class Expression
|
11
|
+
|
12
|
+
def self.lexer
|
13
|
+
space = Grammar::Element[Set[?\ ,?\t,?\n].
|
14
|
+
duck!(:==,:include?,:to_s,:inspect)]
|
15
|
+
spacing = space.discard.list1
|
16
|
+
|
17
|
+
alpha = Grammar::Element[(Set[?_]+(?a..?z)+(?A..?Z)).
|
18
|
+
duck!(:==,:include?,:to_s,:inspect)]
|
19
|
+
alphanum = Grammar::Element[(Set[?_]+(?0..?9)+(?a..?z)+(?A..?Z)).
|
20
|
+
duck!(:==,:include?,:to_s,:inspect)]
|
21
|
+
identifier = (alpha+alphanum.list0).
|
22
|
+
filter(String) { |iden,buf| buf << iden.to_sym }
|
23
|
+
|
24
|
+
int = Grammar::Element[(?0..?9).duck!(:==,:===)].list1
|
25
|
+
number =
|
26
|
+
(int.group(String) +
|
27
|
+
((Grammar::Element[?.]+int).optional +
|
28
|
+
((Grammar::Element[?e]|Grammar::Element[?E])+
|
29
|
+
(Grammar::Element[?+]|Grammar::Element[?-]).optional+int).optional).
|
30
|
+
group(String)).
|
31
|
+
filter(Array) { |num,buf|
|
32
|
+
buf << (num[1].empty? ? num.to_s.to_i : num.to_s.to_f)
|
33
|
+
}
|
34
|
+
|
35
|
+
hex = Grammar::Element[(
|
36
|
+
Set[]+(?0..?9)+(?a..?f)+(?A..?F)
|
37
|
+
).duck!(:==,:include?,:to_s,:inspect)]
|
38
|
+
octal = Grammar::Element[(?0..?7).duck!(:==,:===)]
|
39
|
+
backslashed = Grammar::Element[?\\].discard+(
|
40
|
+
Grammar::Element[?a].filter { "\a" } |
|
41
|
+
Grammar::Element[?b].filter { "\b" } |
|
42
|
+
Grammar::Element[?f].filter { "\f" } |
|
43
|
+
Grammar::Element[?n].filter { "\n" } |
|
44
|
+
Grammar::Element[?r].filter { "\r" } |
|
45
|
+
Grammar::Element[?t].filter { "\t" } |
|
46
|
+
Grammar::Element[?v].filter { "\v" } |
|
47
|
+
Grammar::Element[?x].discard+hex.list1.filter { |n|
|
48
|
+
eval(%Q("\\x#{n}"))
|
49
|
+
} |
|
50
|
+
octal.list1.filter { |n|
|
51
|
+
eval(%Q("\\#{n}"))
|
52
|
+
} |
|
53
|
+
Grammar::ANY
|
54
|
+
)
|
55
|
+
character =
|
56
|
+
backslashed |
|
57
|
+
Grammar::ANY
|
58
|
+
string = Grammar::Element[?\"].discard +
|
59
|
+
character.list1(Grammar::Element[?\"].discard).group(String)
|
60
|
+
other = Grammar::ANY.filter(String) { |op,buf| buf << op.to_sym }
|
61
|
+
|
62
|
+
(
|
63
|
+
spacing |
|
64
|
+
identifier |
|
65
|
+
number |
|
66
|
+
string |
|
67
|
+
other
|
68
|
+
).list0(Grammar::EOF)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.parser
|
72
|
+
integer = Grammar::Element[Integer.duck!(:==,:===)]
|
73
|
+
float = Grammar::Element[Float.duck!(:==,:===)]
|
74
|
+
string = Grammar::Element[String.duck!(:==,:===)]
|
75
|
+
identifier = Grammar::Element[lambda { |v|
|
76
|
+
Symbol===v && ( /\A[_a-zA-Z]/=~v.to_s )
|
77
|
+
}.duck!(:==)]
|
78
|
+
expression = Grammar.new { |expression|
|
79
|
+
primary =
|
80
|
+
integer |
|
81
|
+
float |
|
82
|
+
string |
|
83
|
+
identifier |
|
84
|
+
Grammar::Element[:"("].discard+expression+Grammar::Element[:")"].discard
|
85
|
+
product_op = Grammar::Element[:"*"]|Grammar::Element[:"/"]
|
86
|
+
product = primary.filter(Array) { |x,buf|
|
87
|
+
op = buf.pop
|
88
|
+
buf.concat(x)
|
89
|
+
buf << op if op
|
90
|
+
buf
|
91
|
+
}.list1(nil,product_op)
|
92
|
+
sum_op = Grammar::Element[:"+"]|Grammar::Element[:"-"]
|
93
|
+
sum = product.filter(Array) { |x,buf|
|
94
|
+
op = buf.pop
|
95
|
+
buf.concat(x)
|
96
|
+
buf << op if op
|
97
|
+
buf
|
98
|
+
}.list1(nil,sum_op)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
if __FILE__==$0
|
105
|
+
lexer = Expression.lexer
|
106
|
+
token_buffer = Cursor::Producer.new { |buffer|
|
107
|
+
lexer.scan($stdin.to_cursor,buffer)
|
108
|
+
}
|
109
|
+
parser = Expression.parser
|
110
|
+
result = parser.scan(token_buffer)
|
111
|
+
p result
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
data/samples/tcl.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'cursor/io'
|
5
|
+
require 'cursor/indexed'
|
6
|
+
require 'grammar'
|
7
|
+
require 'duck'
|
8
|
+
require 'set'
|
9
|
+
|
10
|
+
class Tcl
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
|
14
|
+
@variables = {}
|
15
|
+
@procs = {}
|
16
|
+
|
17
|
+
space = Grammar::Element[Set[?\ ,?\t].duck!(:==,:include?)].discard
|
18
|
+
newline = Grammar::Element[?\n]
|
19
|
+
command_separator = newline|Grammar::Element[?;]
|
20
|
+
hex = Grammar::Element[(
|
21
|
+
Set[]+(?0..?9)+(?a..?f)+(?A..?F)
|
22
|
+
).duck!(:==,:include?)]
|
23
|
+
octal = Grammar::Element[(?0..?7).duck!(:==,:===)]
|
24
|
+
alphanum = Grammar::Element[(
|
25
|
+
Set[?_]+(?0..?9)+(?a..?z)+(?A..?Z)
|
26
|
+
).duck!(:==,:include?)]
|
27
|
+
|
28
|
+
backslashed = Grammar::Element[?\\].discard+(
|
29
|
+
Grammar::Element[?a].filter { "\a" } |
|
30
|
+
Grammar::Element[?b].filter { "\b" } |
|
31
|
+
Grammar::Element[?f].filter { "\f" } |
|
32
|
+
Grammar::Element[?n].filter { "\n" } |
|
33
|
+
Grammar::Element[?r].filter { "\r" } |
|
34
|
+
Grammar::Element[?t].filter { "\t" } |
|
35
|
+
Grammar::Element[?v].filter { "\v" } |
|
36
|
+
Grammar::Element[?x].discard+hex.list1.filter { |n|
|
37
|
+
eval(%Q("\\x#{n}"))
|
38
|
+
} |
|
39
|
+
Grammar::Element[?u].discard+hex.list1 { |n| # don't know what to do with unicode
|
40
|
+
eval(%Q("\\x#{n}"))
|
41
|
+
} |
|
42
|
+
octal.list1.filter { |n|
|
43
|
+
eval(%Q("\\#{n}"))
|
44
|
+
} |
|
45
|
+
(newline.discard+space.list0).filter { " " } |
|
46
|
+
Grammar::ANY
|
47
|
+
)
|
48
|
+
|
49
|
+
braced_element = Grammar.new { |braced_element|
|
50
|
+
Grammar::Element[?\{]+braced_element.list0(Grammar::Element[?\}]) |
|
51
|
+
Grammar::Element[?\\].discard+(
|
52
|
+
(newline.discard+space.list0).filter { " " } |
|
53
|
+
Grammar::NULL.filter { "\\" }
|
54
|
+
) |
|
55
|
+
newline |
|
56
|
+
Grammar::ANY
|
57
|
+
}
|
58
|
+
braced = Grammar::Element[?\{].discard+braced_element.list0(Grammar::Element[?\}].discard)
|
59
|
+
|
60
|
+
element = Grammar.new
|
61
|
+
|
62
|
+
varname = (
|
63
|
+
alphanum |
|
64
|
+
Grammar::Element[?\:]*(2..+1.0/0)
|
65
|
+
).list1
|
66
|
+
index = Grammar::Element[?\(]+element.list1(Grammar::Element[?\)])
|
67
|
+
variable = (Grammar::Element[?\$].discard + (
|
68
|
+
varname.group(String) + index.group(String).optional |
|
69
|
+
braced.group(String)
|
70
|
+
)).filter { |var| @variables[var.to_s.to_sym].to_s }
|
71
|
+
|
72
|
+
quoted = Grammar::Element[?\"].discard+element.list1(Grammar::Element[?\"].discard)
|
73
|
+
comment = (Grammar::Element[?\#]+Grammar::ANY.list0(newline)).discard
|
74
|
+
|
75
|
+
commander = lambda { |terminator|
|
76
|
+
word_terminator = space.list1|+command_separator|+terminator
|
77
|
+
word = ((braced|quoted)+word_terminator|element.list1(word_terminator)).
|
78
|
+
group(String).filter {|x,t| t.concat(x)}
|
79
|
+
command = space.list0 + (
|
80
|
+
comment |
|
81
|
+
word.list0(command_separator.discard|+terminator).
|
82
|
+
filter(Array) { |com,ret|
|
83
|
+
com.empty? ? ret : ret.replace(send(*com).to_s)
|
84
|
+
}
|
85
|
+
)
|
86
|
+
command.list0(terminator.discard)
|
87
|
+
}
|
88
|
+
|
89
|
+
bracketed = Grammar::Element[?[].discard+commander[Grammar::Element[?]]]
|
90
|
+
|
91
|
+
@interpreter = commander[Grammar::EOF]
|
92
|
+
|
93
|
+
element << (backslashed | bracketed | variable | newline | Grammar::ANY)
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def interpret(cursor)
|
98
|
+
@interpreter.scan(cursor,"",false)
|
99
|
+
end
|
100
|
+
|
101
|
+
def method_missing(name,*args)
|
102
|
+
vars,body = *@procs[name]
|
103
|
+
return "<#{name}#{args.inspect}>" if !body
|
104
|
+
variables = @variables
|
105
|
+
@variables = {}
|
106
|
+
vars.zip(args).each { |var,val| @variables[var.to_sym] = val }
|
107
|
+
ret = interpret(body.to_cursor)
|
108
|
+
@variables = variables
|
109
|
+
ret
|
110
|
+
end
|
111
|
+
|
112
|
+
def proc(name,args,body)
|
113
|
+
@procs[name.to_sym] = [args,body]
|
114
|
+
""
|
115
|
+
end
|
116
|
+
|
117
|
+
def set(name,value)
|
118
|
+
@variables[name.to_sym] = value
|
119
|
+
end
|
120
|
+
|
121
|
+
def if(condition,body)
|
122
|
+
# should really use expr to get condition
|
123
|
+
unless %w(0 false no off).include?(condition.to_s)
|
124
|
+
interpret(body.to_cursor)
|
125
|
+
end
|
126
|
+
# need to handle elsif and else
|
127
|
+
""
|
128
|
+
end
|
129
|
+
|
130
|
+
def sum(*values)
|
131
|
+
values.inject(0) { |sum, v| sum + eval(v) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def product(*values)
|
135
|
+
values.inject(1) { |sum, v| sum * eval(v) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def subtract(a,b)
|
139
|
+
eval(a)-eval(b)
|
140
|
+
end
|
141
|
+
|
142
|
+
def divide(a,b)
|
143
|
+
eval(a)/eval(b)
|
144
|
+
end
|
145
|
+
|
146
|
+
def puts(str)
|
147
|
+
$stdout.print(str,"\n")
|
148
|
+
""
|
149
|
+
end
|
150
|
+
|
151
|
+
def concat(*args)
|
152
|
+
args.inject("") { |concatenated, arg| concatenated.concat(arg) }
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
if $0==__FILE__
|
158
|
+
result = Tcl.new.interpret($stdin.to_cursor)
|
159
|
+
p result
|
160
|
+
result
|
161
|
+
end
|
162
|
+
|
163
|
+
|
data/samples/test.infix
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'test/unit/collector'
|
6
|
+
require 'cursor/indexed'
|
7
|
+
require 'grammar'
|
8
|
+
|
9
|
+
module Test
|
10
|
+
module Unit
|
11
|
+
class AutoRunner
|
12
|
+
alias_method(:_options_,:options)
|
13
|
+
def options
|
14
|
+
@options = _options_
|
15
|
+
@options.on('-i', '--iterations=NUMBER', Float,
|
16
|
+
"Randomly run tests for a number of iterations.") do |iterations|
|
17
|
+
$random_iterations = iterations
|
18
|
+
end
|
19
|
+
@options.on('-s', '--seed=NUMBER', Integer,
|
20
|
+
"Random seed.") do |seed|
|
21
|
+
$random_seed = seed.nonzero?
|
22
|
+
end
|
23
|
+
@options
|
24
|
+
end
|
25
|
+
alias_method(:_run_,:run)
|
26
|
+
def run
|
27
|
+
$output_level = @output_level
|
28
|
+
$random_seed ||= (srand;srand)
|
29
|
+
srand($random_seed)
|
30
|
+
_run_
|
31
|
+
end
|
32
|
+
end
|
33
|
+
module Collector
|
34
|
+
alias_method(:_add_suite_,:add_suite)
|
35
|
+
def add_suite(destination, suite)
|
36
|
+
_add_suite_(destination, suite)
|
37
|
+
if $random_iterations
|
38
|
+
(class << suite.tests;self;end).class_eval {
|
39
|
+
def each
|
40
|
+
n = size
|
41
|
+
($random_iterations*n).to_i.times {
|
42
|
+
yield(slice(rand(n)))
|
43
|
+
}
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
destination
|
48
|
+
end
|
49
|
+
end
|
50
|
+
class TestSuite
|
51
|
+
def run(result, &progress_block)
|
52
|
+
yield(STARTED, name)
|
53
|
+
catch(:stop_suite) {
|
54
|
+
@tests.each { |test|
|
55
|
+
catch(:invalid_test) {
|
56
|
+
test.run(result, &progress_block)
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
yield(FINISHED, name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
class RandomTestCase < TestCase
|
64
|
+
def self.suite
|
65
|
+
suite = super
|
66
|
+
puts("random_seed: #{$random_seed}") if !suite.tests.empty? and $output_level>=UI::NORMAL
|
67
|
+
suite
|
68
|
+
end
|
69
|
+
undef_method(:default_test) # so that RandomTestCase is empty
|
70
|
+
def teardown
|
71
|
+
if not passed?
|
72
|
+
puts("\nrandom_seed: #{$random_seed}")
|
73
|
+
throw(:stop_suite)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class Grammar
|
82
|
+
|
83
|
+
class Test < ::Test::Unit::RandomTestCase
|
84
|
+
|
85
|
+
class Grammars
|
86
|
+
include ::Test::Unit::Assertions
|
87
|
+
def initialize
|
88
|
+
@grammar = []
|
89
|
+
@match = []
|
90
|
+
@parsed = []
|
91
|
+
@mismatch = []
|
92
|
+
@partial_match = []
|
93
|
+
end
|
94
|
+
def get
|
95
|
+
i = rand(@grammar.size.nonzero? || throw(:invalid_test))
|
96
|
+
return @grammar[i],@match[i],@parsed[i],@mismatch[i],@partial_match[i]
|
97
|
+
end
|
98
|
+
def add(grammar,match,parsed,mismatch=nil,partial_match=nil)
|
99
|
+
puts("#{grammar.inspect} #{match.inspect} #{parsed.inspect} #{mismatch.inspect} #{partial_match.inspect}") if
|
100
|
+
$output_level>=::Test::Unit::UI::VERBOSE
|
101
|
+
match.size.times { |i|
|
102
|
+
assert_equal(parsed[i],grammar.scan(match[i].to_cursor))
|
103
|
+
}
|
104
|
+
if mismatch
|
105
|
+
assert_raise(Grammar::Error){p grammar.scan(mismatch.to_cursor)}
|
106
|
+
assert_equal(false,grammar.scan(mismatch.to_cursor,[],true))
|
107
|
+
end
|
108
|
+
if partial_match
|
109
|
+
assert_raise(Grammar::Error){grammar.scan(partial_match.to_cursor)}
|
110
|
+
end
|
111
|
+
@grammar << grammar
|
112
|
+
@match << match
|
113
|
+
@parsed << parsed
|
114
|
+
@mismatch << mismatch
|
115
|
+
@partial_match << partial_match
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.suite
|
121
|
+
suite = super
|
122
|
+
self.plant
|
123
|
+
suite
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.plant
|
127
|
+
@@grammars = Grammars.new
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_Sequence
|
131
|
+
partial = rand(2)==1
|
132
|
+
value = ["a","bc"][rand(2)]
|
133
|
+
match = value.dup
|
134
|
+
if partial
|
135
|
+
match = [match,match[0,1+rand(value.size)]]
|
136
|
+
else
|
137
|
+
match = [match]
|
138
|
+
end
|
139
|
+
parsed = match.map{|s|s.unpack("C*")}
|
140
|
+
@@grammars.add(Grammar::Sequence.new(value,partial),match,parsed,"")
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_Element
|
144
|
+
value = [?a,?b][rand(2)]
|
145
|
+
match = ["" << value]
|
146
|
+
parsed = [[value]]
|
147
|
+
@@grammars.add(Grammar::Element.new(value),match,parsed,"")
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_NULL
|
151
|
+
@@grammars.add(Grammar::NULL,[""],[[]])
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_or
|
155
|
+
grammar1,match1,parsed1,mismatch1,partial1 = @@grammars.get
|
156
|
+
grammar2,match2,parsed2,mismatch2,partial2 = @@grammars.get
|
157
|
+
i = rand(match1.size)
|
158
|
+
match1 = match1[i]
|
159
|
+
parsed1 = parsed1[i]
|
160
|
+
i = rand(match2.size)
|
161
|
+
match2 = match2[i]
|
162
|
+
parsed2 = parsed2[i]
|
163
|
+
begin
|
164
|
+
# match2 shouldn't match grammar1
|
165
|
+
grammar1.scan(match2.to_cursor,[],true) and throw(:invalid_test)
|
166
|
+
rescue Grammar::Error
|
167
|
+
throw(:invalid_test) # partial match
|
168
|
+
end
|
169
|
+
@@grammars.add(
|
170
|
+
grammar1|grammar2,
|
171
|
+
[match1,match2],
|
172
|
+
[parsed1,parsed2],
|
173
|
+
mismatch1==mismatch2 ? mismatch1 : nil,
|
174
|
+
partial1==partial2 ? partial1 : nil
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_plus
|
179
|
+
grammar1,match1,parsed1,mismatch1,partial1 = @@grammars.get
|
180
|
+
grammar2,match2,parsed2,mismatch2,partial2 = @@grammars.get
|
181
|
+
i = rand(match1.size)
|
182
|
+
match1 = match1[i]
|
183
|
+
parsed1 = parsed1[i]
|
184
|
+
i = rand(match2.size)
|
185
|
+
match2 = match2[i]
|
186
|
+
parsed2 = parsed2[i]
|
187
|
+
# grammar1 shouldn't eat into match2
|
188
|
+
begin
|
189
|
+
grammar1.scan((match1+match2).to_cursor)==parsed1 or
|
190
|
+
throw(:invalid_test)
|
191
|
+
rescue Grammar::Error
|
192
|
+
throw(:invalid_test)
|
193
|
+
end
|
194
|
+
@@grammars.add(
|
195
|
+
grammar1+grammar2,
|
196
|
+
[match1+match2],
|
197
|
+
[parsed1+parsed2],
|
198
|
+
mismatch1,
|
199
|
+
partial1 || (mismatch2 && match1+mismatch2)
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_Grammar
|
204
|
+
grammar1,match1,parsed1,mismatch1,partial1 = @@grammars.get
|
205
|
+
grammar2,match2,parsed2,mismatch2,partial2 = @@grammars.get
|
206
|
+
i = rand(match1.size)
|
207
|
+
match1 = match1[i]
|
208
|
+
parsed1 = parsed1[i]
|
209
|
+
i = rand(match2.size)
|
210
|
+
match2 = match2[i]
|
211
|
+
parsed2 = parsed2[i]
|
212
|
+
!match1.empty? or throw(:invalid_test)
|
213
|
+
begin
|
214
|
+
(grammar1.scan((match1+match1).to_cursor)==parsed1 and
|
215
|
+
grammar1.scan((match1+match2).to_cursor)==parsed1 and
|
216
|
+
grammar2.scan((match2+match2).to_cursor)==parsed2) or
|
217
|
+
throw(:invalid_test)
|
218
|
+
grammar1.scan(match2.to_cursor,[],true) and throw(:invalid_test)
|
219
|
+
rescue Grammar::Error
|
220
|
+
throw(:invalid_test)
|
221
|
+
end
|
222
|
+
grammar = Grammar.new
|
223
|
+
grammar << grammar1+(grammar|Grammar::NULL)+grammar2
|
224
|
+
@@grammars.add(
|
225
|
+
grammar,
|
226
|
+
[match1+match2,match1+match1+match1+match2+match2+match2],
|
227
|
+
[parsed1+parsed2,parsed1+parsed1+parsed1+parsed2+parsed2+parsed2],
|
228
|
+
mismatch1,
|
229
|
+
partial1
|
230
|
+
)
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_times
|
234
|
+
grammar1,match1,parsed1,mismatch1,partial1 = @@grammars.get
|
235
|
+
begin
|
236
|
+
grammar1.scan("".to_cursor,[],true) and throw(:invalid_test)
|
237
|
+
rescue Grammar::Error
|
238
|
+
throw(:invalid_test)
|
239
|
+
end
|
240
|
+
min = rand(2)
|
241
|
+
diff = rand(4)
|
242
|
+
match = ""
|
243
|
+
parsed = []
|
244
|
+
match0 = nil
|
245
|
+
parsed0 = nil
|
246
|
+
(min+rand(diff+1)).times {
|
247
|
+
i = rand(match1.size)
|
248
|
+
match.concat(match1[i])
|
249
|
+
parsed.concat(parsed1[i])
|
250
|
+
if match0
|
251
|
+
begin
|
252
|
+
grammar1.scan((match0+match1[i]).to_cursor)==parsed0 or
|
253
|
+
throw(:invalid_test)
|
254
|
+
rescue Grammar::Error
|
255
|
+
throw(:invalid_test)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
match0 = match1[i]
|
259
|
+
parsed0 = parsed1[i]
|
260
|
+
}
|
261
|
+
diff = 1.0/0.0 if diff==3
|
262
|
+
multiplier = (diff.zero? && rand(2).zero?) ? min : (min..(min+diff))
|
263
|
+
@@grammars.add(
|
264
|
+
grammar1*multiplier,
|
265
|
+
[match],
|
266
|
+
[parsed],
|
267
|
+
min.zero? ? nil : mismatch1,
|
268
|
+
min.zero? ? nil : (partial1 || (min>1 && match1[rand(match1.size)]))
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.10
|
3
|
+
specification_version: 1
|
4
|
+
name: grammar
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.5"
|
7
|
+
date: 2005-10-13
|
8
|
+
summary: BNF-like grammar specified directly in ruby
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: Eric under Mahurin at yahoo dot com
|
12
|
+
homepage: http://rubyforge.org/projects/grammar/
|
13
|
+
rubyforge_project: grammar
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir:
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
-
|
22
|
+
- ">"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.0.0
|
25
|
+
version:
|
26
|
+
platform: ruby
|
27
|
+
authors:
|
28
|
+
- Eric Mahurin
|
29
|
+
files:
|
30
|
+
- lib/grammar.rb
|
31
|
+
- test/test_grammar.rb
|
32
|
+
- samples/fact.tcl
|
33
|
+
- samples/infix2postfix.rb
|
34
|
+
- samples/tcl.rb
|
35
|
+
- samples/test.infix
|
36
|
+
- samples/CVS
|
37
|
+
test_files:
|
38
|
+
- test/test_grammar.rb
|
39
|
+
rdoc_options: []
|
40
|
+
extra_rdoc_files: []
|
41
|
+
executables: []
|
42
|
+
extensions: []
|
43
|
+
requirements: []
|
44
|
+
dependencies:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: cursor
|
47
|
+
version_requirement:
|
48
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
49
|
+
requirements:
|
50
|
+
-
|
51
|
+
- ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0.9"
|
54
|
+
version:
|