parsanol 1.3.88-aarch64-linux → 1.3.90-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f2889605b2e93364b7966c358acff8e3d0314059d9eb9bfce8d755d330de21e
4
- data.tar.gz: aa00f7c4c0791f436c3df18175c7a9f722e4da45de07d7cfe070d283fc190abd
3
+ metadata.gz: 0a5eae02b01793cba6627f5e7e91d63789b3b4870da638d45868b6857dc557bd
4
+ data.tar.gz: c32369c415f06ed312afe42cbb2621c59918b4ca10c8c1945d9b39bd69e9244f
5
5
  SHA512:
6
- metadata.gz: e21fdffdb9736976ddfe2404b1f728ad2d3f273bd7e537df30f27c5926921d6f02a1ec7d24abf9104f1bf313d2b1ebc453ae0834d2cf61d7bf2452ea93f6bf91
7
- data.tar.gz: bf0a69f0018caeeee808d061939bc0ca4afb8b8aa3822c79e219eed6db3b01dc78f504cc1fb2e3dd40ce12c3829bf0514fea99ad5e031c0ee148ae941babea93
6
+ metadata.gz: 9c3fa5252fa8b34f870c741a0df64f27edf52e587217a07c56dbaefd344259870e483def84a1260eebb867af9da4c9b18ac8d799b2d18f678a44865d9c691bfa
7
+ data.tar.gz: 59d50b74eb42a3451108150ba3ba99bb537f561fad472fab7b6645859688adbbda8345e0090a03c975b1da20bc365b7005b4f04ad4a30629c08a419286bc7776
@@ -35,7 +35,7 @@ module Parsanol
35
35
  !@regex_source.nil?
36
36
  end
37
37
 
38
- def try(source, _context, _consume_all)
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
- [matched == @positive, nil]
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)
@@ -421,8 +421,29 @@ module Parsanol
421
421
  Atoms::Alternative.new(*node.a.map { |child| build_atom(child) })
422
422
  when :rep
423
423
  Atoms::Repetition.new(build_atom(node.a), node.b, node.c)
424
- when :opt then Atoms::Repetition.new(build_atom(node.a), 0, 1, :maybe)
425
- when :pred then Atoms::Lookahead.new(build_atom(node.b), node.a)
424
+ when :opt
425
+ # [ … ] is the OPTIONAL form; a bracket whose elements are all
426
+ # character classes reads as an optional SEQUENCE of single
427
+ # characters (matchable empty — captures come back nil), which
428
+ # is almost certainly a mis-typed multi-range class
429
+ # (parsanol-ruby#201). One class alone stays a legitimate
430
+ # optional ([ %x0D ] — CRLF handling).
431
+ if (content = node.a).kind == :seq && content.a.length > 1 &&
432
+ content.a.all? { |el| el.kind == :class }
433
+ raise CompileError,
434
+ "brackets contain a sequence of character classes: " \
435
+ "[ … ] is the optional form, so each class matches one " \
436
+ "character and the sequence can match empty (an as-capture " \
437
+ "over it yields nil). A multi-range character class is an " \
438
+ "alternation: (%x21-2f / %x3a-40 / …)"
439
+ end
440
+ Atoms::Repetition.new(build_atom(node.a), 0, 1, :maybe)
441
+ when :pred
442
+ if node.c == :behind
443
+ build_lookbehind(node)
444
+ else
445
+ Atoms::Lookahead.new(build_atom(node.b), node.a)
446
+ end
426
447
  when :cap
427
448
  # "" as name is the PARG spelling of Ruby parslet's
428
449
  # str("").as(:name): an always-succeeding zero-width marker
@@ -470,6 +491,41 @@ module Parsanol
470
491
  end
471
492
  end
472
493
 
494
+ # `!<X` / `&<X` (parsanol-ruby#197, coradoc#281): the behind
495
+ # guard consumes nothing and constrains only the text behind the
496
+ # position. A case-sensitive literal is the fixed byte window
497
+ # (Literal form); a character class — or builtin class reference,
498
+ # or case-insensitive literal — is the end-anchored flanking
499
+ # regex (Regex form). Anything else cannot be expressed behind.
500
+ def build_lookbehind(node)
501
+ positive = node.a
502
+ body = node.b
503
+ case body.kind
504
+ when :lit
505
+ if body.b
506
+ Atoms::Lookbehind.regex("(?i:#{Regexp.escape(body.a)})",
507
+ positive: positive)
508
+ else
509
+ Atoms::Lookbehind.new(body.a.bytesize, body.a, positive: positive)
510
+ end
511
+ when :class
512
+ Atoms::Lookbehind.regex(class_pattern(body.a), positive: positive)
513
+ when :ref
514
+ if BUILTIN_CLASSES.key?(body.a)
515
+ Atoms::Lookbehind.regex(class_pattern(BUILTIN_CLASSES[body.a]),
516
+ positive: positive)
517
+ else
518
+ raise CompileError,
519
+ "lookbehind body must be a literal or character class " \
520
+ "(got rule #{body.a.inspect})"
521
+ end
522
+ else
523
+ raise CompileError,
524
+ "lookbehind body must be a literal or character class " \
525
+ "(got #{body.kind.inspect})"
526
+ end
527
+ end
528
+
473
529
  def class_pattern(ranges)
474
530
  body = ranges.map do |lo, hi|
475
531
  if lo == hi
@@ -635,7 +691,13 @@ module Parsanol
635
691
  set
636
692
  when :opt then first_set(node.a) + [EPS]
637
693
  when :pred
638
- node.a ? first_set(node.b) : Set.new([ANY])
694
+ # A behind-guard constrains nothing ahead: its first set is
695
+ # ANY for both signs.
696
+ if node.c == :behind
697
+ Set.new([ANY])
698
+ else
699
+ node.a ? first_set(node.b) : Set.new([ANY])
700
+ end
639
701
  when :cap then first_set(node.b)
640
702
  when :ref
641
703
  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 then Node.new(:pred, node.a, copy(node.b, prefix))
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
@@ -23,7 +23,7 @@ module Parsanol
23
23
  | (?<arrow>->)
24
24
  | (?<num>\d+)
25
25
  | (?<ident>[A-Za-z_][A-Za-z0-9_]*)
26
- | (?<punct>[=\/()\[\]{}*!&:,.])
26
+ | (?<punct>[=\/()\[\]{}*!&:,.<])
27
27
  /x
28
28
 
29
29
  def initialize(text)
@@ -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 = nil
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? ? node : Node.new(:pred, sign, node)
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 = nil
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? ? node : Node.new(:pred, sign, node)
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, :cap
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
@@ -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)[0]
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
- injector = Injector.new(skip_atom, markers, whitespace_kind)
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
- # Rule bodies own their injection when the grammar declares
203
- # skip on every rule (PARG); in the DSL surface the root tree
204
- # is walked once, so inject the resolved body as a fresh
205
- # injection scope (even inside a repetition interior — the
206
- # d54a29e exception is for rule-reference children). The
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.88"
4
+ VERSION = "1.3.90"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.88
4
+ version: 1.3.90
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A small Ruby library for constructing parsers in the PEG (Parsing Expression
14
14
  Grammar) fashion. Parsanol provides Parslet-compatible API with additional features