parse-framework 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +3 -0
- data/README.md +318 -0
- data/lib/array/bsearch_index.rb +33 -0
- data/lib/code.rb +179 -0
- data/lib/parse.rb +614 -0
- data/lib/parse/map_position.rb +97 -0
- data/lib/strscan/substr.rb +16 -0
- metadata +58 -0
data/.yardopts
ADDED
data/README.md
ADDED
@@ -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
|
data/lib/code.rb
ADDED
@@ -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
|
data/lib/parse.rb
ADDED
@@ -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:
|