parsanol 1.3.92-arm-linux → 1.3.93-arm-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: 94ab60f4150519a940dd2084dcc399fe2e2691c94865d0673c128ef13e551e6c
4
- data.tar.gz: f032f98ca251fbbff68a71dce5b143a64602b9956b91d5ab3da97b0cbccb8310
3
+ metadata.gz: 38e6903eee4c564df8586705d284a07671cbf0935ed4c7641e8b44015c4c510e
4
+ data.tar.gz: c6f1c81c282193fd596386c717e756aa35d87ffd22d5c3c1554ba3b09d4e0e02
5
5
  SHA512:
6
- metadata.gz: f4f5e09e4f5e30fb52b3704e1348341ee2bdfab777392acdd11f98fce87e8aa0d9f69992abce1ecb137856857cac9d0d68b84717470db9235fb736e599bb54fe
7
- data.tar.gz: 7d14b295b9e53af62f279c4c66201a492e5cf5b57a550077d69d2d40355d55b65e107e6c0b35b71d3e1ccb8b31231a4fc598c6da3228ef27fbeada7a08a9965d
6
+ metadata.gz: 94c5b1eb30a297566ca23877a53b1d63be7cff2fa04a92084fb1e9ff0102b238723fba755469daadb5d38b2113638cf0a02697f06a65671d04e631b2bd8647bf
7
+ data.tar.gz: ce29e977d953b08817a2f425f86090d4272cde2fc41e29d0d68b90571765e01951cdd3b90242671d0e9de018bc36f3aa8e52fc5359e5578a7b0ce8bd14554cab
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.2/parsanol_native.so
3
- size: 34018692 bytes
4
- sha256: 73a0d5df2c8be448527b97be77cf17d2d32406a6fa95bad552ac33c93a3f53de
3
+ size: 34333284 bytes
4
+ sha256: 46c18a5ba0af8926c012eb98c6417dfca64931a837629b317fb7c3cc430b9085
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.3/parsanol_native.so
3
- size: 34203972 bytes
4
- sha256: c076da7d2462c80cffdaa4fee81a97622f90fd996cddc885ed0c1947bda237f4
3
+ size: 34518452 bytes
4
+ sha256: 0752c79e340c4c722b106443ff1faf6ff6d6d7c5800570906d1c548585e06a58
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.4/parsanol_native.so
3
- size: 34198132 bytes
4
- sha256: 42ec5b96bfe8efb05a8377c9ae8410635f8916e9e3ea84e2f9e6a8d741159b58
3
+ size: 34512620 bytes
4
+ sha256: 9b0744875d02d5c1957b82cdf9249129ce62214696606b84886609243342e23f
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/4.0/parsanol_native.so
3
- size: 34207868 bytes
4
- sha256: 56addd9fe8da96b569b0e50b2b30a1a28c382ce12cc035a0debf42d7679d197e
3
+ size: 34522276 bytes
4
+ sha256: 834bad07a21ef4ccaeb47fbfaf85c567866142fb141f6d93205d5d9958e0c2f2
Binary file
@@ -167,59 +167,58 @@ module Parsanol
167
167
  end
168
168
 
169
169
  def grammar_structure_hash(atom)
170
- Digest::MD5.hexdigest(atom_structure(atom).to_s)
170
+ Digest::MD5.hexdigest(append_atom_structure(atom))
171
171
  end
172
172
 
173
- def atom_structure(atom, visited = {})
174
- # Cycle detection - return a placeholder if we've seen this atom before
175
- obj_id = atom.object_id
176
- if visited[obj_id]
177
- return [:cycle, atom.class.name]
173
+ def append_atom_structure(atom, visited = {}.compare_by_identity, output = +"")
174
+ if visited[atom]
175
+ return output << "[:cycle, " << atom.class.name.inspect << "]"
178
176
  end
179
177
 
180
- visited[obj_id] = true
181
-
178
+ visited[atom] = true
182
179
  case atom
