parsanol 1.3.70 → 1.3.71

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: 46a9a34dd51e63ba24ee155131eb13326947d08cf08a9814a6d600046aead88a
4
- data.tar.gz: 679d459fa6c46a6b27816c042af13b0c6e9f15ccae847786140c473731b25ebd
3
+ metadata.gz: 31f449debbc9026b7145e9997f9012f9b4506c9506532b37c29d7700a35c75c2
4
+ data.tar.gz: 0fff18b2ebfa691eca0d08937fb246e39c38cc15bc7e1c13cd82835ee2ea5be2
5
5
  SHA512:
6
- metadata.gz: 5bddc1e0b863c8f50be6573f29e09b17091c95dbfd0db57c660aea348d090b681bb7d99e3870276f3b05f32fcc1944098f71777bdaf4db2b34f348c1e847fbaa
7
- data.tar.gz: bbb0ce39775917e628fcc19a04d15e2f252437d6fdcfe9b9b69f33711e096b2cb9a2b9a40908177068f66e54c3dc9ceb4d4ba25396b4cfc0ac465de34667b65d
6
+ metadata.gz: d93e19f1581d6a55902f9d496ef8fe998e293709983b0572b2c3192f2779f26e88168c785eee43d045cabfc87e4c334168851a953ab5cb7eb3ff74a53c8149d5
7
+ data.tar.gz: '0818e9e2bc93e762f13dc21410f1449838df58555369b223ca91669e4c0247dc137122d3e4b10ba0a8ad56c0642ffa482cfb1d3cce157708f085ea6e3ee966c7'
data/Cargo.lock CHANGED
@@ -307,34 +307,16 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.10.2"
311
- source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "dd43c104276def3f79a54ce6e8d26642ea330c13f97d66380bd94fc057f0a849"
313
- dependencies = [
314
- "ahash",
315
- "getrandom 0.3.4",
316
- "getrandom 0.4.3",
317
- "hashbrown",
318
- "parsanol-derive 0.10.2",
319
- "regex",
320
- "regex-syntax",
321
- "serde",
322
- "serde_json",
323
- "sha2",
324
- ]
325
-
326
- [[package]]
327
- name = "parsanol"
328
- version = "0.12.0"
310
+ version = "0.12.1"
329
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
330
- checksum = "b3754baf8f7f9761748bac497cd60efd1bab93ff54c4212e899e27cf3f09100a"
312
+ checksum = "dab930fe5902a8f92e8bcd45b6521a52b79b9090b89b8c6b5b867aaa7527054a"
331
313
  dependencies = [
332
314
  "ahash",
333
315
  "getrandom 0.3.4",
334
316
  "getrandom 0.4.3",
335
317
  "hashbrown",
336
318
  "magnus",
337
- "parsanol-derive 0.12.0",
319
+ "parsanol-derive",
338
320
  "regex",
339
321
  "regex-syntax",
340
322
  "serde",
@@ -344,20 +326,9 @@ dependencies = [
344
326
 
345
327
  [[package]]
346
328
  name = "parsanol-derive"
347
- version = "0.10.2"
348
- source = "registry+https://github.com/rust-lang/crates.io-index"
349
- checksum = "eca430ba21d7cc2d674080d40dd37a5567e1a0acdc37faef9a9b3738b8651dd6"
350
- dependencies = [
351
- "proc-macro2",
352
- "quote",
353
- "syn 3.0.6",
354
- ]
355
-
356
- [[package]]
357
- name = "parsanol-derive"
358
- version = "0.12.0"
329
+ version = "0.12.1"
359
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
360
- checksum = "7cdc8438988a014ec5a7cb6d7891c3c467154de134b353e8790bd42d4ae2ddd5"
331
+ checksum = "18ef2346916b3d6aca5d6ad50c0b56402cce2050d1ebeeaf5b6a0c24de110eb9"
361
332
  dependencies = [
362
333
  "proc-macro2",
363
334
  "quote",
@@ -368,7 +339,7 @@ dependencies = [
368
339
  name = "parsanol_cdylib"
369
340
  version = "1.3.52"
370
341
  dependencies = [
371
- "parsanol 0.10.2",
342
+ "parsanol",
372
343
  ]
373
344
 
374
345
  [[package]]
@@ -377,7 +348,7 @@ version = "1.0.2"
377
348
  dependencies = [
378
349
  "log",
379
350
  "magnus",
380
- "parsanol 0.12.0",
351
+ "parsanol",
381
352
  "rb-sys",
382
353
  ]
383
354
 
@@ -12,4 +12,5 @@ crate-type = ["cdylib"]
12
12
  doc = false
13
13
 
14
14
  [dependencies]
15
- parsanol = { version = "0.10" }
15
+ # 0.12 carries rs#174 (native dynamic-grammar backtracking fix)
16
+ parsanol = { version = "0.12" }
@@ -29,7 +29,9 @@ magnus = { version = "0.9" }
29
29
 
30
30
  # parsanol parser library (crates.io; 0.12 carries the rs#174 native
31
31
  # backtracking fix: dynamic-dependent memoization + outcome cache)
32
- parsanol = { version = "0.12.0", features = ["ruby"] }
32
+ # parsanol parser library (crates.io; 0.12 carries the rs#174 native
33
+ # backtracking fix: dynamic-dependent memoization + outcome replay)
34
+ parsanol = { version = "0.12.1", features = ["ruby"] }
33
35
 
34
36
  # Logging
35
37
  log = "0.4"
@@ -67,6 +67,11 @@ module Parsanol
67
67
  # Injected-trivia depth: while positive, reported causes are
68
68
  # marked trivia and never surface in diagnostics.
69
69
  @trivia_depth = 0
70
+ # Captured trivia channel (parsanol-ruby#152): when the grammar
71
+ # declares `skip ... capture:`, matched trivia units land here
72
+ # and the NEXT successful Named capture attaches (and drains)
73
+ # them under `comments:`.
74
+ @pending_trivia = []
70
75
  @evict_threshold = 200
71
76
  @evict_counter = 0
72
77
  @evict_interval = 100
@@ -258,6 +263,34 @@ module Parsanol
258
263
  [false, @reporter.err(*)]
259
264
  end
260
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 }
270
+ nil
271
+ end
272
+
273
+ # Snapshot for backtracking: a failed branch must not leak its
274
+ # half-recorded trivia into the successful alternative.
275
+ def take_pending_trivia_snapshot
276
+ @pending_trivia.dup
277
+ end
278
+
279
+ def restore_pending_trivia(snapshot)
280
+ @pending_trivia = snapshot
281
+ nil
282
+ end
283
+
284
+ # Drains pending captured trivia (attached by the next
285
+ # successful Named capture). Backtracking discards whatever has
286
+ # not been attached yet — the caller snapshots position and
287
+ # restores this list alongside.
288
+ def take_pending_trivia
289
+ list = @pending_trivia
290
+ @pending_trivia = []
291
+ list
292
+ end
293
+
261
294
  # Brackets a parse inside injected trivia: causes reported while
262
295
  # the depth is positive carry the trivia flag and never surface
263
296
  # in rendered trees or deepest-failure positions.
@@ -32,10 +32,32 @@ module Parsanol
32
32
  # @param consume_all [Boolean] require full consumption
33
33
  # @return [Array(Boolean, Object)] result
34
34
  def apply(source, context, consume_all)
35
+ trivia_before = context.take_pending_trivia_snapshot
35
36
  success, value = @parslet.apply(source, context, consume_all)
36
- return [false, value] unless success
37
+ unless success
38
+ context.restore_pending_trivia(trivia_before)
39
+ return [false, value]
40
+ end
37
41
 
38
- ok(wrap_result(value))
42
+ comments = context.take_pending_trivia
43
+ result = wrap_result(value)
44
+ result = attach_comments(result, comments) unless comments.empty?
45
+ ok(result)
46
+ end
47
+
48
+ private
49
+
50
+ # parsanol-ruby#152: trivia captured ahead of this capture rides
51
+ # with it under `comments:`. The attachment only fires when the
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] } }
56
+ if result.is_a?(Hash)
57
+ result.merge(comments: list)
58
+ else
59
+ { @name => result, comments: list }
60
+ end
39
61
  end
40
62
 
41
63
  # Named wrappers skip caching (inner parser handles it).
@@ -60,8 +82,6 @@ module Parsanol
60
82
  @parslet.first_set
61
83
  end
62
84
 
63
- private
64
-
65
85
  # Wraps matched value in labeled hash.
66
86
  #
67
87
  # @param matched [Object] matched value
@@ -17,5 +17,53 @@ module Parsanol
17
17
  "trivia(#{@wrapped_atom.to_s(prec)})"
18
18
  end
19
19
  end
20
+
21
+ # parsanol-ruby#152: the skip-declared capture wrapper. Whitespace
22
+ # trivia stays Ignored; comment-shaped units (the grammar names the
23
+ # capture rules in the declaration) are recorded into the context
24
+ # channel for attachment to the next successful Named capture.
25
+ # Span discipline is unchanged: the matched bytes still contribute
26
+ # nothing to values or capture spans.
27
+ class TriviaCapture < Trivia
28
+ # +capturers+ maps leading-literal marker -> kind label (the
29
+ # compiler derives markers from each capturer rule's first Str
30
+ # atom, e.g. "//" => :line_comment); units not matching any
31
+ # marker are whitespace-shaped and stay unrecorded.
32
+ def initialize(atom, capturers)
33
+ super(atom)
34
+ @capturers = capturers
35
+ end
36
+
37
+ def apply(source, context, consume_all)
38
+ context.with_trivia do
39
+ start = source.bytepos
40
+ ok, _value = @wrapped_atom.apply(source, context, consume_all)
41
+ if ok && source.bytepos > start
42
+ record(source, context, start, source.bytepos)
43
+ return ok(nil)
44
+ end
45
+ [ok, _value]
46
+ end
47
+ end
48
+
49
+ def to_s_inner(_prec)
50
+ "trivia!(@wrapped_atom.to_s(prec))"
51
+ end
52
+
53
+ private
54
+
55
+ def record(source, context, start_pos, end_pos)
56
+ return unless @capturers
57
+
58
+ text = source.input[start_pos...end_pos]
59
+ # A trivia unit may lead with whitespace before the comment
60
+ # shape; markers test the comment head.
61
+ stripped = text.lstrip
62
+ marker, label = @capturers.find { |m, _label| stripped.start_with?(m) }
63
+ return unless marker
64
+
65
+ context.push_captured_trivia(label, stripped.strip)
66
+ end
67
+ end
20
68
  end
