walrat 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/lib/walrat.rb +70 -0
  2. data/lib/walrat/additions/proc.rb +32 -0
  3. data/lib/walrat/additions/regexp.rb +33 -0
  4. data/lib/walrat/additions/string.rb +99 -0
  5. data/lib/walrat/additions/symbol.rb +42 -0
  6. data/lib/walrat/and_predicate.rb +49 -0
  7. data/lib/walrat/array_result.rb +29 -0
  8. data/lib/walrat/continuation_wrapper_exception.rb +35 -0
  9. data/lib/walrat/grammar.rb +259 -0
  10. data/lib/walrat/left_recursion_exception.rb +34 -0
  11. data/lib/walrat/location_tracking.rb +126 -0
  12. data/lib/walrat/match_data_wrapper.rb +84 -0
  13. data/lib/walrat/memoizing.rb +55 -0
  14. data/lib/walrat/memoizing_cache.rb +126 -0
  15. data/lib/walrat/no_parameter_marker.rb +30 -0
  16. data/lib/walrat/node.rb +63 -0
  17. data/lib/walrat/not_predicate.rb +49 -0
  18. data/lib/walrat/parse_error.rb +48 -0
  19. data/lib/walrat/parser_state.rb +205 -0
  20. data/lib/walrat/parslet.rb +38 -0
  21. data/lib/walrat/parslet_choice.rb +155 -0
  22. data/lib/walrat/parslet_combination.rb +34 -0
  23. data/lib/walrat/parslet_combining.rb +190 -0
  24. data/lib/walrat/parslet_merge.rb +96 -0
  25. data/lib/walrat/parslet_omission.rb +74 -0
  26. data/lib/walrat/parslet_repetition.rb +114 -0
  27. data/lib/walrat/parslet_repetition_default.rb +77 -0
  28. data/lib/walrat/parslet_sequence.rb +241 -0
  29. data/lib/walrat/predicate.rb +68 -0
  30. data/lib/walrat/proc_parslet.rb +60 -0
  31. data/lib/walrat/regexp_parslet.rb +84 -0
  32. data/lib/walrat/skipped_substring_exception.rb +46 -0
  33. data/lib/walrat/string_enumerator.rb +47 -0
  34. data/lib/walrat/string_parslet.rb +89 -0
  35. data/lib/walrat/string_result.rb +34 -0
  36. data/lib/walrat/symbol_parslet.rb +82 -0
  37. data/lib/walrat/version.rb +26 -0
  38. metadata +110 -0
@@ -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