parsanol 1.3.116 → 1.3.117

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: b6be4ab27d5d8c86b4f214b75cefb0bb16a3de4eb9e55402d93ca807fae280aa
4
- data.tar.gz: cf56a4e2921063811e55a4016342cce471acf24138825ebe24673f5563cf70ea
3
+ metadata.gz: '069ffc4a0b824942b8c6e21cbab7a9b0e450636049338c4f38fce28d3109e2d7'
4
+ data.tar.gz: 3b1341f63b428c23cfe66169eb0c9c5cf52b98c7af09ee785954f3256fa0db1f
5
5
  SHA512:
6
- metadata.gz: 13957312679b48cf55f8455e8a6311c785915dd95e9c940d974f487bc814ec07de54fe30f860912bfc9b42b7b51645110b95e6d52ccba266951d3d9523d864f7
7
- data.tar.gz: 852e7731ce29b534bd383010655cb6380c9bac8840c69204904d1e17de089e5db578c13ad3c65707aa8d686ebd5ffc50ea526035256a474c8cdf7ea555cb1838
6
+ metadata.gz: 7c1d2039bab57923c91d24bf6a791ea86b23d71d576e51e51140f4bcd5044b962fb6fd333ba1c0df4ecdcd8828d0547f380c2d5c54520e3bb7a588c62b427ddb
7
+ data.tar.gz: 7d321301a11d0a437a964930c77d84a80bdd112be959476f8be087b585649efd36fd197f0b915bb51e5b865953ef3670ddbe77e66b910f450c7bb3976958d68d
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.19.12"
310
+ version = "0.19.14"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "2d8c6e0a21c9ee8cc6e661e4109a70d1c8f4f62c963eb12230b8d7035ebb63ee"
312
+ checksum = "27faaa398cc92b331b0e1d08abfa475e697e8d23b0ea90e211a3dc8590e42bbd"
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.19.12"
329
+ version = "0.19.14"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "2662678735acff3124dc2514d4792cc4a82cfaff50ed3b0a88c2a5662ecb9818"
331
+ checksum = "b6a65826a4a1f7ec796e29916963803a3deff86f9f9de9e8180f49f6173292a4"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -86,6 +86,12 @@ module Parsanol
86
86
 
87
87
  def compiler_version = envelope["compiler_version"]
88
88
 
89
+ # The behavior fingerprint (ruby#377): stable across producer
90
+ # metadata, so freshness gates can tell "re-baked by a newer
91
+ # compiler" from "the grammar changed". nil on artifacts baked
92
+ # before the field existed.
93
+ def semantics_hash = envelope["semantics_hash"]
94
+
89
95
  def fresh_against?(source_text)
90
96
  return false if source_hash.nil?
91
97
 
@@ -20,7 +20,7 @@ module Parsanol
20
20
  assign_scalar(out, artifact, binding, captures)
21
21
  end
22
22
  arrays.group_by { |binding| binding["path"].split("[]").first }.each do |prefix, group|
23
- assign_array(out, artifact, prefix, group, captures)
23
+ assign_array(out, artifact, prefix, group, captures, shape)
24
24
  end
25
25
  out
26
26
  end
@@ -63,7 +63,32 @@ module Parsanol
63
63
  out[leaf_key(binding)] = finalize(artifact, binding, value)
64
64
  end
65
65
 
66
- def assign_array(out, artifact, prefix, group, captures)
66
+ def assign_array(out, artifact, prefix, group, captures, shape = nil)
67
+ # Position-aware pairing (parsanol-ruby#381): when the shape
68
+ # nests the group's instances under the prefix key, each
69
+ # instance's subtree owns its captures — pairing by OCCURRENCE
70
+ # index landed a sparse capture (one that fires in only some
71
+ # instances) on the wrong instance. Per-instance collection
72
+ # fixes the pairing and is identical to the old flat pairing
73
+ # when every instance fires every capture. Captures outside
74
+ # any instance keep the legacy occurrence pairing.
75
+ instances = shape && group_instances(shape, prefix)
76
+ if instances
77
+ list = (out[prefix.to_sym] ||= [])
78
+ instances.each do |instance|
79
+ found = collect_captures(instance)
80
+ element = {}
81
+ group.each do |binding|
82
+ value = found[binding["capture"].to_sym].first
83
+ next if value.nil?
84
+
85
+ element[leaf_key(binding)] = finalize(artifact, binding, value)
86
+ end
87
+ list << element
88
+ end
89
+ return
90
+ end
91
+
67
92
  count = group.map { |binding| captures[binding["capture"].to_sym].length }.max || 0
68
93
  return if count.zero?
69
94
 
@@ -82,6 +107,27 @@ module Parsanol
82
107
  end
83
108
  end
84
109
 
