parsanol 1.3.128 → 1.3.130

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: f80a73c511be09a4f5ab40755cf43eed3d7fb004020be3546dbb08adbcee8c48
4
- data.tar.gz: 5b1c6d675ff9a19939a3e560c0b7a0b27565d3c8210ca018bc33c5df425fcd70
3
+ metadata.gz: 55b823bf0f799b613ed22ad730a010327cbfb0b40411bd25690053602371cc0d
4
+ data.tar.gz: 61b8cd059944fc95c2f6b52f4a7be68d3083831a19a7479a1c5f187ce96c0004
5
5
  SHA512:
6
- metadata.gz: ee0819062881fabbc94b6e3ddd0466e4bc06e7753c40f069570f566ddde62894365472633ef0ea5226e8ab5af8a95d98267b7acc9ccc8c35d74347490561bddd
7
- data.tar.gz: e7249db3f3fd1c5a8fb5e70c47c158db6e5ca13ab04209552a39637cb36b00cf9ee08480c0e735cbcbddf20ebe66efe9895de988eb2a9685426553aa75fba2d0
6
+ metadata.gz: '08137851d78de165952bdaf0afc3e64a6f215f47f945ef2be0c4f3f7a94313f7768c698cd72ef0a5b70c7b648a210f1b69028526166f0b770aa25c4175b5233b'
7
+ data.tar.gz: 66897e1afc6cb82c08a202285495d60c7b80e8da7cd5ecb5d16e947f36e036476184342e01f80191bf5be3ee68f6578d1d54d9ad430df69a0cbb1a34f814795d
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.20.0"
310
+ version = "0.20.1"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "fa25545d2493d7c0be6e70054f9a8aef360e5ff6046dce5ceae664065e1d5039"
312
+ checksum = "05d9bc5274d8d4eca8ee343b3f97df52691e303284f26a71b3903048019565c7"
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.20.0"
329
+ version = "0.20.1"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "54c23675b697407e9d2c495c269537407548641b782c6cd79c7c93e079c91988"
331
+ checksum = "80669ec1c0b53f0cab38799570a8df7c0f8edd8decd02940ea8bb124ad401e4b"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -95,6 +95,18 @@ module Parsanol
95
95
  source_hash == Digest::SHA256.hexdigest(source_text.to_s)
96
96
  end
97
97
 
98
+ # The declared per-target binding artifact for this parse artifact
99
+ # (parsanol-ruby#432): the entry binding records extracted into
100
+ # their own checksummed artifact, pinned to this grammar by source
101
+ # hash. Targets: ruby, rs, ts, python.
102
+ def bindings_artifact(target)
103
+ Bindings::Artifact.emit(envelope, target)
104
+ end
105
+
106
+ def bindings_artifact_json(target)
107
+ Bindings::Artifact.emit_json(envelope, target)
108
+ end
109
+
98
110
  def entries = envelope["entries"].keys
99
111
 