21
69
  end
@@ -71,7 +71,10 @@ module Parsanol
71
71
 
72
72
  root_atom(entry_name).parse(input)
73
73
  when :ruby
74
- root_atom(entry_name).parse(input)
74
+ # Deepest-failure reporting parity with the native engine: the
75
+ # default Tree reporter keeps the most recent (root) failure,
76
+ # which reports position 1:1 for grammars with entry wrappers.
77
+ root_atom(entry_name).parse(input, reporter: ErrorReporter::Deepest.new)
75
78
  else
76
79
  raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
77
80
  end
@@ -141,6 +141,51 @@ module Parsanol
141
141
  end
142
142
  end
143
143
 
144
+ # parsanol-ruby#152: capturer rules are the rules transitively
145
+ # referenced by the skip declaration whose leading literal is a
146
+ # non-whitespace marker — the comment shapes ("//", "/*", ...).
147
+ # Whitespace rules stay unrecorded trivia.
148
+ def skip_capturer_markers
149
+ @skip_capturer_markers ||= begin
150
+ markers = {}
151
+ seen = {}
152
+ collect = lambda do |node|
153
+ case node.kind
154
+ when :ref
155
+ next if seen[node.a]
156
+
157
+ seen[node.a] = true
158
+ literal = leading_literal(node.a)
159
+ markers[literal] = node.a.to_sym if literal && !literal.strip.empty?
160
+ collect.call(@document.rules[node.a]) if @document.rules.key?(node.a)
161
+ when :alt, :seq then node.a.each { |b| collect.call(b) }
162
+ when :rep, :opt, :cap then collect.call(node.a)
163
+ when :pred then collect.call(node.b)
164
+ end
165
+ end
166
+ collect.call(@document.skip_declaration)
167
+ markers
168
+ end
169
+ end
170
+
171
+ def leading_literal(rule_name)
172
+ node = @document.rules[rule_name]
173
+ return nil unless node
174
+
175
+ literal = nil
176
+ walk = lambda do |n|
177
+ break if literal
178
+
179
+ case n.kind
180
+ when :lit then literal = n.a
181
+ when :seq, :alt then n.a.each { |c| walk.call(c) unless literal }
182
+ when :rep, :opt, :cap, :pred then walk.call(n.a || n.b) unless literal
183
+ end
184
+ end
185
+ walk.call(node)
186
+ literal
187
+ end
188
+
144
189
  def skip_wrapper?(atom)
145
190
  @skip_wrapper_ids&.key?(atom)