110
+ # The `prefix[]` group's instance subtrees: the first array under
111
+ # a key naming the prefix, walked in collect_captures' BFS order.
112
+ # A repetition-root entry's shape IS the instances array (the wire
113
+ # has no key for it) — that is the fallback when no key matches.
114
+ def group_instances(shape, prefix)
115
+ queue = [shape]
116
+ until queue.empty?
117
+ node = queue.shift
118
+ case node
119
+ when Hash
120
+ node.each do |key, value|
121
+ return value if key.to_s == prefix && value.is_a?(Array)
122
+
123
+ queue << value if value.is_a?(Hash) || value.is_a?(Array)
124
+ end
125
+ when Array then queue.concat(node)
126
+ end
127
+ end
128
+ shape if shape.is_a?(Array) && shape.any?(Hash)
129
+ end
130
+
85
131
  def validate_path!(binding)
86
132
  path = binding["path"].to_s
87
133
  return unless path.include?(".") || path.include?("[")
@@ -32,6 +32,18 @@ module Parsanol
32
32
  "sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonicalize(payload)))}"
33
33
  end
34
34
 
35
+ # Producer metadata: the checksum pins the exact producer, but the
36
+ # semantics hash (ruby#377) skips these fields so a re-bake by a
37
+ # newer compiler — or a different Ruby patch stamping
38
+ # unicode_tables — is byte-stable when the grammar's behavior is
39
+ # unchanged. Mirrors the cross-engine parity view.
40
+ SEMANTICS_EXCLUDED = %w[checksum compiler_version unicode_tables semantics_hash].freeze
41
+
42
+ def self.semantics_hash(envelope)
43
+ payload = envelope.except(*SEMANTICS_EXCLUDED)
44
+ "sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonicalize(payload)))}"
45
+ end
46
+
35
47
  def self.canonicalize(obj)
36
48
  case obj
37
49
  when Hash then obj.sort.to_h.transform_values { |value| canonicalize(value) }
@@ -137,14 +149,24 @@ module Parsanol
137
149
 
138
150
  artifact = Artifact.from_json(JSON.generate(envelope), tables_dir: tables_dir)
139
151
  default = envelope["default_entry"] || artifact.entries.first
152
+ # `Native.available?` is true under either engine tier, but the
153
+ # JSON-grammar fast path rides the MRI C-API extension's
154
+ # `_parse_raw` — an ffi-cdylib install is available without it.
155
+ # Probe rather than assume (same doctrine as
156
+ # Native.reference_available?): without the C-API symbol the
157
+ # self-test runs through the artifact's own parse lane, which
158
+ # picks native-or-pure-Ruby honestly instead of raising
159
+ # NoMethodError mid-run.
160
+ json_capable = Native.respond_to?(:_parse_raw)
140
161
  grammars = {}
141
162
  tests.filter_map do |test|
142
163
  entry = test["entry"] || default
143
- grammar_json = (grammars[entry] ||= JSON.generate(artifact.entry(entry)["grammar"]))
144
- artifact.run_test_list(
145
- [test],
146
- parse_with: ->(input) { Native.parse_json_grammar(grammar_json, input) },
147
- )
164
+ parse_with =
165
+ if json_capable
166
+ grammar_json = (grammars[entry] ||= JSON.generate(artifact.entry(entry)["grammar"]))
167
+ ->(input) { Native.parse_json_grammar(grammar_json, input) }
168
+ end
169
+ artifact.run_test_list([test], parse_with: parse_with)
148
170
  end.flatten
149
171
  end
150
172
 
@@ -1352,6 +1374,7 @@ module Parsanol
1352
1374
  envelope["customs"] = @document.customs unless @document.customs.empty?
1353
1375
  envelope["states"] = @document.states unless @document.states.empty?
1354
1376
  envelope["dynamic"] = true if @dynamic
1377
+ envelope["semantics_hash"] = Compiler.semantics_hash(envelope)
1355
1378
  envelope["checksum"] = Compiler.checksum(envelope)
1356
1379
  envelope
1357
1380
  end
@@ -571,11 +571,16 @@ module Parsanol
571
571
 
572
572
  def parse_postfixed
573
573
  sign, behind = parse_guard_sign
574
- node = capture_postfix(parse_primary)
574
+ node = parse_primary
575
575
  if sign.nil?
576
- node
576
+ capture_postfix(node)
577
577
  else
578
- Node.new(:pred, sign, node, behind ? :behind : nil)
578
+ # The capture binds the GUARD, not the guarded primary
579
+ # (parsanol-ruby#380): `!digit as year` is `( !digit ) as
580
+ # year` — a capture inside the lookahead would never bind
581
+ # (a negated guard consumes nothing), so `as` must wrap the
582
+ # pred, matching the rep path's postfix order (ruby#197).
583
+ capture_postfix(Node.new(:pred, sign, node, behind ? :behind : nil))
579
584
  end
580
585
  end
581
586
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.116"
4
+ VERSION = "1.3.117"
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.116
4
+ version: 1.3.117
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.