100
112
  def entry(name)
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Parsanol
6
+ module PARG
7
+ module Bindings
8
+ # The declared per-target binding artifact (parsanol-ruby#432): from
9
+ # one compiled grammar the toolchain emits one checksummed bindings
10
+ # artifact per target, each carrying the entry binding records and
11
+ # pinned to its grammar by source hash. Declared and diffable, not
12
+ # baked: consumers pin and verify what they load.
13
+ #
14
+ # Boundary: the records carry AST construction only — the
15
+ # production-to-binding mapping. Semantics live in the model layer
16
+ # (LML), never here. Execution resources (preprocess steps and their
17
+ # tables) resolve from the PAIRED parse artifact, which is why
18
+ # #pair_with! is mandatory before #apply.
19
+ class Artifact
20
+ KIND = "parg-bindings"
21
+ TARGETS = %w[ruby rs ts python].freeze
22
+ SUPPORTED_BINDING_VERSION = 1
23
+
24
+ attr_reader :target, :binding_version, :grammar, :grammar_version,
25
+ :grammar_source_hash, :entries, :payload
26
+
27
+ class << self
28
+ # Emit the per-target bindings artifact from a compiled parse
29
+ # artifact envelope. Returns the verified artifact; the payload
30
+ # is checksummed with the artifact checksum scheme (canonical
31
+ # JSON sha256 over everything except the checksum itself), so
32
+ # every engine re-derives it identically.
33
+ def emit(envelope, target)
34
+ target = target.to_s
35
+ unless TARGETS.include?(target)
36
+ raise ArtifactError,
37
+ "unknown binding target #{target.inspect}; known: #{TARGETS.join(', ')}"
38
+ end
39
+
40
+ artifact = new(build(envelope, target))
41
+ artifact.verify
42
+ end
43
+
44
+ # The emitted artifact as JSON (the bake/CLI surface).
45
+ def emit_json(envelope, target)
46
+ JSON.generate(emit(envelope, target).payload)
47
+ end
48
+
49
+ # Parse and fully verify an emitted bindings artifact. `target:`
50
+ # pins the expected target; omit it in multi-target tooling that
51
+ # accepts any declared target.
52
+ def from_json(json, target: nil)
53
+ artifact = new(JSON.parse(json))
54
+ artifact.verify
55
+ artifact.verify_target!(target) unless target.nil?
56
+ artifact
57
+ end
58
+
59
+ private
60
+
61
+ def build(envelope, target)
62
+ payload = {
63
+ "artifact_kind" => KIND,
64
+ "target" => target,
65
+ "binding_version" => SUPPORTED_BINDING_VERSION,
66
+ "grammar" => envelope["grammar"],
67
+ "grammar_version" => envelope["version"],
68
+ "grammar_source_hash" => envelope["source_hash"],
69
+ "shape" => envelope["shape"],
70
+ "entries" => envelope["entries"].to_h do |name, entry|
71
+ [name, { "bindings" => entry["bindings"].to_a }]
72
+ end,
73
+ }
74
+ payload.merge("checksum" => Compiler.checksum(payload))
75
+ end
76
+ end
77
+
78
+ def initialize(payload)
79
+ raise ArtifactError, "bindings artifact payload must be an object" unless payload.is_a?(Hash)
80
+
81
+ @payload = payload
82
+ @target = payload["target"]
83
+ @binding_version = payload["binding_version"]
84
+ @grammar = payload["grammar"]
85
+ @grammar_version = payload["grammar_version"]
86
+ @grammar_source_hash = payload["grammar_source_hash"]
87
+ @entries = payload["entries"]
88
+ @parg_artifact = nil
89
+ end
90
+
91
+ # Structural verification: kind, checksum, binding version,
92
+ # target, entries map. Every loader path goes through here.
93
+ def verify
94
+ unless @payload["artifact_kind"] == KIND
95
+ raise ArtifactError,
96
+ "not a #{KIND} artifact (artifact_kind #{@payload['artifact_kind'].inspect})"
97
+ end
98
+
99
+ expected = Compiler.checksum(@payload)
100
+ actual = @payload["checksum"]
101
+ unless actual.is_a?(String) && actual == expected
102
+ raise ArtifactError,
103
+ "bindings artifact checksum mismatch: expected #{expected}, got #{actual.inspect}"
104
+ end
105
+
106
+ unless TARGETS.include?(@target)
107
+ raise ArtifactError,
108
+ "unknown binding target #{@target.inspect}; known: #{TARGETS.join(', ')}"
109
+ end
110
+ unless @binding_version == SUPPORTED_BINDING_VERSION
111
+ raise ArtifactError,
112
+ "unsupported binding_version #{@binding_version.inspect} " \
113
+ "(supported: #{SUPPORTED_BINDING_VERSION})"
114
+ end
115
+ unless @entries.is_a?(Hash)
116
+ raise ArtifactError, "bindings artifact carries no entries map"
117
+ end
118
+
119
+ self
120
+ end
121
+
122
+ def verify_target!(target)
123
+ target = target.to_s
124
+ return if @target == target
125
+
126
+ raise ArtifactError,
127
+ "bindings artifact targets #{@target.inspect}, not #{target.inspect}"
128
+ end
129
+
130
+ # Pair with the parse artifact this layer was compiled from: same
131
+ # grammar, same version, same source. The pairing is what lets
132
+ # apply resolve preprocess references — they live in the parse
133
+ # artifact's checksummed envelope.
134
+ def pair_with!(parg_artifact)
135
+ envelope = parg_artifact.envelope
136
+ mismatches = []
137
+ mismatches << "grammar (#{@grammar.inspect} vs #{envelope['grammar'].inspect})" if @grammar != envelope["grammar"]
138
+ if @grammar_version != envelope["version"]
139
+ mismatches << "version (#{@grammar_version.inspect} vs #{envelope['version'].inspect})"
140
+ end
141
+ if @grammar_source_hash != envelope["source_hash"]
142
+ mismatches << "source_hash (#{@grammar_source_hash.inspect} vs #{envelope['source_hash'].inspect})"
143
+ end
144
+ unless mismatches.empty?
145
+ raise ArtifactError,
146
+ "bindings artifact does not pair with the parse artifact: #{mismatches.join(', ')}"
147
+ end
148
+
149
+ @parg_artifact = parg_artifact
150
+ self
151
+ end
152
+
153
+ def entry(name)
154
+ @entries.fetch(name) { raise ArtifactError, "unknown entry #{name.inspect}" }
155
+ end
156
+
157
+ def bindings(name)
158
+ entry(name)["bindings"]
159
+ end
160
+
161
+ def entry_names
162
+ @entries.keys
163
+ end
164
+
165
+ # Materialize the bound map from a parse shape using the DECLARED
166
+ # records. The paired parse artifact supplies entry context and
167
+ # preprocess resolution; the same Bindings.apply runs as for the
168
+ # embedded records, so a standalone artifact and its parse
169
+ # artifact's baked records produce identical bound maps.
170
+ def apply(entry_name, shape)
171
+ parg = @parg_artifact
172
+ if parg.nil?
173
+ raise ArtifactError,
174
+ "pair_with! the parse artifact before applying bindings"
175
+ end
176
+
177
+ crafted = { "name" => entry_name, "bindings" => bindings(entry_name) }
178
+ Bindings.apply(parg, crafted, shape)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -8,6 +8,9 @@ module Parsanol
8
8
  # producing the attribute hash that fills a lutaml-model class:
9
9
  # capture collection -> preprocessing -> type cast -> path assignment.
10
10
  module Bindings
11
+ # The declared per-target binding artifact (parsanol-ruby#432).
12
+ autoload :Artifact, "parsanol/parg/bindings/artifact"
13
+
11
14
  module_function
12
15
 
13
16
  def apply(artifact, entry, shape)
@@ -36,11 +39,14 @@ module Parsanol
36
39
  # bound map (parsanol-ruby#413): a capture present in the parse
37
40
  # but bound nowhere is silent data loss, the one case the
38
41
  # binding layer's other checks miss. Absence in the TREE is
39
- # legitimate optionality and never raises. Coverage is checked
40
- # against every hash key anywhere in the bound map, not just the
41
- # binding's own leaf: a `(raw)` binding carries its entire
42
+ # legitimate optionality and never raises. Delivery is EITHER
43
+ # the binding's own leaf/prefix reaching the map (the capture
44
+ # may rename on the way — capture "doc_year" binds under path
45
+ # "year", parsanol-ruby#434) OR the capture name appearing as a
46
+ # key anywhere in the map: a `(raw)` binding carries its entire
42
47
  # subtree, so captures nested inside a raw-bound ancestor are
43
- # delivered through it (parsanol-ruby#427 — BS 9113 bundled_list).
48
+ # delivered through it (parsanol-ruby#427 — BS 9113
49
+ # bundled_list).
44
50
  bound_keys = {}
45
51
  walk_keys = lambda do |node|
46
52
  case node
@@ -53,7 +59,7 @@ module Parsanol
53
59
  walk_keys.call(out)
54
60
  lost = bindings.select do |binding|
55
61
  capture = binding["capture"].to_sym
56
- present.include?(capture) && !bound_keys.key?(capture)
62
+ present.include?(capture) && !bound?(out, binding) && !bound_keys.key?(capture)
57
63
  end
58
64
  unless lost.empty?
59
65
  raise BindError,
@@ -63,6 +69,23 @@ module Parsanol
63
69
  out
64
70
  end
65
71
 
72
+ # Did this binding contribute anything to the bound map? Scalars
73
+ # bind under their leaf key; array groups bind under their prefix.
74
+ # The leaf is the PATH's tail, so a capture-to-path rename reads
75
+ # as delivered when its own path landed (parsanol-ruby#434).
76
+ def bound?(out, binding)
77
+ path = binding["path"].to_s
78
+ if path.include?("[]")
79
+ prefix = path.split("[]").first.to_sym
80
+ rows = out[prefix]
81
+ rows.is_a?(Array) && rows.any? do |row|
82
+ row.is_a?(Hash) && row.key?(leaf_key(binding))
83
+ end
84
+ else
85
+ out.key?(leaf_key(binding))
86
+ end
87
+ end
88
+
66
89
  # Canonical test-value form: one equality surface for capture and
67
90
  # example expectations on both sides of the comparison (Slice vs
68
91
  # source literal was comparing heterogeneous types). Terminals
@@ -46,6 +46,12 @@ module Parsanol
46
46
 
47
47
  document.rules[qualified] = copy(node, prefix)
48
48
  end
49
+ # The `atomic` exemption rides the rule name: a component's
50
+ # atomic rules qualify with everything else, or the exemption
51
+ # never matches post-merge.
52
+ used.atomic_rules.each do |name|
53
+ document.atomic_rules << "#{prefix}#{name}"
54
+ end
49
55
  used.entries.each do |entry, rule|
50
56
  document.entries["#{prefix}#{entry}"] = "#{prefix}#{rule}"
51
57
  end
@@ -79,7 +85,17 @@ module Parsanol
79
85
  when :pred
80
86
  Node.new(:pred, node.a, copy(node.b, prefix), node.c)
81
87
  when :cap then Node.new(:cap, node.a, copy(node.b, prefix))
82
- when :ref then Node.new(:ref, "#{prefix}#{node.a}")
88
+ # A qualified reference (`other.rule`) already names its own
89
+ # imported grammar — prefixing would double-qualify it
90
+ # (cross-import references compose: the other grammar's rules
91
+ # are prefixed by their own merge).
92
+ when :ref
93
+ name = node.a.to_s
94
+ if name.include?(".")
95
+ Node.new(:ref, name)
96
+ else
97
+ Node.new(:ref, "#{prefix}#{name}")
98
+ end
83
99
  else node
84
100
  end
85
101
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.128"
4
+ VERSION = "1.3.130"
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.128
4
+ version: 1.3.130
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -118,6 +118,7 @@ files:
118
118
  - lib/parsanol/parg/artifact.rb
119
119
  - lib/parsanol/parg/authoring.rb
120
120
  - lib/parsanol/parg/bindings.rb
121
+ - lib/parsanol/parg/bindings/artifact.rb
121
122
  - lib/parsanol/parg/cli.rb
122
123
  - lib/parsanol/parg/compiler.rb
123
124
  - lib/parsanol/parg/derive.rb