parsanol 1.3.88 → 1.3.89
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/lib/parsanol/atoms/lookbehind.rb +5 -2
- data/lib/parsanol/parg/compiler.rb +48 -2
- data/lib/parsanol/parg/imports.rb +2 -1
- data/lib/parsanol/parg/lexer.rb +1 -1
- data/lib/parsanol/parg/parser.rb +24 -10
- data/lib/parsanol/parg/visitor.rb +3 -1
- data/lib/parsanol/parser.rb +10 -2
- data/lib/parsanol/skip.rb +46 -26
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c14d89aab19e9406bc569b378659d7a1ca0b058490e4e4345c3eeff82eb49d06
|
|
4
|
+
data.tar.gz: 326c235976048e27e9bb6ad07a5ab9efa295a39b58e19b0ff366456f68cdeeac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05fc2044f427fc9f63ca67e59b4f364384d8905bfe3429d0133925497f5fe026f479c1ffea63a04edf3d9e3da442c4f1982946a756d3748d2475954cc28067b0
|
|
7
|
+
data.tar.gz: 0c12c39b5dd8ae1d2ca23b049aa6ec53787497dd0ec3799fbe1dceffeddf104ec238b6b858b423312671a87b31e98f131d0fda60328c9be7d24140f6b351afd4
|
|
@@ -35,7 +35,7 @@ module Parsanol
|
|
|
35
35
|
!@regex_source.nil?
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def try(source,
|
|
38
|
+
def try(source, context, _consume_all)
|
|
39
39
|
start = source.bytepos
|
|
40
40
|
if regex?
|
|
41
41
|
@compiled ||= Regexp.new("(?:#{@regex_source})\\z")
|
|
@@ -47,7 +47,10 @@ module Parsanol
|
|
|
47
47
|
end
|
|
48
48
|
matched = !behind.nil? && behind == @pattern.b
|
|
49
49
|
end
|
|
50
|
-
|
|
50
|
+
return ok(nil) if matched == @positive
|
|
51
|
+
|
|
52
|
+
message = regex? ? "text behind does not match /#{@regex_source}/" : "text behind is not #{@pattern.inspect}"
|
|
53
|
+
context.err_at(self, source, message, start)
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def to_s_inner(_prec)
|
|
@@ -422,7 +422,12 @@ module Parsanol
|
|
|
422
422
|
when :rep
|
|
423
423
|
Atoms::Repetition.new(build_atom(node.a), node.b, node.c)
|
|
424
424
|
when :opt then Atoms::Repetition.new(build_atom(node.a), 0, 1, :maybe)
|
|
425
|
-
when :pred
|
|
425
|
+
when :pred
|
|
426
|
+
if node.c == :behind
|
|
427
|
+
build_lookbehind(node)
|
|
428
|
+
else
|
|
429
|
+
Atoms::Lookahead.new(build_atom(node.b), node.a)
|
|
430
|
+
end
|
|
426
431
|
when :cap
|
|
427
432
|
# "" as name is the PARG spelling of Ruby parslet's
|
|
428
433
|
# str("").as(:name): an always-succeeding zero-width marker
|
|
@@ -470,6 +475,41 @@ module Parsanol
|
|
|
470
475
|
end
|
|
471
476
|
end
|
|
472
477
|
|
|
478
|
+
# `!<X` / `&<X` (parsanol-ruby#197, coradoc#281): the behind
|
|
479
|
+
# guard consumes nothing and constrains only the text behind the
|
|
480
|
+
# position. A case-sensitive literal is the fixed byte window
|
|
481
|
+
# (Literal form); a character class — or builtin class reference,
|
|
482
|
+
# or case-insensitive literal — is the end-anchored flanking
|
|
483
|
+
# regex (Regex form). Anything else cannot be expressed behind.
|
|
484
|
+
def build_lookbehind(node)
|
|
485
|
+
positive = node.a
|
|
486
|
+
body = node.b
|
|
487
|
+
case body.kind
|
|
488
|
+
when :lit
|
|
489
|
+
if body.b
|
|
490
|
+
Atoms::Lookbehind.regex("(?i:#{Regexp.escape(body.a)})",
|
|
491
|
+
positive: positive)
|
|
492
|
+
else
|
|
493
|
+
Atoms::Lookbehind.new(body.a.bytesize, body.a, positive: positive)
|
|
494
|
+
end
|
|
495
|
+
when :class
|
|
496
|
+
Atoms::Lookbehind.regex(class_pattern(body.a), positive: positive)
|
|
497
|
+
when :ref
|
|
498
|
+
if BUILTIN_CLASSES.key?(body.a)
|
|
499
|
+
Atoms::Lookbehind.regex(class_pattern(BUILTIN_CLASSES[body.a]),
|
|
500
|
+
positive: positive)
|
|
501
|
+
else
|
|
502
|
+
raise CompileError,
|
|
503
|
+
"lookbehind body must be a literal or character class " \
|
|
504
|
+
"(got rule #{body.a.inspect})"
|
|
505
|
+
end
|
|
506
|
+
else
|
|
507
|
+
raise CompileError,
|
|
508
|
+
"lookbehind body must be a literal or character class " \
|
|
509
|
+
"(got #{body.kind.inspect})"
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
473
513
|
def class_pattern(ranges)
|
|
474
514
|
body = ranges.map do |lo, hi|
|
|
475
515
|
if lo == hi
|
|
@@ -635,7 +675,13 @@ module Parsanol
|
|
|
635
675
|
set
|
|
636
676
|
when :opt then first_set(node.a) + [EPS]
|
|
637
677
|
when :pred
|
|
638
|
-
|
|
678
|
+
# A behind-guard constrains nothing ahead: its first set is
|
|
679
|
+
# ANY for both signs.
|
|
680
|
+
if node.c == :behind
|
|
681
|
+
Set.new([ANY])
|
|
682
|
+
else
|
|
683
|
+
node.a ? first_set(node.b) : Set.new([ANY])
|
|
684
|
+
end
|
|
639
685
|
when :cap then first_set(node.b)
|
|
640
686
|
when :ref
|
|
641
687
|
if @ref_stack.include?(node.a)
|
|
@@ -76,7 +76,8 @@ module Parsanol
|
|
|
76
76
|
when :alt then Node.new(:alt, node.a.map { |child| copy(child, prefix) })
|
|
77
77
|
when :rep then Node.new(:rep, copy(node.a, prefix), node.b, node.c)
|
|
78
78
|
when :opt then Node.new(:opt, copy(node.a, prefix))
|
|
79
|
-
when :pred
|
|
79
|
+
when :pred
|
|
80
|
+
Node.new(:pred, node.a, copy(node.b, prefix), node.c)
|
|
80
81
|
when :cap then Node.new(:cap, node.a, copy(node.b, prefix))
|
|
81
82
|
when :ref then Node.new(:ref, "#{prefix}#{node.a}")
|
|
82
83
|
else node
|
data/lib/parsanol/parg/lexer.rb
CHANGED
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -317,12 +317,25 @@ module Parsanol
|
|
|
317
317
|
# parens still binds the body — write `*( ( X ) as name )` for
|
|
318
318
|
# that (naming each iteration's X).
|
|
319
319
|
def parse_rep_body
|
|
320
|
-
sign =
|
|
321
|
-
if peek&.type == :punct && %w[! &].include?(peek.value)
|
|
322
|
-
sign = advance.value == "&"
|
|
323
|
-
end
|
|
320
|
+
sign, behind = parse_guard_sign
|
|
324
321
|
node = parse_primary
|
|
325
|
-
sign.nil?
|
|
322
|
+
if sign.nil?
|
|
323
|
+
node
|
|
324
|
+
else
|
|
325
|
+
Node.new(:pred, sign, node, behind ? :behind : nil)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# `!X` / `&X` are lookaheads; `!<X` / `&<X` (coradoc#281,
|
|
330
|
+
# parsanol-ruby#197) inspect the text BEHIND the position and
|
|
331
|
+
# consume nothing.
|
|
332
|
+
def parse_guard_sign
|
|
333
|
+
return [nil, false] unless peek&.type == :punct && %w[! &].include?(peek.value)
|
|
334
|
+
|
|
335
|
+
positive = advance.value == "&"
|
|
336
|
+
behind = peek&.type == :punct && peek.value == "<"
|
|
337
|
+
advance if behind
|
|
338
|
+
[positive, behind]
|
|
326
339
|
end
|
|
327
340
|
|
|
328
341
|
def capture_postfix(node)
|
|
@@ -334,12 +347,13 @@ module Parsanol
|
|
|
334
347
|
end
|
|
335
348
|
|
|
336
349
|
def parse_postfixed
|
|
337
|
-
sign =
|
|
338
|
-
if peek&.type == :punct && %w[! &].include?(peek.value)
|
|
339
|
-
sign = advance.value == "&"
|
|
340
|
-
end
|
|
350
|
+
sign, behind = parse_guard_sign
|
|
341
351
|
node = capture_postfix(parse_primary)
|
|
342
|
-
sign.nil?
|
|
352
|
+
if sign.nil?
|
|
353
|
+
node
|
|
354
|
+
else
|
|
355
|
+
Node.new(:pred, sign, node, behind ? :behind : nil)
|
|
356
|
+
end
|
|
343
357
|
end
|
|
344
358
|
|
|
345
359
|
def parse_primary
|
|
@@ -53,7 +53,9 @@ module Parsanol
|
|
|
53
53
|
Node.new(node.kind, rewrite(node.a), node.b, node.c)
|
|
54
54
|
when :opt
|
|
55
55
|
Node.new(node.kind, rewrite(node.a))
|
|
56
|
-
when :pred
|
|
56
|
+
when :pred
|
|
57
|
+
Node.new(node.kind, node.a, rewrite(node.b), node.c)
|
|
58
|
+
when :cap
|
|
57
59
|
Node.new(node.kind, node.a, rewrite(node.b))
|
|
58
60
|
else
|
|
59
61
|
yield(node) || node
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -55,7 +55,8 @@ module Parsanol
|
|
|
55
55
|
if skip_name
|
|
56
56
|
Parsanol::Skip.inject(self, atom, skip_name,
|
|
57
57
|
captures: self.class.skip_captures,
|
|
58
|
-
whitespace: self.class.skip_whitespace
|
|
58
|
+
whitespace: self.class.skip_whitespace,
|
|
59
|
+
exempt: self.class.skip_exempt)[0]
|
|
59
60
|
else
|
|
60
61
|
atom
|
|
61
62
|
end
|
|
@@ -90,10 +91,11 @@ module Parsanol
|
|
|
90
91
|
# rule(:line_comment) { str("//") >> match(/[^\n]/).repeat }
|
|
91
92
|
# skip :trivia, capture: { line_comment: :line_comment }
|
|
92
93
|
#
|
|
93
|
-
def skip(rule_name, capture: nil, whitespace: nil)
|
|
94
|
+
def skip(rule_name, capture: nil, whitespace: nil, exempt: nil)
|
|
94
95
|
@skip_rule_name = rule_name
|
|
95
96
|
@skip_captures = capture
|
|
96
97
|
@skip_whitespace = whitespace
|
|
98
|
+
@skip_exempt = exempt
|
|
97
99
|
nil
|
|
98
100
|
end
|
|
99
101
|
|
|
@@ -114,6 +116,12 @@ module Parsanol
|
|
|
114
116
|
def skip_whitespace
|
|
115
117
|
@skip_whitespace
|
|
116
118
|
end
|
|
119
|
+
|
|
120
|
+
# The declared exemption list (rule names whose bodies build
|
|
121
|
+
# without injection), or nil — see {.skip}.
|
|
122
|
+
def skip_exempt
|
|
123
|
+
@skip_exempt
|
|
124
|
+
end
|
|
117
125
|
end
|
|
118
126
|
|
|
119
127
|
# Delegates matching to the root rule.
|
data/lib/parsanol/skip.rb
CHANGED
|
@@ -20,8 +20,16 @@ module Parsanol
|
|
|
20
20
|
# capturer's leading literal attach to the next Named capture
|
|
21
21
|
# under `comments:` — the PARG `skip = trivia capture: comments`
|
|
22
22
|
# shape. Whitespace-shaped units never record.
|
|
23
|
+
#
|
|
24
|
+
# +exempt+ (parsanol-ruby#195) lists rule names whose bodies build
|
|
25
|
+
# WITHOUT injection — the DSL mirror of the PARG compiler's
|
|
26
|
+
# skip_exempt_names closure. Grammars that manage part of their
|
|
27
|
+
# trivia explicitly (expressir's remark rules) must not have
|
|
28
|
+
# wrappers inside those bodies: the wrapper would consume text the
|
|
29
|
+
# rule itself captures (a remark's own spaces) and record it as
|
|
30
|
+
# pending trivia, corrupting the value and double-representing it.
|
|
23
31
|
def self.inject(parser_instance, root_atom, skip_rule_name, captures: nil,
|
|
24
|
-
whitespace: nil)
|
|
32
|
+
whitespace: nil, exempt: nil)
|
|
25
33
|
skip_atom = parser_instance.__send__(skip_rule_name)
|
|
26
34
|
skip_atom = skip_atom.parslet while skip_atom.is_a?(Atoms::Entity)
|
|
27
35
|
|
|
@@ -33,7 +41,8 @@ module Parsanol
|
|
|
33
41
|
|
|
34
42
|
markers = capturer_markers(parser_instance, captures)
|
|
35
43
|
whitespace_kind = whitespace&.to_sym
|
|
36
|
-
|
|
44
|
+
exempt_names = exempt.nil? ? nil : Set.new(exempt.map(&:to_s))
|
|
45
|
+
injector = Injector.new(skip_atom, markers, whitespace_kind, exempt_names)
|
|
37
46
|
[injector.inject(root_atom, entry: true), skip_atom]
|
|
38
47
|
end
|
|
39
48
|
|
|
@@ -102,10 +111,11 @@ module Parsanol
|
|
|
102
111
|
# side observation). In-progress atoms mark a cycle re-entry and
|
|
103
112
|
# return raw, exactly as the path-scoped set did.
|
|
104
113
|
class Injector
|
|
105
|
-
def initialize(skip_atom, markers = nil, whitespace_kind = nil)
|
|
114
|
+
def initialize(skip_atom, markers = nil, whitespace_kind = nil, exempt_names = nil)
|
|
106
115
|
@skip_atom = skip_atom
|
|
107
116
|
@markers = markers
|
|
108
117
|
@whitespace_kind = whitespace_kind
|
|
118
|
+
@exempt_names = exempt_names
|
|
109
119
|
@wrapper_ids = {}.compare_by_identity
|
|
110
120
|
# Results keyed per injection context: the same shared atom
|
|
111
121
|
# can be reached both inside and outside repetition interiors,
|
|
@@ -199,30 +209,14 @@ module Parsanol
|
|
|
199
209
|
when Atoms::Ignored
|
|
200
210
|
Atoms::Ignored.new(walk(atom.wrapped_atom, mode))
|
|
201
211
|
when Atoms::Entity
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
# reference itself stays an Entity pointing at the injected
|
|
208
|
-
# body: inlining the body into every referencing site makes
|
|
209
|
-
# every rendered string (to_s, and failure messages built
|
|
210
|
-
# from inspect) expand the grammar exponentially on
|
|
211
|
-
# cross-referencing grammars — one failing Sequence rendered
|
|
212
|
-
# a multi-gigabyte message (parsanol-ruby#190).
|
|
213
|
-
begin
|
|
214
|
-
body = walk(atom.parslet, mode == :exempt ? :exempt : false)
|
|
215
|
-
if body.is_a?(Atoms::Lookahead)
|
|
216
|
-
# A whole-body lookahead keeps its #192 boundary visible
|
|
217
|
-
# to enclosing sequences (no wrapper before it).
|
|
218
|
-
body
|
|
219
|
-
else
|
|
220
|
-
ref = Atoms::Entity.new(atom.rule_name) { body }
|
|
221
|
-
ref.label = atom.label if atom.label
|
|
222
|
-
ref
|
|
223
|
-
end
|
|
224
|
-
rescue StandardError
|
|
212
|
+
# parsanol-ruby#195: exempt rules build without injection —
|
|
213
|
+
# the reference returns untouched, its body never walked.
|
|
214
|
+
# A wrapper inside a rule that explicitly manages trivia
|
|
215
|
+
# consumes text the rule itself captures.
|
|
216
|
+
if @exempt_names&.include?(atom.rule_name.to_s)
|
|
225
217
|
atom
|
|
218
|
+
else
|
|
219
|
+
inject_entity(atom, mode)
|
|
226
220
|
end
|
|
227
221
|
when Atoms::Str, Atoms::Re
|
|
228
222
|
mode ? atom : Atoms::Sequence.new(maybe, atom)
|
|
@@ -233,6 +227,32 @@ module Parsanol
|
|
|
233
227
|
@memo[mode][atom] = injected
|
|
234
228
|
injected
|
|
235
229
|
end
|
|
230
|
+
|
|
231
|
+
# Rule bodies own their injection when the grammar declares
|
|
232
|
+
# skip on every rule (PARG); in the DSL surface the root tree
|
|
233
|
+
# is walked once, so the resolved body is injected as a fresh
|
|
234
|
+
# injection scope (even inside a repetition interior — the
|
|
235
|
+
# d54a29e exception is for rule-reference children). The
|
|
236
|
+
# reference itself stays an Entity pointing at the injected
|
|
237
|
+
# body: inlining the body into every referencing site makes
|
|
238
|
+
# every rendered string (to_s, and failure messages built
|
|
239
|
+
# from inspect) expand the grammar exponentially on
|
|
240
|
+
# cross-referencing grammars — one failing Sequence rendered
|
|
241
|
+
# a multi-gigabyte message (parsanol-ruby#190).
|
|
242
|
+
def inject_entity(atom, mode)
|
|
243
|
+
body = walk(atom.parslet, mode == :exempt ? :exempt : false)
|
|
244
|
+
if body.is_a?(Atoms::Lookahead)
|
|
245
|
+
# A whole-body lookahead keeps its #192 boundary visible
|
|
246
|
+
# to enclosing sequences (no wrapper before it).
|
|
247
|
+
body
|
|
248
|
+
else
|
|
249
|
+
ref = Atoms::Entity.new(atom.rule_name) { body }
|
|
250
|
+
ref.label = atom.label if atom.label
|
|
251
|
+
ref
|
|
252
|
+
end
|
|
253
|
+
rescue StandardError
|
|
254
|
+
atom
|
|
255
|
+
end
|
|
236
256
|
end
|
|
237
257
|
end
|
|
238
258
|
end
|
data/lib/parsanol/version.rb
CHANGED