citrus 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|