Skip to main content

macroonz_compiler/diagnostic/
type_guard.rs

1//! The diagnostic home's invariant nucleus: the one road that builds a related set, the private diagnostic assembly, and the roads its own values are read through.
2//!
3//! Declared inside `types.rs` as its own child, so the seats a caller may not write are reachable here and nowhere else.
4//!
5//! What lands here is what is about an ACT rather than about a value.
6//! The related-set road takes the issue material — not a count, and not identities somebody else already derived — and builds the set, so the capping and the identities are two readings of one act.
7//! The caller-placed and intrinsic roads obtain one informed projection from `project.rs` and assemble its private seats here, so a line naming one position beside a seat holding another is unrepresentable.
8
9use super::{
10    Diagnostic, DiagnosticName, DiagnosticNameRefusal, DiagnosticProjection, DiagnosticSeats,
11    Family, IntrinsicRefused, Observed, Phase, Placement, Refused, RelatedIdentity, RelatedSet,
12    Repair, Route, Site, SiteCoordinate,
13};
14use crate::bounded::{Bounded, Capping};
15use crate::diagnostic::project::{diagnostic, intrinsic_site, placement_site};
16use crate::identity::{
17    Contract, Identity, RelatedBody, RelatedIssue, Role, ServiceEntry, Transcript, encode_bytes,
18};
19use crate::request::Door;
20use crate::token::{SourceCoordinate, SpanHandle};
21
22impl Family {
23    /// Declare one family under its owner's namespace.
24    ///
25    /// The shape is `namespace/stem`, checked where the constant is written: a name that carries no namespace is a name two crates could write.
26    ///
27    /// # Panics
28    ///
29    /// Stops const evaluation — the build, for the `const` items this is written for — on a name without an interior `/`.
30    #[must_use]
31    pub const fn declared(name: &'static str) -> Self {
32        assert!(
33            interior_separator(name.as_bytes()),
34            "a family name is namespace/stem, with material on both sides"
35        );
36        Self(name)
37    }
38
39    /// The name, exactly as declared.
40    #[must_use]
41    pub const fn name(self) -> &'static str {
42        self.0
43    }
44}
45
46impl DiagnosticName {
47    /// Declares one lowercase ASCII kebab-case diagnostic name without normalizing it.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`DiagnosticNameRefusal::Empty`] where no name was supplied and [`DiagnosticNameRefusal::NotKebabCase`] where the spelling contains another character or a leading, trailing, or doubled separator.
52    pub const fn declared(name: &'static str) -> Result<Self, DiagnosticNameRefusal> {
53        if name.is_empty() {
54            return Err(DiagnosticNameRefusal::Empty);
55        }
56        if !crate::identity::name_is_grammatical(name) {
57            return Err(DiagnosticNameRefusal::NotKebabCase);
58        }
59        Ok(Self(name))
60    }
61
62    /// The name, exactly as declared.
63    #[must_use]
64    pub const fn spelling(self) -> &'static str {
65        self.0
66    }
67}
68
69/// Whether the first `/` in the bytes has material on both sides.
70const fn interior_separator(name: &[u8]) -> bool {
71    let mut rest = name;
72    let mut ahead = 0_usize;
73    while let Some((byte, remaining)) = rest.split_first() {
74        if *byte == b'/' {
75            return ahead != 0 && !remaining.is_empty();
76        }
77        ahead = ahead.saturating_add(1);
78        rest = remaining;
79    }
80    false
81}
82
83/// The content one related identity is derived over, at either level.
84///
85/// The family name separates two spaces so the same bytes raised under two families never encode alike, and both members are framed so no two compositions share a preimage.
86/// One composition serves both levels deliberately: what separates them is the subject, which is a segment of the derive-key context rather than a discriminant somebody could forget to write into a preimage.
87fn related_content(family: Family, material: &[u8]) -> Vec<u8> {
88    let mut content = Vec::new();
89    encode_bytes(family.name().as_bytes(), &mut content);
90    encode_bytes(material, &mut content);
91    content
92}
93
94/// One related-issue identity over one established issue's material.
95///
96/// Private on purpose: an identity of this subject exists only as part of a set this file built.
97/// The position is zero for every family: the family rides inside the content, framed, where a name fits.
98fn issue_identity(family: Family, material: &[u8]) -> Identity<RelatedIssue> {
99    Identity::derived(Transcript::rooted(
100        Role::DiagnosticRelation,
101        &related_content(family, material),
102        0,
103    ))
104}
105
106/// One related-body identity over the framing of a whole body's issues.
107///
108/// The same private discipline and the same seat, under the other subject.
109/// The separation is deliberately not a byte inside the preimage: the subject rides in the derive-key context, so two levels over identical content are separated before a byte of that content is read.
110fn body_identity(family: Family, material: &[u8]) -> Identity<RelatedBody> {
111    Identity::derived(Transcript::rooted(
112        Role::DiagnosticRelation,
113        &related_content(family, material),
114        0,
115    ))
116}
117
118impl RelatedSet {
119    /// The related set one refusal body amounts to, derived over that body's own issue material.
120    ///
121    /// A road taking a body identity and a set of per-issue identities as two arguments takes two halves that do not check each other: each is honestly derived on its own, so the pair can name one refusal's body over another refusal's issues and still read exactly like a set that belongs together.
122    /// Deriving here removes the pairing instead of policing it.
123    ///
124    /// # Construction
125    ///
126    /// This is a mint site, so its grammar is stated in full.
127    /// Both levels derive at [`Role::DiagnosticRelation`], rooted, at position zero, over
128    ///
129    /// ```text
130    /// content = u64be(len(family)) || family || u64be(material.len()) || material
131    /// ```
132    ///
133    /// where the family is its declared name's UTF-8 bytes, the material of an issue is that issue's own canonical bytes, and the material of the body is `u64be(issue.len()) || issue` for every issue in order, concatenated.
134    /// The per-issue identities are derived first and the body's identity over exactly that material in exactly that order, so two different issue sets cannot reach one body identity and one issue set cannot reach two.
135    /// The two subjects are `related-issue` and `related-body`; everything else an independent reader needs is on [`Transcript`].
136    ///
137    /// # Bounds
138    ///
139    /// The set overruns [`RELATED_ISSUE_LIMIT`](super::RELATED_ISSUE_LIMIT) at the boundary by exactly one, because the body's own identity sits ahead of the per-issue ones.
140    /// Where that happens the body's identity is carried alone — a coarser commitment to the same refusal, never a shorter commitment to a different one — and the capping states how many per-issue identities are not there.
141    ///
142    /// Handed no issues, this road answers with [`RelatedSet::nothing_enumerated`] rather than deriving a body identity over empty material, so "nothing was enumerated" has one representation and two diagnostics that enumerated nothing compare equal.
143    #[must_use]
144    pub fn derived_over(family: Family, issues: &[Vec<u8>]) -> Self {
145        if issues.is_empty() {
146            return Self::nothing_enumerated();
147        }
148        let mut body_material = Vec::new();
149        let mut per_issue = Vec::with_capacity(issues.len());
150        for issue in issues {
151            per_issue.push(RelatedIdentity::Issue(issue_identity(family, issue)));
152            encode_bytes(issue, &mut body_material);
153        }
154        let body = RelatedIdentity::Body(body_identity(family, &body_material));
155        let mut all = Vec::with_capacity(per_issue.len().saturating_add(1));
156        all.push(body);
157        all.append(&mut per_issue);
158        match Bounded::new(all) {
159            Ok(carried) => Self {
160                carried,
161                capping: Capping::Complete,
162            },
163            Err(_) => Self {
164                carried: Bounded::from_array([body]),
165                capping: Capping::Truncated {
166                    omitted: issues.len(),
167                },
168            },
169        }
170    }
171
172    /// The canonical empty relation: what a road that enumerated nothing amounts to.
173    ///
174    /// A single-cause refusal establishes one cause and enumerates nothing, so there is no per-issue set to fall short of: zero identities are carried and zero were dropped.
175    ///
176    /// # Nonclaims
177    ///
178    /// Emptiness here is a stated posture about an act that ran — the road looked, and there was nothing to enumerate.
179    /// It is not an absent set, not a set that failed to build, and not a capping that dropped everything.
180    #[must_use]
181    pub const fn nothing_enumerated() -> Self {
182        Self {
183            carried: Bounded::empty(),
184            capping: Capping::Complete,
185        }
186    }
187
188    /// The identities the set carries, body first.
189    #[must_use]
190    pub fn carried(&self) -> &[RelatedIdentity] {
191        self.carried.as_slice()
192    }
193
194    /// Whether that set names every established issue.
195    #[must_use]
196    pub const fn capping(&self) -> Capping {
197        self.capping
198    }
199}
200
201impl Site {
202    /// The site of an observation about one token of a captured declaration.
203    ///
204    /// The handle is the load-bearing half: the producer resolves it to the exact compiler span, and this compiler never does.
205    /// The coordinate beside it is whatever that producer's table answered, including the typed statement that the table does not reach the handle — which is a fact about the TABLE and leaves the observation itself standing.
206    pub const fn at_token(token: SpanHandle, coordinate: SiteCoordinate) -> Self {
207        Self::AtToken { token, coordinate }
208    }
209
210    /// The site of an observation established before any capture existed to issue a handle.
211    ///
212    /// It takes no [`SpanHandle`], which is the whole point of the road: there is no seat here for a caller to fill with handle zero, and no branch in which one is invented.
213    pub const fn before_capture(coordinate: SourceCoordinate) -> Self {
214        Self::BeforeCapture { coordinate }
215    }
216
217    /// The site of an observation about the declaration as a whole.
218    ///
219    /// It takes nothing, which is the statement: there is no narrower token, so no handle and no coordinate exist to be answered — and none is invented for a machine reader to mistake for a resolved one.
220    pub const fn whole_declaration() -> Self {
221        Self::WholeDeclaration
222    }
223
224    /// The token this diagnostic points at, where a capture issued one.
225    ///
226    /// # Nonclaims
227    ///
228    /// It answers with nothing for a site established BEFORE a capture, because no table was built and no handle was issued, and with nothing for a WHOLE-DECLARATION site, because the refusal is not at a token.
229    /// Each is a stated posture rather than a missing value: a handle answered here would index a table that never existed or name a spot the refusal is not about, and either would read exactly like an honest handle.
230    #[must_use]
231    pub const fn token(self) -> Option<SpanHandle> {
232        match self {
233            Self::AtToken { token, .. } => Some(token),
234            Self::BeforeCapture { .. } | Self::WholeDeclaration => None,
235        }
236    }
237
238    /// Where this diagnostic sits, where it sits at a place at all.
239    ///
240    /// The ONE place a pre-capture byte is lifted into the answered posture, and it lifts honestly: nothing was resolved because nothing needed resolving, and the coordinate's own role says which text the position counts into.
241    /// A whole-declaration site answers with nothing — the refusal has no position inside the declaration, and a manufactured one would read exactly like a resolved coordinate.
242    #[must_use]
243    pub const fn coordinate(self) -> Option<SiteCoordinate> {
244        match self {
245            Self::AtToken { coordinate, .. } => Some(coordinate),
246            Self::BeforeCapture { coordinate } => Some(SiteCoordinate::Resolved(coordinate)),
247            Self::WholeDeclaration => None,
248        }
249    }
250}
251
252impl Route {
253    /// The reproduction road one door offers.
254    pub(crate) const fn through(entry: Identity<ServiceEntry>) -> Self {
255        Self { entry }
256    }
257
258    /// The callable entry point that reaches this observation again.
259    #[must_use]
260    pub const fn entry(self) -> Identity<ServiceEntry> {
261        self.entry
262    }
263}
264
265impl Diagnostic {
266    /// Project one caller-placed refused step into the diagnostic its door hands back.
267    ///
268    /// Every seat that could be written two ways is written once on this road: the line through [`composed`](crate::diagnostic::composed), the expected contract and the reproduction route off the door, and the site through the placement the caller states.
269    /// The site is built once and read twice — the prose and the seat are projections of the same value.
270    /// Refusal types whose site is intrinsic expose their own diagnostic road and do not implement this method's public bound.
271    pub fn refused<E: Refused>(refusal: &E, door: &Door, placement: &Placement<'_>) -> Self {
272        assemble_diagnostic(diagnostic(refusal, door, placement_site(placement)))
273    }
274
275    /// The step that was running.
276    #[must_use]
277    pub const fn phase(&self) -> Phase {
278        self.phase
279    }
280
281    /// Where the observation sits.
282    pub const fn site(&self) -> Site {
283        self.site
284    }
285
286    /// The one line this diagnostic projects for a person.
287    ///
288    /// A projection and only a projection: nothing reads it back, and a frontend shows it rather than deciding from it.
289    #[must_use]
290    pub fn summary(&self) -> &str {
291        &self.carried.summary
292    }
293
294    /// The contract that was expected to hold.
295    #[must_use]
296    pub fn expected(&self) -> Identity<Contract> {
297        self.carried.expected
298    }
299
300    /// How what was found differs from it.
301    #[must_use]
302    pub const fn observed(&self) -> Observed {
303        self.observed
304    }
305
306    /// The other issues this one points at, and how that set was capped.
307    #[must_use]
308    pub fn related(&self) -> &RelatedSet {
309        &self.carried.related
310    }
311
312    /// The owner-declared repairs that apply.
313    #[must_use]
314    pub fn repairs(&self) -> &[Repair] {
315        self.carried.repairs.as_slice()
316    }
317
318    /// How to reach this observation again.
319    #[must_use]
320    pub fn route(&self) -> Route {
321        self.carried.route
322    }
323}
324
325/// Compose one intrinsically placed refusal without accepting a second site beside it.
326pub(crate) fn intrinsic_diagnostic<E: IntrinsicRefused>(refusal: &E, door: &Door) -> Diagnostic {
327    assemble_diagnostic(diagnostic(refusal, door, intrinsic_site(refusal)))
328}
329
330/// Assemble one diagnostic from the informed projection without reopening any semantic choice.
331fn assemble_diagnostic(projection: DiagnosticProjection) -> Diagnostic {
332    Diagnostic {
333        phase: projection.phase,
334        site: projection.site,
335        observed: projection.observed,
336        carried: Box::new(DiagnosticSeats {
337            summary: projection.summary,
338            expected: projection.expected,
339            related: projection.related,
340            repairs: projection.repairs,
341            route: projection.route,
342        }),
343    }
344}