parse-framework 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ --protected
2
+ --hide-api private
3
+ --files sample/*
@@ -0,0 +1,318 @@
1
+ Parse
2
+ =====
3
+
4
+ The framework for building parsers.
5
+
6
+ Features
7
+ --------
8
+
9
+ - **Position tracking**. You can always obtain the current position and the current rule's start position while parsing.
10
+ - **Position remapping**. Ever wanted to implement something like C preprocessor's `#line` command? Now it is possible!
11
+ - **Automatic error handling**. If the parsing fails, the raised error contains the failure position and what is expected at that position. No additional coding is required.
12
+ - **Flexible and easy to use**. Parsers are written in plain Ruby. You may implement any parsing algorithms you wish! Also no clumsy DSLs or code generation are used - you may use your favorite IDE!
13
+
14
+ Install
15
+ -------
16
+
17
+ Command line:
18
+
19
+ gem install parse-framework
20
+
21
+ With [Bundler](http://bundler.io/):
22
+
23
+ echo 'gem "parse-framework"' >>Gemfile
24
+ bundle install
25
+
26
+ Usage
27
+ -----
28
+
29
+ Write subclass of {Parse} class.
30
+
31
+ Also you may find {Parse::MapPosition} and {Code} classes useful!
32
+
33
+ Examples
34
+ --------
35
+
36
+ First parser:
37
+
38
+ require 'parse'
39
+
40
+ class HelloWorldParser < Parse
41
+
42
+ def start
43
+ scan("hello world")
44
+ end
45
+
46
+ end
47
+
48
+ Usage:
49
+
50
+ puts HelloWorldParser.new.("hello world")
51
+ #=> hello world
52
+
53
+ Sequence:
54
+
55
+ class HelloWorldParser < Parse
56
+
57
+ def start
58
+ scan("hello ") and scan("world")
59
+ end
60
+
61
+ end
62
+
63
+ puts HelloWorldParser.new.("hello world")
64
+ #=> world
65
+
66
+ Analyze errors:
67
+
68
+ begin
69
+ HelloWorldParser.new.("Hello everyone")
70
+ rescue Parse::Error => e
71
+ puts "error at #{e.pos.line + 1}:#{e.pos.column + 1}: #{e.message}"
72
+ end
73
+
74
+ #=> error at 1:7: "world" expected
75
+
76
+ Regexps:
77
+
78
+ class HelloWorldParser < Parse
79
+
80
+ def start
81
+ scan(/[Hh]ello/) and scan(/\s+/) and scan("world")
82
+ end
83
+
84
+ end
85
+
86
+ puts HelloWorldParser.new.("Hello world")
87
+ #=> "world"
88
+
89
+ Alteration:
90
+
91
+ class HelloWorldParser < Parse
92
+
93
+ def start
94
+ scan(/[Hh]ello/) and scan(/\s+/) and (
95
+ _{ scan("world") } or
96
+ _{ scan("everyone") }
97
+ )
98
+ end
99
+
100
+ end
101
+
102
+ puts HelloWorldParser.new.("Hello everyone")
103
+ #=> everyone
104
+
105
+ Separate rules:
106
+
107
+ class HelloWorldParser < Parse
108
+
109
+ def start
110
+ scan(/[Hh]ello/) and scan(/\s+/) and who
111
+ end
112
+
113
+ def who
114
+ _{ scan("world") } or
115
+ _{ scan("everyone") }
116
+ end
117
+
118
+ end
119
+
120
+ puts HelloWorldParser.new.("Hello everyone")
121
+ #=> everyone
122
+
123
+ Repetition:
124
+
125
+ class MillionThankYousParser < Parse
126
+
127
+ def start
128
+ many { scan("thank you") and scan(/\s*/) }
129
+ end
130
+
131
+ end
132
+
133
+ MillionThankYousParser.new.("thank you thank you thank you")
134
+
135
+ `many` is a Parser Combinator. There are many other Parser Combinators, see {Parse}'s doc.
136
+
137
+ Counting:
138
+
139
+ class MillionThankYousParser < Parse
140
+
141
+ def start
142
+ count = 0
143
+ many {
144
+ scan("thank you") and scan(/\s*/) and
145
+ act { count += 1 } # `act` always returns true regardless of
146
+ # the block's result.
147
+ } and
148
+ count # the last non-nil value in the sequence is the
149
+ # sequence's result.
150
+ end
151
+
152
+ end
153
+
154
+ puts MillionThankYousParser.new.("thank you thank you thank you")
155
+ #=> 3
156
+
157
+ Capture variables:
158
+
159
+ class ChatBot < Parse
160
+
161
+ def start
162
+ scan("My name is ") and n = scan(/\w+/) and scan(/[\!\.]/) and n
163
+ end
164
+
165
+ end
166
+
167
+ puts ChatBot.new.("My name is John!")
168
+ #=> John
169
+
170
+ Parse LISP expressions (using AST nodes):
171
+
172
+ class ParseLISP < Parse
173
+
174
+ # a shorthand for declaring a Struct with member "children" and a method
175
+ # "to_ruby_value" and including the module "ASTNode" into it.
176
+ Cons = ASTNode.new :children do
177
+
178
+ def to_ruby_value
179
+ children.map(&:to_ruby_value)
180
+ end
181
+
182
+ end
183
+
184
+ Atom = ASTNode.new :val do
185
+
186
+ def to_ruby_value
187
+ val
188
+ end
189
+
190
+ end
191
+
192
+ def start
193
+ skipped and
194
+ r = (_{ cons } or _{ atom }) and
195
+ r.to_ruby_value
196
+ end
197
+
198
+ # "rule" is similar to "def" but it allows to use "_(node)" in its body.
199
+ # "_(node)" sets {ASTNode#pos} to the rule's start position.
200
+ rule :atom do
201
+ _{ n = scan(/\d+/) and skipped and act { n = n.to_i } and _(Atom[n]) } or
202
+ _{ s = scan(/"[^"]*"/) and skipped and act { s = s[1...-1] } and _(Atom[s]) } or
203
+ _{ s = scan(/[^\(\)\"\s\;]+/) and skipped and act { s = s.to_sym } and _(Atom[s]) }
204
+ end
205
+
206
+ rule :cons do
207
+ scan("(") and skipped and
208
+ a = many { _{ atom } or _{ cons } } and # "many" results in array of
209
+ # successfully parsed values
210
+ scan(")") and skipped and
211
+ _(Cons[a])
212
+ end
213
+
214
+ def skipped
215
+ opt {
216
+ _{ scan(/\s+/) } or
217
+ _{ scan(/;.*\n/) }
218
+ }
219
+ end
220
+
221
+ end
222
+
223
+ p ParseLISP.new.(' (a b (c d) (e 12 "s")) ')
224
+ #=> [:a, :b, [:c, :d], [:e, 12, "s"]]
225
+
226
+ The same but using `token` macro:
227
+
228
+ class ParseLISP < Parse
229
+
230
+ Cons = ASTNode.new :children do
231
+
232
+ def to_ruby_value
233
+ children.map(&:to_ruby_value)
234
+ end
235
+
236
+ end
237
+
238
+ Atom = ASTNode.new :val do
239
+
240
+ def to_ruby_value
241
+ val
242
+ end
243
+
244
+ end
245
+
246
+ def start
247
+ skipped and
248
+ r = (_{ cons } or _{ atom }) and
249
+ r.to_ruby_value
250
+ end
251
+
252
+ rule :atom do
253
+ _{ n = number and _(Atom[n]) } or # !!!
254
+ _{ s = string and _(Atom[s]) } or # !!!
255
+ _{ s = symbol and _(Atom[s]) } # !!!
256
+ end
257
+
258
+ rule :cons do
259
+ lbrace and # !!!
260
+ a = many { _{ atom } or _{ cons } } and
261
+ rbrace and # !!!
262
+ _(Cons[a])
263
+ end
264
+
265
+ def skipped
266
+ opt {
267
+ _{ scan(/\s+/) } or
268
+ _{ scan(/;.*\n/) }
269
+ }
270
+ end
271
+
272
+ # !!!
273
+
274
+ # `token` is like `rule` but:
275
+ # 1) it handles errors slightly different, see doc.
276
+ # 2) it skips `whitespace_and_comments` after the token
277
+ token :number, "integer number" do
278
+ n = scan(/\d+/) and n.to_i
279
+ end
280
+
281
+ # you may omit description.
282
+ token :string do
283
+ s = scan(/"[^"]*"/) and s[1...-1]
284
+ end
285
+
286
+ token :symbol do
287
+ s = scan(/[^\(\)\"\s\;]+/) and s.to_sym
288
+ end
289
+
290
+ # a shorthand for ``token :lbrace, "`('" do scan("(") end''
291
+ token :lbrace, "("
292
+
293
+ token :rbrace, ")"
294
+
295
+ # required by `token`.
296
+ alias whitespace_and_comments skipped
297
+
298
+ # !!!
299
+
300
+ end
301
+
302
+ p ParseLISP.new.(' (a b (c d) (e 12 "s")) ')
303
+ #=> [:a, :b, [:c, :d], [:e, 12, "s"]]
304
+
305
+ There are many other features, see {Parse}'s documentation, it is pretty detailed!
306
+
307
+ License
308
+ -------
309
+
310
+ The MIT License (MIT)
311
+
312
+ Copyright (c) 2016 Various Furriness
313
+
314
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
315
+
316
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
317
+
318
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ unless Array.method_defined? :bsearch_index
2
+ class Array
3
+ def bsearch_index
4
+ return to_enum(__method__) unless block_given?
5
+ from = 0
6
+ to = size - 1
7
+ satisfied = nil
8
+ while from <= to do
9
+ midpoint = (from + to).div(2)
10
+ cur_index = midpoint
11
+ result = yield(cur = self[midpoint])
12
+ case result
13
+ when Numeric
14
+ return cur_index if result == 0
15
+ result = result < 0
16
+ when true
17
+ satisfied = cur
18
+ satisfied_index = cur_index
19
+ when nil, false
20
+ # nothing to do
21
+ else
22
+ raise TypeError, "wrong argument type #{result.class} (must be numeric, true, false or nil)"
23
+ end
24
+ if result
25
+ to = midpoint - 1
26
+ else
27
+ from = midpoint + 1
28
+ end
29
+ end
30
+ satisfied_index
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,179 @@
1
+
2
+ # To disable YARD warnings:
3
+ #
4
+ # @!parse
5
+ # class Object
6
+ # end
7
+
8
+ #
9
+ # @example
10
+ #
11
+ # method_names = {
12
+ # :eat => "M1",
13
+ # :cut => "M2"
14
+ # }
15
+ #
16
+ # c = code <<
17
+ # "class Apple \n" <<
18
+ # "{ \n" <<
19
+ # "public: \n" <<
20
+ # " void " << non_code(:eat) << "(); \n" <<
21
+ # " void " << non_code(:cut) << "(int PiecesCount); \n" <<
22
+ # "}; \n"
23
+ # c.non_code_parts.each do |part|
24
+ # if part.is_a? Symbol then
25
+ # if not method_names.key? part then
26
+ # raise "method name not found!"
27
+ # end
28
+ # end
29
+ # end
30
+ # c = c.map_non_code_parts do |part|
31
+ # if part.is_a? Symbol then
32
+ # method_names[part]
33
+ # end
34
+ # end
35
+ # puts c
36
+ # # class Apple
37
+ # # {
38
+ # # public:
39
+ # # void M1();
40
+ # # void M2(int PiecesCount);
41
+ # # };
42
+ #
43
+ class Code
44
+
45
+ # @api private
46
+ # @note used by {Code}, {::code}, {::non_code} only.
47
+ #
48
+ # @param [Array<String, NonCodePart>] parts
49
+ # @param [Object, nil] metadata
50
+ #
51
+ def initialize(parts, metadata = nil)
52
+ @parts = parts
53
+ @metadata = metadata
54
+ end
55
+
56
+ # @overload + str
57
+ # @param [String] str
58
+ # @return [Code]
59
+ # @overload + code
60
+ # @param [Code] code
61
+ # @return [Code]
62
+ def + arg
63
+ case arg
64
+ when String then self + Code.new([arg])
65
+ when Code then Code.new(self.parts + arg.parts)
66
+ end
67
+ end
68
+
69
+ # @overload << str
70
+ # Appends +str+ to self.
71
+ # @param [String] str
72
+ # @return [self]
73
+ # @overload << code
74
+ # Appends +str+ to self.
75
+ # @param [Code] code
76
+ # @return [self]
77
+ def << arg
78
+ case arg
79
+ when String then self << Code.new([arg])
80
+ when Code then @parts.concat(arg.parts); self
81
+ end
82
+ end
83
+
84
+ # @overload metadata(obj)
85
+ # @param [Object] obj
86
+ # @return [Code] a {Code} with +obj+ attached to it. The +obj+ can later
87
+ # be retrieved with {#metadata}().
88
+ # @overload metadata
89
+ # @return [Object, nil] an {Object} attached to this {Code} with
90
+ # {#metadata}(obj) or nil if no {Object} was attached.
91
+ def metadata(*args)
92
+ if args.empty?
93
+ then @metadata
94
+ else Code.new(@parts, args.first)
95
+ end
96
+ end
97
+
98
+ # @yieldparam [Object] part
99
+ # @yieldreturn [String]
100
+ # @return [Code] a {Code} with {#non_code_parts} mapped by the passed block.
101
+ def map_non_code_parts(&f)
102
+ Code.new(
103
+ @parts.map do |part|
104
+ case part
105
+ when String then part
106
+ when NonCodePart then f.(part.data)
107
+ end
108
+ end
109
+ )
110
+ end
111
+
112
+ # @return [Enumerable<Object>] non-code parts of this {Code} introduced
113
+ # with {::non_code}. See also {#map_non_code_parts}.
114
+ def non_code_parts
115
+ @parts.select { |part| part.is_a? NonCodePart }.map(&:data)
116
+ end
117
+
118
+ # @return [String]
119
+ def to_s
120
+ if (x = @parts.find { |part| part.is_a? NonCodePart }) then
121
+ raise "non-code part: #{x.inspect}"
122
+ end
123
+ @parts.join
124
+ end
125
+
126
+ # @return [String]
127
+ def inspect
128
+ Inspectable.new(@parts, @metadata).inspect
129
+ end
130
+
131
+ # @api private
132
+ # @note used by {Code}, {::non_code} only.
133
+ NonCodePart = Struct.new :data
134
+
135
+ protected
136
+
137
+ # @!visibility private
138
+ attr_reader :parts
139
+
140
+ private
141
+
142
+ # @!visibility private
143
+ Inspectable = Struct.new :parts, :metadata
144
+
145
+ end
146
+
147
+ class Array
148
+
149
+ # @param [Code, String] delimiter
150
+ # @return [Code]
151
+ def join_code(delimiter = "")
152
+ self.reduce do |r, self_i|
153
+ code << r << delimiter << self_i
154
+ end or
155
+ code
156
+ end
157
+
158
+ end
159
+
160
+ # @overload code
161
+ # @return [Code] an empty {Code}.
162
+ # @overload code(str)
163
+ # @param [String] str
164
+ # @return [Code] +str+ converted to {Code}.
165
+ def code(str = nil)
166
+ if str
167
+ then Code.new([str])
168
+ else Code.new([])
169
+ end
170
+ end
171
+
172
+ # @param [Object] data
173
+ # @return [Code] a {Code} consisting of the single non-code part +data+.
174
+ # See also {Code#non_code_parts}.
175
+ def non_code(data)
176
+ Code.new([Code::NonCodePart.new(data)])
177
+ end
178
+
179
+ alias __non_code__ non_code
@@ -0,0 +1,614 @@
1
+ require 'strscan'
2
+ require 'strscan/substr'
3
+
4
+ # To disable YARD warnings:
5
+ # @!parse
6
+ # class Exception
7
+ # def message
8
+ # end
9
+ # end
10
+ # class Array
11
+ # end
12
+ # class String
13
+ # end
14
+ # class StringScanner
15
+ # attr_reader :pos
16
+ # end
17
+
18
+ #
19
+ class Parse
20
+
21
+ #
22
+ # parses +text+.
23
+ #
24
+ # If $DEBUG == true then it prints {Parse.rule} calls to stdout.
25
+ #
26
+ # @param [String] text
27
+ # @param [String] file file the +text+ is taken from.
28
+ # @raise [Parse::Error, IOError]
29
+ # @return [Object] what {#start} returns (non-nil).
30
+ #
31
+ def call(text, file = "-")
32
+ @text = StringScanner.new(text)
33
+ @file = file
34
+ @most_probable_error = nil
35
+ @allow_errors = true
36
+ @rule_start_pos = nil
37
+ r = start()
38
+ if r.nil? or not @text.eos? then
39
+ if @most_probable_error
40
+ then raise @most_probable_error
41
+ else raise Error.new(StringScannerPosition.new(@text.pos, @text, @file), "syntax error")
42
+ end
43
+ end
44
+ return r
45
+ end
46
+
47
+ module Position
48
+
49
+ include Comparable
50
+
51
+ # @param [Integer] line
52
+ # @param [Integer] column
53
+ # @param [String] file
54
+ # @return [Position]
55
+ def self.new(line, column, file = "-")
56
+ Position2.new(line, column, file)
57
+ end
58
+
59
+ # @!method file
60
+ # @abstract
61
+ # @return [String]
62
+
63
+ # @!method line
64
+ # @abstract
65
+ # @return [Integer]
66
+
67
+ # @!method column
68
+ # @abstract
69
+ # @return [Integer]
70
+
71
+ # @return [-1, 0, 1, nil] nil if +other+.{#file} != self.{#file} or
72
+ # +other+ is not a {Position}. Otherwise it compares {#line} and {#column}
73
+ # and returns -1, 0 or 1. See also {Comparable#<=>}.
74
+ def <=> other
75
+ return nil unless other.is_a? Position
76
+ return nil unless self.file == other.file
77
+ x = self.line <=> other.line
78
+ return x if x != 0
79
+ return self.column <=> other.column
80
+ end
81
+
82
+ # @return [Boolean]
83
+ def == other
84
+ return false unless other.is_a? Position
85
+ return (self <=> other) == 0
86
+ end
87
+
88
+ # @return [String]
89
+ def to_s
90
+ "#{file}:#{line}:#{column}"
91
+ end
92
+
93
+ end
94
+
95
+ class Error < Exception
96
+
97
+ # @param [Position] pos
98
+ # @param [String] message
99
+ def initialize(pos, message)
100
+ super(message)
101
+ @pos = pos
102
+ end
103
+
104
+ # @return [Position]
105
+ attr_reader :pos
106
+
107
+ # @param [Error] other +self+.{#pos} must be equal to +other+.{#pos}.
108
+ # @return [Error] an {Error} with {Exception#message} combined from
109
+ # {Exception#message}s of +self+ and +other+ (using "or" word).
110
+ def or other
111
+ raise "#{self.pos} == #{other.pos} is false" unless self.pos == other.pos
112
+ Error.new(pos, "#{self.message} or #{other.message}")
113
+ end
114
+
115
+ end
116
+
117
+ class Expected < Error
118
+
119
+ # @param [Position] pos
120
+ # @param [String] what_1
121
+ # @param [*String] what_2_n
122
+ def initialize(pos, what_1, *what_2_n)
123
+ @what = [what_1, *what_2_n]
124
+ super(pos, "#{@what.join(", ")} expected")
125
+ end
126
+
127
+ # (see Error#or)
128
+ def or other
129
+ if other.is_a? Expected
130
+ raise "#{self.pos} == #{other.pos} is false" unless self.pos == other.pos
131
+ Expected.new(pos, *(self.what + other.what).uniq)
132
+ else
133
+ super(other)
134
+ end
135
+ end
136
+
137
+ protected
138
+
139
+ # @!visibility private
140
+ # @return [Array<String>]
141
+ attr_reader :what
142
+
143
+ end
144
+
145
+ protected
146
+
147
+ # @!method start()
148
+ # @abstract
149
+ #
150
+ # Implementation of {#call}. Or the starting rule.
151
+ #
152
+ # @return [Object, nil] a result of parsing or nil if the parsing failed.
153
+ # @raise [Parse::Error] if the parsing failed.
154
+ # @raise [IOError]
155
+ #
156
+
157
+ #
158
+ # scans +arg+ in +text+ passed to {#call} starting from {#pos} and,
159
+ # if scanned successfully, advances {#pos} and returns the scanned
160
+ # sub-{String}. Otherwise it calls {#expected} and returns nil.
161
+ #
162
+ # @param [String, Regexp] arg
163
+ # @return [String, nil]
164
+ #
165
+ def scan(arg)
166
+ case arg
167
+ when Regexp then @text.scan(arg) or expected(pos, %(regexp "#{arg.source}"))
168
+ when String then @text.scan(Regexp.new(Regexp.escape(arg))) or expected(pos, %("#{arg}"))
169
+ end
170
+ end
171
+
172
+ # alias for {#_1} and {#_2}
173
+ def _(arg = nil, &block)
174
+ if arg
175
+ then _1(arg)
176
+ else _2(&block)
177
+ end
178
+ end
179
+
180
+ # -------------
181
+ # @!group Rules
182
+ # -------------
183
+
184
+ # defines method +name+ with body +body+. Inside +body+ {#rule_start_pos}
185
+ # is the value of {#pos} right before the defined method's call.
186
+ #
187
+ # @param [Symbol] name
188
+ # @return [void]
189
+ #
190
+ def self.rule(name, &body)
191
+ define_method(name) do
192
+ old_rule_start_pos = @rule_start_pos
193
+ @rule_start_pos = pos
194
+ begin
195
+ if $DEBUG then STDERR.puts("#{pos.file}:#{pos.line+1}:#{pos.column+1}: entering rule :#{name}"); end
196
+ r = instance_eval(&body)
197
+ if $DEBUG then STDERR.puts("#{pos.file}:#{pos.line+1}:#{pos.column+1}: exiting rule :#{name} #{if r.nil? then "(with nil)" end}"); end
198
+ r
199
+ ensure
200
+ @rule_start_pos = old_rule_start_pos
201
+ end
202
+ end
203
+ end
204
+
205
+ # See {Parse::rule}.
206
+ #
207
+ # @return [Position]
208
+ #
209
+ attr_reader :rule_start_pos
210
+
211
+ # @param [ASTNode] node
212
+ # @return [ASTNode] +node+ which is
213
+ # {ASTNode#initialize_pos}({#rule_start_pos})-ed.
214
+ def _1(node)
215
+ node.initialize_pos(rule_start_pos)
216
+ end
217
+
218
+ # @overload token(name, [description], string | regexp | &body)
219
+ #
220
+ # A shorthand for:
221
+ #
222
+ # rule name do
223
+ # expect(description) {
224
+ # no_errors {
225
+ # r = (scan(string) | scan(regexp) | body) and
226
+ # whitespace_and_comments and
227
+ # r
228
+ # }
229
+ # }
230
+ # end
231
+ #
232
+ # Method +whitespace_and_comments+ must be defined!
233
+ #
234
+ # +description+ defaults to <code>"`#{string}'"</code>,
235
+ # <code>%(regexp "#{regexp}")</code> or <code>"#{name}"</code> (in that
236
+ # order, depending on what is defined).
237
+ #
238
+ # @return [void]
239
+ def self.token(name, *args, &body)
240
+ pattern, body =
241
+ if body.nil? then
242
+ pattern = args.pop
243
+ [pattern, proc { scan(pattern) }]
244
+ else
245
+ [nil, body]
246
+ end
247
+ description =
248
+ args.pop ||
249
+ case pattern
250
+ when nil then "#{name}"
251
+ when String then "`#{pattern}'"
252
+ when Regexp then %(regexp "#{pattern.source}")
253
+ end
254
+ raise ArgumentError, "wrong number of arguments" unless args.empty?
255
+ rule name do
256
+ expect(description) {
257
+ no_errors {
258
+ r = instance_eval(&body) and whitespace_and_comments and r
259
+ }
260
+ }
261
+ end
262
+ end
263
+
264
+ # ----------
265
+ # @!endgroup
266
+ # ----------
267
+
268
+ # ----------------
269
+ # @!group Position
270
+ # ----------------
271
+
272
+ # @return [Position] current {Position} in +text+ passed to {#call}.
273
+ def pos
274
+ StringScannerPosition.new(@text.pos, @text, @file)
275
+ end
276
+
277
+ # is {#pos} at the end of the text?
278
+ def end?
279
+ @text.eos?
280
+ end
281
+
282
+ alias eos? end?
283
+
284
+ # is {#pos} at the beginning of the text?
285
+ def begin?
286
+ @text.pos == 0
287
+ end
288
+
289
+ # ----------
290
+ # @!endgroup
291
+ # ----------
292
+
293
+ # --------------------------
294
+ # @!group Parser Combinators
295
+ # --------------------------
296
+
297
+ # calls +f+ and returns true.
298
+ #
299
+ # @return [true]
300
+ def act(&f)
301
+ f.()
302
+ true
303
+ end
304
+
305
+ #
306
+ # calls +f+. If +f+ results in nil then it restores {#pos} to the
307
+ # value before the call.
308
+ #
309
+ # @return what +f+ returns.
310
+ #
311
+ def _2(&f)
312
+ old_text_pos = @text.pos
313
+ f.() or begin
314
+ @text.pos = old_text_pos
315
+ nil
316
+ end
317
+ end
318
+
319
+ # calls +f+ using {#_2} many times until it returns nil.
320
+ #
321
+ # @return [Array] an {Array} of non-nil results of +f+.
322
+ #
323
+ def many(&f)
324
+ r = []
325
+ while true
326
+ f0 = _(&f)
327
+ if f0
328
+ then r.push(f0)
329
+ else break
330
+ end
331
+ end
332
+ r
333
+ end
334
+
335
+ # calls +f+ using {#_2}.
336
+ #
337
+ # @return [Array] an empty {Array} if +f+ results in nil and an {Array}
338
+ # containing the single result of +f+ otherwise.
339
+ #
340
+ def opt(&f)
341
+ [_(&f)].compact
342
+ end
343
+
344
+ # The same as <code>f.() and many(&f)</code>.
345
+ #
346
+ # @return [Array, nil] an {Array} of results of +f+ or nil if the first call
347
+ # to +f+ returned nil.
348
+ #
349
+ def one_or_more(&f)
350
+ f1 = f.() and f2_n = many(&f) and [f1, *f2_n]
351
+ end
352
+
353
+ alias many1 one_or_more
354
+
355
+ # @overload not_follows(*method_ids)
356
+ #
357
+ # calls methods specified by +method_ids+. If any of them returns non-nil
358
+ # then this method returns nil, otherwise it returns true. The methods
359
+ # are called inside {#no_errors}. {#pos} is restored after each method's
360
+ # call.
361
+ #
362
+ # @param [Array<Symbol>] method_ids
363
+ # @return [true, nil]
364
+ #
365
+ # @overload not_follows(&f)
366
+ #
367
+ # calls +f+. If +f+ returns non-nil then this method returns nil,
368
+ # otherwise it returns true. +f+ is called inside {#no_errors}.
369
+ # {#pos} is restored after +f+'s call.
370
+ #
371
+ # @return [true, nil]
372
+ #
373
+ def not_follows(*method_ids, &f)
374
+ if f then
375
+ if no_errors { _{ f.() } }
376
+ then nil
377
+ else true
378
+ end
379
+ else # if not method_ids.empty? then
380
+ if no_errors { method_ids.any? { |method_id| _{ __send__(method_id) } } }
381
+ then nil
382
+ else true
383
+ end
384
+ end
385
+ end
386
+
387
+ # ----------
388
+ # @!endgroup
389
+ # ----------
390
+
391
+ # --------------
392
+ # @!group Errors
393
+ # --------------
394
+
395
+ #
396
+ # sets {#most_probable_error} to +error+ if it is more probable than
397
+ # {#most_probable_error} or if {#most_probable_error} is nil.
398
+ #
399
+ # @param [Error] error
400
+ # @return [nil]
401
+ #
402
+ def error(error)
403
+ if @allow_errors then
404
+ if @most_probable_error.nil? or @most_probable_error.pos < error.pos then
405
+ @most_probable_error = error
406
+ elsif @most_probable_error and @most_probable_error.pos == error.pos then
407
+ @most_probable_error = @most_probable_error.or error
408
+ else
409
+ # do nothing
410
+ end
411
+ end
412
+ return nil
413
+ end
414
+
415
+ # macro
416
+ def expected(pos, what_1, *what_2_n)
417
+ error(Expected.new(pos, what_1, *what_2_n))
418
+ end
419
+
420
+ # macro
421
+ def expect(what_1, *what_2_n, &body)
422
+ p = pos and body.() or expected(p, what_1, *what_2_n)
423
+ end
424
+
425
+ # calls +block+. Inside the +block+ {#error} has no effect.
426
+ #
427
+ # @return what +block+ returns.
428
+ #
429
+ def no_errors(&block)
430
+ old_allow_errors = @allow_errors
431
+ begin
432
+ @allow_errors = false
433
+ block.()
434
+ ensure
435
+ @allow_errors = old_allow_errors
436
+ end
437
+ end
438
+
439
+ # @return [Error, nil]
440
+ attr_reader :most_probable_error
441
+
442
+ # ----------
443
+ # @!endgroup
444
+ # ----------
445
+
446
+ private
447
+
448
+ # @api private
449
+ # @note used by {Position#new} only.
450
+ class Position2
451
+
452
+ include Position
453
+
454
+ # @param [Integer] line
455
+ # @param [Integer] column
456
+ # @param [String] file
457
+ def initialize(line, column, file)
458
+ @line = line
459
+ @column = column
460
+ @file = file
461
+ end
462
+
463
+ # (see Position#file)
464
+ attr_reader :file
465
+
466
+ # (see Position#column)
467
+ attr_reader :column
468
+
469
+ # (see Position#line)
470
+ attr_reader :line
471
+
472
+ end
473
+
474
+ # @!visibility private
475
+ #
476
+ # A {Position} optimized for using with {StringScanner}.
477
+ #
478
+ class StringScannerPosition
479
+
480
+ include Position
481
+
482
+ # @param [Integer] text_pos {StringScanner#pos} in +text+.
483
+ # @param [StringScanner] text
484
+ # @param [String] file see {Position#file}.
485
+ def initialize(text_pos, text, file)
486
+ @text = text
487
+ @text_pos = text_pos
488
+ @file = file
489
+ end
490
+
491
+ # (see Position#file)
492
+ attr_reader :file
493
+
494
+ # (see Position#line)
495
+ def line
496
+ line_and_column[0]
497
+ end
498
+
499
+ # (see Position#column)
500
+ def column
501
+ line_and_column[1]
502
+ end
503
+
504
+ # (see Position#<=>)
505
+ def <=> other
506
+ case other
507
+ when StringScannerPosition
508
+ return nil unless self.file == other.file
509
+ return self.text_pos <=> other.text_pos
510
+ else
511
+ super(other)
512
+ end
513
+ end
514
+
515
+ # @!visibility private
516
+ attr_reader :text_pos
517
+
518
+ # @!visibility private
519
+ attr_reader :text
520
+
521
+ private
522
+
523
+ def line_and_column
524
+ @line_and_column ||= begin
525
+ s = @text.substr(0, @text_pos)
526
+ lines = s.split("\n", -1).to_a
527
+ [
528
+ line = if lines.size == 0 then 0 else lines.size - 1 end,
529
+ column = (lines.last || "").size
530
+ ]
531
+ end
532
+ end
533
+
534
+ end
535
+
536
+ end
537
+
538
+ module ASTNode
539
+
540
+ # macro
541
+ def self.new(*properties, &body)
542
+ (if properties.empty? then Class.new else Struct.new(*properties); end).tap do |c|
543
+ c.class_eval do
544
+
545
+ include ASTNode
546
+
547
+ # @return [Boolean]
548
+ def === other
549
+ self.class == other.class and
550
+ self.members.all? { |member| self.__send__(member) === other.__send__(member) }
551
+ end
552
+
553
+ if properties.empty? then
554
+ # @return [Array<Symbol>]
555
+ def members
556
+ []
557
+ end
558
+ end
559
+
560
+ end
561
+ c.class_eval(&body) if body
562
+ end
563
+ end
564
+
565
+ #
566
+ # Sets {#pos} to +pos+.
567
+ #
568
+ # @param [Parse::Position] pos
569
+ # @return [self]
570
+ def initialize_pos(pos)
571
+ @pos = pos
572
+ self
573
+ end
574
+
575
+ # @note {#initialize_pos} must be called before this method can be used.
576
+ # @return [Parse::Position]
577
+ def pos
578
+ raise "initialize_pos() must be called before this method can be used" unless @pos
579
+ @pos
580
+ end
581
+
582
+ end
583
+
584
+ # Alias for {ASTNode.new}.
585
+ def node(*properties, &body)
586
+ ASTNode.new(*properties, &body)
587
+ end
588
+
589
+ # class Calc < Parse
590
+ #
591
+ # Number = ASTNode.new :val
592
+ #
593
+ # rule :start do
594
+ # x1 = number and x2 = many { comma and number } and [x1, *x2]
595
+ # end
596
+ #
597
+ # rule :number do
598
+ # s = scan(/\d+/) and _(Number[s])
599
+ # end
600
+ #
601
+ # token :comma, ","
602
+ #
603
+ # def whitespace_and_comments
604
+ # scan(/\s+/)
605
+ # end
606
+ #
607
+ # end
608
+ #
609
+ # begin
610
+ # p Calc.new.("10a 20", __FILE__)
611
+ # rescue Parse::Error => e
612
+ # puts "error: #{e.pos.file}:#{e.pos.line}:#{e.pos.column}: #{e.message}"
613
+ # end
614
+
@@ -0,0 +1,97 @@
1
+ require 'parse'
2
+ require 'array/bsearch_index'
3
+
4
+ class Parse
5
+
6
+ #
7
+ # @example
8
+ #
9
+ # def pos(line, column, file)
10
+ # Parse::Position.new(line, column, file)
11
+ # end
12
+ #
13
+ # m = Parse::MapPosition.new
14
+ # m.map_from(pos(5, 10, "src.c"), pos(3, 2, "src.y"))
15
+ # m.map_from(pos(30, 5, "src.c"), pos(0, 0, "extra.y"))
16
+ # p m[pos(1,12,"src.c")] # src.c:1:12
17
+ # p m[pos(5,12,"src.c")] # src.y:3:4
18
+ # p m[pos(6,10,"src.c")] # src.y:4:10
19
+ # p m[pos(30,5,"src.c")] # extra.y:0:0
20
+ # p m[pos(40,0,"src.c")] # extra.y:10:0
21
+ #
22
+ class MapPosition
23
+
24
+ def initialize()
25
+ @mappings = []
26
+ end
27
+
28
+ #
29
+ # Before:
30
+ #
31
+ # - <code>m[pos1] == pos2</code>
32
+ #
33
+ # After:
34
+ #
35
+ # - <code>m[pos1] == new_pos_b.advance(pos1 |-| pos_b)</code>
36
+ # if <code>pos1 >= pos_b</code>
37
+ # - <code>m[pos1] == pos2</code> otherwise
38
+ #
39
+ # Here <code>pos_m |-| pos_n</code> is the number of characters between
40
+ # +pos_m+ and +pos_n+; <code>p.advance(n)</code> is +p+ advanced by
41
+ # +n+ characters.
42
+ #
43
+ # This method can not be called with +pos_b+ with different
44
+ # {Position#file}s!
45
+ #
46
+ # @param [Position] pos_b
47
+ # @param [Position] new_pos_b
48
+ # @return [self]
49
+ #
50
+ def map_from(pos_b, new_pos_b)
51
+ raise "can not call this method with different Position#file (was `#{@mappings.first.pos_b.file}' but `#{pos_b.file}' specified)" if not @mappings.empty? and @mappings.first.pos_b.file != pos_b.file
52
+ pos_b_idx = @mappings.bsearch_index { |mapping| mapping.pos_b >= pos_b } || @mappings.size
53
+ @mappings[pos_b_idx..-1] = [Mapping[pos_b, new_pos_b]]
54
+ end
55
+
56
+ #
57
+ # See also {#map_from}.
58
+ #
59
+ # @param [Position] pos1
60
+ # @return [Position]
61
+ def [](pos1)
62
+ #
63
+ return pos1 if @mappings.empty? or @mappings.first.pos_b.file != pos1.file
64
+ #
65
+ mapping = find_mapping_for pos1
66
+ #
67
+ return pos1 if mapping.nil?
68
+ #
69
+ pos_b, new_pos_b = mapping.pos_b, mapping.new_pos_b
70
+ # new_pos_b.advance(pos1 |-| pos_b)
71
+ if pos1.line == pos_b.line
72
+ then Parse::Position.new(new_pos_b.line, new_pos_b.column + (pos1.column - pos_b.column), new_pos_b.file)
73
+ else Parse::Position.new(new_pos_b.line + (pos1.line - pos_b.line), pos1.column, new_pos_b.file)
74
+ end
75
+ end
76
+
77
+ alias call []
78
+
79
+ private
80
+
81
+ # @return [Mapping, nil] a {Mapping} from +@mappings+ with
82
+ # {Mapping#pos_b} == max and {Mapping#pos_b} <= pos1;
83
+ # or nil if there is no such {Mapping}.
84
+ def find_mapping_for pos1
85
+ idx = @mappings.bsearch_index { |mapping| mapping.pos_b >= pos1 }
86
+ return @mappings.last if idx.nil?
87
+ return @mappings[idx] if pos1 == @mappings[idx].pos_b
88
+ return nil if idx == 0
89
+ return @mappings[idx - 1]
90
+ end
91
+
92
+ # @!visibility private
93
+ Mapping = Struct.new :pos_b, :new_pos_b
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,16 @@
1
+
2
+ class StringScanner
3
+
4
+ # @param [Integer] start some value of {StringScanner#pos}.
5
+ # @param [Integer] end_ some value of {StringScanner#pos}.
6
+ # @return [String]
7
+ def substr(start, end_)
8
+ old_pos = self.pos
9
+ self.pos = start
10
+ result = peek(end_ - start)
11
+ self.pos = old_pos
12
+ return result
13
+ end
14
+
15
+ end
16
+
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parse-framework
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Various Furriness
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-05-27 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'The framework for building parsers. It features position tracking
15
+ and automatic
16
+
17
+ error handling!
18
+
19
+ '
20
+ email: various.furriness@gmail.com
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - lib/strscan/substr.rb
26
+ - lib/parse/map_position.rb
27
+ - lib/array/bsearch_index.rb
28
+ - lib/parse.rb
29
+ - lib/code.rb
30
+ - README.md
31
+ - .yardopts
32
+ homepage: http://furronymous.bitbucket.org/parse
33
+ licenses:
34
+ - MIT
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: 1.9.3
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.23
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Parser building framework
57
+ test_files: []
58
+ has_rdoc: