walrat 0.1
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/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
|