parsanol 1.3.87 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0b4e99564a7a93dc454c5e004acb05f2fb6884dc993a75c3eb30565c0360687
4
- data.tar.gz: 64ce48196a227e8c4f73dbc1d16a5415a278ef8a7730aaa7c8e85b45b28e75b3
3
+ metadata.gz: c14d89aab19e9406bc569b378659d7a1ca0b058490e4e4345c3eeff82eb49d06
4
+ data.tar.gz: 326c235976048e27e9bb6ad07a5ab9efa295a39b58e19b0ff366456f68cdeeac
5
5
  SHA512:
6
- metadata.gz: 2fde0240109a63a418d2851ca9578293934c784a0e14c74ecf74032dab99c361c729fdb468c4a4c8fdbc39e0f836a00a04202ccc7cfc96485230adfe636c7772
7
- data.tar.gz: 516e9a87cc5577b0f00d1f994783cb0da6132105052f466f3ed4afb45b41445d29234d96443c15e39b1557d14c93395457c10d9bc5b4a2d7d30ef8393dcc4ae1
6
+ metadata.gz: 05fc2044f427fc9f63ca67e59b4f364384d8905bfe3429d0133925497f5fe026f479c1ffea63a04edf3d9e3da442c4f1982946a756d3748d2475954cc28067b0
7
+ data.tar.gz: 0c12c39b5dd8ae1d2ca23b049aa6ec53787497dd0ec3799fbe1dceffeddf104ec238b6b858b423312671a87b31e98f131d0fda60328c9be7d24140f6b351afd4
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.18.0"
310
+ version = "0.18.2"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "dc899494d6b94ea8df14279b7dcbac89d59ba4d1be52f7b8ffe840674c6c8e3c"
312
+ checksum = "b059d18a92c61ba7c7c6241bb446196729dadf618e6e34c0b79935f0292d7a27"
313
313
  dependencies = [
314
314
  "ahash",
315
315
  "getrandom 0.3.4",
@@ -326,9 +326,9 @@ dependencies = [
326
326
 
327
327
  [[package]]
328
328
  name = "parsanol-derive"
329
- version = "0.18.0"
329
+ version = "0.18.2"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "b437046956f356eee0eb9b407ff1f1f6286af1f70f1af1d305c54a64c65b64e2"
331
+ checksum = "4b9a6de83c376137ccce01cb8890427a8879b34b6ac4832cc0413c5624923001"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -263,10 +263,10 @@ module Parsanol
263
263
  [false, @reporter.err(*)]
264
264
  end
265
265
 
266
- # Records one captured trivia unit (kind, text) for attachment to
267
- # the next successful Named capture.
268
- def push_captured_trivia(kind, text)
269
- @pending_trivia << { kind: kind, text: text }
266
+ # Records one captured trivia unit (kind, text, input offset)
267
+ # for attachment to the next successful Named capture.
268
+ def push_captured_trivia(kind, text, pos)
269
+ @pending_trivia << { kind: kind, text: text, pos: pos }
270
270
  nil
271
271
  end
272
272
 
@@ -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)
@@ -41,7 +41,7 @@ module Parsanol
41
41
 
42
42
  comments = context.take_pending_trivia
43
43
  result = wrap_result(value)
44
- result = attach_comments(result, comments) unless comments.empty?
44
+ result = attach_comments(result, comments, source) unless comments.empty?
45
45
  ok(result)
46
46
  end
47
47
 
@@ -50,9 +50,13 @@ module Parsanol
50
50
  # parsanol-ruby#152: trivia captured ahead of this capture rides
51
51
  # with it under `comments:`. The attachment only fires when the
52
52
  # grammar declared capture AND trivia actually preceded — key
53
- # absence everywhere else keeps v1 trees byte-identical.
54
- def attach_comments(result, comments)
55
- list = comments.map { |unit| { unit[:kind] => unit[:text] } }
53
+ # absence everywhere else keeps v1 trees byte-identical. Units
54
+ # carry their input offset so the value is a positioned Slice,
55
+ # like every other capture value (native parity, ruby#180).
56
+ def attach_comments(result, comments, source)
57
+ list = comments.map do |unit|
58
+ { unit[:kind] => Slice.new(unit[:pos], unit[:text], source.input) }
59
+ end
56
60
  if result.is_a?(Hash)
57
61
  result.merge(comments: list)
58
62
  else
@@ -61,13 +61,17 @@ module Parsanol
61
61
  def record(source, context, start_pos, end_pos)
62
62
  text = source.input[start_pos...end_pos]
63
63
  # A trivia unit may lead with whitespace before the comment
64
- # shape; markers test the comment head.
64
+ # shape; markers test the comment head. The recorded offset
65
+ # must agree with the recorded content as a slice of the
66
+ # input: trimmed marker text starts after the unit's leading
67
+ # whitespace, not at the wrapper (native parity, ruby#180).
65
68
  stripped = text.lstrip
66
69
  marker, label = @capturers&.find { |m, _label| stripped.start_with?(m) }
67
70
  if marker
68
- context.push_captured_trivia(label, stripped.strip)
71
+ context.push_captured_trivia(label, stripped.strip,
72
+ start_pos + (text.length - stripped.length))
69
73
  elsif @whitespace_kind
70
- context.push_captured_trivia(@whitespace_kind, text)
74
+ context.push_captured_trivia(@whitespace_kind, text, start_pos)
71
75
  end
72
76
  end
73
77
  end
@@ -310,9 +310,16 @@ module Parsanol
310
310
  transformed
311
311
  elsif is_repetition
312
312
  if transformed.is_a?(Array)
313
- if transformed.all? do |item|
314
- item.is_a?(Hash) && item.key?(sym_key)
315
- end
313
+ if transformed.all?(Hash)
314
+ # Hash items carry their own capture
315
+ # names (parslet semantics) — the
316
+ # single-key path
317
+ # (transform_repetition_value) already
318
+ # keeps them; this multi-key path (a
319
+ # Named with a trivia attachment, or
320
+ # merged siblings) must agree — only
321
+ # unnamed items take the parent name
322
+ # per item.
316
323
  transformed
317
324
  else
318
325
  transformed.map { |item| { sym_key => item } }
@@ -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 then Atoms::Lookahead.new(build_atom(node.b), node.a)
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
- node.a ? first_set(node.b) : Set.new([ANY])
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 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)
@@ -296,30 +296,64 @@ module Parsanol
296
296
  if peek&.type == :punct && peek.value == "*"
