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}