183
- when ::Parsanol::Atoms::Entity
184
- # Recursively resolve entity to get actual structure for hash
185
- atom_structure(atom.parslet, visited)
186
- when ::Parsanol::Atoms::Str
187
- [:str, atom.str]
188
- when ::Parsanol::Atoms::Re
189
- [:re, atom.match]
190
- when ::Parsanol::Atoms::Sequence
191
- [:seq, atom.parslets.map { |p| atom_structure(p, visited) }]
192
- when ::Parsanol::Atoms::Alternative
193
- [:alt, atom.alternatives.map { |p| atom_structure(p, visited) }]
194
- when ::Parsanol::Atoms::Repetition
195
- [:rep, atom.min, atom.max, atom_structure(atom.parslet, visited)]
196
- when ::Parsanol::Atoms::Named
197
- [:named, atom.name.to_s, atom_structure(atom.parslet, visited)]
198
- when ::Parsanol::Atoms::Lookahead
199
- [:lookahead, atom.positive,
200
- atom_structure(atom.bound_parslet, visited)]
201
- when ::Parsanol::Atoms::Constant
202
- # The value differentiates otherwise-identical constant
203
- # atoms — without it every Constant hashes alike and the
204
- # grammar cache serves the first wire forever.
205
- [:constant, atom.value.inspect]
206
- when ::Parsanol::Atoms::Lookbehind
207
- [:lookbehind, atom.count, atom.pattern, atom.positive]
208
- # Subclass first: TriviaCapture < Trivia < Ignored (the same
209
- # dispatch discipline as the serializer's).
210
- when ::Parsanol::Atoms::TriviaCapture
211
- # The capturer table differentiates capture-carrying
212
- # grammars from plain-trivia ones — without it the grammar
213
- # cache serves one variant's wire to every other and the
214
- # wrong marker table reaches the engine (parsanol-ruby#180).
215
- [:trivia_capture, atom.capturers.to_a.sort,
216
- atom_structure(atom.wrapped_atom, visited)]
217
- when ::Parsanol::Atoms::Trivia
218
- [:trivia, atom_structure(atom.wrapped_atom, visited)]
219
- when ::Parsanol::Atoms::Ignored
220
- [:ignored, atom_structure(atom.wrapped_atom, visited)]
180
+ when Parsanol::Atoms::Entity
181
+ append_atom_structure(atom.parslet, visited, output)
182
+ when Parsanol::Atoms::Str
183
+ output << "[:str, " << atom.str.inspect << "]"
184
+ when Parsanol::Atoms::Re
185
+ output << "[:re, " << atom.match.inspect << "]"
186
+ when Parsanol::Atoms::Sequence, Parsanol::Atoms::Alternative
187
+ children = atom.is_a?(Parsanol::Atoms::Sequence) ? atom.parslets : atom.alternatives
188
+ output << (atom.is_a?(Parsanol::Atoms::Sequence) ? "[:seq, [" : "[:alt, [")
189
+ index = 0
190
+ while index < children.length
191
+ output << ", " unless index.zero?
192
+ append_atom_structure(children[index], visited, output)
193
+ index += 1
194
+ end
195
+ output << "]]"
196
+ when Parsanol::Atoms::Repetition
197
+ output << "[:rep, " << atom.min.inspect << ", " << atom.max.inspect << ", "
198
+ append_atom_structure(atom.parslet, visited, output) << "]"
199
+ when Parsanol::Atoms::Named
200
+ output << "[:named, " << atom.name.to_s.inspect << ", "
201
+ append_atom_structure(atom.parslet, visited, output) << "]"
202
+ when Parsanol::Atoms::Lookahead
203
+ output << "[:lookahead, " << atom.positive.inspect << ", "
204
+ append_atom_structure(atom.bound_parslet, visited, output) << "]"
205
+ when Parsanol::Atoms::Constant
206
+ # The inspected constant remains a string inside the fingerprint.
207
+ output << "[:constant, " << atom.value.inspect.inspect << "]"
208
+ when Parsanol::Atoms::Lookbehind
209
+ output << "[:lookbehind, " << atom.count.inspect << ", " << atom.pattern.inspect << ", " << atom.positive.inspect << "]"
210
+ # Capturing trivia must precede its Trivia and Ignored parents.
211
+ when Parsanol::Atoms::TriviaCapture
212
+ output << "[:trivia_capture, " << atom.capturers.to_a.sort.inspect << ", "
213
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
214
+ when Parsanol::Atoms::Trivia
215
+ output << "[:trivia, "
216
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
217
+ when Parsanol::Atoms::Ignored
218
+ output << "[:ignored, "
219
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
221
220
  else
