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}