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.
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