222
- [:unknown, atom.class.name]
221
+ output << "[:unknown, " << atom.class.name.inspect << "]"
223
222
  end
224
223
  end
225
224
  end
@@ -157,16 +157,33 @@ module Parsanol
157
157
  hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
158
158
  entry_name = hash["entry"] || entries.first
159
159
  begin
160
- bound = parse_and_bind(entry_name, hash["input"], mode: mode)
161
- if hash["kind"] == "reject"
160
+ shape = parse(entry_name, hash["input"], mode: mode)
161
+ case hash["kind"]
162
+ when "reject"
162
163
  "test #{hash['input'].inspect}: expected the input to be rejected"
163
- elsif hash["kind"] == "example"
164
+ when "example"
165
+ bound = apply_bindings(entry_name, shape)
164
166
  expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
165
- mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
167
+ mismatched = expect.reject do |key, value|
168
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
169
+ end
166
170
  next if mismatched.empty?
167
171
 
168
172
  "test #{hash['input'].inspect}: expected captures " \
169
173
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
174
+ when "capture"
175
+ captures = Bindings.collect_captures(shape)
176
+ mismatched = hash["expect"].reject do |key, value|
177
+ actual = captures[key.to_sym]&.first
178
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
179
+ end
180
+ next if mismatched.empty?
181
+
182
+ actuals = mismatched.keys.to_h do |key|
183
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
184
+ end
185
+ "test #{hash['input'].inspect}: expected captures " \
186
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
170
187
  end
171
188
  rescue Parsanol::ParseFailed
172
189
  unless hash["kind"] == "reject"
@@ -23,6 +23,19 @@ module Parsanol
23
23
  out
24
24
  end
25
25
 
26
+ # Canonical test-value form: one equality surface for capture and
27
+ # example expectations on both sides of the comparison (Slice vs
28
+ # source literal was comparing heterogeneous types). Terminals
29
+ # reduce to their text; structure recurses; keys stringify.
30
+ def canonical(value)
31
+ case value
32
+ when Hash then value.to_h { |key, item| [key.to_s, canonical(item)] }
33
+ when Array then value.map { |item| canonical(item) }
34
+ when nil, true, false, Integer then value
35
+ else String(value)
36
+ end
37
+ end
38
+
26
39
  def collect_captures(shape)
27
40
  found = Hash.new { |hash, key| hash[key] = [] }
28
41
  queue = [shape]
@@ -314,22 +314,43 @@ module Parsanol
314
314
  def run_tests
315
315
  return [] if @document.tests.empty?
316
316
 
317
- default_entry = resolve_default_entry
318
317
  view = test_view
319
318
  @document.tests.filter_map do |test|
320
319
  entry_name = test.entry || default_entry
321
320
  rule = @document.entries.fetch(entry_name)
322
321
  begin
323
322
  shape = atom_for(rule).parse(test.input)
324
- if test.kind == :reject
323
+ case test.kind
324
+ when :reject
325
325
  "test #{test.input.inspect}: expected the input to be rejected"
