citrus 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/benchmark/seqpar.citrus +14 -0
- data/benchmark/seqpar.gnuplot +15 -0
- data/benchmark/seqpar.rb +110 -0
- data/citrus.gemspec +6 -3
- data/extras/citrus.vim +3 -3
- data/lib/citrus.rb +101 -56
- data/lib/citrus/file.rb +1 -1
- data/test/{calc_peg_test.rb → calc_file_test.rb} +1 -1
- data/test/file_test.rb +4 -0
- data/test/repeat_test.rb +9 -4
- data/test/rule_test.rb +4 -4
- metadata +12 -9
@@ -0,0 +1,14 @@
|
|
1
|
+
grammar SeqPar
|
2
|
+
rule statement
|
3
|
+
'par ' (statement ' ')+ 'end'
|
4
|
+
| 'sequence' ' ' (statement ' ')+ 'end'
|
5
|
+
| 'seq' ' ' (statement ' ')+ 'end'
|
6
|
+
| ('fit' [\s] (statement ' ')+ 'end') {
|
7
|
+
def foo
|
8
|
+
"foo"
|
9
|
+
end
|
10
|
+
}
|
11
|
+
| 'art'+ [ ] (statement ' ')+ 'end'
|
12
|
+
| [A-Z] [a-zA-z0-9]*
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
f1(x) = a*x
|
2
|
+
a = 0.5
|
3
|
+
fit f1(x) 'before.dat' using 1:2 via a
|
4
|
+
|
5
|
+
f2(x) = b*x
|
6
|
+
b = 0.5
|
7
|
+
fit f2(x) 'after.dat' using 1:2 via b
|
8
|
+
|
9
|
+
set xlabel "Length of input"
|
10
|
+
set ylabel "CPU time to parse"
|
11
|
+
|
12
|
+
plot a*x title 'a*x (Before)',\
|
13
|
+
b*x title 'b*x (After)',\
|
14
|
+
"before.dat" using 1:2 title 'Before', \
|
15
|
+
"after.dat" using 1:2 title 'After'
|
data/benchmark/seqpar.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Benchmarking written by Bernard Lambeau and Jason Garber of the Treetop
|
2
|
+
# project.
|
3
|
+
#
|
4
|
+
# To test your optimizations:
|
5
|
+
# 1. Run ruby seqpar.rb
|
6
|
+
# 2. cp after.dat before.dat
|
7
|
+
# 3. Make your modifications to the Citrus code
|
8
|
+
# 4. Run ruby seqpar.rb
|
9
|
+
# 5. Run gnuplot seqpar.gnuplot
|
10
|
+
#
|
11
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
12
|
+
require 'citrus'
|
13
|
+
require 'benchmark'
|
14
|
+
|
15
|
+
srand(47562) # So it runs the same each time
|
16
|
+
|
17
|
+
class Array
|
18
|
+
def sum
|
19
|
+
inject(0) {|m, x| m + x }
|
20
|
+
end
|
21
|
+
|
22
|
+
def mean
|
23
|
+
sum / size
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class SeqParBenchmark
|
28
|
+
OPERATORS = ["seq", "fit", "art" * 5, "par", "sequence"]
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@where = File.expand_path('..', __FILE__)
|
32
|
+
Citrus.load(File.join(@where, 'seqpar'))
|
33
|
+
@grammar = SeqPar
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks the grammar
|
37
|
+
def check
|
38
|
+
[ "Task",
|
39
|
+
"seq Task end",
|
40
|
+
"par Task end",
|
41
|
+
"seq Task Task end",
|
42
|
+
"par Task Task end",
|
43
|
+
"par seq Task end Task end",
|
44
|
+
"par seq seq Task end end Task end",
|
45
|
+
"seq Task par seq Task end Task end Task end"
|
46
|
+
].each do |input|
|
47
|
+
@grammar.parse(input)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates an input text
|
52
|
+
def generate(depth=0)
|
53
|
+
return "Task" if depth > 7
|
54
|
+
return "seq #{generate(depth + 1)} end" if depth == 0
|
55
|
+
|
56
|
+
which = rand(OPERATORS.length)
|
57
|
+
|
58
|
+
case which
|
59
|
+
when 0
|
60
|
+
"Task"
|
61
|
+
else
|
62
|
+
raise unless OPERATORS[which]
|
63
|
+
buffer = "#{OPERATORS[which]} "
|
64
|
+
0.upto(rand(4) + 1) do
|
65
|
+
buffer << generate(depth + 1) << " "
|
66
|
+
end
|
67
|
+
buffer << "end"
|
68
|
+
buffer
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Launches benchmarking
|
73
|
+
def benchmark
|
74
|
+
number_by_size = Hash.new {|h,k| h[k] = 0}
|
75
|
+
time_by_size = Hash.new {|h,k| h[k] = 0}
|
76
|
+
0.upto(250) do |i|
|
77
|
+
input = generate
|
78
|
+
length = input.length
|
79
|
+
puts "Launching #{i}: #{input.length}"
|
80
|
+
# puts input
|
81
|
+
tms = Benchmark.measure { @grammar.parse(input) }
|
82
|
+
number_by_size[length] += 1
|
83
|
+
time_by_size[length] += tms.total * 1000
|
84
|
+
end
|
85
|
+
# puts number_by_size.inspect
|
86
|
+
# puts time_by_size.inspect
|
87
|
+
|
88
|
+
File.open(File.join(@where, 'after.dat'), 'w') do |dat|
|
89
|
+
number_by_size.keys.sort.each do |size|
|
90
|
+
dat << "#{size} #{(time_by_size[size]/number_by_size[size]).truncate}\n"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if File.exists?(File.join(@where, 'before.dat'))
|
95
|
+
before = {}
|
96
|
+
performance_increases = []
|
97
|
+
File.foreach(File.join(@where, 'before.dat')) do |line|
|
98
|
+
size, time = line.split(' ')
|
99
|
+
before[size] = time
|
100
|
+
end
|
101
|
+
File.foreach(File.join(@where, 'after.dat')) do |line|
|
102
|
+
size, time = line.split(' ')
|
103
|
+
performance_increases << (before[size].to_f - time.to_f) / before[size].to_f unless time == "0" || before[size] == "0"
|
104
|
+
end
|
105
|
+
puts "Average performance increase: #{(performance_increases.mean * 100 * 10).round / 10.0}%"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
SeqParBenchmark.new.benchmark
|
data/citrus.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'citrus'
|
3
|
-
s.version = '1.
|
4
|
-
s.date = '2010-
|
3
|
+
s.version = '1.2.0'
|
4
|
+
s.date = '2010-06-02'
|
5
5
|
|
6
6
|
s.summary = 'Parsing Expressions for Ruby'
|
7
7
|
s.description = 'Parsing Expressions for Ruby'
|
@@ -11,9 +11,12 @@ Gem::Specification.new do |s|
|
|
11
11
|
|
12
12
|
s.require_paths = %w< lib >
|
13
13
|
|
14
|
-
s.files = Dir['
|
14
|
+
s.files = Dir['benchmark/*.rb'] +
|
15
|
+
Dir['benchmark/*.citrus'] +
|
16
|
+
Dir['benchmark/*.gnuplot'] +
|
15
17
|
Dir['examples/**/*'] +
|
16
18
|
Dir['extras/**/*'] +
|
19
|
+
Dir['lib/**/*.rb'] +
|
17
20
|
Dir['test/*.rb'] +
|
18
21
|
%w< citrus.gemspec Rakefile README >
|
19
22
|
|
data/extras/citrus.vim
CHANGED
@@ -15,8 +15,8 @@ syn case match
|
|
15
15
|
|
16
16
|
syn match ctDoubleColon "::" contained
|
17
17
|
syn match ctConstant "\u\w*" contained
|
18
|
-
syn match ctVariable "\l\w*" contained
|
19
18
|
syn match ctModule "\(\(::\)\?\u\w*\)\+" contains=ctDoubleColon,ctConstant contained
|
19
|
+
syn match ctVariable "\a[a-zA-Z_-]*" contained
|
20
20
|
|
21
21
|
" Comments
|
22
22
|
syn match ctComment "#.*" contains=@Spell
|
@@ -56,7 +56,7 @@ syn match ctRule "\<rule\>" nextgroup=ctVariable skipwhite skipnl containe
|
|
56
56
|
|
57
57
|
" Blocks
|
58
58
|
syn region ctGrammarBlock start="\<grammar\>" matchgroup=ctGrammar end="\<end\>" contains=ctComment,ctGrammar,ctInclude,ctRoot,ctRuleBlock fold
|
59
|
-
syn region ctRuleBlock start="\<rule\>" matchgroup=ctRule end="\<end\>" contains=ALLBUT,ctRequire,ctGrammar,ctInclude,ctRoot,ctConstant
|
59
|
+
syn region ctRuleBlock start="\<rule\>" matchgroup=ctRule end="\<end\>" contains=ALLBUT,ctRequire,ctGrammar,ctInclude,ctRoot,ctConstant fold
|
60
60
|
|
61
61
|
" Groups
|
62
62
|
hi def link ctComment Comment
|
@@ -84,7 +84,7 @@ hi def link ctStringDelimiter Delimiter
|
|
84
84
|
hi def link ctRegexpSpecial ctStringSpecial
|
85
85
|
hi def link ctStringSpecial Special
|
86
86
|
|
87
|
-
hi def link ctQuantifier
|
87
|
+
hi def link ctQuantifier Number
|
88
88
|
hi def link ctOperator Operator
|
89
89
|
|
90
90
|
let b:current_syntax = "citrus"
|
data/lib/citrus.rb
CHANGED
@@ -4,11 +4,11 @@
|
|
4
4
|
#
|
5
5
|
# http://github.com/mjijackson/citrus
|
6
6
|
module Citrus
|
7
|
-
VERSION = [1,
|
7
|
+
VERSION = [1, 2, 0]
|
8
8
|
|
9
9
|
Infinity = 1.0 / 0
|
10
10
|
|
11
|
-
autoload
|
11
|
+
autoload :File, 'citrus/file'
|
12
12
|
|
13
13
|
# Returns the current version of Citrus as a string.
|
14
14
|
def self.version
|
@@ -28,8 +28,7 @@ module Citrus
|
|
28
28
|
# Evaluates the given Citrus parsing expression grammar +code+ in the global
|
29
29
|
# scope. Returns an array of any grammar modules that were created.
|
30
30
|
def self.eval(code)
|
31
|
-
|
32
|
-
file.value
|
31
|
+
File.parse(code).value
|
33
32
|
end
|
34
33
|
|
35
34
|
# This error is raised whenever a parse fails.
|
@@ -147,7 +146,6 @@ module Citrus
|
|
147
146
|
# grammar.
|
148
147
|
def rule(name, obj=nil)
|
149
148
|
sym = name.to_sym
|
150
|
-
|
151
149
|
obj = Proc.new.call if block_given?
|
152
150
|
|
153
151
|
if obj
|
@@ -166,7 +164,8 @@ module Citrus
|
|
166
164
|
raise "Cannot create rule \"#{name}\": " + e.message
|
167
165
|
end
|
168
166
|
|
169
|
-
# Gets/sets the +name+ of the root rule of this grammar.
|
167
|
+
# Gets/sets the +name+ of the root rule of this grammar. If no root rule is
|
168
|
+
# explicitly specified, the name of this grammar's first rule is returned.
|
170
169
|
def root(name=nil)
|
171
170
|
@root = name.to_sym if name
|
172
171
|
# The first rule in a grammar is the default root.
|
@@ -241,24 +240,47 @@ module Citrus
|
|
241
240
|
rule
|
242
241
|
end
|
243
242
|
|
244
|
-
# Parses the given +string+
|
245
|
-
#
|
246
|
-
#
|
247
|
-
def parse(string,
|
248
|
-
|
243
|
+
# Parses the given input +string+ using the given +options+. If no match can
|
244
|
+
# be made, a ParseError is raised. See #default_parse_options for a detailed
|
245
|
+
# description of available parse options.
|
246
|
+
def parse(string, options={})
|
247
|
+
opts = default_parse_options.merge(options)
|
248
|
+
|
249
|
+
raise "No root rule specified" unless opts[:root]
|
249
250
|
|
250
|
-
root_rule = rule(root)
|
251
|
+
root_rule = rule(opts[:root])
|
251
252
|
raise "No rule named \"#{root}\"" unless root_rule
|
252
253
|
|
253
|
-
input = Input.new(string, enable_memo)
|
254
|
-
match = input.match(root_rule, offset)
|
254
|
+
input = Input.new(string, opts[:enable_memo])
|
255
|
+
match = input.match(root_rule, opts[:offset])
|
255
256
|
|
256
|
-
if !match || (consume_all && match.length != string.length)
|
257
|
+
if !match || (opts[:consume_all] && match.length != string.length)
|
257
258
|
raise ParseError.new(input)
|
258
259
|
end
|
259
260
|
|
260
261
|
match
|
261
262
|
end
|
263
|
+
|
264
|
+
# The default set of options that is used in #parse. The options hash may
|
265
|
+
# have any of the following keys:
|
266
|
+
#
|
267
|
+
# offset:: The offset at which the parse should start. Defaults to 0.
|
268
|
+
# root:: The name of the root rule to use for the parse. Defaults
|
269
|
+
# to the name supplied by calling #root.
|
270
|
+
# consume_all:: If this is +true+ and the entire input string cannot be
|
271
|
+
# consumed, a ParseError will be raised. Defaults to +true+.
|
272
|
+
# enable_memo:: If this is +true+ the matches generated during a parse are
|
273
|
+
# memoized. This technique (also known as Packrat parsing)
|
274
|
+
# guarantees parsers will operate in linear time but costs
|
275
|
+
# significantly more in terms of time and memory required.
|
276
|
+
# Defaults to +false+.
|
277
|
+
def default_parse_options
|
278
|
+
{ :offset => 0,
|
279
|
+
:root => root,
|
280
|
+
:consume_all => true,
|
281
|
+
:enable_memo => false
|
282
|
+
}
|
283
|
+
end
|
262
284
|
end
|
263
285
|
|
264
286
|
# This class represents the core of the parsing algorithm. It wraps the input
|
@@ -339,11 +361,11 @@ module Citrus
|
|
339
361
|
end
|
340
362
|
end
|
341
363
|
|
342
|
-
@
|
364
|
+
@unique_id = 0
|
343
365
|
|
344
366
|
# Generates a new rule id.
|
345
367
|
def self.new_id
|
346
|
-
@
|
368
|
+
@unique_id += 1
|
347
369
|
end
|
348
370
|
|
349
371
|
# The grammar this rule belongs to.
|
@@ -397,7 +419,7 @@ module Citrus
|
|
397
419
|
private
|
398
420
|
|
399
421
|
def extend_match(match)
|
400
|
-
match.
|
422
|
+
match.ext = ext if ext
|
401
423
|
end
|
402
424
|
|
403
425
|
def create_match(data, offset)
|
@@ -446,15 +468,15 @@ module Citrus
|
|
446
468
|
end
|
447
469
|
|
448
470
|
# An Alias is a Proxy for a rule in the same grammar. It is used in rule
|
449
|
-
# definitions when a rule calls some other rule by name. The
|
450
|
-
# simply the name of another rule without any other punctuation, e.g.:
|
471
|
+
# definitions when a rule calls some other rule by name. The Citrus notation
|
472
|
+
# is simply the name of another rule without any other punctuation, e.g.:
|
451
473
|
#
|
452
474
|
# name
|
453
475
|
#
|
454
476
|
class Alias
|
455
477
|
include Proxy
|
456
478
|
|
457
|
-
# Returns the
|
479
|
+
# Returns the Citrus notation of this rule as a string.
|
458
480
|
def to_s
|
459
481
|
rule_name.to_s
|
460
482
|
end
|
@@ -473,15 +495,15 @@ module Citrus
|
|
473
495
|
|
474
496
|
# A Super is a Proxy for a rule of the same name that was defined previously
|
475
497
|
# in the grammar's inheritance chain. Thus, Super's work like Ruby's +super+,
|
476
|
-
# only for rules in a grammar instead of methods in a module. The
|
477
|
-
# is the word +super+ without any other punctuation, e.g.:
|
498
|
+
# only for rules in a grammar instead of methods in a module. The Citrus
|
499
|
+
# notation is the word +super+ without any other punctuation, e.g.:
|
478
500
|
#
|
479
501
|
# super
|
480
502
|
#
|
481
503
|
class Super
|
482
504
|
include Proxy
|
483
505
|
|
484
|
-
# Returns the
|
506
|
+
# Returns the Citrus notation of this rule as a string.
|
485
507
|
def to_s
|
486
508
|
'super'
|
487
509
|
end
|
@@ -510,13 +532,13 @@ module Citrus
|
|
510
532
|
# The actual String or Regexp object this rule uses to match.
|
511
533
|
attr_reader :rule
|
512
534
|
|
513
|
-
# Returns the
|
535
|
+
# Returns the Citrus notation of this rule as a string.
|
514
536
|
def to_s
|
515
537
|
rule.inspect
|
516
538
|
end
|
517
539
|
end
|
518
540
|
|
519
|
-
# A FixedWidth is a Terminal that matches based on its length. The
|
541
|
+
# A FixedWidth is a Terminal that matches based on its length. The Citrus
|
520
542
|
# notation is any sequence of characters enclosed in either single or double
|
521
543
|
# quotes, e.g.:
|
522
544
|
#
|
@@ -540,13 +562,13 @@ module Citrus
|
|
540
562
|
|
541
563
|
# An Expression is a Terminal that has the same semantics as a regular
|
542
564
|
# expression in Ruby. The expression must match at the beginning of the input
|
543
|
-
# (index 0). The
|
565
|
+
# (index 0). The Citrus notation is identical to Ruby's regular expression
|
544
566
|
# notation, e.g.:
|
545
567
|
#
|
546
568
|
# /expr/
|
547
569
|
#
|
548
|
-
# Character classes and the dot symbol may also be used in
|
549
|
-
# compatibility with other
|
570
|
+
# Character classes and the dot symbol may also be used in Citrus notation for
|
571
|
+
# compatibility with other parsing expression implementations, e.g.:
|
550
572
|
#
|
551
573
|
# [a-zA-Z]
|
552
574
|
# .
|
@@ -602,7 +624,7 @@ module Citrus
|
|
602
624
|
end
|
603
625
|
|
604
626
|
# An AndPredicate is a Predicate that contains a rule that must match. Upon
|
605
|
-
# success an empty match is returned and no input is consumed. The
|
627
|
+
# success an empty match is returned and no input is consumed. The Citrus
|
606
628
|
# notation is any expression preceeded by an ampersand, e.g.:
|
607
629
|
#
|
608
630
|
# &expr
|
@@ -616,14 +638,14 @@ module Citrus
|
|
616
638
|
create_match('', offset) if input.match(rule, offset)
|
617
639
|
end
|
618
640
|
|
619
|
-
# Returns the
|
641
|
+
# Returns the Citrus notation of this rule as a string.
|
620
642
|
def to_s
|
621
643
|
'&' + rule.embed
|
622
644
|
end
|
623
645
|
end
|
624
646
|
|
625
647
|
# A NotPredicate is a Predicate that contains a rule that must not match. Upon
|
626
|
-
# success an empty match is returned and no input is consumed. The
|
648
|
+
# success an empty match is returned and no input is consumed. The Citrus
|
627
649
|
# notation is any expression preceeded by an exclamation mark, e.g.:
|
628
650
|
#
|
629
651
|
# !expr
|
@@ -637,14 +659,14 @@ module Citrus
|
|
637
659
|
create_match('', offset) unless input.match(rule, offset)
|
638
660
|
end
|
639
661
|
|
640
|
-
# Returns the
|
662
|
+
# Returns the Citrus notation of this rule as a string.
|
641
663
|
def to_s
|
642
664
|
'!' + rule.embed
|
643
665
|
end
|
644
666
|
end
|
645
667
|
|
646
668
|
# A Label is a Predicate that applies a new name to any matches made by its
|
647
|
-
# rule. The
|
669
|
+
# rule. The Citrus notation is any sequence of word characters (i.e.
|
648
670
|
# <tt>[a-zA-Z0-9_]</tt>) followed by a colon, followed by any other
|
649
671
|
# expression, e.g.:
|
650
672
|
#
|
@@ -673,14 +695,14 @@ module Citrus
|
|
673
695
|
end
|
674
696
|
end
|
675
697
|
|
676
|
-
# Returns the
|
698
|
+
# Returns the Citrus notation of this rule as a string.
|
677
699
|
def to_s
|
678
700
|
label.to_s + ':' + rule.embed
|
679
701
|
end
|
680
702
|
end
|
681
703
|
|
682
704
|
# A Repeat is a Predicate that specifies a minimum and maximum number of times
|
683
|
-
# its rule must match. The
|
705
|
+
# its rule must match. The Citrus notation is an integer, +N+, followed by an
|
684
706
|
# asterisk, followed by another integer, +M+, all of which follow any other
|
685
707
|
# expression, e.g.:
|
686
708
|
#
|
@@ -721,23 +743,29 @@ module Citrus
|
|
721
743
|
create_match(matches, offset) if @range.include?(matches.length)
|
722
744
|
end
|
723
745
|
|
746
|
+
# The minimum number of times this rule must match.
|
747
|
+
def min
|
748
|
+
@range.begin
|
749
|
+
end
|
750
|
+
|
751
|
+
# The maximum number of times this rule may match.
|
752
|
+
def max
|
753
|
+
@range.end
|
754
|
+
end
|
755
|
+
|
724
756
|
# Returns the operator this rule uses as a string. Will be one of
|
725
757
|
# <tt>+</tt>, <tt>?</tt>, or <tt>N*M</tt>.
|
726
758
|
def operator
|
727
|
-
|
728
|
-
|
729
|
-
|
759
|
+
@operator ||= case [min, max]
|
760
|
+
when [0, 0] then ''
|
761
|
+
when [0, 1] then '?'
|
762
|
+
when [1, Infinity] then '+'
|
763
|
+
else
|
764
|
+
[min, max].map {|n| n == 0 || n == Infinity ? '' : n.to_s }.join('*')
|
730
765
|
end
|
731
|
-
@operator = case m
|
732
|
-
when ['', '1'] then '?'
|
733
|
-
when ['1', ''] then '+'
|
734
|
-
else m.join('*')
|
735
|
-
end
|
736
|
-
end
|
737
|
-
@operator
|
738
766
|
end
|
739
767
|
|
740
|
-
# Returns the
|
768
|
+
# Returns the Citrus notation of this rule as a string.
|
741
769
|
def to_s
|
742
770
|
rule.embed + operator
|
743
771
|
end
|
@@ -753,8 +781,8 @@ module Citrus
|
|
753
781
|
end
|
754
782
|
end
|
755
783
|
|
756
|
-
# A Choice is a List where only one rule must match. The
|
757
|
-
# or more expressions separated by a vertical bar, e.g.:
|
784
|
+
# A Choice is a List where only one rule must match. The Citrus notation is
|
785
|
+
# two or more expressions separated by a vertical bar, e.g.:
|
758
786
|
#
|
759
787
|
# expr | expr
|
760
788
|
#
|
@@ -771,14 +799,14 @@ module Citrus
|
|
771
799
|
nil
|
772
800
|
end
|
773
801
|
|
774
|
-
# Returns the
|
802
|
+
# Returns the Citrus notation of this rule as a string.
|
775
803
|
def to_s
|
776
804
|
rules.map {|r| r.embed }.join(' | ')
|
777
805
|
end
|
778
806
|
end
|
779
807
|
|
780
|
-
# A Sequence is a List where all rules must match. The
|
781
|
-
# more expressions separated by a space, e.g.:
|
808
|
+
# A Sequence is a List where all rules must match. The Citrus notation is two
|
809
|
+
# or more expressions separated by a space, e.g.:
|
782
810
|
#
|
783
811
|
# expr expr
|
784
812
|
#
|
@@ -799,7 +827,7 @@ module Citrus
|
|
799
827
|
create_match(matches, offset) if matches.length == rules.length
|
800
828
|
end
|
801
829
|
|
802
|
-
# Returns the
|
830
|
+
# Returns the Citrus notation of this rule as a string.
|
803
831
|
def to_s
|
804
832
|
rules.map {|r| r.embed }.join(' ')
|
805
833
|
end
|
@@ -829,6 +857,9 @@ module Citrus
|
|
829
857
|
# the label.
|
830
858
|
attr_accessor :name
|
831
859
|
|
860
|
+
# A module that will be used to extend this match.
|
861
|
+
attr_accessor :ext
|
862
|
+
|
832
863
|
# The offset in the input at which this match occurred.
|
833
864
|
attr_reader :offset
|
834
865
|
|
@@ -894,9 +925,23 @@ module Citrus
|
|
894
925
|
# Uses #match to allow sub-matches of this match to be called by name as
|
895
926
|
# instance methods.
|
896
927
|
def method_missing(sym, *args)
|
897
|
-
|
898
|
-
|
899
|
-
|
928
|
+
# Extend this object only when needed and immediately redefine
|
929
|
+
# #method_missing so that the new version is used on all future calls.
|
930
|
+
extend(ext) if ext
|
931
|
+
redefine_method_missing!
|
932
|
+
__send__(sym, *args)
|
933
|
+
end
|
934
|
+
|
935
|
+
private
|
936
|
+
|
937
|
+
def redefine_method_missing! # :nodoc:
|
938
|
+
instance_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
939
|
+
def method_missing(sym, *args)
|
940
|
+
m = first(sym)
|
941
|
+
return m if m
|
942
|
+
raise 'No match named "%s" in %s (%s)' % [sym, self, name]
|
943
|
+
end
|
944
|
+
RUBY
|
900
945
|
end
|
901
946
|
end
|
902
947
|
end
|
data/lib/citrus/file.rb
CHANGED
data/test/file_test.rb
CHANGED
@@ -333,6 +333,10 @@ class CitrusFileTest < Test::Unit::TestCase
|
|
333
333
|
match = grammar.parse('some_rule ')
|
334
334
|
assert(match)
|
335
335
|
assert('some_rule', match.value)
|
336
|
+
|
337
|
+
assert_raise ParseError do
|
338
|
+
match = grammar.parse('some_rule1')
|
339
|
+
end
|
336
340
|
end
|
337
341
|
|
338
342
|
def test_terminal
|
data/test/repeat_test.rb
CHANGED
@@ -51,22 +51,27 @@ class RepeatTest < Test::Unit::TestCase
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def test_operator
|
54
|
-
rule = Repeat.new(1, 2
|
54
|
+
rule = Repeat.new(1, 2)
|
55
55
|
assert_equal('1*2', rule.operator)
|
56
56
|
end
|
57
57
|
|
58
|
+
def test_operator_empty
|
59
|
+
rule = Repeat.new(0, 0)
|
60
|
+
assert_equal('', rule.operator)
|
61
|
+
end
|
62
|
+
|
58
63
|
def test_operator_asterisk
|
59
|
-
rule = Repeat.new(0, Infinity
|
64
|
+
rule = Repeat.new(0, Infinity)
|
60
65
|
assert_equal('*', rule.operator)
|
61
66
|
end
|
62
67
|
|
63
68
|
def test_operator_question_mark
|
64
|
-
rule = Repeat.new(0, 1
|
69
|
+
rule = Repeat.new(0, 1)
|
65
70
|
assert_equal('?', rule.operator)
|
66
71
|
end
|
67
72
|
|
68
73
|
def test_operator_plus
|
69
|
-
rule = Repeat.new(1, Infinity
|
74
|
+
rule = Repeat.new(1, Infinity)
|
70
75
|
assert_equal('+', rule.operator)
|
71
76
|
end
|
72
77
|
|
data/test/rule_test.rb
CHANGED
@@ -3,7 +3,9 @@ require File.dirname(__FILE__) + '/helper'
|
|
3
3
|
class RuleTest < Test::Unit::TestCase
|
4
4
|
|
5
5
|
module MatchModule
|
6
|
-
def a_test
|
6
|
+
def a_test
|
7
|
+
:test
|
8
|
+
end
|
7
9
|
end
|
8
10
|
|
9
11
|
NumericProc = Proc.new {
|
@@ -23,8 +25,7 @@ class RuleTest < Test::Unit::TestCase
|
|
23
25
|
rule.ext = MatchModule
|
24
26
|
match = rule.match(input('a'))
|
25
27
|
assert(match)
|
26
|
-
|
27
|
-
assert_respond_to(match, :a_test)
|
28
|
+
assert_equal(:test, match.a_test)
|
28
29
|
end
|
29
30
|
|
30
31
|
def test_numeric_proc
|
@@ -41,7 +42,6 @@ class RuleTest < Test::Unit::TestCase
|
|
41
42
|
rule.ext = NumericModule
|
42
43
|
match = rule.match(input('1'))
|
43
44
|
assert(match)
|
44
|
-
assert_kind_of(NumericModule, match)
|
45
45
|
assert_equal(1, match.to_i)
|
46
46
|
assert_instance_of(Float, match.to_f)
|
47
47
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 1
|
7
|
-
-
|
7
|
+
- 2
|
8
8
|
- 0
|
9
|
-
version: 1.
|
9
|
+
version: 1.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Michael Jackson
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-06-02 00:00:00 -06:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -50,17 +50,20 @@ extensions: []
|
|
50
50
|
extra_rdoc_files:
|
51
51
|
- README
|
52
52
|
files:
|
53
|
-
-
|
54
|
-
-
|
55
|
-
-
|
56
|
-
- lib/citrus.rb
|
53
|
+
- benchmark/seqpar.rb
|
54
|
+
- benchmark/seqpar.citrus
|
55
|
+
- benchmark/seqpar.gnuplot
|
57
56
|
- examples/calc.citrus
|
58
57
|
- examples/calc.rb
|
59
58
|
- examples/calc_sugar.rb
|
60
59
|
- extras/citrus.vim
|
60
|
+
- lib/citrus/debug.rb
|
61
|
+
- lib/citrus/file.rb
|
62
|
+
- lib/citrus/sugar.rb
|
63
|
+
- lib/citrus.rb
|
61
64
|
- test/alias_test.rb
|
62
65
|
- test/and_predicate_test.rb
|
63
|
-
- test/
|
66
|
+
- test/calc_file_test.rb
|
64
67
|
- test/calc_sugar_test.rb
|
65
68
|
- test/calc_test.rb
|
66
69
|
- test/choice_test.rb
|
@@ -117,7 +120,7 @@ summary: Parsing Expressions for Ruby
|
|
117
120
|
test_files:
|
118
121
|
- test/alias_test.rb
|
119
122
|
- test/and_predicate_test.rb
|
120
|
-
- test/
|
123
|
+
- test/calc_file_test.rb
|
121
124
|
- test/calc_sugar_test.rb
|
122
125
|
- test/calc_test.rb
|
123
126
|
- test/choice_test.rb
|