Skip to main content

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}