326
- elsif test.kind == :example
327
- bound = Bindings.apply(view, { "bindings" => binding_list(rule) }, shape)
328
- mismatched = test.expect.reject { |key, value| bound.key?(key) && bound[key] == value }
326
+ when :example
327
+ bindings = binding_list(rule)
328
+ if bindings.empty?
329
+ raise CompileError,
330
+ "example tests need a bindings section (use `capture` " \
331
+ "for raw-capture assertions)"
332
+ end
333
+ bound = Bindings.apply(view, { "bindings" => bindings }, shape)
334
+ mismatched = test.expect.reject do |key, value|
335
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
336
+ end
329
337
  next if mismatched.empty?
330
338
 
331
339
  "test #{test.input.inspect}: expected captures " \
332
340
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
341
+ when :capture
342
+ captures = Bindings.collect_captures(shape)
343
+ mismatched = test.expect.reject do |key, value|
344
+ actual = captures[key.to_sym]&.first
345
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
346
+ end
347
+ next if mismatched.empty?
348
+
349
+ actuals = mismatched.keys.to_h do |key|
350
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
351
+ end
352
+ "test #{test.input.inspect}: expected captures " \
353
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
333
354
  end
334
355
  rescue Parsanol::ParseFailed
335
356
  unless test.kind == :reject
@@ -339,6 +360,13 @@ module Parsanol
339
360
  end
340
361
  end
341
362
 
363
+ # Only consulted when a test omits its entry: a multi-entry
364
+ # grammar whose tests are all explicit must compile (parsanol-
365
+ # ruby#216). Memoized so the raise happens once, on first need.
366
+ def default_entry
367
+ @default_entry ||= resolve_default_entry
368
+ end
369
+
342
370
  def resolve_default_entry
343
371
  own = @document.own_entries
344
372
  return own.first if own && own.size == 1
@@ -5,7 +5,15 @@ module Parsanol
5
5
  # Parsed PARG document: grammar rules plus the binding sections.
6
6
  class Document
7
7
  Binding = Struct.new(:capture, :path, :type, :card, :preprocess)
8
- TEST_KINDS = %i[accept reject example].freeze
8
+ # `capture` = phase 1 (grammar layer): asserts the raw capture tree,
9
+ # no bindings section needed. `example` = phase 2 (bindings layer):
10
+ # asserts the bound, preprocessed attribute hash.
11
+ # `capture` = phase 1 (grammar layer): asserts the raw capture tree,
12
+ # no bindings section needed. `example` = phase 2 (bindings layer):
13
+ # asserts the bound, preprocessed attribute hash.
14
+ TEST_KINDS = %i[accept reject example capture].freeze
15
+ # Test kinds whose expectations are `{ key: value }` pairs.
16
+ PAIR_KINDS = %w[example capture].freeze
9
17
 
10
18
  Test = Struct.new(:entry, :kind, :input, :expect)
11
19
 
@@ -83,7 +83,7 @@ module Parsanol
83
83
  kind = ident.value
84
84
  input = unquote(expect(:str).value)
85
85
  expect_pairs = {}
86
- if kind == "example" && peek&.type == :punct && peek.value == "{"
86
+ if Document::PAIR_KINDS.include?(kind) && peek&.type == :punct && peek.value == "{"
87
87
  advance
88
88
  skip_newlines
89
89
  until peek&.type == :punct && peek.value == "}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.92"
4
+ VERSION = "1.3.93"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -146,7 +146,12 @@ module Parsanol
146
146
  # The shared program's compile-time analysis (heavy-memo
147
147
  # seeding) covered this structure; the fresh root inherits
148
148
  # it so its first parse memoizes like the original's.
149
- (@heavy ||= {}.compare_by_identity)[root] = true if @heavy_by_hash&.[](hash_key)
149
+ if @heavy_by_hash&.[](hash_key)
150
+ (@heavy ||= {}.compare_by_identity)[root] = true
151
+ trim_cache(@heavy)
152
+ end
153
+ cache[root] = shared
154
+ trim_cache(cache)
150
155
  return shared
151
156
  end
152
157
 
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.92
4
+ version: 1.3.93
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-15 00:00:00.000000000 Z
11
+ date: 2026-10-16 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