grammar 0.5
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/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:
|