parsanol 1.3.92-aarch64-linux → 1.3.94-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: d97e0ef9b1ba9281cfce8b847384654d3420fdc8c0ad4fbd0284af5a35522653
4
- data.tar.gz: 9836bdd4bf811c8bc7ef99820e0b0c96a088bfbf33cbf52749ffdf3adb4aa2f3
3
+ metadata.gz: 9e0c1efcdfb93af513ae908d45c90b6f2d6c42015fb643e474bffbb37ae39316
4
+ data.tar.gz: 86ad51bce683affc8b5b94426151bf56694c41fa87b91c9e56c98b7e801c1efc
5
5
  SHA512:
6
- metadata.gz: f0d8078fe690e009410e0c54268468370fe7897007cdbb261de6bca40f64301b1fb45a098f41beffe7beb3bfba1be2fbe0533864484e063d89081ebee4d0f791
7
- data.tar.gz: 37346c31aeb72a9e5ca78fbee2135a43d10414134644633b8b224414a6eca32e162292d81bd8b9009dd91e9806617a72365d68681e670699c5f46b9f7b329086
6
+ metadata.gz: 1b5aea575e016166e39822daa41c36c9ef80da76c121610231c6c359071d31362f4e5ed0efd24b4278d8db896ff70f7702e71cbfdf5d4ff4e2f36469222e5085
7
+ data.tar.gz: b89fe36dabaa496b63c7bb220bd5c40c5698fbc3ecac8329359d53d2319ec56df283accc410e15fcfe14d8e0729cbfd876e58c6de382079ce716f1f94e795725
@@ -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: 40176952 bytes
4
- sha256: 58f5e7bcb9c7665ab38daba62f63533a89966dbe2b9e2d8c56d2c37bd7afeb37
3
+ size: 40550656 bytes
4
+ sha256: cff224d02b36baf6455b961827d069556a7fed497e0a8e778009065f12613f00
@@ -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: 40412064 bytes
4
- sha256: c2b4e8331d61feecb2146feb05047ff9b20da8543910429eb87c3111b6a871ed
3
+ size: 40785744 bytes
4
+ sha256: ae67525c0f460813aed0b9fccb58809b517da84d72dae383bc232bd3c0c5d4f0
@@ -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: 40403520 bytes
4
- sha256: 35be9d4c9905ab8a9d0fcce4c55d7909850b093d1065cf658f131a2081f9ca31
3
+ size: 40781360 bytes
4
+ sha256: e30f6696ef7f343f3edf49240039e9287277f6973d4e0bfee918939d82684056
@@ -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: 40413544 bytes
4
- sha256: 62c8e268e3990442aa746edfd13a0d24bc2fe40cbd58db8a845c7dfb67efdab0
3
+ size: 40791392 bytes
4
+ sha256: 2f8c4e6eb46bc01f1271aeb19e5b3beb3b1c81ce1deb99b500094fabf3b11517
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/native/libparsanol.so
3
- size: 35960704 bytes
4
- sha256: 579c6ce133fca3eec741131db52977ea97348767e59d3cb1c8e0149dc6f1bb91
3
+ size: 36352992 bytes
4
+ sha256: fc4e4581f2ff40db6d747bd6b6377e34da0d7389eccc7b1ecb30f485f2e59e25
@@ -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
@@ -152,21 +152,42 @@ module Parsanol
152
152
 
153
153
  # Run an external test list (suite files yield Document::Test
154
154
  # structs; embedded tests are artifact-shaped hashes).
155
- def run_test_list(tests, mode: :native)
155
+ # parse_with overrides how the input is turned into a shape
156
+ # (the envelope self-test drives the baked grammar JSON natively
157
+ # instead of root_atom's source rebuild); bindings and
158
+ # expectations still flow through the standard surface.
159
+ def run_test_list(tests, mode: :native, parse_with: nil)
156
160
  tests.filter_map do |test|
157
161
  hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
158
162
  entry_name = hash["entry"] || entries.first
159
163
  begin
160
- bound = parse_and_bind(entry_name, hash["input"], mode: mode)
161
- if hash["kind"] == "reject"
164
+ shape = parse_with ? parse_with.call(hash["input"]) : parse(entry_name, hash["input"], mode: mode)
165
+ case hash["kind"]
166
+ when "reject"
162
167
  "test #{hash['input'].inspect}: expected the input to be rejected"
163
- elsif hash["kind"] == "example"
168
+ when "example"
169
+ bound = apply_bindings(entry_name, shape)
164
170
  expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
165
- mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
171
+ mismatched = expect.reject do |key, value|
172
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
173
+ end
166
174
  next if mismatched.empty?
167
175
 
168
176
  "test #{hash['input'].inspect}: expected captures " \
