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 two placement-safe roads that build diagnostics, 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 road takes a refusal, a door, and the public placement vocabulary; the intrinsic road takes a refusal projection whose own type supplies its site.
8//! Both end at the same private composition, so a line naming one position beside a seat holding another is unrepresentable.
9
10use super::{
11    Diagnostic, DiagnosticName, DiagnosticNameRefusal, DiagnosticSeats, Family, IntrinsicRefused,
12    Line, LineSite, Observed, Phase, Placement, Refused, RelatedIdentity, RelatedSet, Repair,
13    Route, Site, SiteCoordinate,
14};
15use crate::bounded::{Bounded, Capping};
16use crate::diagnostic::project::{composed, witnessed};
17use crate::identity::{
18    Contract, Identity, RelatedBody, RelatedIssue, Role, ServiceEntry, Transcript, encode_bytes,
19};
20use crate::request::Door;
21use crate::token::{SourceCoordinate, SpanHandle};
22
23impl Family {
24    /// Declare one family under its owner's namespace.
25    ///
26    /// The shape is `namespace/stem`, checked where the constant is written: a name that carries no namespace is a name two crates could write.
27    ///
28    /// # Panics
29    ///
30    /// Stops const evaluation — the build, for the `const` items this is written for — on a name without an interior `/`.
31    #[must_use]
32    pub const fn declared(name: &'static str) -> Self {
33        assert!(
34            interior_separator(name.as_bytes()),
35            "a family name is namespace/stem, with material on both sides"
36        );
37        Self(name)
38    }
39
40    /// The name, exactly as declared.
41    #[must_use]
42    pub const fn name(self) -> &'static str {
43        self.0
44    }
45}
46
47impl DiagnosticName {
48    /// Declares one lowercase ASCII kebab-case diagnostic name without normalizing it.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`DiagnosticNameRefusal::Empty`] where no name was supplied and [`DiagnosticNameRefusal::NotKebabCase`] where the spelling contains another character or a leading, trailing, or doubled separator.
53    pub const fn declared(name: &'static str) -> Result<Self, DiagnosticNameRefusal> {
54        if name.is_empty() {
55            return Err(DiagnosticNameRefusal::Empty);
56        }
57        if !diagnostic_name_is_kebab_case(name.as_bytes()) {
58            return Err(DiagnosticNameRefusal::NotKebabCase);
59        }
60        Ok(Self(name))
61    }
62
63    /// The name, exactly as declared.
64    #[must_use]
65    pub const fn spelling(self) -> &'static str {
66        self.0
67    }
68}
69
70/// Whether one non-empty spelling is lowercase ASCII kebab-case.
71const fn diagnostic_name_is_kebab_case(name: &[u8]) -> bool {
72    let mut rest = name;
73    let mut separator = true;
74    while let Some((byte, remaining)) = rest.split_first() {
75        if *byte == b'-' {
76            if separator {
77                return false;
78            }
79            separator = true;
80        } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
81            separator = false;
82        } else {
83            return false;
84        }
85        rest = remaining;
86    }
87    !separator
88}
89
90/// Whether the first `/` in the bytes has material on both sides.
91const fn interior_separator(name: &[u8]) -> bool {
92    let mut rest = name;
93    let mut ahead = 0_usize;
94    while let Some((byte, remaining)) = rest.split_first() {
95        if *byte == b'/' {
96            return ahead != 0 && !remaining.is_empty();
97        }
98        ahead = ahead.saturating_add(1);
99        rest = remaining;
100    }
101    false
102}
103
104/// The content one related identity is derived over, at either level.
105///
106/// 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.
107/// 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.
108fn related_content(family: Family, material: &[u8]) -> Vec<u8> {
109    let mut content = Vec::new();
110    encode_bytes(family.name().as_bytes(), &mut content);
111    encode_bytes(material, &mut content);
112    content
113}
114
115/// One related-issue identity over one established issue's material.
116///
117/// Private on purpose: an identity of this subject exists only as part of a set this file built.
118/// The position is zero for every family: the family rides inside the content, framed, where a name fits.
119fn issue_identity(family: Family, material: &[u8]) -> Identity<RelatedIssue> {
120    Identity::derived(Transcript::rooted(
121        Role::DiagnosticRelation,
122        &related_content(family, material),
123        0,
124    ))
125}
126
127/// One related-body identity over the framing of a whole body's issues.
128///
129/// The same private discipline and the same seat, under the other subject.
130/// 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.
131fn body_identity(family: Family, material: &[u8]) -> Identity<RelatedBody> {
132    Identity::derived(Transcript::rooted(
133        Role::DiagnosticRelation,
134        &related_content(family, material),
135        0,
136    ))
137}
138
139impl RelatedSet {
140    /// The related set one refusal body amounts to, derived over that body's own issue material.
141    ///
142    /// 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.
143    /// Deriving here removes the pairing instead of policing it.
144    ///
145    /// # Construction
146    ///
147    /// This is a mint site, so its grammar is stated in full.
148    /// Both levels derive at [`Role::DiagnosticRelation`], rooted, at position zero, over
149    ///
150    /// ```text
151    /// content = u64be(len(family)) || family || u64be(material.len()) || material
152    /// ```
153    ///
154    /// 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.
155    /// 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.
156    /// The two subjects are `related-issue` and `related-body`; everything else an independent reader needs is on [`Transcript`].
157    ///
158    /// # Bounds
159    ///
160    /// 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.
161    /// 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.
162    ///
163    /// 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.
164    #[must_use]
165    pub fn derived_over(family: Family, issues: &[Vec<u8>]) -> Self {
166        if issues.is_empty() {
167            return Self::nothing_enumerated();
168        }
169        let mut body_material = Vec::new();
170        let mut per_issue = Vec::with_capacity(issues.len());
171        for issue in issues {
172            per_issue.push(RelatedIdentity::Issue(issue_identity(family, issue)));
173            encode_bytes(issue, &mut body_material);
174        }
175        let body = RelatedIdentity::Body(body_identity(family, &body_material));
176        let mut all = Vec::with_capacity(per_issue.len().saturating_add(1));
177        all.push(body);
178        all.append(&mut per_issue);
179        match Bounded::new(all) {
180            Ok(carried) => Self {
181                carried,
182                capping: Capping::Complete,
183            },
184            Err(_) => Self {
185                carried: Bounded::from_array([body]),
186                capping: Capping::Truncated {
187                    omitted: issues.len(),
188                },
189            },
190        }
191    }
192
193    /// The canonical empty relation: what a road that enumerated nothing amounts to.
194    ///
195    /// 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.
196    ///
197    /// # Nonclaims
198    ///
199    /// Emptiness here is a stated posture about an act that ran — the road looked, and there was nothing to enumerate.
200    /// It is not an absent set, not a set that failed to build, and not a capping that dropped everything.
201    #[must_use]
202    pub const fn nothing_enumerated() -> Self {
203        Self {
204            carried: Bounded::empty(),
205            capping: Capping::Complete,
206        }
207    }
208
209    /// The identities the set carries, body first.
210    #[must_use]
211    pub fn carried(&self) -> &[RelatedIdentity] {
212        self.carried.as_slice()
213    }
214
215    /// Whether that set names every established issue.
216    #[must_use]
217    pub const fn capping(&self) -> Capping {
218        self.capping
219    }
220}
221
222impl Site {
223    /// The site of an observation about one token of a captured declaration.
224    ///
225    /// The handle is the load-bearing half: the producer resolves it to the exact compiler span, and this compiler never does.
226    /// 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.
227    pub const fn at_token(token: SpanHandle, coordinate: SiteCoordinate) -> Self {
228        Self::AtToken { token, coordinate }
229    }
230
231    /// The site of an observation established before any capture existed to issue a handle.
232    ///
233    /// 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.
234    pub const fn before_capture(coordinate: SourceCoordinate) -> Self {
235        Self::BeforeCapture { coordinate }
236    }
237
238    /// The site of an observation about the declaration as a whole.
239    ///
240    /// 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.
241    pub const fn whole_declaration() -> Self {
242        Self::WholeDeclaration
243    }
244
245    /// The token this diagnostic points at, where a capture issued one.
246    ///
247    /// # Nonclaims
248    ///
249    /// 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.
250    /// 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.
251    #[must_use]
252    pub const fn token(self) -> Option<SpanHandle> {
253        match self {
254            Self::AtToken { token, .. } => Some(token),
255            Self::BeforeCapture { .. } | Self::WholeDeclaration => None,
256        }
257    }
258
259    /// Where this diagnostic sits, where it sits at a place at all.
260    ///
261    /// 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.
262    /// 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.
263    #[must_use]
264    pub const fn coordinate(self) -> Option<SiteCoordinate> {
265        match self {
266            Self::AtToken { coordinate, .. } => Some(coordinate),
267            Self::BeforeCapture { coordinate } => Some(SiteCoordinate::Resolved(coordinate)),
268            Self::WholeDeclaration => None,
269        }
270    }
271}
272
273impl Route {
274    /// The reproduction road one door offers.
275    pub(crate) const fn through(entry: Identity<ServiceEntry>) -> Self {
276        Self { entry }
277    }
278
279    /// The callable entry point that reaches this observation again.
280    #[must_use]
281    pub const fn entry(self) -> Identity<ServiceEntry> {
282        self.entry
283    }
284}
285
286impl Diagnostic {
287    /// Project one caller-placed refused step into the diagnostic its door hands back.
288    ///
289    /// Every seat that could be written two ways is written once on this road: the line through [`composed`], the expected contract and the reproduction route off the door, and the site through the placement the caller states.
290    /// The site is built once and read twice — the prose and the seat are projections of the same value.
291    /// Refusal types whose site is intrinsic expose their own diagnostic road and do not implement this method's public bound.
292    pub fn refused<E: Refused>(refusal: &E, door: &Door, placement: &Placement<'_>) -> Self {
293        compose_diagnostic(refusal, door, placement_site(placement))
294    }
295
296    /// The step that was running.
297    #[must_use]
298    pub const fn phase(&self) -> Phase {
299        self.phase
300    }
301
302    /// Where the observation sits.
303    pub const fn site(&self) -> Site {
304        self.site
305    }
306
307    /// The one line this diagnostic projects for a person.
308    ///
309    /// A projection and only a projection: nothing reads it back, and a frontend shows it rather than deciding from it.
310    #[must_use]
311    pub fn summary(&self) -> &str {
312        &self.carried.summary
313    }
314
315    /// The contract that was expected to hold.
316    #[must_use]
317    pub fn expected(&self) -> Identity<Contract> {
318        self.carried.expected
319    }
320
321    /// How what was found differs from it.
322    #[must_use]
323    pub const fn observed(&self) -> Observed {
324        self.observed
325    }
326
327    /// The other issues this one points at, and how that set was capped.
328    #[must_use]
329    pub fn related(&self) -> &RelatedSet {
330        &self.carried.related
331    }
332
333    /// The owner-declared repairs that apply.
334    #[must_use]
335    pub fn repairs(&self) -> &[Repair] {
336        self.carried.repairs.as_slice()
337    }
338
339    /// How to reach this observation again.
340    #[must_use]
341    pub fn route(&self) -> Route {
342        self.carried.route
343    }
344}
345
346/// Compose one intrinsically placed refusal without accepting a second site beside it.
347pub(crate) fn intrinsic_diagnostic<E: IntrinsicRefused>(refusal: &E, door: &Door) -> Diagnostic {
348    compose_diagnostic(refusal, door, refusal.site())
349}
350
351/// Compose one refusal over the site its lawful road established.
352fn compose_diagnostic<E: Refused>(refusal: &E, door: &Door, site: Site) -> Diagnostic {
353    let related = RelatedSet::derived_over(E::FAMILY, &refusal.related());
354    let first = refusal.first();
355    let line = Line {
356        class: refusal.class(),
357        first: &first,
358        body: refusal.body(),
359    };
360    let composed_line = composed(door, &line, placement_line_site(site));
361    Diagnostic {
362        phase: E::PHASE,
363        site,
364        observed: refusal.observed(),
365        carried: Box::new(DiagnosticSeats {
366            summary: witnessed(&composed_line, related.capping()),
367            expected: door.grammar(),
368            related,
369            repairs: refusal.repairs(),
370            route: Route::through(door.entry()),
371        }),
372    }
373}
374
375/// The site one placement amounts to.
376///
377/// The whole-declaration placement answers with the whole-declaration site: the placement says there is nowhere narrower to point, and the site says exactly the same thing — no token, no coordinate, no manufactured first-token stand-in a machine reader could mistake for a resolved position.
378fn placement_site(placement: &Placement<'_>) -> Site {
379    match *placement {
380        Placement::WholeDeclaration => Site::whole_declaration(),
381        Placement::AtToken { token, spans } => {
382            Site::at_token(token, SiteCoordinate::answered(spans.coordinate_of(token)))
383        }
384    }
385}
386
387/// What the composed line says about where the refusal sits — a projection of the site, so the prose and the seat cannot disagree.
388fn placement_line_site(site: Site) -> LineSite {
389    match site {
390        Site::WholeDeclaration => LineSite::WholeDeclaration,
391        Site::AtToken { coordinate, .. } => LineSite::At(coordinate),
392        Site::BeforeCapture { coordinate } => LineSite::At(SiteCoordinate::Resolved(coordinate)),
393    }
394}