birman 0.2.2 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +204 -0
- data/README.md +381 -14
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +25 -14
- data/lib/birman/compiler/actions.rb +309 -0
- data/lib/birman/compiler/emitter.rb +940 -0
- data/lib/birman/compiler/literals.rb +29 -0
- data/lib/birman/compiler/nodes.rb +158 -0
- data/lib/birman/compiler/regexps.rb +182 -0
- data/lib/birman/compiler/runtime.rb +758 -0
- data/lib/birman/compiler/types.rb +128 -0
- data/lib/birman/compiler.rb +155 -0
- data/lib/birman/context.rb +124 -15
- data/lib/birman/grammar.rb +93 -16
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +224 -38
- data/lib/birman/result.rb +10 -1
- data/lib/birman/rule_set.rb +19 -6
- data/lib/birman/rules.rb +10 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +22 -0
- metadata +11 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# Finds left recursion from a grammar's ASTs ({ rule_name => ast },
|
|
5
|
+
# as Grammar#to_ast returns), without parsing anything: a rule that
|
|
6
|
+
# can call itself, directly or through other rules, before consuming
|
|
7
|
+
# input would loop forever on some input. Grammar#left_recursion runs
|
|
8
|
+
# it on the first parse or to_ruby, and keeps it once it passes.
|
|
9
|
+
# Left recursion is supported, direct (a rule that calls itself) or
|
|
10
|
+
# indirect (through other rules), the way pegen does it: each cycle
|
|
11
|
+
# has a leader, a rule that every way round it passes through, and
|
|
12
|
+
# Context#memoize grows the leader's match from a seed. A cycle with
|
|
13
|
+
# no such rule, two cycles that share none, raises in check!.
|
|
14
|
+
class LeftRecursion
|
|
15
|
+
FLAGS = { "i" => Regexp::IGNORECASE, "x" => Regexp::EXTENDED, "m" => Regexp::MULTILINE,
|
|
16
|
+
"n" => Regexp::NOENCODING }.freeze
|
|
17
|
+
|
|
18
|
+
# Each cycle's rules (a strongly connected component of the left-call
|
|
19
|
+
# graph), in the order rules has them: first-use order, from
|
|
20
|
+
# Grammar#to_ast.
|
|
21
|
+
attr_reader :cycles
|
|
22
|
+
|
|
23
|
+
# { rule => its cycle's leader } for every rule in a cycle that has
|
|
24
|
+
# one. The leader is the first of its cycle's rules that every way
|
|
25
|
+
# round the cycle passes through: take it out and the rest can't
|
|
26
|
+
# reach each other in a loop. Only it grows; see Context#memoize.
|
|
27
|
+
attr_reader :leaders
|
|
28
|
+
|
|
29
|
+
# ignore: the grammar's ignore rule, which a lexeme calls after its
|
|
30
|
+
# token; at its start, if the token was empty.
|
|
31
|
+
def initialize(rules, ignore: nil)
|
|
32
|
+
@rules = rules
|
|
33
|
+
@ignore = ignore
|
|
34
|
+
@patterns = {}
|
|
35
|
+
@nullable = nullable_rules
|
|
36
|
+
@calls = rules.to_h { |name, ast| [name, calls(ast).uniq] }
|
|
37
|
+
@cycles = strongly_connected.select { cyclic?(_1) }
|
|
38
|
+
@cycle_of = @cycles.each_with_object({}) { |cycle, h| cycle.each { h[_1] = cycle } }
|
|
39
|
+
@leaders = @cycles.each_with_object({}) do |cycle, h|
|
|
40
|
+
leader = cycle.find { acyclic?(cycle - [_1]) } or next
|
|
41
|
+
cycle.each { h[_1] = leader }
|
|
42
|
+
end.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raises ArgumentError naming the cycles of every component with no
|
|
46
|
+
# leader, or for the first cycle that could never grow (see
|
|
47
|
+
# check_growth!); returns self otherwise.
|
|
48
|
+
def check!
|
|
49
|
+
leaderless = cycles.reject { @leaders.key?(_1.first) }
|
|
50
|
+
unless leaderless.empty?
|
|
51
|
+
loops = leaderless.flat_map { cycles_with_no_rule_in_common(_1) }
|
|
52
|
+
names = loops.map { _1.map(&:inspect).join(" -> ") }.join("; ")
|
|
53
|
+
raise ArgumentError, "left recursion through cycles with no rule in common isn't supported: #{names} " \
|
|
54
|
+
"(each rule can call the next without consuming input)"
|
|
55
|
+
end
|
|
56
|
+
cycles.each { check_growth!(_1) }
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Can the rule match without consuming input? Undefined rules can't.
|
|
61
|
+
def nullable?(name) = @nullable.key?(name)
|
|
62
|
+
|
|
63
|
+
# The rules name can call at the position it starts at, before it
|
|
64
|
+
# consumes anything, in the order its AST mentions them.
|
|
65
|
+
def left_calls(name) = @calls.fetch(name, [])
|
|
66
|
+
|
|
67
|
+
def left_recursive?(name) = @cycle_of.key?(name)
|
|
68
|
+
def cycle(name) = @cycle_of[name]
|
|
69
|
+
def leader(name) = @leaders[name]
|
|
70
|
+
|
|
71
|
+
# Can node match without consuming input? Every node type gets an
|
|
72
|
+
# answer; anything else (a custom matcher's own AST) is assumed to
|
|
73
|
+
# consume, which can only hide left recursion, never invent it.
|
|
74
|
+
def nullable_node?(node) # rubocop:disable Metrics
|
|
75
|
+
case node
|
|
76
|
+
in [:str, s] then s.empty? # a literal's ignored spacing comes after the literal
|
|
77
|
+
in [:pat, source] then nullable_pattern?(source)
|
|
78
|
+
in [:ref, name] then nullable?(name)
|
|
79
|
+
in [:seq, _, parts] then parts.all? { nullable_node?(_1) }
|
|
80
|
+
in [:choice, alts] then alts.any? { nullable_node?(_1) }
|
|
81
|
+
in [:rep, min, _, inner] then min.zero? || nullable_node?(inner)
|
|
82
|
+
in [:not, _] | [:and, _] then true
|
|
83
|
+
in [:both, _, right] then nullable_node?(right) # the guard consumes nothing
|
|
84
|
+
in [:action, inner] then nullable_node?(inner)
|
|
85
|
+
in [:quiet, inner] then nullable_node?(inner)
|
|
86
|
+
in [:label, _, inner] then nullable_node?(inner)
|
|
87
|
+
in [:check, _, inner] then nullable_node?(inner) # it may reject, but it can match empty
|
|
88
|
+
in [:lexeme, inner] then nullable_node?(inner)
|
|
89
|
+
else false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Least fixpoint: start with no rule nullable and add rules until
|
|
96
|
+
# nothing changes, so a rule that only calls itself stays non-nullable.
|
|
97
|
+
def nullable_rules
|
|
98
|
+
@nullable = {}
|
|
99
|
+
loop do
|
|
100
|
+
grown = @rules.reject { |name, ast| @nullable.key?(name) || !nullable_node?(ast) }
|
|
101
|
+
return @nullable if grown.empty?
|
|
102
|
+
|
|
103
|
+
grown.each_key { @nullable[_1] = true }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Rules node calls at its start position. A seq's parts run there
|
|
108
|
+
# until one consumes input; lookaheads count too, since re-entering
|
|
109
|
+
# a rule at the same position loops just the same.
|
|
110
|
+
def calls(node) # rubocop:disable Metrics
|
|
111
|
+
case node
|
|
112
|
+
in [:ref, name] then [name]
|
|
113
|
+
in [:seq, _, parts] then leading(parts).flat_map { calls(_1) }
|
|
114
|
+
in [:choice, alts] then alts.flat_map { calls(_1) }
|
|
115
|
+
in [:both, left, right] then calls(left) + calls(right)
|
|
116
|
+
in [:rep, _, 0, _] then [] # at most none: inner never runs
|
|
117
|
+
in [:rep, _, _, inner] then calls(inner)
|
|
118
|
+
in [:not, inner] then calls(inner)
|
|
119
|
+
in [:and, inner] then calls(inner)
|
|
120
|
+
in [:action, inner] then calls(inner)
|
|
121
|
+
in [:quiet, inner] then calls(inner)
|
|
122
|
+
in [:label, _, inner] then calls(inner)
|
|
123
|
+
in [:check, _, inner] then calls(inner)
|
|
124
|
+
in [:lexeme, inner] then calls(inner) + (@ignore && nullable_node?(inner) ? [@ignore] : [])
|
|
125
|
+
else []
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A cycle grows from a first match that didn't go round it (its
|
|
130
|
+
# seed), so one of its rules needs an alternative that doesn't start
|
|
131
|
+
# with any of them: without one it never matches. And in each of its
|
|
132
|
+
# rules, an alternative like that has to come after the ones that do
|
|
133
|
+
# start with one: tried first, it wins every round, so they never
|
|
134
|
+
# grow. Only each rule's own choice is checked, through the wrappers
|
|
135
|
+
# around it, not choices nested inside its parts.
|
|
136
|
+
def check_growth!(cycle) # rubocop:disable Metrics
|
|
137
|
+
raise ArgumentError, no_base_case(cycle) unless cycle.any? { seedable?(@rules[_1], cycle) }
|
|
138
|
+
|
|
139
|
+
cycle.each do |name|
|
|
140
|
+
alts = alternatives(@rules[name])
|
|
141
|
+
base = alts.index { !enters?(_1, cycle) } or next
|
|
142
|
+
grows = alts.drop(base).find { enters?(_1, cycle) } or next
|
|
143
|
+
first, later = [alts[base], grows].map { EBNF.expr(_1, EBNF::CHOICE) }
|
|
144
|
+
raise ArgumentError, "in left-recursive rule :#{name}, the alternative `#{first}` comes before `#{later}`, " \
|
|
145
|
+
"so the rule can never grow past it; " \
|
|
146
|
+
"put the alternatives that start with #{names(cycle, "or")} first"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def no_base_case(cycle)
|
|
151
|
+
if cycle.one?
|
|
152
|
+
name = cycle.first
|
|
153
|
+
"left-recursive rule :#{name} has no alternative that doesn't start with :#{name}, " \
|
|
154
|
+
"so it can never match; add one that doesn't, as a base case"
|
|
155
|
+
else
|
|
156
|
+
"left-recursive rules #{names(cycle, "and")}, which can each reach the others without consuming " \
|
|
157
|
+
"input, have no alternative that doesn't start with one of them, so they can never match; " \
|
|
158
|
+
"add one that doesn't, as a base case"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def enters?(node, cycle) = calls(node).intersect?(cycle)
|
|
163
|
+
|
|
164
|
+
# ":a, :b and :c"
|
|
165
|
+
def names(rules, conjunction)
|
|
166
|
+
[rules[0...-1].map(&:inspect).join(", "), rules.last.inspect].reject(&:empty?).join(" #{conjunction} ")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Can node match while its calls to the cycle's rules at its start
|
|
170
|
+
# fail?
|
|
171
|
+
def seedable?(node, cycle) # rubocop:disable Metrics
|
|
172
|
+
case node
|
|
173
|
+
in [:ref, name] then !cycle.include?(name)
|
|
174
|
+
in [:choice, alts] then alts.any? { seedable?(_1, cycle) }
|
|
175
|
+
in [:seq, _, parts] then leading(parts).all? { seedable?(_1, cycle) }
|
|
176
|
+
in [:rep, 0, _, _] | [:rep, _, 0, _] | [:not, _] then true
|
|
177
|
+
in [:rep | :label | :check, *] | [:and | :action | :quiet | :lexeme, _]
|
|
178
|
+
seedable?(node.last, cycle) # a wrapper: its inner node is its last element
|
|
179
|
+
in [:both, left, right] then seedable?(left, cycle) && seedable?(right, cycle)
|
|
180
|
+
else true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The alternatives of node's choice, looking through wrappers; node
|
|
185
|
+
# itself if it isn't one.
|
|
186
|
+
def alternatives(node)
|
|
187
|
+
case node
|
|
188
|
+
in [:choice, alts] then alts
|
|
189
|
+
in [:action | :quiet, _] | [:label | :check, _, _] then alternatives(node.last)
|
|
190
|
+
else [node]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The parts up to and including the first that consumes input.
|
|
195
|
+
def leading(parts) = parts.take((parts.index { !nullable_node?(_1) } || parts.size) + 1)
|
|
196
|
+
|
|
197
|
+
# The regexp is rebuilt from its inspect form and tried on "" with
|
|
198
|
+
# every assertion (see Assertions) taken to hold, since each holds
|
|
199
|
+
# somewhere: /(?=a)/ matches empty before an "a", though not in "".
|
|
200
|
+
# Assertions that contradict each other side by side are caught;
|
|
201
|
+
# other combinations that can't hold, like /(?=a)(?=b)/, still count
|
|
202
|
+
# as nullable. A pattern that can't be rebuilt is assumed to consume.
|
|
203
|
+
def nullable_pattern?(source)
|
|
204
|
+
@patterns.fetch(source) do
|
|
205
|
+
body, flags = %r{\A/(.*)/([a-z]*)\z}m.match(source)&.captures
|
|
206
|
+
@patterns[source] = body ? empty_match?(body, flags.chars.sum { FLAGS.fetch(_1, 0) }) : false
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Falls back to the regexp as written when taking out its assertions
|
|
211
|
+
# breaks it, or could make a backreference name another group: taking
|
|
212
|
+
# out (?=(a)) renumbers the groups after it.
|
|
213
|
+
def empty_match?(body, options)
|
|
214
|
+
stripped = Assertions.strip(body, options.anybits?(Regexp::EXTENDED))
|
|
215
|
+
stripped = body if stripped != body && body.match?(/\\(?:[1-9]|k[<'])/)
|
|
216
|
+
[stripped, body].each do |source|
|
|
217
|
+
return Regexp.new(source, options).match?("")
|
|
218
|
+
rescue RegexpError
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def cyclic?(rules) = rules.size > 1 || left_calls(rules.first).include?(rules.first)
|
|
225
|
+
|
|
226
|
+
# Tarjan's algorithm over the left-call graph. Each component's rules,
|
|
227
|
+
# and the components, come back in rule order.
|
|
228
|
+
def strongly_connected
|
|
229
|
+
@index = {}
|
|
230
|
+
@low = {}
|
|
231
|
+
@stack = []
|
|
232
|
+
@components = []
|
|
233
|
+
@rules.each_key { visit(_1) unless @index.key?(_1) }
|
|
234
|
+
order = @rules.keys.each_with_index.to_h
|
|
235
|
+
@components.map { |c| c.sort_by { order[_1] } }.sort_by { order[_1.first] }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def visit(name)
|
|
239
|
+
@index[name] = @low[name] = @index.size
|
|
240
|
+
@stack.push(name)
|
|
241
|
+
left_calls(name).each do |callee|
|
|
242
|
+
next unless @rules.key?(callee)
|
|
243
|
+
|
|
244
|
+
visit(callee) unless @index.key?(callee)
|
|
245
|
+
@low[name] = [@low[name], @low[callee]].min if @stack.include?(callee)
|
|
246
|
+
end
|
|
247
|
+
@components << pop_component(name) if @low[name] == @index[name]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def pop_component(name)
|
|
251
|
+
component = []
|
|
252
|
+
component << @stack.pop until component.last == name
|
|
253
|
+
component
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Can none of names reach itself through the others' left calls?
|
|
257
|
+
def acyclic?(names)
|
|
258
|
+
done = {}
|
|
259
|
+
names.none? { looping?(_1, names, done, {}) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def looping?(name, names, done, on_path)
|
|
263
|
+
return true if on_path[name]
|
|
264
|
+
return false if done[name]
|
|
265
|
+
|
|
266
|
+
on_path[name] = done[name] = true
|
|
267
|
+
left_calls(name).any? { names.include?(_1) && looping?(_1, names, done, on_path) }
|
|
268
|
+
ensure
|
|
269
|
+
on_path.delete(name)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# For a component with no leader, cycles no one rule is in all of
|
|
273
|
+
# (though two of them may share one): for each rule, the first cycle
|
|
274
|
+
# through the others that leaves it out.
|
|
275
|
+
def cycles_with_no_rule_in_common(component)
|
|
276
|
+
order = @rules.keys.each_with_index.to_h
|
|
277
|
+
cycles = component.filter_map do |left_out|
|
|
278
|
+
rest = component - [left_out]
|
|
279
|
+
rest.lazy.filter_map { path(_1, rest) }.first
|
|
280
|
+
end
|
|
281
|
+
cycles.uniq(&:to_set).each_with_index.sort_by { |cycle, i| [order[cycle.first], i] }.map(&:first)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The shortest way round from start back to it through names, through
|
|
285
|
+
# another rule if there is one (a leader may also call itself); nil
|
|
286
|
+
# if there's none.
|
|
287
|
+
def path(start, names) # rubocop:disable Metrics/MethodLength
|
|
288
|
+
from = {}
|
|
289
|
+
queue = [start]
|
|
290
|
+
while (name = queue.shift)
|
|
291
|
+
left_calls(name).each do |callee|
|
|
292
|
+
if callee == start
|
|
293
|
+
next if name == start
|
|
294
|
+
|
|
295
|
+
return [*trail(from, name), start]
|
|
296
|
+
end
|
|
297
|
+
next if !names.include?(callee) || from.key?(callee)
|
|
298
|
+
|
|
299
|
+
from[callee] = name
|
|
300
|
+
queue << callee
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
[start, start] if left_calls(start).include?(start)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def trail(from, name)
|
|
307
|
+
names = [name]
|
|
308
|
+
names.unshift(from[names.first]) while from.key?(names.first)
|
|
309
|
+
names
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A regexp's source with each assertion, which matches only where the
|
|
313
|
+
# text around it allows, replaced by (?:), which matches anywhere:
|
|
314
|
+
# lookarounds, anchors (\A, ^, $, \z, \Z, \G) and \b, \B. Assertions
|
|
315
|
+
# side by side hold at one position, so a pair there that can't both
|
|
316
|
+
# hold, (?=X) with (?!X), (?<=X) with (?<!X), or \b with \B, becomes
|
|
317
|
+
# (?!), which matches nowhere. Only that shape: (?=a)(?=b) can't hold
|
|
318
|
+
# either, but stays (?:)(?:). So does a negative lookaround of what
|
|
319
|
+
# always matches empty, as never's (?!) or (?!a*).
|
|
320
|
+
# Escapes, character classes and comments are copied as they are.
|
|
321
|
+
# Only /x makes # start a comment; an inline (?x) doesn't, so a "[" or
|
|
322
|
+
# "(" in such a comment can hide an assertion after it (it's then
|
|
323
|
+
# taken as written, as before this rewrite).
|
|
324
|
+
module Assertions
|
|
325
|
+
module_function
|
|
326
|
+
|
|
327
|
+
# kind: "=", "!", "<=" or "<!" for a lookaround (body: its source),
|
|
328
|
+
# or the anchor itself ("^", "\\b"...).
|
|
329
|
+
Assertion = Data.define(:kind, :body)
|
|
330
|
+
OPPOSITE = { "=" => "!", "<=" => "<!", "\\b" => "\\B" }.freeze
|
|
331
|
+
|
|
332
|
+
def strip(source, extended)
|
|
333
|
+
scanner = StringScanner.new(source)
|
|
334
|
+
out = +""
|
|
335
|
+
run = []
|
|
336
|
+
until scanner.eos?
|
|
337
|
+
item = piece(scanner, extended)
|
|
338
|
+
next run << item if item.is_a?(Assertion)
|
|
339
|
+
|
|
340
|
+
flush(run, out, extended, quantified: item.match?(/\A[?*+{]/))
|
|
341
|
+
out << item
|
|
342
|
+
end
|
|
343
|
+
flush(run, out, extended, quantified: false)
|
|
344
|
+
out
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Writes out a run of assertions side by side. A quantifier after the
|
|
348
|
+
# last one may skip it, so then it can't fail the rest.
|
|
349
|
+
def flush(run, out, extended, quantified:)
|
|
350
|
+
held = quantified ? run[0...-1] : run
|
|
351
|
+
never = held.any? { |a| held.include?(Assertion.new(OPPOSITE[a.kind], a.body)) || never?(a, extended) }
|
|
352
|
+
out << "(?!)" if never
|
|
353
|
+
out << ("(?:)" * run.size)
|
|
354
|
+
run.clear
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# A negative lookaround of something with no assertions of its own
|
|
358
|
+
# that matches "" never holds: that matches empty everywhere.
|
|
359
|
+
def never?(assertion, extended)
|
|
360
|
+
return false unless %w[! <!].include?(assertion.kind) && strip(assertion.body, extended) == assertion.body
|
|
361
|
+
|
|
362
|
+
Regexp.new(assertion.body, extended ? Regexp::EXTENDED : 0).match?("")
|
|
363
|
+
rescue RegexpError
|
|
364
|
+
false
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The next piece of the source: an Assertion, or text to copy.
|
|
368
|
+
def piece(s, extended) # rubocop:disable Metrics
|
|
369
|
+
if s.scan(/\\[bBAzZG]|[\^$]/) then Assertion.new(s.matched, nil)
|
|
370
|
+
elsif s.scan(/\(\?(<?[=!])/) then lookaround(s, s[1], extended)
|
|
371
|
+
elsif s.scan(/\\./m) || s.scan(/\(\?#[^)]*\)/) then s.matched
|
|
372
|
+
elsif s.skip(/\[/) then character_class(s)
|
|
373
|
+
elsif extended && s.scan(/#[^\n]*/) then s.matched
|
|
374
|
+
else s.getch
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# The rest of a lookaround, after its "(?=" (or "(?!", "(?<=", "(?<!").
|
|
379
|
+
def lookaround(s, kind, extended)
|
|
380
|
+
start = finish = s.pos
|
|
381
|
+
depth = 1
|
|
382
|
+
until depth.zero? || s.eos?
|
|
383
|
+
finish = s.pos
|
|
384
|
+
case piece(s, extended)
|
|
385
|
+
when "(" then depth += 1
|
|
386
|
+
when ")" then depth -= 1
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
Assertion.new(kind, s.string.byteslice(start, finish - start))
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# A character class (nested ones included), after its "[". A "]"
|
|
393
|
+
# right after "[" or "[^" is a character, not the end.
|
|
394
|
+
def character_class(s)
|
|
395
|
+
start = s.pos - 1
|
|
396
|
+
s.skip(/\^?\]?/)
|
|
397
|
+
depth = 1
|
|
398
|
+
while depth.positive? && !s.eos?
|
|
399
|
+
if s.skip(/\\./m) then next
|
|
400
|
+
elsif s.skip(/\[/) then depth += 1
|
|
401
|
+
elsif s.skip(/\]/) then depth -= 1
|
|
402
|
+
else s.getch
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
s.string.byteslice(start, s.pos - start)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|