169
177
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
178
+ when "capture"
179
+ captures = Bindings.collect_captures(shape)
180
+ mismatched = hash["expect"].reject do |key, value|
181
+ actual = captures[key.to_sym]&.first
182
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
183
+ end
184
+ next if mismatched.empty?
185
+
186
+ actuals = mismatched.keys.to_h do |key|
187
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
188
+ end
189
+ "test #{hash['input'].inspect}: expected captures " \
190
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
170
191
  end
171
192
  rescue Parsanol::ParseFailed
172
193
  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]
@@ -76,9 +76,40 @@ module Parsanol
76
76
  raise CompileError, failures.join("\n") unless failures.empty?
77
77
 
78
78
  envelope = build_envelope
79
+ failures = self.class.self_test(envelope, tables_dir: @tables_dir)
80
+ raise CompileError, failures.join("\n") unless failures.empty?
81
+
79
82
  Result.new(@atom_cache, @warnings, envelope)
80
83
  end
81
84
 
85
+ # Bake-time self-test (parsanol-ruby#223): replay the grammar's
86
+ # embedded tests against the BAKED ENVELOPE atom JSON, not the
87
+ # source this process just compiled. A bake that drops or
88
+ # rewrites a member (the 1.3.91 ordered-choice class) makes the
89
+ # envelope disagree with its own tests, and every Ruby-side
90
+ # parse path would mask it — root_atom rebuilds from the
91
+ # embedded source, so only the native engine's JSON registration
92
+ # executes the atom graph a wasm/rs consumer actually runs.
93
+ # Skips (empty) when the native engine is unavailable; the CI
94
+ # and release bakes always have it. Returns failure descriptions.
95
+ def self.self_test(envelope, tables_dir: nil)
96
+ tests = envelope["tests"].to_a
97
+ return [] if tests.empty?
98
+ return [] unless Native.available?
99
+
100
+ artifact = Artifact.from_json(JSON.generate(envelope), tables_dir: tables_dir)
101
+ default = envelope["default_entry"] || artifact.entries.first
102
+ grammars = {}
103
+ tests.filter_map do |test|
104
+ entry = test["entry"] || default
105
+ grammar_json = (grammars[entry] ||= JSON.generate(artifact.entry(entry)["grammar"]))
106
+ artifact.run_test_list(
107
+ [test],
108
+ parse_with: ->(input) { Native.parse_json_grammar(grammar_json, input) },
109
+ )
110
+ end.flatten
111
+ end
112
+
82
113
  def resolve_custom(name)
83
114
  class_name = @document.customs.fetch(name)
84
115
  klass = Object.const_get(class_name)
@@ -314,22 +345,43 @@ module Parsanol
314
345
  def run_tests
315
346
  return [] if @document.tests.empty?
316
347
 
317
- default_entry = resolve_default_entry
318
348
  view = test_view
319
349
  @document.tests.filter_map do |test|
320
350
  entry_name = test.entry || default_entry
321
351
  rule = @document.entries.fetch(entry_name)
322
352
  begin
323
353
  shape = atom_for(rule).parse(test.input)
324
- if test.kind == :reject
354
+ case test.kind
355
+ when :reject
325
356
  "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 }
357
+ when :example
358
+ bindings = binding_list(rule)
359
+ if bindings.empty?
360
+ raise CompileError,
361
+ "example tests need a bindings section (use `capture` " \
362
+ "for raw-capture assertions)"
363
+ end
364
+ bound = Bindings.apply(view, { "bindings" => bindings }, shape)
365
+ mismatched = test.expect.reject do |key, value|
366
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
367
+ end
329
368
  next if mismatched.empty?
330
369
 
331
370
  "test #{test.input.inspect}: expected captures " \
332
371
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
372
+ when :capture
373
+ captures = Bindings.collect_captures(shape)
374
+ mismatched = test.expect.reject do |key, value|
375
+ actual = captures[key.to_sym]&.first
376
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
377
+ end
378
+ next if mismatched.empty?
379
+
380
+ actuals = mismatched.keys.to_h do |key|
381
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
382
+ end
383
+ "test #{test.input.inspect}: expected captures " \
384
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
333
385
  end
334
386
  rescue Parsanol::ParseFailed
335
387
  unless test.kind == :reject
@@ -339,6 +391,13 @@ module Parsanol
339
391
  end
340
392
  end
341
393
 
394
+ # Only consulted when a test omits its entry: a multi-entry
395
+ # grammar whose tests are all explicit must compile (parsanol-
396
+ # ruby#216). Memoized so the raise happens once, on first need.
397
+ def default_entry
398
+ @default_entry ||= resolve_default_entry
399
+ end
400
+
342
401
  def resolve_default_entry
343
402
  own = @document.own_entries
344
403
  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.94"
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.94
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-15 00:00:00.000000000 Z
11
+ date: 2026-10-18 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