citrus 2.1.2 → 2.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/README +106 -49
- data/benchmark/after.dat +192 -0
- data/benchmark/before.dat +192 -0
- data/citrus.gemspec +0 -1
- data/doc/extras.markdown +16 -0
- data/doc/syntax.markdown +76 -29
- data/doc/testing.markdown +12 -20
- data/examples/calc.citrus +12 -11
- data/examples/calc.rb +12 -11
- data/lib/citrus.rb +416 -253
- data/lib/citrus/file.rb +66 -33
- data/test/_files/super.citrus +1 -1
- data/test/_files/super2.citrus +13 -0
- data/test/alias_test.rb +18 -34
- data/test/and_predicate_test.rb +15 -10
- data/test/but_predicate_test.rb +22 -17
- data/test/calc_file_test.rb +1 -1
- data/test/choice_test.rb +12 -37
- data/test/{rule_test.rb → extension_test.rb} +17 -16
- data/test/file_test.rb +350 -244
- data/test/grammar_test.rb +5 -11
- data/test/helper.rb +1 -17
- data/test/input_test.rb +172 -2
- data/test/label_test.rb +0 -10
- data/test/match_test.rb +91 -35
- data/test/multibyte_test.rb +4 -4
- data/test/not_predicate_test.rb +15 -10
- data/test/parse_error_test.rb +1 -3
- data/test/repeat_test.rb +59 -32
- data/test/sequence_test.rb +19 -31
- data/test/string_terminal_test.rb +55 -0
- data/test/super_test.rb +31 -31
- data/test/terminal_test.rb +12 -37
- metadata +13 -23
- data/lib/citrus/debug.rb +0 -69
- data/test/debug_test.rb +0 -23
data/test/grammar_test.rb
CHANGED
@@ -1,17 +1,10 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
3
|
class GrammarTest < Test::Unit::TestCase
|
4
|
-
|
5
4
|
def test_new
|
6
|
-
|
7
|
-
assert_kind_of(Module,
|
8
|
-
assert(
|
9
|
-
end
|
10
|
-
|
11
|
-
def test_non_module_fail
|
12
|
-
assert_raise ArgumentError do
|
13
|
-
''.extend(GrammarMethods)
|
14
|
-
end
|
5
|
+
grammar = Grammar.new
|
6
|
+
assert_kind_of(Module, grammar)
|
7
|
+
assert(grammar.include?(Grammar))
|
15
8
|
end
|
16
9
|
|
17
10
|
def test_name
|
@@ -72,6 +65,8 @@ class GrammarTest < Test::Unit::TestCase
|
|
72
65
|
}
|
73
66
|
match = grammar.parse('1234')
|
74
67
|
assert(match)
|
68
|
+
assert_equal('123', match)
|
69
|
+
assert_equal(3, match.length)
|
75
70
|
end
|
76
71
|
|
77
72
|
def test_parse_sequence_short
|
@@ -135,5 +130,4 @@ class GrammarTest < Test::Unit::TestCase
|
|
135
130
|
grammar(:abc)
|
136
131
|
end
|
137
132
|
end
|
138
|
-
|
139
133
|
end
|
data/test/helper.rb
CHANGED
@@ -2,15 +2,11 @@ lib = File.expand_path('../../lib', __FILE__)
|
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
|
4
4
|
require 'test/unit'
|
5
|
-
require 'citrus
|
5
|
+
require 'citrus'
|
6
6
|
|
7
7
|
class Test::Unit::TestCase
|
8
8
|
include Citrus
|
9
9
|
|
10
|
-
def input(str='')
|
11
|
-
Input.new(str)
|
12
|
-
end
|
13
|
-
|
14
10
|
TestGrammar = Grammar.new do
|
15
11
|
rule :alpha do
|
16
12
|
/[a-zA-Z]/
|
@@ -49,18 +45,6 @@ class Test::Unit::TestCase
|
|
49
45
|
end
|
50
46
|
end
|
51
47
|
|
52
|
-
class EqualRule
|
53
|
-
include Citrus::Rule
|
54
|
-
|
55
|
-
def initialize(value)
|
56
|
-
@value = value
|
57
|
-
end
|
58
|
-
|
59
|
-
def match(input)
|
60
|
-
create_match(@value.to_s.dup) if @value.to_s == input.string
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
48
|
module CalcTestMethods
|
65
49
|
# A helper method that tests the successful parsing and evaluation of the
|
66
50
|
# given mathematical expression.
|
data/test/input_test.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
3
|
class InputTest < Test::Unit::TestCase
|
4
|
+
def test_memoized?
|
5
|
+
input = Input.new('')
|
6
|
+
input.memoize!
|
7
|
+
assert(input.memoized?)
|
8
|
+
end
|
4
9
|
|
5
|
-
def
|
10
|
+
def test_offsets_new
|
6
11
|
input = Input.new("abc\ndef\nghi")
|
7
12
|
assert_equal(0, input.line_offset)
|
8
13
|
assert_equal(0, input.line_index)
|
@@ -10,7 +15,7 @@ class InputTest < Test::Unit::TestCase
|
|
10
15
|
assert_equal("abc\n", input.line)
|
11
16
|
end
|
12
17
|
|
13
|
-
def
|
18
|
+
def test_offsets_advanced
|
14
19
|
input = Input.new("abc\ndef\nghi")
|
15
20
|
input.pos = 6
|
16
21
|
assert_equal(2, input.line_offset)
|
@@ -19,4 +24,169 @@ class InputTest < Test::Unit::TestCase
|
|
19
24
|
assert_equal("def\n", input.line)
|
20
25
|
end
|
21
26
|
|
27
|
+
def test_events
|
28
|
+
a = Rule.new('a')
|
29
|
+
b = Rule.new('b')
|
30
|
+
c = Rule.new('c')
|
31
|
+
s = Rule.new([ a, b, c ])
|
32
|
+
r = Repeat.new(s, 0, Infinity)
|
33
|
+
|
34
|
+
input = Input.new("abcabcabc")
|
35
|
+
events = input.exec(r)
|
36
|
+
|
37
|
+
expected_events = [
|
38
|
+
r.id,
|
39
|
+
s.id,
|
40
|
+
a.id, CLOSE, 1,
|
41
|
+
b.id, CLOSE, 1,
|
42
|
+
c.id, CLOSE, 1,
|
43
|
+
CLOSE, 3,
|
44
|
+
s.id,
|
45
|
+
a.id, CLOSE, 1,
|
46
|
+
b.id, CLOSE, 1,
|
47
|
+
c.id, CLOSE, 1,
|
48
|
+
CLOSE, 3,
|
49
|
+
s.id,
|
50
|
+
a.id, CLOSE, 1,
|
51
|
+
b.id, CLOSE, 1,
|
52
|
+
c.id, CLOSE, 1,
|
53
|
+
CLOSE, 3,
|
54
|
+
CLOSE, 9
|
55
|
+
]
|
56
|
+
|
57
|
+
assert_equal(expected_events, events)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_events2
|
61
|
+
a = Rule.new('a')
|
62
|
+
b = Rule.new('b')
|
63
|
+
c = Choice.new([ a, b ])
|
64
|
+
r = Repeat.new(c, 0, Infinity)
|
65
|
+
s = Rule.new([ a, r ])
|
66
|
+
|
67
|
+
input = Input.new('abbababba')
|
68
|
+
events = input.exec(s)
|
69
|
+
|
70
|
+
expected_events = [
|
71
|
+
s.id,
|
72
|
+
a.id, CLOSE, 1,
|
73
|
+
r.id,
|
74
|
+
c.id,
|
75
|
+
b.id, CLOSE, 1,
|
76
|
+
CLOSE, 1,
|
77
|
+
c.id,
|
78
|
+
b.id, CLOSE, 1,
|
79
|
+
CLOSE, 1,
|
80
|
+
c.id,
|
81
|
+
a.id, CLOSE, 1,
|
82
|
+
CLOSE, 1,
|
83
|
+
c.id,
|
84
|
+
b.id, CLOSE, 1,
|
85
|
+
CLOSE, 1,
|
86
|
+
c.id,
|
87
|
+
a.id, CLOSE, 1,
|
88
|
+
CLOSE, 1,
|
89
|
+
c.id,
|
90
|
+
b.id, CLOSE, 1,
|
91
|
+
CLOSE, 1,
|
92
|
+
c.id,
|
93
|
+
b.id, CLOSE, 1,
|
94
|
+
CLOSE, 1,
|
95
|
+
c.id,
|
96
|
+
a.id, CLOSE, 1,
|
97
|
+
CLOSE, 1,
|
98
|
+
CLOSE, 8,
|
99
|
+
CLOSE, 9
|
100
|
+
]
|
101
|
+
|
102
|
+
assert_equal(expected_events, events)
|
103
|
+
end
|
104
|
+
|
105
|
+
grammar :LetterA do
|
106
|
+
rule :top do
|
107
|
+
any(:three_as, :two_as, :one_a)
|
108
|
+
end
|
109
|
+
|
110
|
+
rule :three_as do
|
111
|
+
rep(:one_a, 3, 3)
|
112
|
+
end
|
113
|
+
|
114
|
+
rule :two_as do
|
115
|
+
rep(:one_a, 2, 2)
|
116
|
+
end
|
117
|
+
|
118
|
+
rule :one_a do
|
119
|
+
"a"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_cache_hits1
|
124
|
+
input = Input.new('a')
|
125
|
+
input.memoize!
|
126
|
+
input.exec(LetterA.rule(:top))
|
127
|
+
assert_equal(3, input.cache_hits)
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_cache_hits2
|
131
|
+
input = Input.new('aa')
|
132
|
+
input.memoize!
|
133
|
+
input.exec(LetterA.rule(:top))
|
134
|
+
assert_equal(2, input.cache_hits)
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_cache_hits3
|
138
|
+
input = Input.new('aaa')
|
139
|
+
input.memoize!
|
140
|
+
input.exec(LetterA.rule(:top))
|
141
|
+
assert_equal(0, input.cache_hits)
|
142
|
+
end
|
143
|
+
|
144
|
+
grammar :Addition do
|
145
|
+
rule :additive do
|
146
|
+
all(:number, :plus, label(any(:additive, :number), 'term')) {
|
147
|
+
number.value + term.value
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
rule :number do
|
152
|
+
all(/[0-9]+/, :space) {
|
153
|
+
strip.to_i
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
rule :plus do
|
158
|
+
all('+', :space)
|
159
|
+
end
|
160
|
+
|
161
|
+
rule :space do
|
162
|
+
/[ \t]*/
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_match
|
167
|
+
match = Addition.parse('+', :root => :plus)
|
168
|
+
assert(match)
|
169
|
+
assert(match.matches)
|
170
|
+
assert_equal(2, match.matches.length)
|
171
|
+
|
172
|
+
match = Addition.parse('+ ', :root => :plus)
|
173
|
+
assert(match)
|
174
|
+
assert(match.matches)
|
175
|
+
assert_equal(2, match.matches.length)
|
176
|
+
|
177
|
+
match = Addition.parse('99', :root => :number)
|
178
|
+
assert(match)
|
179
|
+
assert(match.matches)
|
180
|
+
assert_equal(2, match.matches.length)
|
181
|
+
|
182
|
+
match = Addition.parse('99 ', :root => :number)
|
183
|
+
assert(match)
|
184
|
+
assert(match.matches)
|
185
|
+
assert_equal(2, match.matches.length)
|
186
|
+
|
187
|
+
match = Addition.parse('1+2')
|
188
|
+
assert(match)
|
189
|
+
assert(match.matches)
|
190
|
+
assert_equal(3, match.matches.length)
|
191
|
+
end
|
22
192
|
end
|
data/test/label_test.rb
CHANGED
@@ -1,20 +1,11 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
3
|
class LabelTest < Test::Unit::TestCase
|
4
|
-
|
5
4
|
def test_terminal?
|
6
5
|
rule = Label.new
|
7
6
|
assert_equal(false, rule.terminal?)
|
8
7
|
end
|
9
8
|
|
10
|
-
def test_match
|
11
|
-
rule = Label.new('a', 'label')
|
12
|
-
|
13
|
-
match = rule.match(input('a'))
|
14
|
-
assert(match)
|
15
|
-
assert_equal(:label, match.name)
|
16
|
-
end
|
17
|
-
|
18
9
|
def test_to_s
|
19
10
|
rule = Label.new('a', 'label')
|
20
11
|
assert_equal('label:"a"', rule.to_s)
|
@@ -22,5 +13,4 @@ class LabelTest < Test::Unit::TestCase
|
|
22
13
|
rule = Label.new(Sequence.new(%w< a b >), 'label')
|
23
14
|
assert_equal('label:("a" "b")', rule.to_s)
|
24
15
|
end
|
25
|
-
|
26
16
|
end
|
data/test/match_test.rb
CHANGED
@@ -1,56 +1,112 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
3
|
class MatchTest < Test::Unit::TestCase
|
4
|
-
|
5
|
-
def test_string
|
4
|
+
def test_string_equality
|
6
5
|
match = Match.new('hello')
|
7
6
|
assert_equal('hello', match)
|
8
|
-
assert_equal(5, match.length)
|
9
|
-
end
|
10
|
-
|
11
|
-
def test_array_string
|
12
|
-
match1 = Match.new('a')
|
13
|
-
match2 = Match.new('b')
|
14
|
-
match = Match.new([match1, match2])
|
15
|
-
assert_equal('ab', match)
|
16
|
-
assert_equal(2, match.length)
|
17
|
-
assert_equal(2, match.matches.length)
|
18
7
|
end
|
19
8
|
|
20
|
-
def
|
9
|
+
def test_match_equality
|
21
10
|
match1 = Match.new('a')
|
22
11
|
match2 = Match.new('a')
|
23
|
-
assert(match1 == 'a')
|
24
12
|
assert(match1 == match2)
|
25
13
|
assert(match2 == match1)
|
14
|
+
end
|
26
15
|
|
27
|
-
|
28
|
-
|
29
|
-
|
16
|
+
def test_match_inequality
|
17
|
+
match1 = Match.new('a')
|
18
|
+
match2 = Match.new('b')
|
19
|
+
assert_equal(false, match1 == match2)
|
20
|
+
assert_equal(false, match2 == match1)
|
30
21
|
end
|
31
22
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
23
|
+
def test_names
|
24
|
+
a = Rule.new('a')
|
25
|
+
a.name = 'a'
|
26
|
+
b = Rule.new('b')
|
27
|
+
b.name = 'b'
|
28
|
+
c = Rule.new('c')
|
29
|
+
c.name = 'c'
|
30
|
+
s = Rule.new([ a, b, c ])
|
31
|
+
s.name = 's'
|
32
|
+
r = Repeat.new(s, 0, Infinity)
|
33
|
+
r.name = 'r'
|
34
|
+
|
35
|
+
events = [
|
36
|
+
r.id,
|
37
|
+
s.id,
|
38
|
+
a.id, CLOSE, 1,
|
39
|
+
b.id, CLOSE, 1,
|
40
|
+
c.id, CLOSE, 1,
|
41
|
+
CLOSE, 3,
|
42
|
+
s.id,
|
43
|
+
a.id, CLOSE, 1,
|
44
|
+
b.id, CLOSE, 1,
|
45
|
+
c.id, CLOSE, 1,
|
46
|
+
CLOSE, 3,
|
47
|
+
s.id,
|
48
|
+
a.id, CLOSE, 1,
|
49
|
+
b.id, CLOSE, 1,
|
50
|
+
c.id, CLOSE, 1,
|
51
|
+
CLOSE, 3,
|
52
|
+
CLOSE, 9
|
53
|
+
]
|
54
|
+
|
55
|
+
match = Match.new("abcabcabc", events)
|
56
|
+
assert(match.names)
|
57
|
+
assert_equal([:r], match.names)
|
58
|
+
|
59
|
+
match.matches.each do |m|
|
60
|
+
assert_equal([:s], m.names)
|
61
|
+
end
|
37
62
|
end
|
38
63
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
64
|
+
def test_matches
|
65
|
+
a = Rule.new('a')
|
66
|
+
b = Rule.new('b')
|
67
|
+
c = Rule.new('c')
|
68
|
+
s = Rule.new([ a, b, c ])
|
69
|
+
s.name = 's'
|
70
|
+
r = Repeat.new(s, 0, Infinity)
|
71
|
+
|
72
|
+
events = [
|
73
|
+
r.id,
|
74
|
+
s.id,
|
75
|
+
a.id, CLOSE, 1,
|
76
|
+
b.id, CLOSE, 1,
|
77
|
+
c.id, CLOSE, 1,
|
78
|
+
CLOSE, 3,
|
79
|
+
s.id,
|
80
|
+
a.id, CLOSE, 1,
|
81
|
+
b.id, CLOSE, 1,
|
82
|
+
c.id, CLOSE, 1,
|
83
|
+
CLOSE, 3,
|
84
|
+
s.id,
|
85
|
+
a.id, CLOSE, 1,
|
86
|
+
b.id, CLOSE, 1,
|
87
|
+
c.id, CLOSE, 1,
|
88
|
+
CLOSE, 3,
|
89
|
+
CLOSE, 9
|
90
|
+
]
|
91
|
+
|
92
|
+
match = Match.new("abcabcabc", events)
|
93
|
+
assert(match.matches)
|
42
94
|
assert_equal(3, match.matches.length)
|
43
95
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
96
|
+
sub_events = [
|
97
|
+
s.id,
|
98
|
+
a.id, CLOSE, 1,
|
99
|
+
b.id, CLOSE, 1,
|
100
|
+
c.id, CLOSE, 1,
|
101
|
+
CLOSE, 3
|
102
|
+
]
|
49
103
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
104
|
+
match.matches.each do |m|
|
105
|
+
assert_equal(sub_events, m.events)
|
106
|
+
assert_equal(:s, m.name)
|
107
|
+
assert_equal("abc", m)
|
108
|
+
assert(m.matches)
|
109
|
+
assert_equal(3, m.matches.length)
|
110
|
+
end
|
54
111
|
end
|
55
|
-
|
56
112
|
end
|
data/test/multibyte_test.rb
CHANGED
@@ -2,7 +2,7 @@ require File.expand_path('../helper', __FILE__)
|
|
2
2
|
|
3
3
|
class MultibyteTest < Test::Unit::TestCase
|
4
4
|
Citrus.eval(<<-'CODE')
|
5
|
-
grammar MultibyteTest::
|
5
|
+
grammar MultibyteTest::Multibyte
|
6
6
|
rule string
|
7
7
|
"\xFF"
|
8
8
|
end
|
@@ -18,17 +18,17 @@ class MultibyteTest < Test::Unit::TestCase
|
|
18
18
|
CODE
|
19
19
|
|
20
20
|
def test_multibyte_string
|
21
|
-
m =
|
21
|
+
m = Multibyte.parse("\xFF", :root => :string)
|
22
22
|
assert(m)
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_multibyte_regexp
|
26
|
-
m =
|
26
|
+
m = Multibyte.parse("\xFF", :root => :regexp)
|
27
27
|
assert(m)
|
28
28
|
end
|
29
29
|
|
30
30
|
def test_multibyte_character_class
|
31
|
-
m =
|
31
|
+
m = Multibyte.parse("\xFF", :root => :character_class)
|
32
32
|
assert(m)
|
33
33
|
end
|
34
34
|
end
|