parsanol 1.3.129 → 1.3.131
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 +4 -4
- data/Cargo.lock +4 -4
- data/lib/parsanol/parg/artifact.rb +48 -4
- data/lib/parsanol/parg/bindings/artifact.rb +183 -0
- data/lib/parsanol/parg/bindings.rb +3 -0
- data/lib/parsanol/parg/cli.rb +28 -2
- data/lib/parsanol/parg/compiler.rb +40 -4
- data/lib/parsanol/parg/imports.rb +17 -1
- data/lib/parsanol/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fda87ae372d092b330079308377f9ed93a5cf03d79a42a9e3e24a82fb82775d3
|
|
4
|
+
data.tar.gz: b4e249f4d827cb56d027d9f0dd05d333870e862a11a71e9de6ada0cc695a2d47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac94ae5b14c7fe2c7300d45cf230c2a5210199d7645432a343eb730061644b00014209c678be05b4416717e7bd97e8b5c04dcb906f1c8289016a7e283c49ad57
|
|
7
|
+
data.tar.gz: 83f77073bfbf15550f82b14663a31b2565e9d434444bc46b986cc29d4d225e19dd3e1cf8eba14a16d2c330a0a412cd0cb6911149781ac827072c756148fb2c55
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.20.
|
|
310
|
+
version = "0.20.3"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "c452ad0ffc59ff11bcdb6c38e24cb0eb4613f0875d912caf10e8d2c58237934c"
|
|
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.
|
|
329
|
+
version = "0.20.3"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "c012f01d67b456b25cb64fdc050f56eee2037c30780b2e8143b61d2a08cf5637"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -15,13 +15,15 @@ module Parsanol
|
|
|
15
15
|
attr_reader :envelope, :path, :tables_dir, :imports_dir
|
|
16
16
|
|
|
17
17
|
def self.load(path, tables_dir: nil, imports_dir: nil)
|
|
18
|
-
|
|
18
|
+
text = PARG.read_utf8(path)
|
|
19
|
+
envelope = JSON.parse(text)
|
|
19
20
|
new(envelope, path, tables_dir || File.dirname(path),
|
|
20
|
-
imports_dir: imports_dir)
|
|
21
|
+
imports_dir: imports_dir, raw_text: text)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def self.from_json(text, tables_dir: nil, imports_dir: nil)
|
|
24
|
-
new(JSON.parse(text), nil, tables_dir, imports_dir: imports_dir
|
|
25
|
+
new(JSON.parse(text), nil, tables_dir, imports_dir: imports_dir,
|
|
26
|
+
raw_text: text)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
SUPPORTED_SHAPE = "parsanol-tree/v2"
|
|
@@ -37,7 +39,7 @@ module Parsanol
|
|
|
37
39
|
2 => 2, "2" => 2, "v2" => 2
|
|
38
40
|
}.freeze
|
|
39
41
|
|
|
40
|
-
def initialize(envelope, path, tables_dir, imports_dir: nil)
|
|
42
|
+
def initialize(envelope, path, tables_dir, imports_dir: nil, raw_text: nil)
|
|
41
43
|
unless envelope["shape"] == SUPPORTED_SHAPE
|
|
42
44
|
raise ArtifactError,
|
|
43
45
|
"unsupported artifact shape #{envelope['shape'].inspect} " \
|
|
@@ -63,6 +65,7 @@ module Parsanol
|
|
|
63
65
|
# sources when the pure-Ruby path recompiles the embedded source;
|
|
64
66
|
# in-memory artifacts (path nil) pass the directory explicitly.
|
|
65
67
|
@imports_dir = imports_dir
|
|
68
|
+
@raw_text = raw_text
|
|
66
69
|
verify_checksum!
|
|
67
70
|
@compiler = nil
|
|
68
71
|
end
|
|
@@ -95,6 +98,18 @@ module Parsanol
|
|
|
95
98
|
source_hash == Digest::SHA256.hexdigest(source_text.to_s)
|
|
96
99
|
end
|
|
97
100
|
|
|
101
|
+
# The declared per-target binding artifact for this parse artifact
|
|
102
|
+
# (parsanol-ruby#432): the entry binding records extracted into
|
|
103
|
+
# their own checksummed artifact, pinned to this grammar by source
|
|
104
|
+
# hash. Targets: ruby, rs, ts, python.
|
|
105
|
+
def bindings_artifact(target)
|
|
106
|
+
Bindings::Artifact.emit(envelope, target)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def bindings_artifact_json(target)
|
|
110
|
+
Bindings::Artifact.emit_json(envelope, target)
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
def entries = envelope["entries"].keys
|
|
99
114
|
|
|
100
115
|
def entry(name)
|
|
@@ -643,7 +658,16 @@ module Parsanol
|
|
|
643
658
|
@compiled[rule] ||= compiler.atom_for(rule)
|
|
644
659
|
end
|
|
645
660
|
|
|
661
|
+
# Two tiers. FAST: when the bake sealed the file, a zero-hole
|
|
662
|
+
# sha256 over the raw bytes proves byte integrity without parsing
|
|
663
|
+
# semantics — O(bytes), no canonicalization. DEEP: the canonical
|
|
664
|
+
# checksum (cross-engine content equality), which also covers
|
|
665
|
+
# artifacts baked before byte_checksum existed. A sealed artifact
|
|
666
|
+
# loaded with its raw text verifies BOTH; a bare envelope (direct
|
|
667
|
+
# construction) verifies canonical only.
|
|
646
668
|
def verify_checksum!
|
|
669
|
+
verify_byte_checksum! if envelope["byte_checksum"]
|
|
670
|
+
|
|
647
671
|
stored = envelope["checksum"]
|
|
648
672
|
computed = Compiler.checksum(envelope)
|
|
649
673
|
return if stored == computed
|
|
@@ -652,6 +676,26 @@ module Parsanol
|
|
|
652
676
|
"artifact checksum mismatch: stored #{stored.inspect}, " \
|
|
653
677
|
"computed #{computed.inspect}"
|
|
654
678
|
end
|
|
679
|
+
|
|
680
|
+
# The bake computed the digest over the file bytes with the
|
|
681
|
+
# digest's own 64-hex region zeroed (Compiler.seal_json). Any
|
|
682
|
+
# flipped bit anywhere else in the file mismatches — corrupted
|
|
683
|
+
# distribution, wrong file, hand-edited payload: all rejected
|
|
684
|
+
# before any parse work.
|
|
685
|
+
def verify_byte_checksum!
|
|
686
|
+
stored = envelope["byte_checksum"]
|
|
687
|
+
return if stored.nil? || @raw_text.nil?
|
|
688
|
+
|
|
689
|
+
placeholder = Compiler::BYTE_CHECKSUM_PLACEHOLDER
|
|
690
|
+
zeroed = @raw_text.sub(stored.to_s, placeholder)
|
|
691
|
+
computed = "sha256:#{Digest::SHA256.hexdigest(zeroed)}"
|
|
692
|
+
return if stored == computed
|
|
693
|
+
|
|
694
|
+
raise ArtifactError,
|
|
695
|
+
"artifact byte checksum mismatch: stored #{stored.inspect}, " \
|
|
696
|
+
"computed #{computed.inspect} — the file bytes are not the " \
|
|
697
|
+
"sealed bake output"
|
|
698
|
+
end
|
|
655
699
|
end
|
|
656
700
|
end
|
|
657
701
|
end
|
|
@@ -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)
|
data/lib/parsanol/parg/cli.rb
CHANGED
|
@@ -18,7 +18,11 @@ module Parsanol
|
|
|
18
18
|
usage: parsanol parg <command> <grammar> [arguments]
|
|
19
19
|
|
|
20
20
|
commands:
|
|
21
|
-
compile <file.parg> [-o out.json]
|
|
21
|
+
compile <file.parg> [-o out.json] [--bindings TARGET|all]
|
|
22
|
+
compile to an artifact envelope;
|
|
23
|
+
--bindings also writes one
|
|
24
|
+
checksummed per-target binding
|
|
25
|
+
artifact beside it (#432)
|
|
22
26
|
test <file.parg|artifact.json> run the grammar's test section
|
|
23
27
|
parse <file> [--entry E] INPUT parse one input; prints the tree
|
|
24
28
|
and the bound captures
|
|
@@ -141,11 +145,33 @@ module Parsanol
|
|
|
141
145
|
source = @argv.shift
|
|
142
146
|
out = flag_value("-o") || default_artifact_name(source)
|
|
143
147
|
envelope = compile_source(source).envelope
|
|
144
|
-
|
|
148
|
+
# Seal: the exact text written is the exact text the byte-level
|
|
149
|
+
# checksum covers (Compiler.seal_json includes the trailing
|
|
150
|
+
# newline).
|
|
151
|
+
File.write(out, Compiler.seal_json(envelope), encoding: Encoding::UTF_8)
|
|
145
152
|
puts "#{out} #{envelope['checksum']} warnings=#{envelope['lint']['order_warnings'].size}"
|
|
153
|
+
bindings_targets.each do |target|
|
|
154
|
+
# The declared per-target binding artifacts (#432): one
|
|
155
|
+
# checksummed bindings payload per target, written beside the
|
|
156
|
+
# artifact. `--bindings all` expands to the full target set.
|
|
157
|
+
artifact = Artifact.from_json(JSON.generate(envelope))
|
|
158
|
+
path = out.sub(/\.json\z/i, ".bindings.#{target}.json")
|
|
159
|
+
File.write(path, "#{artifact.bindings_artifact_json(target)}\n", encoding: Encoding::UTF_8)
|
|
160
|
+
puts "#{path} #{artifact.bindings_artifact(target).payload['checksum']}"
|
|
161
|
+
end
|
|
146
162
|
0
|
|
147
163
|
end
|
|
148
164
|
|
|
165
|
+
# Every --bindings flag value, `all` expanded to the target set.
|
|
166
|
+
# Empty when the flag is absent (compile emits no binding artifacts).
|
|
167
|
+
def bindings_targets
|
|
168
|
+
values = []
|
|
169
|
+
while (value = flag_value("--bindings"))
|
|
170
|
+
values.concat(value == "all" ? Bindings::Artifact::TARGETS.dup : [value])
|
|
171
|
+
end
|
|
172
|
+
values.uniq
|
|
173
|
+
end
|
|
174
|
+
|
|
149
175
|
# Freshness (parsanol-ruby#209): verify an artifact was compiled
|
|
150
176
|
# from the given source. The stale-shipped-artifact incident class
|
|
151
177
|
# (an old artifact silently taking precedence over the corrected
|
|
@@ -25,19 +25,43 @@ module Parsanol
|
|
|
25
25
|
new(document, tables_dir).compile
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Canonical checksum over an envelope (everything except the
|
|
29
|
-
# itself
|
|
28
|
+
# Canonical checksum over an envelope (everything except the
|
|
29
|
+
# checksum itself and the byte-level checksum — the byte digest
|
|
30
|
+
# attests the FILE serialization, which is producer-specific and
|
|
31
|
+
# must never enter the cross-engine canonical payload). Used both
|
|
32
|
+
# when building an artifact and when verifying one.
|
|
30
33
|
def self.checksum(envelope)
|
|
31
|
-
payload = envelope.except("checksum")
|
|
34
|
+
payload = envelope.except("checksum", "byte_checksum")
|
|
32
35
|
"sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonicalize(payload)))}"
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
# The byte-level checksum's placeholder: a fixed-width zero region,
|
|
39
|
+
# so the sealed bytes and the verification bytes differ only inside
|
|
40
|
+
# that region (sha256 hex is 64 chars — same width).
|
|
41
|
+
BYTE_CHECKSUM_PLACEHOLDER = ("0" * 64).freeze
|
|
42
|
+
BYTE_CHECKSUM_TRAILING = "\n"
|
|
43
|
+
|
|
44
|
+
# Seal an envelope for writing: stamp the byte-level checksum over
|
|
45
|
+
# the exact file bytes (the final serialization with a trailing
|
|
46
|
+
# newline and the digest's own region zeroed) and return the exact
|
|
47
|
+
# text to write. No sidecar — the integrity travels inside the
|
|
48
|
+
# artifact. The canonical `checksum` is untouched: it attests
|
|
49
|
+
# content, `byte_checksum` attests bytes.
|
|
50
|
+
def self.seal_json(envelope)
|
|
51
|
+
sealed = envelope.merge("byte_checksum" => BYTE_CHECKSUM_PLACEHOLDER)
|
|
52
|
+
text = JSON.generate(sealed) + BYTE_CHECKSUM_TRAILING
|
|
53
|
+
digest = Digest::SHA256.hexdigest(text)
|
|
54
|
+
sealed["byte_checksum"] = "sha256:#{digest}"
|
|
55
|
+
JSON.generate(sealed) + BYTE_CHECKSUM_TRAILING
|
|
56
|
+
end
|
|
57
|
+
|
|
35
58
|
# Producer metadata: the checksum pins the exact producer, but the
|
|
36
59
|
# semantics hash (ruby#377) skips these fields so a re-bake by a
|
|
37
60
|
# newer compiler — or a different Ruby patch stamping
|
|
38
61
|
# unicode_tables — is byte-stable when the grammar's behavior is
|
|
39
62
|
# unchanged. Mirrors the cross-engine parity view.
|
|
40
|
-
SEMANTICS_EXCLUDED = %w[checksum compiler_version
|
|
63
|
+
SEMANTICS_EXCLUDED = %w[checksum byte_checksum compiler_version
|
|
64
|
+
unicode_tables semantics_hash validated].freeze
|
|
41
65
|
|
|
42
66
|
def self.semantics_hash(envelope)
|
|
43
67
|
payload = envelope.except(*SEMANTICS_EXCLUDED)
|
|
@@ -129,6 +153,18 @@ module Parsanol
|
|
|
129
153
|
failures = self.class.self_test(envelope, tables_dir: @tables_dir)
|
|
130
154
|
raise CompileError, failures.join("\n") unless failures.empty?
|
|
131
155
|
|
|
156
|
+
# The validation receipt: what this bake actually checked, inside
|
|
157
|
+
# the envelope the checksums cover. "valid/validated" is then an
|
|
158
|
+
# inspectable field of the artifact, not an out-of-band claim.
|
|
159
|
+
envelope["validated"] = {
|
|
160
|
+
"lint_errors" => 0, # compile raises on lint errors before this
|
|
161
|
+
"lint_warnings" => @envelope_warnings.size,
|
|
162
|
+
"tests_total" => @document.tests.size,
|
|
163
|
+
"tests_failures" => 0, # run_tests raises on failures before this
|
|
164
|
+
"self_test" => Native.available?,
|
|
165
|
+
}
|
|
166
|
+
envelope["checksum"] = Compiler.checksum(envelope)
|
|
167
|
+
|
|
132
168
|
Result.new(@atom_cache, @warnings, envelope)
|
|
133
169
|
end
|
|
134
170
|
|
|
@@ -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
|
-
|
|
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
|
data/lib/parsanol/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.3.131
|
|
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
|