walrat 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/walrat.rb +70 -0
- data/lib/walrat/additions/proc.rb +32 -0
- data/lib/walrat/additions/regexp.rb +33 -0
- data/lib/walrat/additions/string.rb +99 -0
- data/lib/walrat/additions/symbol.rb +42 -0
- data/lib/walrat/and_predicate.rb +49 -0
- data/lib/walrat/array_result.rb +29 -0
- data/lib/walrat/continuation_wrapper_exception.rb +35 -0
- data/lib/walrat/grammar.rb +259 -0
- data/lib/walrat/left_recursion_exception.rb +34 -0
- data/lib/walrat/location_tracking.rb +126 -0
- data/lib/walrat/match_data_wrapper.rb +84 -0
- data/lib/walrat/memoizing.rb +55 -0
- data/lib/walrat/memoizing_cache.rb +126 -0
- data/lib/walrat/no_parameter_marker.rb +30 -0
- data/lib/walrat/node.rb +63 -0
- data/lib/walrat/not_predicate.rb +49 -0
- data/lib/walrat/parse_error.rb +48 -0
- data/lib/walrat/parser_state.rb +205 -0
- data/lib/walrat/parslet.rb +38 -0
- data/lib/walrat/parslet_choice.rb +155 -0
- data/lib/walrat/parslet_combination.rb +34 -0
- data/lib/walrat/parslet_combining.rb +190 -0
- data/lib/walrat/parslet_merge.rb +96 -0
- data/lib/walrat/parslet_omission.rb +74 -0
- data/lib/walrat/parslet_repetition.rb +114 -0
- data/lib/walrat/parslet_repetition_default.rb +77 -0
- data/lib/walrat/parslet_sequence.rb +241 -0
- data/lib/walrat/predicate.rb +68 -0
- data/lib/walrat/proc_parslet.rb +60 -0
- data/lib/walrat/regexp_parslet.rb +84 -0
- data/lib/walrat/skipped_substring_exception.rb +46 -0
- data/lib/walrat/string_enumerator.rb +47 -0
- data/lib/walrat/string_parslet.rb +89 -0
- data/lib/walrat/string_result.rb +34 -0
- data/lib/walrat/symbol_parslet.rb +82 -0
- data/lib/walrat/version.rb +26 -0
- metadata +110 -0
data/lib/walrat/node.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
# Make subclasses of this for us in Abstract Syntax Trees (ASTs).
|
27
|
+
class Node
|
28
|
+
include Walrat::LocationTracking
|
29
|
+
|
30
|
+
attr_reader :lexeme
|
31
|
+
|
32
|
+
def initialize lexeme
|
33
|
+
@string_value = lexeme.to_s
|
34
|
+
@lexeme = lexeme
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
@string_value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Overrides the default initialize method to accept the defined
|
42
|
+
# attributes and sets up an read accessor for each.
|
43
|
+
#
|
44
|
+
# Raises an error if called directly on Node itself rather than
|
45
|
+
# a subclass.
|
46
|
+
def self.production *results
|
47
|
+
raise 'Node#production called directly on Node' if self == Node
|
48
|
+
|
49
|
+
# set up accessors
|
50
|
+
results.each { |result| attr_reader result }
|
51
|
+
|
52
|
+
# set up initializer
|
53
|
+
initialize_body = "def initialize #{results.map { |symbol| symbol.to_s}.join(', ')}\n"
|
54
|
+
initialize_body << %Q{ @string_value = ""\n}
|
55
|
+
results.each do |result|
|
56
|
+
initialize_body << " @#{result} = #{result}\n"
|
57
|
+
initialize_body << " @string_value << #{result}.to_s\n"
|
58
|
+
end
|
59
|
+
initialize_body << "end\n"
|
60
|
+
class_eval initialize_body
|
61
|
+
end
|
62
|
+
end # class Node
|
63
|
+
end # module Walrat
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
class NotPredicate < Predicate
|
27
|
+
def parse string, options = {}
|
28
|
+
raise ArgumentError, 'nil string' if string.nil?
|
29
|
+
catch :ZeroWidthParseSuccess do
|
30
|
+
begin
|
31
|
+
@parseable.memoizing_parse(string, options)
|
32
|
+
rescue ParseError # failed to pass (which is just what we wanted)
|
33
|
+
throw :NotPredicateSuccess
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# getting this far means that parsing succeeded (not what we wanted)
|
38
|
+
raise ParseError.new('predicate not satisfied ("%s" not allowed) while parsing "%s"' % [@parseable.to_s, string],
|
39
|
+
:line_end => options[:line_start],
|
40
|
+
:column_end => options[:column_start])
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def hash_offset
|
46
|
+
11
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end # module Walrat
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
class ParseError < Exception
|
27
|
+
include Walrat::LocationTracking
|
28
|
+
|
29
|
+
# Takes an optional hash (for packing extra info into exception).
|
30
|
+
# position in string (irrespective of line number, column number)
|
31
|
+
# line number, column number
|
32
|
+
# filename
|
33
|
+
def initialize message, info = {}
|
34
|
+
super message
|
35
|
+
self.line_start = info[:line_start]
|
36
|
+
self.column_start = info[:column_start]
|
37
|
+
self.line_end = info[:line_end]
|
38
|
+
self.column_end = info[:column_end]
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect
|
42
|
+
# TODO: also return filename if available
|
43
|
+
'#<%s: %s @line_end=%d, @column_end=%d>' %
|
44
|
+
[ self.class.to_s, self.to_s, self.line_end, self.column_end ]
|
45
|
+
end
|
46
|
+
end # class ParseError
|
47
|
+
end # module Walrat
|
48
|
+
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
# Simple class for maintaining state during a parse operation.
|
27
|
+
class ParserState
|
28
|
+
attr_reader :options
|
29
|
+
|
30
|
+
# Returns the remainder (the unparsed portion) of the string. Will return
|
31
|
+
# an empty string if already at the end of the string.
|
32
|
+
attr_reader :remainder
|
33
|
+
|
34
|
+
# Raises an ArgumentError if string is nil.
|
35
|
+
def initialize string, options = {}
|
36
|
+
raise ArgumentError, 'nil string' if string.nil?
|
37
|
+
self.base_string = string
|
38
|
+
@results = ArrayResult.new # for accumulating results
|
39
|
+
@remainder = @base_string.clone
|
40
|
+
@scanned = ''
|
41
|
+
@options = options.clone
|
42
|
+
|
43
|
+
# start wherever we last finished (doesn't seem to behave different to
|
44
|
+
# the alternative)
|
45
|
+
@options[:line_start] = (@options[:line_end] or @options[:line_start] or 0)
|
46
|
+
@options[:column_start] = (@options[:column_end] or @options[:column_start] or 0)
|
47
|
+
#@options[:line_start] = 0 if @options[:line_start].nil?
|
48
|
+
#@options[:column_start] = 0 if @options[:column_start].nil?
|
49
|
+
|
50
|
+
# before parsing begins, end point is equal to start point
|
51
|
+
@options[:line_end] = @options[:line_start]
|
52
|
+
@options[:column_end] = @options[:column_start]
|
53
|
+
@original_line_start = @options[:line_start]
|
54
|
+
@original_column_start = @options[:column_start]
|
55
|
+
end
|
56
|
+
|
57
|
+
# The parsed method is used to inform the receiver of a successful parsing
|
58
|
+
# event.
|
59
|
+
#
|
60
|
+
# Note that substring need not actually be a String but it must respond to
|
61
|
+
# the following messages:
|
62
|
+
# - "line_end" and "column_end" so that the end position of the receiver
|
63
|
+
# can be updated
|
64
|
+
# As a convenience returns the remainder.
|
65
|
+
# Raises an ArgumentError if substring is nil.
|
66
|
+
def parsed substring
|
67
|
+
raise ArgumentError if substring.nil?
|
68
|
+
update_and_return_remainder_for_string substring, true
|
69
|
+
end
|
70
|
+
|
71
|
+
# The skipped method is used to inform the receiver of a successful parsing
|
72
|
+
# event where the parsed substring should be consumed but not included in
|
73
|
+
# the accumulated results.
|
74
|
+
# The substring should respond to "line_end" and "column_end".
|
75
|
+
# In all other respects this method behaves exactly like the parsed method.
|
76
|
+
def skipped substring
|
77
|
+
raise ArgumentError if substring.nil?
|
78
|
+
update_and_return_remainder_for_string substring
|
79
|
+
end
|
80
|
+
|
81
|
+
# The auto_skipped method is used to inform the receiver of a successful
|
82
|
+
# parsing event where the parsed substring should be consumed but not
|
83
|
+
# included in the accumulated results and furthermore the parse event
|
84
|
+
# should not affect the overall bounds of the parse result. In reality this
|
85
|
+
# means that the method is only ever called upon the successful use of a
|
86
|
+
# automatic intertoken "skipping" parslet. By definition this method should
|
87
|
+
# only be called for intertoken skipping otherwise incorrect results will
|
88
|
+
# be produced.
|
89
|
+
def auto_skipped substring
|
90
|
+
raise ArgumentError if substring.nil?
|
91
|
+
a, b, c, d = @options[:line_start], @options[:column_start],
|
92
|
+
@options[:line_end], @options[:column_end] # save
|
93
|
+
remainder = update_and_return_remainder_for_string(substring)
|
94
|
+
@options[:line_start], @options[:column_start],
|
95
|
+
@options[:line_end], @options[:column_end] = a, b, c, d # restore
|
96
|
+
remainder
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the results accumulated so far.
|
100
|
+
# Returns an empty array if no results have been accumulated.
|
101
|
+
# Returns a single object if only one result has been accumulated.
|
102
|
+
# Returns an array of objects if multiple results have been accumulated.
|
103
|
+
def results
|
104
|
+
updated_start = [@original_line_start, @original_column_start]
|
105
|
+
updated_end = [@options[:line_end], @options[:column_end]]
|
106
|
+
updated_source_text = @scanned.clone
|
107
|
+
|
108
|
+
if @results.length == 1
|
109
|
+
# here we ask the single result to exhibit container-like properties
|
110
|
+
# use the "outer" variants so as to not overwrite any data internal to
|
111
|
+
# the result itself
|
112
|
+
# this can happen where a lone result is surrounded only by skipped
|
113
|
+
# elements
|
114
|
+
# the result has to convey data about its own limits, plus those of the
|
115
|
+
# context just around it
|
116
|
+
results = @results[0]
|
117
|
+
results.outer_start = updated_start if results.start != updated_start
|
118
|
+
results.outer_end = updated_end if results.end != updated_end
|
119
|
+
results.outer_source_text = updated_source_text if results.source_text != updated_source_text
|
120
|
+
|
121
|
+
# the above trick fixes some of the location tracking issues but opens
|
122
|
+
# up another can of worms
|
123
|
+
# uncomment this line to see
|
124
|
+
#return results
|
125
|
+
|
126
|
+
# need some way of handling unwrapped results (raw results, not AST
|
127
|
+
# nodes) as well
|
128
|
+
results.start = updated_start
|
129
|
+
results.end = updated_end
|
130
|
+
results.source_text = updated_source_text
|
131
|
+
else
|
132
|
+
results = @results
|
133
|
+
results.start = updated_start
|
134
|
+
results.end = updated_end
|
135
|
+
results.source_text = updated_source_text
|
136
|
+
end
|
137
|
+
results
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the number of results accumulated so far.
|
141
|
+
def length
|
142
|
+
@results.length
|
143
|
+
end
|
144
|
+
|
145
|
+
# TODO: possibly implement "undo/rollback" and "reset" methods
|
146
|
+
# if I implement "undo" will probbaly do it as a stack
|
147
|
+
# will have the option of implementing "redo" as well but I'd only do that if I could think of a use for it
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def update_and_return_remainder_for_string input, store = false
|
152
|
+
previous_line_end = @options[:line_end] # remember old end point
|
153
|
+
previous_column_end = @options[:column_end] # remember old end point
|
154
|
+
|
155
|
+
# special case handling for literal String objects
|
156
|
+
if input.instance_of? String
|
157
|
+
input = StringResult.new(input)
|
158
|
+
input.start = [previous_line_end, previous_column_end]
|
159
|
+
if (line_count = input.scan(/\r\n|\r|\n/).length) != 0 # count number of newlines in receiver
|
160
|
+
column_end = input.jlength - input.jrindex(/\r|\n/) - 1 # calculate characters on last line
|
161
|
+
else # no newlines in match
|
162
|
+
column_end = input.jlength + previous_column_end
|
163
|
+
end
|
164
|
+
input.end = [previous_line_end + line_count, column_end]
|
165
|
+
end
|
166
|
+
|
167
|
+
@results << input if store
|
168
|
+
|
169
|
+
if input.line_end > previous_line_end # end line has advanced
|
170
|
+
@options[:line_end] = input.line_end
|
171
|
+
@options[:column_end] = 0
|
172
|
+
end
|
173
|
+
|
174
|
+
if input.column_end > @options[:column_end] # end column has advanced
|
175
|
+
@options[:column_end] = input.column_end
|
176
|
+
end
|
177
|
+
|
178
|
+
@options[:line_start] = @options[:line_end] # new start point is old end point
|
179
|
+
@options[:column_start] = @options[:column_end] # new start point is old end point
|
180
|
+
|
181
|
+
# calculate remainder
|
182
|
+
line_delta = @options[:line_end] - previous_line_end
|
183
|
+
if line_delta > 0 # have consumed newline(s)
|
184
|
+
line_delta.times do # remove them from remainder
|
185
|
+
newline_location = @remainder.jindex_plus_length /\r\n|\r|\n/ # find the location of the next newline
|
186
|
+
@scanned << @remainder[0...newline_location] # record scanned text
|
187
|
+
@remainder = @remainder[newline_location..-1] # strip everything up to and including the newline
|
188
|
+
end
|
189
|
+
@scanned << @remainder[0...@options[:column_end]]
|
190
|
+
@remainder = @remainder[@options[:column_end]..-1] # delete up to the current column
|
191
|
+
else # no newlines consumed
|
192
|
+
column_delta = @options[:column_end] - previous_column_end
|
193
|
+
if column_delta > 0 # there was movement within currentline
|
194
|
+
@scanned << @remainder[0...column_delta]
|
195
|
+
@remainder = @remainder[column_delta..-1] # delete up to the current column
|
196
|
+
end
|
197
|
+
end
|
198
|
+
@remainder
|
199
|
+
end
|
200
|
+
|
201
|
+
def base_string=(string)
|
202
|
+
@base_string = (string.clone rescue string)
|
203
|
+
end
|
204
|
+
end # class ParserState
|
205
|
+
end # module Walrat
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
class Parslet
|
27
|
+
include Walrat::ParsletCombining
|
28
|
+
include Walrat::Memoizing
|
29
|
+
|
30
|
+
def to_parseable
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse string, options = {}
|
35
|
+
raise NotImplementedError # subclass responsibility
|
36
|
+
end
|
37
|
+
end # class Parslet
|
38
|
+
end # module Walrat
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
|
2
|
+
# Redistribution and use in source and binary forms, with or without
|
3
|
+
# modification, are permitted provided that the following conditions are met:
|
4
|
+
#
|
5
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
6
|
+
# this list of conditions and the following disclaimer.
|
7
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer in the documentation
|
9
|
+
# and/or other materials provided with the distribution.
|
10
|
+
#
|
11
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
12
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
13
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
14
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
|
15
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
16
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
17
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
18
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
19
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
20
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
21
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
22
|
+
|
23
|
+
require 'walrat'
|
24
|
+
|
25
|
+
module Walrat
|
26
|
+
class ParsletChoice < ParsletCombination
|
27
|
+
attr_reader :hash
|
28
|
+
|
29
|
+
# Either parameter may be a Parslet or a ParsletCombination.
|
30
|
+
# Neither parmeter may be nil.
|
31
|
+
def initialize left, right, *others
|
32
|
+
raise ArgumentError if left.nil?
|
33
|
+
raise ArgumentError if right.nil?
|
34
|
+
@alternatives = [left, right] + others
|
35
|
+
update_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
# Override so that alternatives are appended to an existing sequence:
|
39
|
+
# Consider the following example:
|
40
|
+
#
|
41
|
+
# A | B
|
42
|
+
#
|
43
|
+
# This constitutes a single choice:
|
44
|
+
#
|
45
|
+
# (A | B)
|
46
|
+
#
|
47
|
+
# If we then make this a three-element sequence:
|
48
|
+
#
|
49
|
+
# A | B | C
|
50
|
+
#
|
51
|
+
# We are effectively creating an nested sequence containing the original
|
52
|
+
# sequence and an additional element:
|
53
|
+
#
|
54
|
+
# ((A | B) | C)
|
55
|
+
#
|
56
|
+
# Although such a nested sequence is correctly parsed it is not as
|
57
|
+
# architecturally clean as a single sequence without nesting:
|
58
|
+
#
|
59
|
+
# (A | B | C)
|
60
|
+
#
|
61
|
+
# This method allows us to use the architecturally cleaner format.
|
62
|
+
def |(next_parslet)
|
63
|
+
append next_parslet
|
64
|
+
end
|
65
|
+
|
66
|
+
# First tries to parse the left option, falling back and trying the right
|
67
|
+
# option and then the any subsequent options in the others instance
|
68
|
+
# variable on failure. If no options successfully complete parsing then an
|
69
|
+
# ParseError is raised. Any zero-width parse successes thrown by
|
70
|
+
# alternative parsers will flow on to a higher level.
|
71
|
+
def parse string, options = {}
|
72
|
+
raise ArgumentError if string.nil?
|
73
|
+
error = nil # for error reporting purposes will track which parseable gets farthest to the right before failing
|
74
|
+
left_recursion = nil # will also track any left recursion that we detect
|
75
|
+
@alternatives.each do |parseable|
|
76
|
+
begin
|
77
|
+
result = parseable.memoizing_parse(string, options) # successful parse
|
78
|
+
if left_recursion and left_recursion.continuation # and we have a continuation
|
79
|
+
continuation = left_recursion.continuation # continuations are once-only, one-way tickets
|
80
|
+
left_recursion = nil # set this to nil so as not to call it again without meaning to
|
81
|
+
continuation.call(result) # so jump back to where we were before
|
82
|
+
end
|
83
|
+
return result
|
84
|
+
rescue LeftRecursionException => e
|
85
|
+
left_recursion = e
|
86
|
+
|
87
|
+
# TODO:
|
88
|
+
# it's not enough to just catch this kind of exception and remember
|
89
|
+
# the last one
|
90
|
+
# may need to accumulate these in an array
|
91
|
+
# consider the example rule:
|
92
|
+
# :a, :a & :b | :a & :c | :a & :d | :b
|
93
|
+
# the first option will raise a LeftRecursionException
|
94
|
+
# the next option will raise for the same reason
|
95
|
+
# the third likewise
|
96
|
+
# finally we get to the fourth option, the first which might succeed
|
97
|
+
# at that point we should have three continuations
|
98
|
+
# we should try the first, falling back to the second and third if
|
99
|
+
# necessary
|
100
|
+
# on successfully retrying, need to start all over again and try all
|
101
|
+
# the options again, just in case further recursion is possible
|
102
|
+
# so it is quite complicated
|
103
|
+
# the question is, is it more complicated than the other ways of
|
104
|
+
# getting right-associativity into Walrat-generated parsers?
|
105
|
+
rescue ParseError => e
|
106
|
+
if error.nil?
|
107
|
+
error = e
|
108
|
+
else
|
109
|
+
error = e unless error.rightmost?(e)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# should generally report the rightmost error
|
115
|
+
raise ParseError.new('no valid alternatives while parsing "%s" (%s)' % [string, error.to_s],
|
116
|
+
:line_end => error.line_end,
|
117
|
+
:column_end => error.column_end)
|
118
|
+
end
|
119
|
+
|
120
|
+
def eql? other
|
121
|
+
return false if not other.instance_of? ParsletChoice
|
122
|
+
other_alternatives = other.alternatives
|
123
|
+
return false if @alternatives.length != other_alternatives.length
|
124
|
+
for i in 0..(@alternatives.length - 1)
|
125
|
+
return false unless @alternatives[i].eql? other_alternatives[i]
|
126
|
+
end
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
# For determining equality.
|
133
|
+
attr_reader :alternatives
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def update_hash
|
138
|
+
# fixed offset to avoid unwanted collisions with similar classes
|
139
|
+
@hash = 30
|
140
|
+
@alternatives.each { |parseable| @hash += parseable.hash }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Appends another Parslet (or ParsletCombination) to the receiver and
|
144
|
+
# returns the receiver.
|
145
|
+
# Raises if parslet is nil.
|
146
|
+
# Cannot use << as a method name because Ruby cannot parse it without
|
147
|
+
# the self, and self is not allowed as en explicit receiver for private messages.
|
148
|
+
def append next_parslet
|
149
|
+
raise ArgumentError if next_parslet.nil?
|
150
|
+
@alternatives << next_parslet.to_parseable
|
151
|
+
update_hash
|
152
|
+
self
|
153
|
+
end
|
154
|
+
end # class ParsletChoice
|
155
|
+
end # module Walrat
|