146
191
  end
@@ -163,7 +208,13 @@ module Parsanol
163
208
  # span-joined captures — and failures inside it never surface
164
209
  # in diagnostics (rendered trees and deepest-failure
165
210
  # positions skip trivia causes).
166
- wrapper = Atoms::Trivia.new(Atoms::Repetition.new(atom, 0, 1))
211
+ inner = Atoms::Repetition.new(atom, 0, 1)
212
+ wrapper =
213
+ if @document.skip_capture_key
214
+ Atoms::TriviaCapture.new(inner, skip_capturer_markers)
215
+ else
216
+ Atoms::Trivia.new(inner)
217
+ end
167
218
  (@skip_wrapper_ids ||= {}.compare_by_identity)[wrapper] = true
168
219
  wrapper
169
220
  end
@@ -10,7 +10,7 @@ module Parsanol
10
10
  Test = Struct.new(:entry, :kind, :input, :expect)
11
11
 
12
12
  attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source,
13
- :atomic_rules
13
+ :atomic_rules, :skip_capture_key
14
14
  attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
15
15
  :customs, :states
16
16
  # Entries authored in this file (pre-import); nil-entry tests resolve
@@ -21,7 +21,7 @@ module Parsanol
21
21
  STATE_KEYWORDS = %w[custom state].freeze
22
22
  KEYWORDS = %w[grammar as alt from_table column bindings render derive
23
23
  preprocess entry table_lookup custom state set switch
24
- skip until atomic].freeze
24
+ skip until atomic capture].freeze
25
25
 
26
26
  def initialize(text)
27
27
  @tokens = Lexer.new(text).tokens
@@ -170,6 +170,15 @@ module Parsanol
170
170
  end
171
171
 
172
172
  node = parse_choice
173
+ # parsanol-ruby#152 phase 2: `skip = trivia capture:
174
+ # comments` attaches comment-shaped trivia (the rules the
175
+ # declaration references, identified by leading literal)
176
+ # to the next successful capture under `comments:`.
177
+ if peek&.type == :ident && peek.value == "capture"
178
+ advance
179
+ punct(":")
180
+ document.skip_capture_key = ident.value.to_sym
181
+ end
173
182
  document.skip_declaration = validate_skip_node(node)
174
183
  document.skip_source = skip_source_string(node)
175
184
  skip_newlines
@@ -260,7 +269,12 @@ module Parsanol
260
269
 
261
270
  def stop_element?
262
271
  peek.nil? || peek.type == :newline ||
263
- (peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value))
272
+ (peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value)) ||
273
+ # `capture:` suffix terminates the skip declaration's RHS
274
+ # (parsanol-ruby#152); everywhere else parse_element's guard
275
+ # rejects it.
276
+ (peek&.type == :ident && peek.value == "capture" &&
277
+ @tokens[@pos + 1]&.type == :punct && @tokens[@pos + 1]&.value == ":")
264
278
  end
265
279
 
266
280
  def parse_element
@@ -269,6 +283,13 @@ module Parsanol
269
283
  raise ParseError, "unexpected end of input inside sequence"
270
284
  end
271
285
 
286
+ # `capture:` is a skip-declaration suffix, never an expression
287
+ # element (reserved keyword; parsanol-ruby#152).
288
+ if token.type == :ident && token.value == "capture" &&
289
+ @tokens[@pos + 1]&.type == :punct && @tokens[@pos + 1]&.value == ":"
290
+ raise ParseError, "capture: only modifies the skip declaration"
291
+ end
292
+
272
293
  if token.type == :num
273
294
  advance
274
295
  min = token.value.to_i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.70"
4
+ VERSION = "1.3.71"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -649,6 +649,12 @@ module Parsanol
649
649
  compile_entity(atom, consume_all)
650
650
  when Parsanol::Atoms::Lookahead
651
651
  compile_lookahead(atom, consume_all)
652
+ when Parsanol::Atoms::TriviaCapture
653
+ # Captured trivia records into the context channel at match
654
+ # time (parsanol-ruby#152); the bytecode program has no
655
+ # channel, so the interpreter (which does) must own the
656
+ # grammar. Declining here routes program_for to :fallback.
657
+ nil
652
658
  when Parsanol::Atoms::Ignored
653
659
  inner = atom.wrapped_atom
654
660
  return nil if inner.nil?
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.70
4
+ version: 1.3.71
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.