macroonz_compiler/explanation/type_guard.rs
1//! The explanation home's invariant nucleus: every road that reaches a private field.
2//!
3//! Declared inside `types.rs` as its own child, which is what makes this home's two central claims structural.
4//! A view is completed here, after the coverage pass agreed, so there is no partial view for a reader to mistake for a complete one.
5//! Its parentage is taken here too — off the plan and the proof themselves — and its own identity is minted over the three, so a view naming a plan or a closure it was not answered over is a value nobody can build.
6
7use super::super::encode::seats_into;
8use super::super::establish::coverage_issues;
9use super::{
10 AnsweredOutput, EXPLANATION_ISSUE_LIMIT, ExplanationError, ExplanationIssue, UniversalAnswer,
11 UniversalQuestion, View,
12};
13use crate::bounded::{Bounded, Capped, Capping, NonEmpty, Overflow};
14use crate::closure::Closure;
15use crate::identity::{
16 self, ClosureId, ExplanationId, PlanId, Provenance, Transcript, encode_bytes,
17};
18use crate::kind::{Answer, Kind, Question, Role};
19use crate::plan::Plan;
20use crate::render::RenderedProjection;
21use core::marker::PhantomData;
22
23/// The refusal one established issue list amounts to, or nothing where the passes established none.
24///
25/// One road for everything [`View::complete`](super::View::complete) establishes: the coverage pass and the output pass co-establish into one list, so no pass can establish issues and then walk on past them, and no caller repairs one pass's findings only to meet the other's.
26fn refused(issues: Vec<ExplanationIssue>) -> Option<ExplanationError> {
27 let mut established = issues.into_iter();
28 let first = established.next()?;
29 Some(ExplanationError::over(first, established.collect()))
30}
31
32impl AnsweredOutput {
33 /// Every seat's half of the output-and-digest answer, in roster order, read off the proof itself.
34 ///
35 /// Roster order and never rendering order, so the answer does not turn on the sequence a renderer happened to write its units in.
36 /// The whole roster and never a chosen row: a kind may fill several seats, and an answer naming fewer than all of them would flatten the expansion's denominator to whichever row was picked.
37 /// Seated once: the request road composes its answer through this walk, and [`View::complete`] rebuilds the same walk to compare — one derivation, so the claim and its check cannot drift apart.
38 pub(crate) fn roster<R: Role>(rendered: &RenderedProjection<R>) -> Vec<Self> {
39 R::ALL
40 .iter()
41 .copied()
42 .filter_map(|role| rendered.under(role))
43 .map(|unit| Self {
44 output: Box::new(unit.reconstructed().output),
45 digest: unit.digest(),
46 })
47 .collect()
48 }
49}
50
51/// The issue the output answer establishes against the proof's own rendered roster, or nothing where it restates it exactly.
52///
53/// The lawful rows are derivable from the closure, so this pass rebuilds them and compares whole — count, order, members, and digests in one equality.
54/// An absent output answer is the coverage pass's finding and establishes nothing here.
55fn outputs_beside_proof<R: Role>(
56 universal: &[UniversalAnswer],
57 closure: &Closure<R>,
58) -> Vec<ExplanationIssue> {
59 let supplied = universal.iter().find_map(|answer| match answer {
60 UniversalAnswer::OutputAndDigest { outputs } => Some(outputs),
61 UniversalAnswer::Kind { .. }
62 | UniversalAnswer::Owner { .. }
63 | UniversalAnswer::CausingDeclarations { .. }
64 | UniversalAnswer::Profile { .. }
65 | UniversalAnswer::Assumptions { .. }
66 | UniversalAnswer::Invalidators { .. }
67 | UniversalAnswer::RelatedDispositions { .. }
68 | UniversalAnswer::Repairs { .. } => None,
69 });
70 let Some(supplied) = supplied else {
71 return Vec::new();
72 };
73 let lawful = AnsweredOutput::roster(closure.rendered());
74 if supplied.len() == lawful.len() && supplied.iter().eq(lawful.iter()) {
75 return Vec::new();
76 }
77 let diverges = supplied
78 .iter()
79 .zip(lawful.iter())
80 .position(|(offered, proved)| offered != proved)
81 .unwrap_or_else(|| supplied.len().min(lawful.len()));
82 vec![ExplanationIssue::OutputsBesideTheProof {
83 expected: u16::try_from(lawful.len()).unwrap_or(u16::MAX),
84 observed: u16::try_from(supplied.len()).unwrap_or(u16::MAX),
85 diverges: u16::try_from(diverges).unwrap_or(u16::MAX),
86 }]
87}
88
89/// The supplied answers, restated in their roster's own declared order.
90///
91/// Reached only after the coverage pass agreed, which is what makes it total: every row of the roster has exactly one answer and no answer stands outside it, so the walk seats each answer once and leaves none behind.
92/// The roster is the quantifier, so the result's order is the protocol's rather than a call site's.
93fn in_roster_order<Q: Question>(answers: Vec<Q::Answer>) -> Vec<Q::Answer> {
94 let mut supplied: Vec<Option<Q::Answer>> = answers.into_iter().map(Some).collect();
95 let mut ordered: Vec<Q::Answer> = Vec::with_capacity(supplied.len());
96 for question in Q::ALL {
97 let seated = supplied.iter_mut().find(|held| {
98 held.as_ref()
99 .is_some_and(|answer| answer.question() == *question)
100 });
101 if let Some(answer) = seated.and_then(Option::take) {
102 ordered.push(answer);
103 }
104 }
105 ordered
106}
107
108impl ExplanationError {
109 /// The refusal one established issue makes.
110 pub fn of(issue: ExplanationIssue) -> Self {
111 Self {
112 body: Capped::all(NonEmpty::one(issue)),
113 }
114 }
115
116 /// The refusal a pass whose checks co-establish makes.
117 ///
118 /// The caller arrives holding every issue its pass established, so the posture the body writes is about the REPORT and never about the pass: where the issues fit it carries all of them, and where they do not it carries what fits and counts the rest.
119 pub fn over(first: ExplanationIssue, rest: Vec<ExplanationIssue>) -> Self {
120 Self {
121 body: Capped::first_n(first, rest.into_iter()),
122 }
123 }
124
125 /// The refusal a seat bound makes, out of the two counts the overflow already carries.
126 pub fn bounded(overflow: Overflow) -> Self {
127 Self::of(ExplanationIssue::SeatBoundExceeded {
128 bound: u64::try_from(overflow.capacity).unwrap_or(u64::MAX),
129 observed: u64::try_from(overflow.offered).unwrap_or(u64::MAX),
130 })
131 }
132
133 /// The first issue the pass established, which every refusal has.
134 #[must_use]
135 pub fn first_issue(&self) -> &ExplanationIssue {
136 self.body.items().first()
137 }
138
139 /// Every issue this refusal carries, in the order the pass established them; structurally at least one.
140 #[must_use]
141 pub fn issues(&self) -> &NonEmpty<ExplanationIssue, EXPLANATION_ISSUE_LIMIT> {
142 self.body.items()
143 }
144
145 /// Whether this refusal carries every issue its pass established.
146 #[must_use]
147 pub const fn capping(&self) -> Capping {
148 self.body.capping()
149 }
150}
151
152impl<K: Kind> View<K> {
153 /// Complete one view over the universal questions and the kind's own, answered over one plan and the proof of its rendering.
154 ///
155 /// # The parentage is taken, never supplied
156 ///
157 /// The plan arrives as the PLAN and the closure as the PROOF, and both identities are read off them here.
158 /// A road that took two identities beside the answers would take two values any caller can spell, and the view it built would name a parentage it was never written over — which is a complete, well-formed explanation about something else.
159 /// A [`Closure`] is reachable only by proving a rendering against a plan, so a caller standing here has done that or has nothing to hand in, and its role roster is the kind's own.
160 ///
161 /// # The explanation transcript
162 ///
163 /// This is a mint site, so its content grammar is stated in full.
164 /// The identity is derived under [`Role::Explanation`](crate::identity::Role::Explanation), anchored on the CLOSURE's identity at full width — an explanation is written after a closure and over it — at position zero, over
165 ///
166 /// ```text
167 /// content = bytes(plan) || u64be(universal seats) || seat* || u64be(declared seats) || seat*
168 /// ```
169 ///
170 /// where each `seat` is the question's roster position in two big-endian bytes followed by the answer's own canonical bytes, in that roster's declared order.
171 /// The two rosters are written behind two counts, so the split between them is framed rather than inferred: a universal seat and a declared seat may share a position.
172 /// Human prose is not a member — a rendered line is a projection of a typed answer, so a preimage carrying one would commit to a rendering rather than to what was answered.
173 ///
174 /// # Errors
175 ///
176 /// Returns [`ExplanationError`] naming every unanswered question, every doubled question, every answer standing outside its own roster, the seat bound where a kind's roster outgrows it, and an output answer that does not restate the proof's own rendered roster.
177 /// All of them together: a caller repairing a view one question per attempt is a caller the protocol failed.
178 pub fn complete(
179 plan: &Plan<K>,
180 closure: &Closure<K::Role>,
181 universal: Vec<UniversalAnswer>,
182 declared: Vec<<K::Question as Question>::Answer>,
183 ) -> Result<Self, ExplanationError> {
184 let mut issues = coverage_issues::<K>(&universal, &declared);
185 issues.extend(outputs_beside_proof(&universal, closure));
186 if let Some(refusal) = refused(issues) {
187 return Err(refusal);
188 }
189 let seated_universal = in_roster_order::<UniversalQuestion>(universal);
190 let seated_declared = in_roster_order::<K::Question>(declared);
191
192 let plan_identity = plan.identity();
193 let closure_identity = closure.identity();
194 let mut content = Vec::new();
195 encode_bytes(plan_identity.as_bytes(), &mut content);
196 seats_into(&seated_universal, &mut content);
197 seats_into(&seated_declared, &mut content);
198 let (derived, provenance) =
199 ExplanationId::derived_with_provenance(Transcript::under_projection(
200 identity::Role::Explanation,
201 &closure_identity,
202 &content,
203 0,
204 ));
205
206 let held_universal = Bounded::new(seated_universal).map_err(ExplanationError::bounded)?;
207 let held_declared = Bounded::new(seated_declared).map_err(ExplanationError::bounded)?;
208 Ok(Self {
209 plan: plan_identity,
210 closure: closure_identity,
211 universal: held_universal,
212 declared: held_declared,
213 identity: derived,
214 provenance,
215 kind: PhantomData,
216 })
217 }
218
219 /// This view's own identity — the name a binding commits to.
220 #[must_use]
221 pub const fn identity(&self) -> ExplanationId {
222 self.identity
223 }
224
225 /// The record of how that identity was derived.
226 #[must_use]
227 pub const fn provenance(&self) -> &Provenance {
228 &self.provenance
229 }
230
231 /// The plan this view was answered over.
232 ///
233 /// Read back so a binding establishes that the plan it seals is the plan the answers are about, rather than assuming it.
234 #[must_use]
235 pub const fn plan(&self) -> PlanId {
236 self.plan
237 }
238
239 /// The proved closure this view was answered over, on the same terms.
240 #[must_use]
241 pub const fn closure(&self) -> ClosureId {
242 self.closure
243 }
244
245 /// The universal answers, in the compiler's roster order.
246 #[must_use]
247 pub fn universal(&self) -> &[UniversalAnswer] {
248 self.universal.as_slice()
249 }
250
251 /// The kind's own answers, in the kind's declared roster order.
252 #[must_use]
253 pub fn declared(&self) -> &[<K::Question as Question>::Answer] {
254 self.declared.as_slice()
255 }
256
257 /// How many seats this view fills, across both rosters.
258 #[must_use]
259 pub fn seats(&self) -> usize {
260 self.universal.len().saturating_add(self.declared.len())
261 }
262}