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, JoinOrder, Kind, Role, rows_to, rows_under};
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
244/// The role one planned member stands under.
245const fn planned_role<R: Role>(member: &PlannedMember<R>) -> R {
246    member.role
247}
248
249impl<R: Role> Membership<R> {
250    /// The one-member output set.
251    ///
252    /// # Errors
253    ///
254    /// Returns one [`PlanIssue::MembershipForeign`] where the member's seat is absent from the kind's declared roster.
255    /// 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.
256    pub fn from_member(member: PlannedMember<R>) -> Result<Self, PlanError> {
257        match foreign(&member) {
258            Some(issue) => Err(PlanError::over(issue, Vec::new())),
259            None => Ok(Self {
260                members: NonEmpty::one(member),
261            }),
262        }
263    }
264
265    /// Declares the complete output set, the first member and the rest.
266    ///
267    /// # Errors
268    ///
269    /// 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.
270    /// 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.
271    pub fn declared(
272        first: PlannedMember<R>,
273        rest: Vec<PlannedMember<R>>,
274    ) -> Result<Self, PlanError> {
275        let offered = rest.len().saturating_add(1);
276        let mut offering = vec![first];
277        offering.extend(rest);
278        let declared = NonEmpty::new(offering)
279            .map(|admitted| Self { members: admitted })
280            .map_err(|_| {
281                PlanError::bounded(
282                    BoundAxis::Outputs,
283                    Overflow {
284                        capacity: MEMBERSHIP_LIMIT,
285                        offered,
286                    },
287                )
288            })?;
289        let mut established: Vec<PlanIssue> = declared
290            .members
291            .iter()
292            .filter_map(|member| foreign(member))
293            .collect();
294        established.extend(R::ALL.iter().filter_map(|role| declared.doubling(*role)));
295        let mut walked = established.into_iter();
296        match walked.next() {
297            Some(issue) => Err(PlanError::over(issue, walked.collect())),
298            None => Ok(declared),
299        }
300    }
301
302    /// The issue one seat raises where two members stand under it.
303    fn doubling(&self, role: R) -> Option<PlanIssue> {
304        let observed = self.count_under(role);
305        (observed > 1).then(|| PlanIssue::MembershipDoubled {
306            role_slot: role.slot(),
307            observed: u32::try_from(observed).unwrap_or(u32::MAX),
308        })
309    }
310
311    /// The guaranteed first member.
312    #[must_use]
313    pub fn first(&self) -> &PlannedMember<R> {
314        self.members.first()
315    }
316
317    /// The declared members, the guaranteed first one ahead of the rest.
318    ///
319    /// # Ordering
320    ///
321    /// 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.
322    #[must_use]
323    pub fn members(&self) -> &NonEmpty<PlannedMember<R>, MEMBERSHIP_LIMIT> {
324        &self.members
325    }
326
327    /// The member planned under one seat, where one is.
328    #[must_use]
329    pub fn under(&self, role: R) -> Option<&PlannedMember<R>> {
330        self.members_under(role).next()
331    }
332
333    /// Every member planned under one seat, in declaration order.
334    ///
335    /// 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.
336    pub fn members_under(&self, role: R) -> impl Iterator<Item = &PlannedMember<R>> {
337        rows_under(self.members(), role, planned_role::<R>)
338    }
339
340    /// How many members are planned under one seat.
341    #[must_use]
342    pub fn count_under(&self, role: R) -> usize {
343        self.members_under(role).count()
344    }
345
346    /// Every member this plan declared into one delivery, in declaration order.
347    ///
348    /// 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.
349    pub fn members_to(&self, destination: Destination) -> impl Iterator<Item = &PlannedMember<R>> {
350        rows_to(
351            self.members(),
352            destination,
353            JoinOrder::Offering,
354            planned_role::<R>,
355        )
356    }
357
358    /// How many members this plan declared into one delivery.
359    ///
360    /// 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.
361    #[must_use]
362    pub fn count_to(&self, destination: Destination) -> usize {
363        self.members_to(destination).count()
364    }
365
366    /// Whether two memberships name the same members under one seat, as sets.
367    #[must_use]
368    pub fn agrees_under(&self, other: &Self, role: R) -> bool {
369        let mine: Vec<&PlannedMember<R>> = self.members_under(role).collect();
370        let theirs: Vec<&PlannedMember<R>> = other.members_under(role).collect();
371        mine == theirs
372    }
373
374    /// The number of members declared; structurally at least one.
375    #[must_use]
376    pub fn count(&self) -> usize {
377        self.members.count()
378    }
379}
380
381impl PlanError {
382    /// The refusal one established issue makes.
383    pub fn of(issue: PlanIssue) -> Self {
384        Self {
385            body: Capped::all(NonEmpty::one(issue)),
386        }
387    }
388
389    /// The refusal a pass whose checks co-establish makes.
390    ///
391    /// 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.
392    pub fn over(first: PlanIssue, rest: Vec<PlanIssue>) -> Self {
393        Self {
394            body: Capped::first_n(first, rest.into_iter()),
395        }
396    }
397
398    /// The refusal a magnitude makes: the axis that was overrun, and the two counts the overflow already carries.
399    pub fn bounded(axis: BoundAxis, overflow: Overflow) -> Self {
400        Self::of(PlanIssue::BoundExceeded {
401            axis,
402            bound: u64::try_from(overflow.capacity).unwrap_or(u64::MAX),
403            observed: u64::try_from(overflow.offered).unwrap_or(u64::MAX),
404        })
405    }
406
407    /// The refusal a trail that could not be drawn makes, over the unit it was to be drawn for.
408    ///
409    /// 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.
410    pub fn over_trail(node: Identity<identity::GeneratedUnit>, refusal: TrailError) -> Self {
411        match refusal {
412            TrailError::Discontinuous { at } => Self::of(PlanIssue::TrailDiscontinuous { at }),
413            TrailError::Empty(_) => Self::of(PlanIssue::OrphanGeneratedNode { node }),
414            TrailError::Overflow(overflow) => Self::bounded(BoundAxis::OriginEdges, overflow),
415        }
416    }
417
418    /// The first issue the pass established, which every refusal has.
419    #[must_use]
420    pub fn first_issue(&self) -> &PlanIssue {
421        self.body.items().first()
422    }
423
424    /// Every issue this refusal carries, in the order the pass established them; structurally at least one.
425    #[must_use]
426    pub fn issues(&self) -> &NonEmpty<PlanIssue, PLAN_ISSUE_LIMIT> {
427        self.body.items()
428    }
429
430    /// Whether this refusal carries every issue its pass established.
431    #[must_use]
432    pub const fn capping(&self) -> Capping {
433        self.body.capping()
434    }
435}
436
437impl<K: Kind> Plan<K> {
438    /// Plans one projection over the account the content walked in with.
439    ///
440    /// 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.
441    /// Total: everything that could refuse refused where it was declared, so a plan is assembled out of values that already hold.
442    pub fn planned(
443        account: Account<K>,
444        decided_under: Context,
445        decisions: PlanDecisions<K::Role>,
446    ) -> Self {
447        // Destructured at the door: every seat the bundle carries is moved into
448        // the plan below, so a seat added to the bundle and forgotten here fails
449        // to compile at this pattern instead of arriving unwritten.
450        let PlanDecisions {
451            membership,
452            invalidation,
453            trace,
454            origin,
455            nonclaims,
456        } = decisions;
457        let mut claim = Vec::new();
458        account.encode_into(&mut claim);
459        decided_under.encode_into(&mut claim);
460        membership.encode_into(&mut claim);
461        encode_set(
462            invalidation.iter(),
463            InvalidationTrigger::encode_into,
464            &mut claim,
465        );
466        trace.encode_into(&mut claim);
467        origin.encode_into(&mut claim);
468        encode_set(nonclaims.iter(), Nonclaim::encode_into, &mut claim);
469        let (derived, provenance) = PlanId::derived_with_provenance(Transcript::under(
470            identity::Role::Plan,
471            account.anchoring(),
472            &claim,
473            0,
474        ));
475        Self {
476            identity: derived,
477            provenance,
478            account,
479            context: decided_under,
480            membership,
481            invalidation,
482            trace,
483            origin,
484            nonclaims,
485        }
486    }
487
488    /// This plan's own identity.
489    #[must_use]
490    pub const fn identity(&self) -> PlanId {
491        self.identity
492    }
493
494    /// The record of how that identity was derived.
495    #[must_use]
496    pub const fn provenance(&self) -> &Provenance {
497        &self.provenance
498    }
499
500    /// The account this plan was planned over, whole.
501    ///
502    /// 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.
503    pub const fn account(&self) -> &Account<K> {
504        &self.account
505    }
506
507    /// What this plan MEANT.
508    ///
509    /// The comparison equivalence is stated over — never the plan identity, which carries origin and is required to differ between distinct requests.
510    /// 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.
511    #[must_use]
512    pub fn intent(&self) -> Intent {
513        self.account.intent()
514    }
515
516    /// The exact facts this plan was decided under.
517    #[must_use]
518    pub const fn context(&self) -> &Context {
519        &self.context
520    }
521
522    /// The kind-specific facts.
523    #[must_use]
524    pub const fn content(&self) -> &K::Content {
525        self.account.content()
526    }
527
528    /// The complete declared output set.
529    #[must_use]
530    pub const fn membership(&self) -> &Membership<K::Role> {
531        &self.membership
532    }
533
534    /// The triggers whose change invalidates this plan; structurally at least one.
535    #[must_use]
536    pub fn invalidation(&self) -> &InvalidationSet {
537        &self.invalidation
538    }
539
540    /// The decisions that produced this plan, in selection order.
541    #[must_use]
542    pub const fn trace(&self) -> &DecisionTrace {
543        &self.trace
544    }
545
546    /// Where this plan itself came from.
547    #[must_use]
548    pub const fn origin(&self) -> &OriginTrail {
549        &self.origin
550    }
551
552    /// What this plan explicitly does not claim.
553    #[must_use]
554    pub fn nonclaims(&self) -> &[Nonclaim] {
555        self.nonclaims.as_slice()
556    }
557}