297
297
  advance
298
298
  max = peek&.type == :num ? advance.value.to_i : nil
299
- node = Node.new(:rep, parse_postfixed, min, max)
299
+ node = Node.new(:rep, parse_rep_body, min, max)
300
300
  else
301
- node = Node.new(:rep, parse_postfixed, min, min)
301
+ node = Node.new(:rep, parse_rep_body, min, min)
302
302
  end
303
- return node
303
+ return capture_postfix(node)
304
304
  elsif token.type == :punct && token.value == "*"
305
305
  advance
306
306
  max = peek&.type == :num ? advance.value.to_i : nil
307
- return Node.new(:rep, parse_postfixed, 0, max)
307
+ return capture_postfix(Node.new(:rep, parse_rep_body, 0, max))
308
308
  end
309
309
  parse_postfixed
310
310
  end
311
311
 
312
- def parse_postfixed
313
- sign = nil
314
- if peek&.type == :punct && %w[! &].include?(peek.value)
315
- sign = advance.value == "&"
316
- end
312
+ # A repetition's body must not claim the `as` that follows the
313
+ # repetition's own closing paren (parsanol-ruby#197): the body
314
+ # parser is postfix-blind, and the postfix wraps the REPETITION —
315
+ # `*( X ) as name` captures the whole repeated run, matching the
316
+ # Ruby DSL's `.repeat.as(:name)`. An `as` inside the body's own
317
+ # parens still binds the body — write `*( ( X ) as name )` for
318
+ # that (naming each iteration's X).
319
+ def parse_rep_body
320
+ sign, behind = parse_guard_sign
317
321
  node = parse_primary
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]
339
+ end
340
+
341
+ def capture_postfix(node)
318
342
  if peek&.type == :ident && peek.value == "as"
319
343
  advance
320
344
  node = Node.new(:cap, ident.value, node)
321
345
  end
322
- sign.nil? ? node : Node.new(:pred, sign, node)
346
+ node
347
+ end
348
+
349
+ def parse_postfixed
350
+ sign, behind = parse_guard_sign
351
+ node = capture_postfix(parse_primary)
352
+ if sign.nil?
353
+ node
354
+ else
355
+ Node.new(:pred, sign, node, behind ? :behind : nil)
356
+ end
323
357
  end
324
358
 
325
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.87"
4
+ VERSION = "1.3.89"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.87
4
+ version: 1.3.89
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.