macroonz_compiler/plan/type_guard.rs
1//! The plan 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 membership's members are unreachable except through the roads below, so a plan's declared output set is whatever one of them admitted; an account's seats are unreachable the same way, so the one account of a request's content is whatever walked in one of these doors.
5
6use super::super::encode::encode_set;
7use super::{
8 Account, BoundAxis, ContentBinding, Context, Intent, InvalidationSet, InvalidationTrigger,
9 MEMBERSHIP_LIMIT, Membership, PLAN_ISSUE_LIMIT, Plan, PlanDecisions, PlanError, PlanIssue,
10 PlannedMember, TRIGGER_LIMIT,
11};
12use crate::bounded::{Bounded, Capped, Capping, NonEmpty, Overflow};
13use crate::identity::{
14 self, GENERATOR, Identity, PlanId, Profile, Provenance, Transcript, encode_bytes,
15};
16use crate::kind::{CanonicalContent, Destination, Kind, Role};
17use crate::origin::{DecisionTrace, Nonclaim, OriginTrail, TrailError};
18
19impl<K: Kind> ContentBinding<K> {
20 /// Bind one content value to the capture and owner-qualified kind it was presented under.
21 pub(crate) fn bound(
22 capture: Identity<identity::CapturedDeclaration>,
23 kind: Identity<identity::ProjectionKind>,
24 content: K::Content,
25 ) -> Self {
26 let mut material = Vec::new();
27 encode_bytes(kind.as_bytes(), &mut material);
28 encode_bytes(&content.canonical_content_bytes(), &mut material);
29 let commitment = Identity::derived(Transcript::under_projection(
30 identity::Role::ProjectionContent,
31 &capture,
32 &material,
33 0,
34 ));
35 Self {
36 capture,
37 kind,
38 commitment,
39 content,
40 }
41 }
42
43 /// The captured declaration this content was bound under.
44 #[must_use]
45 pub const fn capture(&self) -> Identity<identity::CapturedDeclaration> {
46 self.capture
47 }
48
49 /// The owner-qualified kind this content was bound as.
50 #[must_use]
51 pub const fn kind(&self) -> Identity<identity::ProjectionKind> {
52 self.kind
53 }
54
55 /// The commitment over this content's canonical bytes.
56 #[must_use]
57 pub const fn commitment(&self) -> Identity<identity::ProjectionContent> {
58 self.commitment
59 }
60
61 /// The exact kind-specific content the binding carries.
62 #[must_use]
63 pub const fn content(&self) -> &K::Content {
64 &self.content
65 }
66}
67
68impl<K: Kind> Account<K> {
69 /// The account of content that stands on nothing.
70 pub fn over(binding: ContentBinding<K>) -> Self {
71 Self {
72 binding,
73 dependencies: Bounded::empty(),
74 }
75 }
76
77 /// The account of content that stands on the captures the caller declares.
78 ///
79 /// The set is canonicalized here — ordered by identity, exact repeats dropped — so two callers declaring one set in two orders reach one plan.
80 ///
81 /// # Errors
82 ///
83 /// Returns the planning refusal naming [`BoundAxis::Declarations`] where the declared set outgrows [`DEPENDENCY_LIMIT`](super::DEPENDENCY_LIMIT).
84 pub fn standing_on(
85 binding: ContentBinding<K>,
86 mut dependencies: Vec<Identity<identity::CapturedDeclaration>>,
87 ) -> Result<Self, PlanError> {
88 dependencies.sort_unstable_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
89 dependencies.dedup();
90 Bounded::new(dependencies)
91 .map(|admitted| Self {
92 binding,
93 dependencies: admitted,
94 })
95 .map_err(|overflow| PlanError::bounded(BoundAxis::Declarations, overflow))
96 }
97
98 /// The one address the caller supplied at the door.
99 ///
100 /// The reading a plan's anchor, its causing-declaration answer, and its own trigger are all taken from — one value read three times rather than three seats that could disagree.
101 #[must_use]
102 pub const fn commitment(&self) -> Identity<identity::CapturedDeclaration> {
103 self.binding.capture()
104 }
105
106 /// The owner-qualified kind identity this account carries.
107 #[must_use]
108 pub const fn kind(&self) -> Identity<identity::ProjectionKind> {
109 self.binding.kind()
110 }
111
112 /// The commitment over the kind-specific content's canonical bytes.
113 #[must_use]
114 pub const fn content_commitment(&self) -> Identity<identity::ProjectionContent> {
115 self.binding.commitment()
116 }
117
118 /// The kind-specific content itself.
119 #[must_use]
120 pub const fn content(&self) -> &K::Content {
121 self.binding.content()
122 }
123
124 /// The captures this content declares it stands on, in canonical order.
125 #[must_use]
126 pub fn dependencies(&self) -> &[Identity<identity::CapturedDeclaration>] {
127 self.dependencies.as_slice()
128 }
129
130 /// What was MEANT: the owner-qualified kind over the content commitment, derived into the intent layer's own identity.
131 ///
132 /// The preimage is [`Account::intent_bytes`], derived at [`Role::ProjectionIntent`](crate::identity::Role::ProjectionIntent), rooted, at position zero.
133 /// Rooted deliberately: the preimage already carries the commitment at full width, so anchoring on that same commitment would write it twice into one derivation and separate nothing.
134 #[must_use]
135 pub fn intent(&self) -> Intent {
136 Intent::derived(Transcript::rooted(
137 identity::Role::ProjectionIntent,
138 &self.intent_bytes(),
139 0,
140 ))
141 }
142}
143
144impl Context {
145 /// The context a request is decided under: the profile it selected, and the generator answering.
146 ///
147 /// The generator is this crate's own and is derived here rather than supplied, so a plan cannot be told a producer it was not produced by.
148 /// The derivation is the one [`GENERATOR_VERSION_PROFILE`](crate::identity::GENERATOR_VERSION_PROFILE) states: the declared name framed, then the shape position in four big-endian bytes, rooted at position zero.
149 #[must_use]
150 pub fn under(profile: Profile) -> Self {
151 let mut material = Vec::new();
152 encode_bytes(GENERATOR.name().as_bytes(), &mut material);
153 material.extend_from_slice(&GENERATOR.shape().position().to_be_bytes());
154 Self {
155 profile,
156 generator: Identity::derived(Transcript::rooted(
157 identity::Role::GeneratorVersion,
158 &material,
159 0,
160 )),
161 }
162 }
163
164 /// The profile this context selected.
165 #[must_use]
166 pub const fn profile(&self) -> Profile {
167 self.profile
168 }
169
170 /// The generator answering under it.
171 #[must_use]
172 pub const fn generator(&self) -> Identity<identity::GeneratorVersion> {
173 self.generator
174 }
175
176 /// Every trigger one plan's own facts require, as a set.
177 ///
178 /// The shared half of any plan's invalidation, derived from the seats this context declares and the commitments the account names rather than listed at a plan site.
179 /// A kind adds whatever its own anchors require on top, through [`InvalidationTrigger::Declared`].
180 ///
181 /// Exact repeats are dropped before construction: a repeat would be written twice by the transcript's set encoding, so two plans watching the same things would carry two identities depending only on whether a call site remembered to skip it.
182 ///
183 /// # Errors
184 ///
185 /// Returns the planning refusal naming [`BoundAxis::Triggers`] where the derived set outgrows [`TRIGGER_LIMIT`].
186 pub fn watch_set<K: Kind>(&self, account: &Account<K>) -> Result<InvalidationSet, PlanError> {
187 // Exhaustive on purpose: a seat added to the context stops compiling HERE
188 // until somebody decides whether it is watched, so the watch set cannot
189 // fall a seat behind the context it is derived from.
190 let Self { profile, generator } = self;
191 let (first, mut rest) = account.caused_by();
192 let shared = [
193 InvalidationTrigger::Profile { watched: *profile },
194 InvalidationTrigger::Generator {
195 watched: *generator,
196 },
197 ];
198 for trigger in shared {
199 if trigger != first && !rest.contains(&trigger) {
200 rest.push(trigger);
201 }
202 }
203 InvalidationTrigger::watched(first, rest)
204 }
205}
206
207impl InvalidationTrigger {
208 /// The one-trigger watch set. Total: one trigger always fits.
209 #[must_use]
210 pub fn one_watched(trigger: Self) -> InvalidationSet {
211 NonEmpty::one(trigger)
212 }
213
214 /// Watches these triggers, the first one and the rest.
215 ///
216 /// Several triggers of one row are lawful where they watch distinct things.
217 ///
218 /// # Errors
219 ///
220 /// Returns the planning refusal naming [`BoundAxis::Triggers`] where the set outgrows [`TRIGGER_LIMIT`].
221 pub fn watched(first: Self, rest: Vec<Self>) -> Result<InvalidationSet, PlanError> {
222 let offered = rest.len().saturating_add(1);
223 let mut triggers = vec![first];
224 triggers.extend(rest);
225 NonEmpty::new(triggers).map_err(|_| {
226 PlanError::bounded(
227 BoundAxis::Triggers,
228 Overflow {
229 capacity: TRIGGER_LIMIT,
230 offered,
231 },
232 )
233 })
234 }
235}
236
237/// The issue one member raises where its seat is absent from the kind's declared roster.
238fn foreign<R: Role>(member: &PlannedMember<R>) -> Option<PlanIssue> {
239 (!R::ALL.contains(&member.role)).then(|| PlanIssue::MembershipForeign {
240 seat: member.role.name(),
241 })
242}
243
244impl<R: Role> Membership<R> {
245 /// The one-member output set.
246 ///
247 /// # Errors
248 ///
249 /// Returns one [`PlanIssue::MembershipForeign`] where the member's seat is absent from the kind's declared roster.
250 /// The roster is every downstream walk's denominator, so a member outside it is refused here rather than admitted, rendered, and dropped from a proof that claims the whole set.
251 pub fn from_member(member: PlannedMember<R>) -> Result<Self, PlanError> {
252 match foreign(&member) {
253 Some(issue) => Err(PlanError::over(issue, Vec::new())),
254 None => Ok(Self {
255 members: NonEmpty::one(member),
256 }),
257 }
258 }
259
260 /// Declares the complete output set, the first member and the rest.
261 ///
262 /// # Errors
263 ///
264 /// Returns the planning refusal naming [`BoundAxis::Outputs`] where the set outgrows [`MEMBERSHIP_LIMIT`], one [`PlanIssue::MembershipForeign`] per member whose seat the kind's roster does not declare, and one [`PlanIssue::MembershipDoubled`] per seat two members stand under.
265 /// Both checks are here rather than downstream because each is a defect in the DECLARATION of the set: closure matches by seat over the roster, so a membership that reaches it doubled has already made that match elect one member and ignore the other, and one that reaches it with a foreign seat holds a member no walk will ever look at.
266 pub fn declared(
267 first: PlannedMember<R>,
268 rest: Vec<PlannedMember<R>>,
269 ) -> Result<Self, PlanError> {
270 let offered = rest.len().saturating_add(1);
271 let mut offering = vec![first];
272 offering.extend(rest);
273 let declared = NonEmpty::new(offering)
274 .map(|admitted| Self { members: admitted })
275 .map_err(|_| {
276 PlanError::bounded(
277 BoundAxis::Outputs,
278 Overflow {
279 capacity: MEMBERSHIP_LIMIT,
280 offered,
281 },
282 )
283 })?;
284 let mut established: Vec<PlanIssue> = declared
285 .members
286 .iter()
287 .filter_map(|member| foreign(member))
288 .collect();
289 established.extend(R::ALL.iter().filter_map(|role| declared.doubling(*role)));
290 let mut walked = established.into_iter();
291 match walked.next() {
292 Some(issue) => Err(PlanError::over(issue, walked.collect())),
293 None => Ok(declared),
294 }
295 }
296
297 /// The issue one seat raises where two members stand under it.
298 fn doubling(&self, role: R) -> Option<PlanIssue> {
299 let observed = self.count_under(role);
300 (observed > 1).then(|| PlanIssue::MembershipDoubled {
301 role_slot: role.slot(),
302 observed: u32::try_from(observed).unwrap_or(u32::MAX),
303 })
304 }
305
306 /// The guaranteed first member.
307 #[must_use]
308 pub fn first(&self) -> &PlannedMember<R> {
309 self.members.first()
310 }
311
312 /// The declared members, the guaranteed first one ahead of the rest.
313 ///
314 /// # Ordering
315 ///
316 /// A declared output set is order-insensitive, and nothing identity-bearing is derived from this order: the canonical encoding walks the kind's roster instead, so the same members declared in another order reach one plan.
317 #[must_use]
318 pub fn members(&self) -> &NonEmpty<PlannedMember<R>, MEMBERSHIP_LIMIT> {
319 &self.members
320 }
321
322 /// The member planned under one seat, where one is.
323 #[must_use]
324 pub fn under(&self, role: R) -> Option<&PlannedMember<R>> {
325 self.members().iter().find(|member| member.role == role)
326 }
327
328 /// Every member planned under one seat, in declaration order.
329 ///
330 /// The road a complete-set comparison walks: comparing two memberships by their first member per seat would agree about two sets that differ in their second, which is exactly what a doubled seat produces.
331 pub fn members_under(&self, role: R) -> impl Iterator<Item = &PlannedMember<R>> {
332 self.members()
333 .iter()
334 .filter(move |member| member.role == role)
335 }
336
337 /// How many members are planned under one seat.
338 #[must_use]
339 pub fn count_under(&self, role: R) -> usize {
340 self.members_under(role).count()
341 }
342
343 /// Every member this plan declared into one delivery, in declaration order.
344 ///
345 /// The reading that routes, and it elects nothing: a member's delivery is its seat's own constant answer ([`Role::destination`]), so a join asking which members it emits and a consumption target asking which cargo it receives take one answer rather than two that agree until one is edited.
346 pub fn members_to(&self, destination: Destination) -> impl Iterator<Item = &PlannedMember<R>> {
347 self.members()
348 .iter()
349 .filter(move |member| member.role.destination() == destination)
350 }
351
352 /// How many members this plan declared into one delivery.
353 ///
354 /// Zero is a stated answer rather than an absence: a plan that declared nothing into a delivery has an unoccupied one there, which is a different fact from a delivery that carries no bytes.
355 #[must_use]
356 pub fn count_to(&self, destination: Destination) -> usize {
357 self.members_to(destination).count()
358 }
359
360 /// Whether two memberships name the same members under one seat, as sets.
361 #[must_use]
362 pub fn agrees_under(&self, other: &Self, role: R) -> bool {
363 let mine: Vec<&PlannedMember<R>> = self.members_under(role).collect();
364 let theirs: Vec<&PlannedMember<R>> = other.members_under(role).collect();
365 mine == theirs
366 }
367
368 /// The number of members declared; structurally at least one.
369 #[must_use]
370 pub fn count(&self) -> usize {
371 self.members.count()
372 }
373}
374
375impl PlanError {
376 /// The refusal one established issue makes.
377 pub fn of(issue: PlanIssue) -> Self {
378 Self {
379 body: Capped::all(NonEmpty::one(issue)),
380 }
381 }
382
383 /// The refusal a pass whose checks co-establish makes.
384 ///
385 /// 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.
386 pub fn over(first: PlanIssue, rest: Vec<PlanIssue>) -> Self {
387 Self {
388 body: Capped::first_n(first, rest.into_iter()),
389 }
390 }
391
392 /// The refusal a magnitude makes: the axis that was overrun, and the two counts the overflow already carries.
393 pub fn bounded(axis: BoundAxis, overflow: Overflow) -> Self {
394 Self::of(PlanIssue::BoundExceeded {
395 axis,
396 bound: u64::try_from(overflow.capacity).unwrap_or(u64::MAX),
397 observed: u64::try_from(overflow.offered).unwrap_or(u64::MAX),
398 })
399 }
400
401 /// The refusal a trail that could not be drawn makes, over the unit it was to be drawn for.
402 ///
403 /// The unit is the caller's to name because a trail refusal carries none, and an origin that cannot be drawn at all is that unit standing with no origin.
404 pub fn over_trail(node: Identity<identity::GeneratedUnit>, refusal: TrailError) -> Self {
405 match refusal {
406 TrailError::Discontinuous { at } => Self::of(PlanIssue::TrailDiscontinuous { at }),
407 TrailError::Empty(_) => Self::of(PlanIssue::OrphanGeneratedNode { node }),
408 TrailError::Overflow(overflow) => Self::bounded(BoundAxis::OriginEdges, overflow),
409 }
410 }
411
412 /// The first issue the pass established, which every refusal has.
413 #[must_use]
414 pub fn first_issue(&self) -> &PlanIssue {
415 self.body.items().first()
416 }
417
418 /// Every issue this refusal carries, in the order the pass established them; structurally at least one.
419 #[must_use]
420 pub fn issues(&self) -> &NonEmpty<PlanIssue, PLAN_ISSUE_LIMIT> {
421 self.body.items()
422 }
423
424 /// Whether this refusal carries every issue its pass established.
425 #[must_use]
426 pub const fn capping(&self) -> Capping {
427 self.body.capping()
428 }
429}
430
431impl<K: Kind> Plan<K> {
432 /// Plans one projection over the account the content walked in with.
433 ///
434 /// The account arrives first because it is what was MEANT; the context is what that intent was decided under, and the decisions are the record of the decision.
435 /// Total: everything that could refuse refused where it was declared, so a plan is assembled out of values that already hold.
436 pub fn planned(
437 account: Account<K>,
438 decided_under: Context,
439 decisions: PlanDecisions<K::Role>,
440 ) -> Self {
441 // Destructured at the door: every seat the bundle carries is moved into
442 // the plan below, so a seat added to the bundle and forgotten here fails
443 // to compile at this pattern instead of arriving unwritten.
444 let PlanDecisions {
445 membership,
446 invalidation,
447 trace,
448 origin,
449 nonclaims,
450 } = decisions;
451 let mut claim = Vec::new();
452 account.encode_into(&mut claim);
453 decided_under.encode_into(&mut claim);
454 membership.encode_into(&mut claim);
455 encode_set(
456 invalidation.iter(),
457 InvalidationTrigger::encode_into,
458 &mut claim,
459 );
460 trace.encode_into(&mut claim);
461 origin.encode_into(&mut claim);
462 encode_set(nonclaims.iter(), Nonclaim::encode_into, &mut claim);
463 let (derived, provenance) = PlanId::derived_with_provenance(Transcript::under(
464 identity::Role::Plan,
465 account.anchoring(),
466 &claim,
467 0,
468 ));
469 Self {
470 identity: derived,
471 provenance,
472 account,
473 context: decided_under,
474 membership,
475 invalidation,
476 trace,
477 origin,
478 nonclaims,
479 }
480 }
481
482 /// This plan's own identity.
483 #[must_use]
484 pub const fn identity(&self) -> PlanId {
485 self.identity
486 }
487
488 /// The record of how that identity was derived.
489 #[must_use]
490 pub const fn provenance(&self) -> &Provenance {
491 &self.provenance
492 }
493
494 /// The account this plan was planned over, whole.
495 ///
496 /// A reader asking what invalidates it, what caused it, or what it stands on reads the seats of this value rather than a summary of them.
497 pub const fn account(&self) -> &Account<K> {
498 &self.account
499 }
500
501 /// What this plan MEANT.
502 ///
503 /// The comparison equivalence is stated over — never the plan identity, which carries origin and is required to differ between distinct requests.
504 /// Read off the plan's own account, so the intent a plan reports and the intent its transcript opened with are one derivation rather than two.
505 #[must_use]
506 pub fn intent(&self) -> Intent {
507 self.account.intent()
508 }
509
510 /// The exact facts this plan was decided under.
511 #[must_use]
512 pub const fn context(&self) -> &Context {
513 &self.context
514 }
515
516 /// The kind-specific facts.
517 #[must_use]
518 pub const fn content(&self) -> &K::Content {
519 self.account.content()
520 }
521
522 /// The complete declared output set.
523 #[must_use]
524 pub const fn membership(&self) -> &Membership<K::Role> {
525 &self.membership
526 }
527
528 /// The triggers whose change invalidates this plan; structurally at least one.
529 #[must_use]
530 pub fn invalidation(&self) -> &InvalidationSet {
531 &self.invalidation
532 }
533
534 /// The decisions that produced this plan, in selection order.
535 #[must_use]
536 pub const fn trace(&self) -> &DecisionTrace {
537 &self.trace
538 }
539
540 /// Where this plan itself came from.
541 #[must_use]
542 pub const fn origin(&self) -> &OriginTrail {
543 &self.origin
544 }
545
546 /// What this plan explicitly does not claim.
547 #[must_use]
548 pub fn nonclaims(&self) -> &[Nonclaim] {
549 self.nonclaims.as_slice()
550 }
551}