1use crate::identity::{
2 EffectVariantId, EnumTypeId, EnumVariantId, FieldId, InputVariantId, MachineId,
3 NamedTypeBinding, NamedTypeId, PhaseId, ProtocolId, RustTypeAtom, SignalVariantId,
4 TransitionId,
5};
6use crate::seam::SeamClassification;
7use indexmap::{IndexMap, IndexSet};
8use std::fmt;
9
10const NATIVE_MOB_MACHINE_HELPERS: &[&str] = &[
11 "meerkat_machine_session_id_matches_string",
12 "meerkat_peer_endpoint_set_cardinality_matches",
13 "meerkat_peer_endpoint_set_contains_peer_id",
14 "meerkat_peer_endpoint_option_peer_id_matches",
15 "meerkat_peer_endpoint_peer_id_matches",
16 "meerkat_peer_endpoint_set_peer_ids_unique",
17 "mob_machine_identity_has_session_binding",
18 "mob_machine_remote_turn_custody_admits",
19 "mob_machine_placed_completion_obligation_well_formed",
20 "mob_machine_placed_completion_custody_admits",
21 "mob_machine_placed_kickoff_obligation_well_formed",
22 "mob_machine_finalized_placed_kickoff_recovery_well_formed",
23 "mob_machine_placed_kickoff_correlation_available",
24 "mob_machine_placed_kickoff_custody_absent_for_identity",
25 "mob_machine_placed_kickoff_custody_absent_for_host",
26 "mob_machine_remote_turn_input_id_available_to_kickoff",
27 "mob_machine_adaptive_lifecycle_drained",
28 "mob_machine_adaptive_run_custody_drained",
29 "mob_machine_external_peer_edge_has_matching_key",
30 "mob_machine_external_peer_edge_local",
31 "mob_machine_external_peer_edge_peer_id",
32 "mob_machine_external_peer_identity_absent",
33 "mob_machine_external_peer_key_matches_edge",
34 "mob_machine_external_peer_edges_by_key_without_identity",
35 "mob_machine_external_peer_edges_without_identity",
36 "mob_machine_external_peer_key_matches_local",
37 "mob_machine_session_bound_live_runtime_ids_match",
38 "mob_machine_member_peer_endpoint_peer_id",
39 "mob_machine_host_id_matches_peer_id",
40 "mob_machine_member_peer_id_available_for_identity",
41 "mob_machine_member_has_no_external_peer_edges",
42 "mob_machine_member_prior_peer_endpoints_after_migration",
43 "mob_machine_member_endpoint_migration_cleanup_peer_id",
44 "mob_machine_member_peer_overlay",
45 "mob_machine_member_peer_overlay_complete",
46 "mob_machine_member_peer_overlay_peer_ids_unique",
47 "mob_machine_member_peer_overlay_without_identity",
48 "mob_machine_member_peer_overlay_without_identity_complete",
49 "mob_machine_member_peer_overlay_without_identity_peer_ids_unique",
50 "mob_machine_wiring_edge_a",
51 "mob_machine_wiring_edge_b",
52 "mob_machine_wiring_edge_contains_identity",
53 "mob_machine_wiring_contains_pair",
54 "mob_machine_wiring_edge_matches_members",
55 "mob_machine_wiring_edges_without_identity",
56 "mob_machine_run_step_status_after_set",
57 "mob_machine_run_step_bool_after_set",
58 "mob_machine_run_step_condition_result_after_set",
59 "mob_machine_run_step_u64_after_set",
60 "mob_machine_run_step_u64_after_increment",
61 "mob_machine_run_retry_count_after_increment",
62 "mob_machine_frame_node_bool_after_set",
63 "mob_machine_frame_node_status_after_admit",
64 "mob_machine_frame_ready_queue_after_admit",
65 "mob_machine_frame_node_status_after_terminal",
66 "mob_machine_frame_ready_queue_after_terminal",
67 "mob_machine_node_terminal",
68 "mob_machine_step_status_from_frame_node_status",
69 "mob_coordination_work_intent_unexpired",
70 "mob_coordination_resource_claim_unexpired",
71 "mob_coordination_resource_claim_active_at",
72 "mob_coordination_resource_claim_inactive_at",
73 "mob_machine_next_respawn_generation",
75 "mob_machine_u64_is_exact_successor",
76 "mob_machine_optional_u64_is_exact_successor",
77 "mob_machine_members_to_spawn",
78 "mob_machine_members_to_retire",
79 "mob_machine_placed_cleanup_absent_for_identity",
80 "mob_machine_placed_cleanup_obligation",
81 "mob_machine_placed_carrier_binding_active",
82 "mob_machine_placed_carrier_binding_confirmed_revoked",
83 "mob_machine_host_binding_generation_tombstone",
84 "mob_host_binding_authority_member_rows_without_mob",
90 "mob_host_binding_authority_turn_rows_without_mob",
91 "mob_host_binding_authority_turn_rows_after_release",
92 "mob_host_binding_authority_turn_rows_for_materialization",
93 "mob_host_binding_authority_turn_key_is_current",
94 "mob_host_binding_authority_turn_occupancy",
95];
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct MachineSchema {
99 pub machine: MachineId,
100 pub version: u32,
101 pub rust: RustBinding,
102 pub state: StateSchema,
103 pub inputs: EnumSchema,
104 pub surface_only_inputs: Vec<InputVariantId>,
105 pub runtime_internal_inputs: Vec<InputVariantId>,
108 pub tlc_representative_inputs: Vec<InputVariantId>,
119 pub signals: EnumSchema,
120 pub effects: EnumSchema,
121 pub helpers: Vec<HelperSchema>,
122 pub derived: Vec<HelperSchema>,
123 pub command_plans: Vec<CommandPlanSchema>,
126 pub invariants: Vec<InvariantSchema>,
127 pub transitions: Vec<TransitionSchema>,
128 pub effect_dispositions: Vec<EffectDispositionRule>,
129 pub named_types: Vec<NamedTypeBinding>,
134 pub ci_step_limit: Option<u32>,
139 pub deep_domain_overrides: std::collections::BTreeMap<String, usize>,
145}
146
147impl MachineSchema {
148 pub fn named_type_binding(&self, name: &NamedTypeId) -> Option<&NamedTypeBinding> {
151 self.named_types
152 .iter()
153 .find(|binding| binding.name == *name)
154 }
155}
156
157impl MachineSchema {
158 pub fn validate(&self) -> Result<(), MachineSchemaError> {
159 let phase_names = self.state.phase.variants_by_name()?;
160 let input_variants = self.inputs.variants_by_name()?;
161 let surface_only_inputs = unique_names(
162 self.surface_only_inputs.iter().map(AsRef::as_ref),
163 "surface-only input",
164 )?;
165 let _runtime_internal_inputs = unique_names(
166 self.runtime_internal_inputs.iter().map(AsRef::as_ref),
167 "runtime-internal input",
168 )?;
169 let tlc_representative_inputs = unique_names(
170 self.tlc_representative_inputs.iter().map(AsRef::as_ref),
171 "TLC representative input",
172 )?;
173 let signal_variants = self.signals.variants_by_name()?;
174 let effect_variants = self.effects.variants_by_name()?;
175 let field_names = self.state.fields_by_name()?;
176 let helper_names = unique_names(
177 self.helpers
178 .iter()
179 .map(|helper| helper.name.as_str())
180 .chain(self.derived.iter().map(|helper| helper.name.as_str())),
181 "helper/derived",
182 )?;
183 let _command_plan_names = unique_names(
184 self.command_plans.iter().map(|plan| plan.name.as_str()),
185 "command plan",
186 )?;
187
188 if !phase_names.contains(self.state.init.phase.as_str()) {
189 return Err(MachineSchemaError::UnknownPhase {
190 phase: self.state.init.phase.as_str().to_owned(),
191 });
192 }
193
194 for terminal in &self.state.terminal_phases {
195 if !phase_names.contains(terminal.as_str()) {
196 return Err(MachineSchemaError::UnknownPhase {
197 phase: terminal.as_str().to_owned(),
198 });
199 }
200 }
201
202 for initializer in &self.state.init.fields {
203 if !field_names.contains(initializer.field.as_str()) {
204 return Err(MachineSchemaError::UnknownField {
205 field: initializer.field.as_str().to_owned(),
206 });
207 }
208 }
209
210 let initialized_fields: IndexSet<&str> = self
225 .state
226 .init
227 .fields
228 .iter()
229 .map(|initializer| initializer.field.as_str())
230 .collect();
231 for field in &self.state.fields {
232 if !initialized_fields.contains(field.name.as_str()) {
233 return Err(MachineSchemaError::MissingInitializer {
234 field: field.name.as_str().to_owned(),
235 });
236 }
237 }
238
239 for surface_only_input in &self.surface_only_inputs {
240 if !input_variants
241 .iter()
242 .any(|variant| *variant == surface_only_input.as_str())
243 {
244 return Err(MachineSchemaError::UnknownSurfaceOnlyInputVariant {
245 variant: surface_only_input.as_str().to_owned(),
246 });
247 }
248 }
249
250 for runtime_internal_input in &self.runtime_internal_inputs {
251 if !input_variants
252 .iter()
253 .any(|variant| *variant == runtime_internal_input.as_str())
254 {
255 return Err(MachineSchemaError::UnknownRuntimeInternalInputVariant {
256 variant: runtime_internal_input.as_str().to_owned(),
257 });
258 }
259 }
260
261 for representative_input in &self.tlc_representative_inputs {
262 if !input_variants
263 .iter()
264 .any(|variant| *variant == representative_input.as_str())
265 {
266 return Err(MachineSchemaError::UnknownTlcRepresentativeInputVariant {
267 variant: representative_input.as_str().to_owned(),
268 });
269 }
270 }
271
272 for plan in &self.command_plans {
273 if plan.authority_type.is_empty() {
274 return Err(MachineSchemaError::EmptyName("command-plan authority type"));
275 }
276 for input in &plan.source_inputs {
277 if !input_variants.contains(input.as_str()) {
278 return Err(MachineSchemaError::UnknownCommandPlanInput {
279 plan: plan.name.clone(),
280 input: input.as_str().to_owned(),
281 });
282 }
283 }
284 for signal in &plan.source_signals {
285 if !signal_variants.contains(signal.as_str()) {
286 return Err(MachineSchemaError::UnknownCommandPlanSignal {
287 plan: plan.name.clone(),
288 signal: signal.as_str().to_owned(),
289 });
290 }
291 }
292 for effect in &plan.effects {
293 if !effect_variants.contains(effect.as_str()) {
294 return Err(MachineSchemaError::UnknownCommandPlanEffect {
295 plan: plan.name.clone(),
296 effect: effect.as_str().to_owned(),
297 });
298 }
299 }
300 for closure in &plan.effect_closures {
301 if closure.authority_type.is_empty() {
302 return Err(MachineSchemaError::EmptyName(
303 "command-plan effect-closure authority type",
304 ));
305 }
306 if closure.closure_policy.is_empty() {
307 return Err(MachineSchemaError::EmptyName(
308 "command-plan effect-closure policy",
309 ));
310 }
311 if closure.lifecycle.is_empty() || closure.lifecycle.iter().any(String::is_empty) {
312 return Err(MachineSchemaError::EmptyName(
313 "command-plan effect-closure lifecycle state",
314 ));
315 }
316 if !effect_variants.contains(closure.effect.as_str()) {
317 return Err(MachineSchemaError::UnknownCommandPlanEffect {
318 plan: plan.name.clone(),
319 effect: closure.effect.as_str().to_owned(),
320 });
321 }
322 if !plan.effects.contains(&closure.effect) {
323 return Err(MachineSchemaError::UnknownCommandPlanClosureEffect {
324 plan: plan.name.clone(),
325 effect: closure.effect.as_str().to_owned(),
326 });
327 }
328 }
329 }
330
331 for invariant in &self.invariants {
332 if invariant.name.is_empty() {
333 return Err(MachineSchemaError::EmptyName("invariant"));
334 }
335 invariant.expr.validate(
336 &phase_names,
337 &field_names,
338 &input_variants,
339 &signal_variants,
340 &effect_variants,
341 &helper_names,
342 &IndexSet::new(),
343 )?;
344 }
345
346 let mut transition_names = IndexSet::new();
347 for transition in &self.transitions {
348 if transition.name.as_str().is_empty() {
349 return Err(MachineSchemaError::EmptyName("transition"));
350 }
351 if !transition_names.insert(transition.name.as_str()) {
352 return Err(MachineSchemaError::DuplicateName {
353 kind: "transition",
354 name: transition.name.as_str().to_owned(),
355 });
356 }
357 for from in &transition.from {
358 if !phase_names.contains(from.as_str()) {
359 return Err(MachineSchemaError::UnknownPhase {
360 phase: from.as_str().to_owned(),
361 });
362 }
363 }
364 match &transition.on {
365 TriggerMatch::Input { variant, .. }
366 if !input_variants.contains(variant.as_str()) =>
367 {
368 return Err(MachineSchemaError::UnknownInputVariant {
369 variant: variant.as_str().to_owned(),
370 });
371 }
372 TriggerMatch::Signal { variant, .. }
373 if !signal_variants.contains(variant.as_str()) =>
374 {
375 return Err(MachineSchemaError::UnknownSignalVariant {
376 variant: variant.as_str().to_owned(),
377 });
378 }
379 _ => {}
380 }
381 if let TriggerMatch::Input { variant, .. } = &transition.on
382 && surface_only_inputs.contains(variant.as_str())
383 {
384 return Err(MachineSchemaError::SurfaceOnlyInputHasTransition {
385 variant: variant.as_str().to_owned(),
386 transition: transition.name.as_str().to_owned(),
387 });
388 }
389 if let TriggerMatch::Input { variant, .. } = &transition.on
390 && tlc_representative_inputs.contains(variant.as_str())
391 && (transition.from.len() != 1
392 || transition.to != transition.from[0]
393 || !transition.guards.is_empty()
394 || !transition.updates.is_empty())
395 {
396 return Err(
397 MachineSchemaError::TlcRepresentativeInputNotStateIndependent {
398 variant: variant.as_str().to_owned(),
399 transition: transition.name.as_str().to_owned(),
400 },
401 );
402 }
403 if !phase_names.contains(transition.to.as_str()) {
404 return Err(MachineSchemaError::UnknownPhase {
405 phase: transition.to.as_str().to_owned(),
406 });
407 }
408
409 let bindings = unique_names(
410 transition.on.bindings().iter().map(AsRef::as_ref),
411 "transition binding",
412 )?;
413
414 for guard in &transition.guards {
415 guard.expr.validate(
416 &phase_names,
417 &field_names,
418 &input_variants,
419 &signal_variants,
420 &effect_variants,
421 &helper_names,
422 &bindings,
423 )?;
424 }
425 for update in &transition.updates {
426 update.validate(
427 &phase_names,
428 &field_names,
429 &input_variants,
430 &signal_variants,
431 &effect_variants,
432 &helper_names,
433 &bindings,
434 )?;
435 }
436 for effect in &transition.emit {
437 if !effect_variants.contains(effect.variant.as_str()) {
438 return Err(MachineSchemaError::UnknownEffectVariant {
439 variant: effect.variant.as_str().to_owned(),
440 });
441 }
442 for expr in effect.fields.values() {
443 expr.validate(
444 &phase_names,
445 &field_names,
446 &input_variants,
447 &signal_variants,
448 &effect_variants,
449 &helper_names,
450 &bindings,
451 )?;
452 }
453 }
454 }
455
456 for representative_input in &self.tlc_representative_inputs {
457 if !self.transitions.iter().any(|transition| {
458 matches!(
459 &transition.on,
460 TriggerMatch::Input { variant, .. }
461 if variant == representative_input
462 )
463 }) {
464 return Err(
465 MachineSchemaError::TlcRepresentativeInputWithoutTransition {
466 variant: representative_input.as_str().to_owned(),
467 },
468 );
469 }
470 }
471
472 let transition_names: IndexSet<&str> = self
473 .transitions
474 .iter()
475 .map(|transition| transition.name.as_str())
476 .collect();
477 for plan in &self.command_plans {
478 for transition in &plan.transitions {
479 if !transition_names.contains(transition.as_str()) {
480 return Err(MachineSchemaError::UnknownCommandPlanTransition {
481 plan: plan.name.clone(),
482 transition: transition.as_str().to_owned(),
483 });
484 }
485 }
486 }
487
488 let mut seen_bindings: IndexSet<&str> = IndexSet::new();
494 for binding in &self.named_types {
495 if !seen_bindings.insert(binding.name.as_str()) {
496 return Err(MachineSchemaError::DuplicateNamedTypeBinding {
497 name: binding.name.as_str().to_owned(),
498 });
499 }
500 validate_named_type_binding_payload(binding)?;
501 }
502 {
503 let mut referenced: IndexSet<String> = IndexSet::new();
504 collect_named_type_references_machine(self, &mut referenced);
505 for binding in &self.named_types {
506 collect_named_type_references_binding(binding, &mut referenced);
507 }
508 for name in &referenced {
509 if !seen_bindings.contains(name.as_str()) {
510 return Err(MachineSchemaError::MissingNamedTypeBinding { name: name.clone() });
511 }
512 }
513 }
514 {
515 let mut referenced: IndexSet<String> = IndexSet::new();
516 collect_enum_type_references_machine(self, &mut referenced);
517 for name in &referenced {
518 let Some(binding) = self
519 .named_types
520 .iter()
521 .find(|binding| binding.name.as_str() == name.as_str())
522 else {
523 return Err(MachineSchemaError::MissingStringEnumBinding {
524 name: name.clone(),
525 });
526 };
527 if !matches!(binding.rust, RustTypeAtom::StringEnum { .. }) {
528 return Err(MachineSchemaError::InvalidStringEnumBinding {
529 name: name.clone(),
530 reason: "TypeRef::Enum domains must use RustTypeAtom::StringEnum"
531 .to_owned(),
532 });
533 }
534 }
535 }
536 validate_string_enum_named_variants_machine(self)?;
537
538 {
545 let mut disposed_variants: IndexSet<&str> = IndexSet::new();
546 for rule in &self.effect_dispositions {
547 if !effect_variants.contains(rule.effect_variant.as_str()) {
548 return Err(MachineSchemaError::UnknownEffectDispositionVariant {
549 variant: rule.effect_variant.as_str().to_owned(),
550 });
551 }
552 if !disposed_variants.insert(rule.effect_variant.as_str()) {
553 return Err(MachineSchemaError::DuplicateEffectDisposition {
554 variant: rule.effect_variant.as_str().to_owned(),
555 });
556 }
557 if rule.handoff_protocol.is_some()
558 && matches!(rule.disposition, EffectDisposition::Routed { .. })
559 {
560 return Err(MachineSchemaError::HandoffProtocolOnRoutedEffect {
561 variant: rule.effect_variant.as_str().to_owned(),
562 });
563 }
564 }
565 for variant in &effect_variants {
566 if !disposed_variants.contains(*variant) {
567 return Err(MachineSchemaError::MissingEffectDisposition {
568 variant: (*variant).to_owned(),
569 });
570 }
571 }
572 }
573
574 Ok(())
575 }
576}
577
578#[derive(Debug, Clone, PartialEq, Eq)]
579pub enum EffectDisposition {
580 Local,
582 External,
584 Routed { consumer_machines: Vec<MachineId> },
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct EffectDispositionRule {
590 pub effect_variant: EffectVariantId,
591 pub disposition: EffectDisposition,
592 pub handoff_protocol: Option<ProtocolId>,
597 pub seam_classification: SeamClassification,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct RustBinding {
605 pub crate_name: String,
606 pub module: String,
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
610pub struct StateSchema {
611 pub phase: EnumSchema,
612 pub fields: Vec<FieldSchema>,
613 pub init: InitSchema,
614 pub terminal_phases: Vec<PhaseId>,
615}
616
617impl StateSchema {
618 fn fields_by_name(&self) -> Result<IndexSet<&str>, MachineSchemaError> {
619 unique_names(
620 self.fields.iter().map(|field| field.name.as_str()),
621 "state field",
622 )
623 }
624}
625
626#[derive(Debug, Clone, PartialEq, Eq)]
627pub struct InitSchema {
628 pub phase: PhaseId,
629 pub fields: Vec<FieldInit>,
630}
631
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct FieldInit {
634 pub field: FieldId,
635 pub expr: Expr,
636}
637
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct EnumSchema {
640 pub name: String,
641 pub variants: Vec<VariantSchema>,
642}
643
644impl EnumSchema {
645 pub(crate) fn variants_by_name(&self) -> Result<IndexSet<&str>, MachineSchemaError> {
646 let mut names: IndexSet<&str> = IndexSet::new();
647 for variant in &self.variants {
648 if variant.name.as_str().is_empty() {
649 return Err(MachineSchemaError::EmptyName("variant"));
650 }
651 if !names.insert(variant.name.as_str()) {
652 return Err(MachineSchemaError::DuplicateName {
653 kind: "variant",
654 name: variant.name.as_str().to_owned(),
655 });
656 }
657 unique_names(
658 variant.fields.iter().map(|field| field.name.as_str()),
659 "variant field",
660 )?;
661 }
662 Ok(names)
663 }
664
665 pub fn variant_named(
666 &self,
667 name: impl AsRef<str>,
668 ) -> Result<&VariantSchema, MachineSchemaError> {
669 let name = name.as_ref();
670 self.variants
671 .iter()
672 .find(|variant| variant.name.as_str() == name)
673 .ok_or_else(|| MachineSchemaError::UnknownVariant {
674 variant: name.to_owned(),
675 })
676 }
677}
678
679#[derive(Debug, Clone, PartialEq, Eq)]
680pub struct VariantSchema {
681 pub name: EnumVariantId,
682 pub fields: Vec<FieldSchema>,
683}
684
685impl VariantSchema {
686 pub fn field_named(&self, name: impl AsRef<str>) -> Result<&FieldSchema, MachineSchemaError> {
687 let name = name.as_ref();
688 self.fields
689 .iter()
690 .find(|field| field.name.as_str() == name)
691 .ok_or_else(|| MachineSchemaError::UnknownVariantField {
692 variant: self.name.as_str().to_owned(),
693 field: name.to_owned(),
694 })
695 }
696}
697
698#[derive(Debug, Clone, PartialEq, Eq)]
699pub struct FieldSchema {
700 pub name: FieldId,
701 pub ty: TypeRef,
702}
703
704#[derive(Debug, Clone, PartialEq, Eq)]
705pub enum TypeRef {
706 Bool,
707 U32,
708 U64,
709 String,
710 Named(NamedTypeId),
711 Enum(EnumTypeId),
712 Option(Box<TypeRef>),
713 Set(Box<TypeRef>),
714 Seq(Box<TypeRef>),
715 Map(Box<TypeRef>, Box<TypeRef>),
716}
717
718pub type FieldType = TypeRef;
719
720#[derive(Debug, Clone, PartialEq, Eq)]
721pub struct HelperSchema {
722 pub name: String,
723 pub params: Vec<FieldSchema>,
724 pub returns: TypeRef,
725 pub body: Expr,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct InvariantSchema {
730 pub name: String,
731 pub expr: Expr,
732}
733
734#[derive(Debug, Clone, PartialEq, Eq)]
735pub struct CommandPlanSchema {
736 pub name: String,
737 pub authority_type: String,
738 pub source_inputs: Vec<InputVariantId>,
739 pub source_signals: Vec<SignalVariantId>,
740 pub transitions: Vec<TransitionId>,
741 pub effects: Vec<EffectVariantId>,
742 pub effect_closures: Vec<EffectClosureSchema>,
743}
744
745#[derive(Debug, Clone, PartialEq, Eq)]
746pub struct EffectClosureSchema {
747 pub effect: EffectVariantId,
748 pub authority_type: String,
749 pub closure_policy: String,
750 pub lifecycle: Vec<String>,
751}
752
753#[derive(Debug, Clone, PartialEq, Eq)]
754pub struct TransitionSchema {
755 pub name: TransitionId,
756 pub from: Vec<PhaseId>,
757 pub on: TriggerMatch,
758 pub guards: Vec<Guard>,
759 pub updates: Vec<Update>,
760 pub to: PhaseId,
761 pub emit: Vec<EffectEmit>,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765pub enum TriggerKind {
766 Input,
767 Signal,
768}
769
770#[derive(Debug, Clone, PartialEq, Eq)]
780pub enum TriggerMatch {
781 Input {
782 variant: InputVariantId,
783 bindings: Vec<FieldId>,
784 },
785 Signal {
786 variant: SignalVariantId,
787 bindings: Vec<FieldId>,
788 },
789}
790
791impl TriggerMatch {
792 pub fn kind(&self) -> TriggerKind {
795 match self {
796 Self::Input { .. } => TriggerKind::Input,
797 Self::Signal { .. } => TriggerKind::Signal,
798 }
799 }
800
801 pub fn variant_str(&self) -> &str {
804 match self {
805 Self::Input { variant, .. } => variant.as_str(),
806 Self::Signal { variant, .. } => variant.as_str(),
807 }
808 }
809
810 pub fn bindings(&self) -> &[FieldId] {
812 match self {
813 Self::Input { bindings, .. } | Self::Signal { bindings, .. } => bindings,
814 }
815 }
816}
817
818pub type InputMatch = TriggerMatch;
819
820#[derive(Debug, Clone, PartialEq, Eq)]
821pub struct Guard {
822 pub name: String,
823 pub expr: Expr,
824}
825
826#[derive(Debug, Clone, PartialEq, Eq)]
827pub struct EffectEmit {
828 pub variant: EffectVariantId,
829 pub fields: IndexMap<FieldId, Expr>,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq)]
833pub enum Update {
834 Assign {
835 field: FieldId,
836 expr: Expr,
837 },
838 Increment {
839 field: FieldId,
840 amount: u64,
841 },
842 Decrement {
843 field: FieldId,
844 amount: u64,
845 },
846 MapInsert {
847 field: FieldId,
848 key: Expr,
849 value: Expr,
850 },
851 MapIncrement {
852 field: FieldId,
853 key: Expr,
854 amount: u64,
855 },
856 MapDecrement {
857 field: FieldId,
858 key: Expr,
859 amount: u64,
860 },
861 MapRemove {
862 field: FieldId,
863 key: Expr,
864 },
865 SetInsert {
866 field: FieldId,
867 value: Expr,
868 },
869 SetRemove {
870 field: FieldId,
871 value: Expr,
872 },
873 SeqAppend {
874 field: FieldId,
875 value: Expr,
876 },
877 SeqPrepend {
878 field: FieldId,
879 values: Expr,
880 },
881 SeqPopFront {
882 field: FieldId,
883 },
884 SeqRemoveValue {
885 field: FieldId,
886 value: Expr,
887 },
888 SeqRemoveAll {
889 field: FieldId,
890 values: Expr,
891 },
892 Conditional {
893 condition: Expr,
894 then_updates: Vec<Update>,
895 else_updates: Vec<Update>,
896 },
897 ForEach {
898 binding: String,
899 over: Expr,
900 updates: Vec<Update>,
901 },
902}
903
904impl Update {
905 #[allow(clippy::too_many_arguments)]
906 fn validate(
907 &self,
908 phase_names: &IndexSet<&str>,
909 field_names: &IndexSet<&str>,
910 input_variants: &IndexSet<&str>,
911 signal_variants: &IndexSet<&str>,
912 effect_variants: &IndexSet<&str>,
913 helper_names: &IndexSet<&str>,
914 bindings: &IndexSet<&str>,
915 ) -> Result<(), MachineSchemaError> {
916 match self {
917 Self::Assign { field, .. }
918 | Self::Increment { field, .. }
919 | Self::Decrement { field, .. }
920 | Self::SeqPopFront { field } => {
921 if !field_names.contains(field.as_str()) {
922 return Err(MachineSchemaError::UnknownField {
923 field: field.as_str().to_owned(),
924 });
925 }
926 if let Self::Assign { expr, .. } = self {
927 expr.validate(
928 phase_names,
929 field_names,
930 input_variants,
931 signal_variants,
932 effect_variants,
933 helper_names,
934 bindings,
935 )?;
936 }
937 }
938 Self::MapInsert { field, key, value } => {
939 if !field_names.contains(field.as_str()) {
940 return Err(MachineSchemaError::UnknownField {
941 field: field.as_str().to_owned(),
942 });
943 }
944 key.validate(
945 phase_names,
946 field_names,
947 input_variants,
948 signal_variants,
949 effect_variants,
950 helper_names,
951 bindings,
952 )?;
953 value.validate(
954 phase_names,
955 field_names,
956 input_variants,
957 signal_variants,
958 effect_variants,
959 helper_names,
960 bindings,
961 )?;
962 }
963 Self::MapRemove { field, key } => {
964 if !field_names.contains(field.as_str()) {
965 return Err(MachineSchemaError::UnknownField {
966 field: field.as_str().to_owned(),
967 });
968 }
969 key.validate(
970 phase_names,
971 field_names,
972 input_variants,
973 signal_variants,
974 effect_variants,
975 helper_names,
976 bindings,
977 )?;
978 }
979 Self::MapIncrement { field, key, .. } | Self::MapDecrement { field, key, .. } => {
980 if !field_names.contains(field.as_str()) {
981 return Err(MachineSchemaError::UnknownField {
982 field: field.as_str().to_owned(),
983 });
984 }
985 key.validate(
986 phase_names,
987 field_names,
988 input_variants,
989 signal_variants,
990 effect_variants,
991 helper_names,
992 bindings,
993 )?;
994 }
995 Self::SetInsert { field, value }
996 | Self::SetRemove { field, value }
997 | Self::SeqAppend { field, value }
998 | Self::SeqRemoveValue { field, value } => {
999 if !field_names.contains(field.as_str()) {
1000 return Err(MachineSchemaError::UnknownField {
1001 field: field.as_str().to_owned(),
1002 });
1003 }
1004 value.validate(
1005 phase_names,
1006 field_names,
1007 input_variants,
1008 signal_variants,
1009 effect_variants,
1010 helper_names,
1011 bindings,
1012 )?;
1013 }
1014 Self::SeqPrepend { field, values } | Self::SeqRemoveAll { field, values } => {
1015 if !field_names.contains(field.as_str()) {
1016 return Err(MachineSchemaError::UnknownField {
1017 field: field.as_str().to_owned(),
1018 });
1019 }
1020 values.validate(
1021 phase_names,
1022 field_names,
1023 input_variants,
1024 signal_variants,
1025 effect_variants,
1026 helper_names,
1027 bindings,
1028 )?;
1029 }
1030 Self::ForEach {
1031 binding,
1032 over,
1033 updates,
1034 } => {
1035 over.validate(
1036 phase_names,
1037 field_names,
1038 input_variants,
1039 signal_variants,
1040 effect_variants,
1041 helper_names,
1042 bindings,
1043 )?;
1044 let mut nested_bindings = bindings.clone();
1045 nested_bindings.insert(binding.as_str());
1046 for update in updates {
1047 update.validate(
1048 phase_names,
1049 field_names,
1050 input_variants,
1051 signal_variants,
1052 effect_variants,
1053 helper_names,
1054 &nested_bindings,
1055 )?;
1056 }
1057 }
1058 Self::Conditional {
1059 condition,
1060 then_updates,
1061 else_updates,
1062 } => {
1063 condition.validate(
1064 phase_names,
1065 field_names,
1066 input_variants,
1067 signal_variants,
1068 effect_variants,
1069 helper_names,
1070 bindings,
1071 )?;
1072 for update in then_updates {
1073 update.validate(
1074 phase_names,
1075 field_names,
1076 input_variants,
1077 signal_variants,
1078 effect_variants,
1079 helper_names,
1080 bindings,
1081 )?;
1082 }
1083 for update in else_updates {
1084 update.validate(
1085 phase_names,
1086 field_names,
1087 input_variants,
1088 signal_variants,
1089 effect_variants,
1090 helper_names,
1091 bindings,
1092 )?;
1093 }
1094 }
1095 }
1096 Ok(())
1097 }
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq)]
1101pub enum Quantifier {
1102 Any,
1103 All,
1104}
1105
1106#[derive(Debug, Clone, PartialEq, Eq)]
1107pub enum Expr {
1108 Bool(bool),
1109 U64(u64),
1110 U64Max,
1111 String(String),
1112 NamedVariant {
1113 enum_name: EnumTypeId,
1114 variant: EnumVariantId,
1115 },
1116 FieldAccess {
1117 base: Box<Expr>,
1118 field: FieldId,
1119 },
1120 EnumVariantIs {
1121 value: Box<Expr>,
1122 enum_name: EnumTypeId,
1123 variant: EnumVariantId,
1124 },
1125 EnumStringSetPayload {
1126 value: Box<Expr>,
1127 enum_name: EnumTypeId,
1128 variant: EnumVariantId,
1129 field: FieldId,
1130 },
1131 EmptySet,
1132 EmptyMap,
1133 SeqLiteral(Vec<Expr>),
1134 CurrentPhase,
1135 Phase(PhaseId),
1136 Field(FieldId),
1137 Binding(String),
1138 Variant(String),
1139 None,
1140 IfElse {
1141 condition: Box<Expr>,
1142 then_expr: Box<Expr>,
1143 else_expr: Box<Expr>,
1144 },
1145 Not(Box<Expr>),
1146 And(Vec<Expr>),
1147 Or(Vec<Expr>),
1148 Eq(Box<Expr>, Box<Expr>),
1149 Neq(Box<Expr>, Box<Expr>),
1150 Add(Box<Expr>, Box<Expr>),
1151 Sub(Box<Expr>, Box<Expr>),
1152 Gt(Box<Expr>, Box<Expr>),
1153 Gte(Box<Expr>, Box<Expr>),
1154 Lt(Box<Expr>, Box<Expr>),
1155 Lte(Box<Expr>, Box<Expr>),
1156 Contains {
1157 collection: Box<Expr>,
1158 value: Box<Expr>,
1159 },
1160 MapContainsKey {
1161 map: Box<Expr>,
1162 key: Box<Expr>,
1163 },
1164 SeqStartsWith {
1165 seq: Box<Expr>,
1166 prefix: Box<Expr>,
1167 },
1168 SeqElements(Box<Expr>),
1169 Len(Box<Expr>),
1170 Count {
1171 collection: Box<Expr>,
1172 value: Box<Expr>,
1173 },
1174 Head(Box<Expr>),
1175 MapKeys(Box<Expr>),
1176 MapGet {
1177 map: Box<Expr>,
1178 key: Box<Expr>,
1179 },
1180 Some(Box<Expr>),
1181 Call {
1182 helper: String,
1183 args: Vec<Expr>,
1184 },
1185 Quantified {
1186 quantifier: Quantifier,
1187 binding: String,
1188 over: Box<Expr>,
1189 body: Box<Expr>,
1190 },
1191}
1192
1193impl Expr {
1194 #[allow(clippy::too_many_arguments)]
1195 fn validate(
1196 &self,
1197 phase_names: &IndexSet<&str>,
1198 field_names: &IndexSet<&str>,
1199 input_variants: &IndexSet<&str>,
1200 signal_variants: &IndexSet<&str>,
1201 effect_variants: &IndexSet<&str>,
1202 helper_names: &IndexSet<&str>,
1203 bindings: &IndexSet<&str>,
1204 ) -> Result<(), MachineSchemaError> {
1205 match self {
1206 Self::Bool(_)
1207 | Self::U64(_)
1208 | Self::U64Max
1209 | Self::String(_)
1210 | Self::NamedVariant { .. }
1211 | Self::EmptySet
1212 | Self::EmptyMap
1213 | Self::None
1214 | Self::CurrentPhase => {}
1215 Self::SeqLiteral(items) => {
1216 for item in items {
1217 item.validate(
1218 phase_names,
1219 field_names,
1220 input_variants,
1221 signal_variants,
1222 effect_variants,
1223 helper_names,
1224 bindings,
1225 )?;
1226 }
1227 }
1228 Self::Phase(phase) => {
1229 if !phase_names.contains(phase.as_str()) {
1230 return Err(MachineSchemaError::UnknownPhase {
1231 phase: phase.as_str().to_owned(),
1232 });
1233 }
1234 }
1235 Self::Field(field) => {
1236 if !field_names.contains(field.as_str()) {
1237 return Err(MachineSchemaError::UnknownField {
1238 field: field.as_str().to_owned(),
1239 });
1240 }
1241 }
1242 Self::Binding(binding) => {
1243 if !bindings.contains(binding.as_str()) {
1244 return Err(MachineSchemaError::UnknownBinding {
1245 binding: binding.clone(),
1246 });
1247 }
1248 }
1249 Self::Variant(variant) => {
1250 if !input_variants.contains(variant.as_str())
1251 && !signal_variants.contains(variant.as_str())
1252 && !effect_variants.contains(variant.as_str())
1253 {
1254 return Err(MachineSchemaError::UnknownVariant {
1255 variant: variant.clone(),
1256 });
1257 }
1258 }
1259 Self::FieldAccess { base, .. }
1260 | Self::EnumVariantIs { value: base, .. }
1261 | Self::EnumStringSetPayload { value: base, .. } => {
1262 base.validate(
1263 phase_names,
1264 field_names,
1265 input_variants,
1266 signal_variants,
1267 effect_variants,
1268 helper_names,
1269 bindings,
1270 )?;
1271 }
1272 Self::IfElse {
1273 condition,
1274 then_expr,
1275 else_expr,
1276 } => {
1277 condition.validate(
1278 phase_names,
1279 field_names,
1280 input_variants,
1281 signal_variants,
1282 effect_variants,
1283 helper_names,
1284 bindings,
1285 )?;
1286 then_expr.validate(
1287 phase_names,
1288 field_names,
1289 input_variants,
1290 signal_variants,
1291 effect_variants,
1292 helper_names,
1293 bindings,
1294 )?;
1295 else_expr.validate(
1296 phase_names,
1297 field_names,
1298 input_variants,
1299 signal_variants,
1300 effect_variants,
1301 helper_names,
1302 bindings,
1303 )?;
1304 }
1305 Self::Not(inner)
1306 | Self::Len(inner)
1307 | Self::Head(inner)
1308 | Self::MapKeys(inner)
1309 | Self::Some(inner) => inner.validate(
1310 phase_names,
1311 field_names,
1312 input_variants,
1313 signal_variants,
1314 effect_variants,
1315 helper_names,
1316 bindings,
1317 )?,
1318 Self::And(items) | Self::Or(items) => {
1319 for item in items {
1320 item.validate(
1321 phase_names,
1322 field_names,
1323 input_variants,
1324 signal_variants,
1325 effect_variants,
1326 helper_names,
1327 bindings,
1328 )?;
1329 }
1330 }
1331 Self::Eq(left, right)
1332 | Self::Neq(left, right)
1333 | Self::Add(left, right)
1334 | Self::Sub(left, right)
1335 | Self::Gt(left, right)
1336 | Self::Gte(left, right)
1337 | Self::Lt(left, right)
1338 | Self::Lte(left, right) => {
1339 left.validate(
1340 phase_names,
1341 field_names,
1342 input_variants,
1343 signal_variants,
1344 effect_variants,
1345 helper_names,
1346 bindings,
1347 )?;
1348 right.validate(
1349 phase_names,
1350 field_names,
1351 input_variants,
1352 signal_variants,
1353 effect_variants,
1354 helper_names,
1355 bindings,
1356 )?;
1357 }
1358 Self::Contains { collection, value } => {
1359 collection.validate(
1360 phase_names,
1361 field_names,
1362 input_variants,
1363 signal_variants,
1364 effect_variants,
1365 helper_names,
1366 bindings,
1367 )?;
1368 value.validate(
1369 phase_names,
1370 field_names,
1371 input_variants,
1372 signal_variants,
1373 effect_variants,
1374 helper_names,
1375 bindings,
1376 )?;
1377 }
1378 Self::Count { collection, value } => {
1379 collection.validate(
1380 phase_names,
1381 field_names,
1382 input_variants,
1383 signal_variants,
1384 effect_variants,
1385 helper_names,
1386 bindings,
1387 )?;
1388 value.validate(
1389 phase_names,
1390 field_names,
1391 input_variants,
1392 signal_variants,
1393 effect_variants,
1394 helper_names,
1395 bindings,
1396 )?;
1397 }
1398 Self::MapContainsKey { map, key } => {
1399 map.validate(
1400 phase_names,
1401 field_names,
1402 input_variants,
1403 signal_variants,
1404 effect_variants,
1405 helper_names,
1406 bindings,
1407 )?;
1408 key.validate(
1409 phase_names,
1410 field_names,
1411 input_variants,
1412 signal_variants,
1413 effect_variants,
1414 helper_names,
1415 bindings,
1416 )?;
1417 }
1418 Self::SeqStartsWith { seq, prefix } => {
1419 seq.validate(
1420 phase_names,
1421 field_names,
1422 input_variants,
1423 signal_variants,
1424 effect_variants,
1425 helper_names,
1426 bindings,
1427 )?;
1428 prefix.validate(
1429 phase_names,
1430 field_names,
1431 input_variants,
1432 signal_variants,
1433 effect_variants,
1434 helper_names,
1435 bindings,
1436 )?;
1437 }
1438 Self::SeqElements(inner) => {
1439 inner.validate(
1440 phase_names,
1441 field_names,
1442 input_variants,
1443 signal_variants,
1444 effect_variants,
1445 helper_names,
1446 bindings,
1447 )?;
1448 }
1449 Self::MapGet { map, key } => {
1450 map.validate(
1451 phase_names,
1452 field_names,
1453 input_variants,
1454 signal_variants,
1455 effect_variants,
1456 helper_names,
1457 bindings,
1458 )?;
1459 key.validate(
1460 phase_names,
1461 field_names,
1462 input_variants,
1463 signal_variants,
1464 effect_variants,
1465 helper_names,
1466 bindings,
1467 )?;
1468 }
1469 Self::Call { helper, args } => {
1470 if !helper_names.contains(helper.as_str())
1471 && !NATIVE_MOB_MACHINE_HELPERS.contains(&helper.as_str())
1472 {
1473 return Err(MachineSchemaError::UnknownHelper {
1474 helper: helper.clone(),
1475 });
1476 }
1477 for arg in args {
1478 arg.validate(
1479 phase_names,
1480 field_names,
1481 input_variants,
1482 signal_variants,
1483 effect_variants,
1484 helper_names,
1485 bindings,
1486 )?;
1487 }
1488 }
1489 Self::Quantified {
1490 binding,
1491 over,
1492 body,
1493 ..
1494 } => {
1495 over.validate(
1496 phase_names,
1497 field_names,
1498 input_variants,
1499 signal_variants,
1500 effect_variants,
1501 helper_names,
1502 bindings,
1503 )?;
1504 let mut nested_bindings = bindings.clone();
1505 nested_bindings.insert(binding.as_str());
1506 body.validate(
1507 phase_names,
1508 field_names,
1509 input_variants,
1510 signal_variants,
1511 effect_variants,
1512 helper_names,
1513 &nested_bindings,
1514 )?;
1515 }
1516 }
1517 Ok(())
1518 }
1519}
1520
1521fn validate_named_type_binding_payload(
1522 binding: &NamedTypeBinding,
1523) -> Result<(), MachineSchemaError> {
1524 match &binding.rust {
1525 RustTypeAtom::StringEnum { variants } => {
1526 validate_named_variant_domain(binding.name.as_str(), variants)
1527 }
1528 RustTypeAtom::TypePathEnum {
1529 unit_variants,
1530 structural_variants,
1531 ..
1532 } => {
1533 let variants = unit_variants
1534 .iter()
1535 .chain(structural_variants.iter().map(|variant| &variant.variant))
1536 .cloned()
1537 .collect::<Vec<_>>();
1538 validate_named_variant_domain(binding.name.as_str(), &variants)?;
1539 validate_type_path_enum_structural_variants(binding.name.as_str(), structural_variants)
1540 }
1541 RustTypeAtom::TypePathFieldPresenceSet { fields, .. } => {
1542 validate_type_path_field_presence_set(binding.name.as_str(), fields)
1543 }
1544 RustTypeAtom::TypePathStruct { fields, .. } => {
1545 validate_type_path_struct(binding.name.as_str(), fields)
1546 }
1547 _ => Ok(()),
1548 }
1549}
1550
1551fn validate_type_path_field_presence_set(
1552 name: &str,
1553 fields: &[FieldId],
1554) -> Result<(), MachineSchemaError> {
1555 if fields.is_empty() {
1556 return Err(MachineSchemaError::InvalidStringEnumBinding {
1557 name: name.to_owned(),
1558 reason: "field-presence named-type binding must define at least one field".to_owned(),
1559 });
1560 }
1561 let mut seen: IndexSet<&str> = IndexSet::new();
1562 for field in fields {
1563 if !seen.insert(field.as_str()) {
1564 return Err(MachineSchemaError::InvalidStringEnumBinding {
1565 name: name.to_owned(),
1566 reason: format!("field-presence binding defines duplicate field `{field}`"),
1567 });
1568 }
1569 }
1570 Ok(())
1571}
1572
1573fn validate_type_path_struct(
1574 name: &str,
1575 fields: &[crate::TypePathStructField],
1576) -> Result<(), MachineSchemaError> {
1577 if fields.is_empty() {
1578 return Err(MachineSchemaError::InvalidStringEnumBinding {
1579 name: name.to_owned(),
1580 reason: "struct named-type binding must define at least one field".to_owned(),
1581 });
1582 }
1583 let mut seen: IndexSet<&str> = IndexSet::new();
1584 for field in fields {
1585 if !seen.insert(field.name.as_str()) {
1586 return Err(MachineSchemaError::InvalidStringEnumBinding {
1587 name: name.to_owned(),
1588 reason: format!("struct binding defines duplicate field `{}`", field.name),
1589 });
1590 }
1591 }
1592 Ok(())
1593}
1594
1595fn validate_type_path_enum_structural_variants(
1596 name: &str,
1597 variants: &[crate::TypePathEnumStructuralVariant],
1598) -> Result<(), MachineSchemaError> {
1599 for variant in variants {
1600 if variant.fields.is_empty() {
1601 return Err(MachineSchemaError::InvalidStringEnumBinding {
1602 name: name.to_owned(),
1603 reason: format!(
1604 "structural variant `{}` must define at least one payload field",
1605 variant.variant
1606 ),
1607 });
1608 }
1609 let mut fields: IndexSet<&str> = IndexSet::new();
1610 for field in &variant.fields {
1611 if !fields.insert(field.name.as_str()) {
1612 return Err(MachineSchemaError::InvalidStringEnumBinding {
1613 name: name.to_owned(),
1614 reason: format!(
1615 "structural variant `{}` defines duplicate payload field `{}`",
1616 variant.variant, field.name
1617 ),
1618 });
1619 }
1620 }
1621 }
1622 Ok(())
1623}
1624
1625fn validate_named_variant_domain(
1626 name: &str,
1627 variants: &[EnumVariantId],
1628) -> Result<(), MachineSchemaError> {
1629 if variants.is_empty() {
1630 return Err(MachineSchemaError::InvalidStringEnumBinding {
1631 name: name.to_owned(),
1632 reason: "must define at least one variant".to_owned(),
1633 });
1634 }
1635
1636 let mut seen_values: IndexSet<&str> = IndexSet::new();
1637 let mut seen_rust_idents: IndexMap<String, &str> = IndexMap::new();
1638 for variant in variants {
1639 let raw = variant.as_str();
1640 if !seen_values.insert(raw) {
1641 return Err(MachineSchemaError::InvalidStringEnumBinding {
1642 name: name.to_owned(),
1643 reason: format!("defines duplicate variant `{raw}`"),
1644 });
1645 }
1646
1647 let rust_identifier = string_enum_variant_rust_ident(raw);
1648 if let Some(first) = seen_rust_idents.get(&rust_identifier) {
1649 return Err(MachineSchemaError::InvalidStringEnumBinding {
1650 name: name.to_owned(),
1651 reason: format!(
1652 "variants `{first}` and `{raw}` sanitize to duplicate Rust identifier `{rust_identifier}`"
1653 ),
1654 });
1655 }
1656 seen_rust_idents.insert(rust_identifier, raw);
1657 }
1658
1659 Ok(())
1660}
1661
1662fn string_enum_variant_rust_ident(value: &str) -> String {
1663 value
1664 .chars()
1665 .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
1666 .collect()
1667}
1668
1669fn collect_named_type_references_type(ty: &TypeRef, out: &mut IndexSet<String>) {
1671 match ty {
1672 TypeRef::Bool | TypeRef::U32 | TypeRef::U64 | TypeRef::String | TypeRef::Enum(_) => {}
1673 TypeRef::Named(id) => {
1674 out.insert(id.as_str().to_owned());
1675 }
1676 TypeRef::Option(inner) | TypeRef::Set(inner) | TypeRef::Seq(inner) => {
1677 collect_named_type_references_type(inner, out);
1678 }
1679 TypeRef::Map(key, value) => {
1680 collect_named_type_references_type(key, out);
1681 collect_named_type_references_type(value, out);
1682 }
1683 }
1684}
1685
1686pub(crate) fn collect_named_type_references_machine(
1690 schema: &MachineSchema,
1691 out: &mut IndexSet<String>,
1692) {
1693 for field in &schema.state.fields {
1694 collect_named_type_references_type(&field.ty, out);
1695 }
1696 for enum_schema in [&schema.inputs, &schema.signals, &schema.effects] {
1697 for variant in &enum_schema.variants {
1698 for field in &variant.fields {
1699 collect_named_type_references_type(&field.ty, out);
1700 }
1701 }
1702 }
1703 for helper in schema.helpers.iter().chain(schema.derived.iter()) {
1704 for param in &helper.params {
1705 collect_named_type_references_type(¶m.ty, out);
1706 }
1707 collect_named_type_references_type(&helper.returns, out);
1708 }
1709}
1710
1711fn collect_named_type_references_binding(binding: &NamedTypeBinding, out: &mut IndexSet<String>) {
1712 if let RustTypeAtom::TypePathStruct { fields, .. } = &binding.rust {
1713 for field in fields {
1714 match &field.atom {
1715 crate::TypePathStructFieldAtom::Named(name)
1716 | crate::TypePathStructFieldAtom::OptionalNamed(name) => {
1717 out.insert(name.as_str().to_owned());
1718 }
1719 crate::TypePathStructFieldAtom::String => {}
1720 }
1721 }
1722 }
1723}
1724
1725fn collect_enum_type_references_type(ty: &TypeRef, out: &mut IndexSet<String>) {
1726 match ty {
1727 TypeRef::Enum(id) => {
1728 out.insert(id.as_str().to_owned());
1729 }
1730 TypeRef::Option(inner) | TypeRef::Set(inner) | TypeRef::Seq(inner) => {
1731 collect_enum_type_references_type(inner, out);
1732 }
1733 TypeRef::Map(key, value) => {
1734 collect_enum_type_references_type(key, out);
1735 collect_enum_type_references_type(value, out);
1736 }
1737 TypeRef::Bool | TypeRef::U32 | TypeRef::U64 | TypeRef::String | TypeRef::Named(_) => {}
1738 }
1739}
1740
1741fn collect_enum_type_references_machine(schema: &MachineSchema, out: &mut IndexSet<String>) {
1742 for field in &schema.state.fields {
1743 collect_enum_type_references_type(&field.ty, out);
1744 }
1745 for enum_schema in [&schema.inputs, &schema.signals, &schema.effects] {
1746 for variant in &enum_schema.variants {
1747 for field in &variant.fields {
1748 collect_enum_type_references_type(&field.ty, out);
1749 }
1750 }
1751 }
1752 for helper in schema.helpers.iter().chain(schema.derived.iter()) {
1753 for param in &helper.params {
1754 collect_enum_type_references_type(¶m.ty, out);
1755 }
1756 collect_enum_type_references_type(&helper.returns, out);
1757 }
1758}
1759
1760fn validate_string_enum_named_variants_machine(
1761 schema: &MachineSchema,
1762) -> Result<(), MachineSchemaError> {
1763 for init in &schema.state.init.fields {
1764 validate_string_enum_named_variants_expr(schema, &init.expr)?;
1765 }
1766 for invariant in &schema.invariants {
1767 validate_string_enum_named_variants_expr(schema, &invariant.expr)?;
1768 }
1769 for helper in schema.helpers.iter().chain(schema.derived.iter()) {
1770 validate_string_enum_named_variants_expr(schema, &helper.body)?;
1771 }
1772 for transition in &schema.transitions {
1773 for guard in &transition.guards {
1774 validate_string_enum_named_variants_expr(schema, &guard.expr)?;
1775 }
1776 for update in &transition.updates {
1777 validate_string_enum_named_variants_update(schema, update)?;
1778 }
1779 for effect in &transition.emit {
1780 for expr in effect.fields.values() {
1781 validate_string_enum_named_variants_expr(schema, expr)?;
1782 }
1783 }
1784 }
1785 Ok(())
1786}
1787
1788fn validate_string_enum_named_variants_update(
1789 schema: &MachineSchema,
1790 update: &Update,
1791) -> Result<(), MachineSchemaError> {
1792 match update {
1793 Update::Assign { expr, .. } => validate_string_enum_named_variants_expr(schema, expr)?,
1794 Update::Increment { .. } | Update::Decrement { .. } | Update::SeqPopFront { .. } => {}
1795 Update::MapInsert { key, value, .. } => {
1796 validate_string_enum_named_variants_expr(schema, key)?;
1797 validate_string_enum_named_variants_expr(schema, value)?;
1798 }
1799 Update::MapIncrement { key, .. }
1800 | Update::MapDecrement { key, .. }
1801 | Update::MapRemove { key, .. } => validate_string_enum_named_variants_expr(schema, key)?,
1802 Update::SetInsert { value, .. }
1803 | Update::SetRemove { value, .. }
1804 | Update::SeqAppend { value, .. }
1805 | Update::SeqRemoveValue { value, .. } => {
1806 validate_string_enum_named_variants_expr(schema, value)?;
1807 }
1808 Update::SeqPrepend { values, .. } | Update::SeqRemoveAll { values, .. } => {
1809 validate_string_enum_named_variants_expr(schema, values)?;
1810 }
1811 Update::Conditional {
1812 condition,
1813 then_updates,
1814 else_updates,
1815 } => {
1816 validate_string_enum_named_variants_expr(schema, condition)?;
1817 for nested in then_updates.iter().chain(else_updates.iter()) {
1818 validate_string_enum_named_variants_update(schema, nested)?;
1819 }
1820 }
1821 Update::ForEach { over, updates, .. } => {
1822 validate_string_enum_named_variants_expr(schema, over)?;
1823 for nested in updates {
1824 validate_string_enum_named_variants_update(schema, nested)?;
1825 }
1826 }
1827 }
1828 Ok(())
1829}
1830
1831fn validate_string_enum_named_variants_expr(
1832 schema: &MachineSchema,
1833 expr: &Expr,
1834) -> Result<(), MachineSchemaError> {
1835 match expr {
1836 Expr::NamedVariant { enum_name, variant } => {
1837 let named_type_name = NamedTypeId::parse(enum_name.as_str()).map_err(|_| {
1838 MachineSchemaError::InvalidStringEnumBinding {
1839 name: enum_name.as_str().to_owned(),
1840 reason: "Expr::NamedVariant enum domains must be valid NamedTypeId entries"
1841 .to_owned(),
1842 }
1843 })?;
1844 let Some(binding) = schema.named_type_binding(&named_type_name) else {
1845 return Err(MachineSchemaError::MissingStringEnumBinding {
1846 name: enum_name.as_str().to_owned(),
1847 });
1848 };
1849 match &binding.rust {
1850 RustTypeAtom::StringEnum { variants } => {
1851 if !variants.iter().any(|allowed| allowed == variant) {
1852 return Err(MachineSchemaError::UnknownStringEnumVariant {
1853 enum_name: enum_name.as_str().to_owned(),
1854 variant: variant.as_str().to_owned(),
1855 });
1856 }
1857 }
1858 RustTypeAtom::TypePathEnum { unit_variants, .. } => {
1859 if !unit_variants.iter().any(|allowed| allowed == variant) {
1860 return Err(MachineSchemaError::UnknownStringEnumVariant {
1861 enum_name: enum_name.as_str().to_owned(),
1862 variant: variant.as_str().to_owned(),
1863 });
1864 }
1865 }
1866 _ => {
1867 return Err(MachineSchemaError::InvalidStringEnumBinding {
1868 name: enum_name.as_str().to_owned(),
1869 reason:
1870 "Expr::NamedVariant domains must use RustTypeAtom::StringEnum or RustTypeAtom::TypePathEnum"
1871 .to_owned(),
1872 });
1873 }
1874 }
1875 }
1876 Expr::SeqLiteral(items) | Expr::And(items) | Expr::Or(items) => {
1877 for item in items {
1878 validate_string_enum_named_variants_expr(schema, item)?;
1879 }
1880 }
1881 Expr::IfElse {
1882 condition,
1883 then_expr,
1884 else_expr,
1885 } => {
1886 validate_string_enum_named_variants_expr(schema, condition)?;
1887 validate_string_enum_named_variants_expr(schema, then_expr)?;
1888 validate_string_enum_named_variants_expr(schema, else_expr)?;
1889 }
1890 Expr::Not(inner)
1891 | Expr::Len(inner)
1892 | Expr::Head(inner)
1893 | Expr::MapKeys(inner)
1894 | Expr::SeqElements(inner)
1895 | Expr::Some(inner)
1896 | Expr::FieldAccess { base: inner, .. }
1897 | Expr::EnumVariantIs { value: inner, .. }
1898 | Expr::EnumStringSetPayload { value: inner, .. } => {
1899 validate_string_enum_named_variants_expr(schema, inner)?;
1900 }
1901 Expr::Eq(left, right)
1902 | Expr::Neq(left, right)
1903 | Expr::Add(left, right)
1904 | Expr::Sub(left, right)
1905 | Expr::Gt(left, right)
1906 | Expr::Gte(left, right)
1907 | Expr::Lt(left, right)
1908 | Expr::Lte(left, right) => {
1909 validate_string_enum_named_variants_expr(schema, left)?;
1910 validate_string_enum_named_variants_expr(schema, right)?;
1911 }
1912 Expr::Contains { collection, value } => {
1913 validate_string_enum_named_variants_expr(schema, collection)?;
1914 validate_string_enum_named_variants_expr(schema, value)?;
1915 }
1916 Expr::Count { collection, value } => {
1917 validate_string_enum_named_variants_expr(schema, collection)?;
1918 validate_string_enum_named_variants_expr(schema, value)?;
1919 }
1920 Expr::MapContainsKey { map, key } | Expr::MapGet { map, key } => {
1921 validate_string_enum_named_variants_expr(schema, map)?;
1922 validate_string_enum_named_variants_expr(schema, key)?;
1923 }
1924 Expr::SeqStartsWith { seq, prefix } => {
1925 validate_string_enum_named_variants_expr(schema, seq)?;
1926 validate_string_enum_named_variants_expr(schema, prefix)?;
1927 }
1928 Expr::Call { args, .. } => {
1929 for arg in args {
1930 validate_string_enum_named_variants_expr(schema, arg)?;
1931 }
1932 }
1933 Expr::Quantified { over, body, .. } => {
1934 validate_string_enum_named_variants_expr(schema, over)?;
1935 validate_string_enum_named_variants_expr(schema, body)?;
1936 }
1937 Expr::Bool(_)
1938 | Expr::U64(_)
1939 | Expr::U64Max
1940 | Expr::String(_)
1941 | Expr::EmptySet
1942 | Expr::EmptyMap
1943 | Expr::CurrentPhase
1944 | Expr::Phase(_)
1945 | Expr::Field(_)
1946 | Expr::Binding(_)
1947 | Expr::Variant(_)
1948 | Expr::None => {}
1949 }
1950 Ok(())
1951}
1952
1953fn unique_names<'a>(
1954 names: impl IntoIterator<Item = &'a str>,
1955 kind: &'static str,
1956) -> Result<IndexSet<&'a str>, MachineSchemaError> {
1957 let mut seen = IndexSet::new();
1958 for name in names {
1959 if name.is_empty() {
1960 return Err(MachineSchemaError::EmptyName(kind));
1961 }
1962 if !seen.insert(name) {
1963 return Err(MachineSchemaError::DuplicateName {
1964 kind,
1965 name: name.to_owned(),
1966 });
1967 }
1968 }
1969 Ok(seen)
1970}
1971
1972#[derive(Debug, PartialEq, Eq)]
1973pub enum MachineSchemaError {
1974 DuplicateName { kind: &'static str, name: String },
1975 EmptyName(&'static str),
1976 UnknownPhase { phase: String },
1977 UnknownField { field: String },
1978 MissingInitializer { field: String },
1979 UnknownInputVariant { variant: String },
1980 UnknownSurfaceOnlyInputVariant { variant: String },
1981 UnknownRuntimeInternalInputVariant { variant: String },
1982 UnknownTlcRepresentativeInputVariant { variant: String },
1983 TlcRepresentativeInputWithoutTransition { variant: String },
1984 TlcRepresentativeInputNotStateIndependent { variant: String, transition: String },
1985 UnknownSignalVariant { variant: String },
1986 UnknownEffectVariant { variant: String },
1987 UnknownHelper { helper: String },
1988 UnknownBinding { binding: String },
1989 UnknownVariant { variant: String },
1990 UnknownVariantField { variant: String, field: String },
1991 UnknownEffectDispositionVariant { variant: String },
1992 DuplicateEffectDisposition { variant: String },
1993 MissingEffectDisposition { variant: String },
1994 HandoffProtocolOnRoutedEffect { variant: String },
1995 SurfaceOnlyInputHasTransition { variant: String, transition: String },
1996 DuplicateNamedTypeBinding { name: String },
1997 MissingNamedTypeBinding { name: String },
1998 MissingStringEnumBinding { name: String },
1999 UnknownStringEnumVariant { enum_name: String, variant: String },
2000 InvalidStringEnumBinding { name: String, reason: String },
2001 UnknownCommandPlanInput { plan: String, input: String },
2002 UnknownCommandPlanSignal { plan: String, signal: String },
2003 UnknownCommandPlanTransition { plan: String, transition: String },
2004 UnknownCommandPlanEffect { plan: String, effect: String },
2005 UnknownCommandPlanClosureEffect { plan: String, effect: String },
2006}
2007
2008impl fmt::Display for MachineSchemaError {
2009 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2010 match self {
2011 Self::DuplicateName { kind, name } => write!(f, "duplicate {kind} name `{name}`"),
2012 Self::EmptyName(kind) => write!(f, "empty {kind} name"),
2013 Self::UnknownPhase { phase } => write!(f, "unknown phase `{phase}`"),
2014 Self::UnknownField { field } => write!(f, "unknown field `{field}`"),
2015 Self::MissingInitializer { field } => write!(
2016 f,
2017 "state field `{field}` has no `state.init.fields` initializer; \
2018 declare a typed initial fact (it must not rely on the runtime \
2019 type-generic auto-seed / `_Unset` sentinel)"
2020 ),
2021 Self::UnknownInputVariant { variant } => {
2022 write!(f, "unknown input variant `{variant}`")
2023 }
2024 Self::UnknownSurfaceOnlyInputVariant { variant } => {
2025 write!(f, "unknown surface-only input variant `{variant}`")
2026 }
2027 Self::UnknownRuntimeInternalInputVariant { variant } => {
2028 write!(f, "unknown runtime-internal input variant `{variant}`")
2029 }
2030 Self::UnknownTlcRepresentativeInputVariant { variant } => {
2031 write!(f, "unknown TLC representative input variant `{variant}`")
2032 }
2033 Self::TlcRepresentativeInputWithoutTransition { variant } => {
2034 write!(
2035 f,
2036 "TLC representative input `{variant}` has no transition to explore"
2037 )
2038 }
2039 Self::TlcRepresentativeInputNotStateIndependent {
2040 variant,
2041 transition,
2042 } => {
2043 write!(
2044 f,
2045 "TLC representative input `{variant}` transition `{transition}` must be unguarded, phase-preserving, and state-update-free"
2046 )
2047 }
2048 Self::UnknownSignalVariant { variant } => {
2049 write!(f, "unknown signal variant `{variant}`")
2050 }
2051 Self::UnknownEffectVariant { variant } => {
2052 write!(f, "unknown effect variant `{variant}`")
2053 }
2054 Self::UnknownHelper { helper } => write!(f, "unknown helper `{helper}`"),
2055 Self::UnknownBinding { binding } => write!(f, "unknown binding `{binding}`"),
2056 Self::UnknownVariant { variant } => write!(f, "unknown variant `{variant}`"),
2057 Self::UnknownVariantField { variant, field } => {
2058 write!(f, "unknown field `{field}` on variant `{variant}`")
2059 }
2060 Self::UnknownEffectDispositionVariant { variant } => {
2061 write!(
2062 f,
2063 "effect disposition references unknown effect variant `{variant}`"
2064 )
2065 }
2066 Self::DuplicateEffectDisposition { variant } => {
2067 write!(f, "duplicate effect disposition for variant `{variant}`")
2068 }
2069 Self::MissingEffectDisposition { variant } => {
2070 write!(f, "effect variant `{variant}` has no disposition rule")
2071 }
2072 Self::HandoffProtocolOnRoutedEffect { variant } => {
2073 write!(
2074 f,
2075 "effect variant `{variant}` has handoff_protocol set but disposition is Routed (use routes instead)"
2076 )
2077 }
2078 Self::SurfaceOnlyInputHasTransition {
2079 variant,
2080 transition,
2081 } => {
2082 write!(
2083 f,
2084 "surface-only input `{variant}` must not have transition `{transition}`"
2085 )
2086 }
2087 Self::DuplicateNamedTypeBinding { name } => {
2088 write!(f, "duplicate named-type binding for `{name}`")
2089 }
2090 Self::MissingNamedTypeBinding { name } => {
2091 write!(
2092 f,
2093 "named type `{name}` is referenced by this schema but has no NamedTypeBinding entry in `named_types`"
2094 )
2095 }
2096 Self::MissingStringEnumBinding { name } => {
2097 write!(
2098 f,
2099 "enum type `{name}` is referenced by this schema but has no StringEnum NamedTypeBinding entry in `named_types`"
2100 )
2101 }
2102 Self::UnknownStringEnumVariant { enum_name, variant } => {
2103 write!(
2104 f,
2105 "string enum `{enum_name}` does not define variant `{variant}`"
2106 )
2107 }
2108 Self::InvalidStringEnumBinding { name, reason } => {
2109 write!(
2110 f,
2111 "invalid string enum named-type binding `{name}`: {reason}"
2112 )
2113 }
2114 Self::UnknownCommandPlanInput { plan, input } => {
2115 write!(
2116 f,
2117 "command plan `{plan}` references unknown input `{input}`"
2118 )
2119 }
2120 Self::UnknownCommandPlanSignal { plan, signal } => {
2121 write!(
2122 f,
2123 "command plan `{plan}` references unknown signal `{signal}`"
2124 )
2125 }
2126 Self::UnknownCommandPlanTransition { plan, transition } => {
2127 write!(
2128 f,
2129 "command plan `{plan}` references unknown transition `{transition}`"
2130 )
2131 }
2132 Self::UnknownCommandPlanEffect { plan, effect } => {
2133 write!(
2134 f,
2135 "command plan `{plan}` references unknown effect `{effect}`"
2136 )
2137 }
2138 Self::UnknownCommandPlanClosureEffect { plan, effect } => {
2139 write!(
2140 f,
2141 "command plan `{plan}` declares closure for `{effect}` without listing it as a command effect"
2142 )
2143 }
2144 }
2145 }
2146}
2147
2148impl std::error::Error for MachineSchemaError {}
2149
2150#[cfg(test)]
2151#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
2152mod tests {
2153 use crate::identity::{
2154 EffectVariantId, EnumTypeId, EnumVariantId, InputVariantId, NamedTypeId,
2155 };
2156 use crate::{
2157 Expr, InvariantSchema, MachineSchema, MachineSchemaError, NamedTypeBinding, RustTypeAtom,
2158 Update, catalog::dsl::dsl_meerkat_machine as meerkat_machine,
2159 catalog::dsl::dsl_mob_machine as mob_machine,
2160 };
2161
2162 #[test]
2163 fn validates_meerkat_machine_schema() {
2164 let schema = meerkat_machine();
2165
2166 assert_eq!(schema.machine.as_str(), "MeerkatMachine");
2167 assert_eq!(schema.rust.crate_name, "self");
2171 assert_eq!(schema.rust.module, "catalog::dsl::meerkat_machine");
2172 assert_eq!(schema.state.phase.name, "MeerkatPhase");
2173 assert!(
2174 schema
2175 .transitions
2176 .iter()
2177 .any(|transition| transition.name.as_str() == "PrepareBindingsIdle")
2178 );
2179 assert!(
2180 schema
2181 .transitions
2182 .iter()
2183 .any(|transition| transition.name.as_str() == "Destroy")
2184 );
2185 assert_eq!(
2186 schema
2187 .state
2188 .terminal_phases
2189 .iter()
2190 .map(|phase| phase.as_str().to_owned())
2191 .collect::<Vec<_>>(),
2192 vec!["Destroyed".to_owned()]
2193 );
2194 assert_eq!(schema.validate(), Ok(()));
2195 }
2196
2197 #[test]
2198 fn mob_identity_classifier_tlc_abstraction_is_state_independent() {
2199 let schema = mob_machine();
2200 assert_eq!(schema.validate(), Ok(()));
2201 assert_eq!(
2202 schema
2203 .tlc_representative_inputs
2204 .iter()
2205 .map(|input| input.as_str())
2206 .collect::<Vec<_>>(),
2207 vec!["ClassifyIdentityReconciliation"]
2208 );
2209
2210 let classifier_transitions = schema
2211 .transitions
2212 .iter()
2213 .filter(|transition| transition.on.variant_str() == "ClassifyIdentityReconciliation")
2214 .collect::<Vec<_>>();
2215 assert!(!classifier_transitions.is_empty());
2216 assert!(classifier_transitions.iter().all(|transition| {
2217 transition.from.len() == 1
2218 && transition.to == transition.from[0]
2219 && transition.guards.is_empty()
2220 && transition.updates.is_empty()
2221 }));
2222 }
2223
2224 #[test]
2225 fn tlc_representative_input_rejects_stateful_transitions() {
2226 let mut schema = mob_machine();
2227 schema
2228 .tlc_representative_inputs
2229 .push(InputVariantId::parse("WireMembers").expect("valid input variant"));
2230
2231 assert!(matches!(
2232 schema.validate(),
2233 Err(MachineSchemaError::TlcRepresentativeInputNotStateIndependent {
2234 variant,
2235 ..
2236 }) if variant == "WireMembers"
2237 ));
2238 }
2239
2240 #[test]
2241 fn meerkat_queue_to_run_command_plans_are_schema_owned() {
2242 let schema = meerkat_machine();
2243 let plan_names = schema
2244 .command_plans
2245 .iter()
2246 .map(|plan| plan.name.as_str())
2247 .collect::<Vec<_>>();
2248 assert_eq!(
2249 plan_names,
2250 vec![
2251 "AuthorizedAcceptedInputMaterialization",
2252 "AuthorizeRuntimeLoopBatch",
2253 "AuthorizedStageForRun",
2254 "AuthorizedRuntimeLoopRunCommit",
2255 "AuthorizedInteractionTerminalOutboxAdoption",
2256 "AuthorizedRuntimeCompletionResultClosure"
2257 ]
2258 );
2259 let stage_plan = schema
2260 .command_plans
2261 .iter()
2262 .find(|plan| plan.name == "AuthorizedStageForRun")
2263 .expect("stage command plan");
2264 let stage_transitions = stage_plan
2265 .transitions
2266 .iter()
2267 .map(|transition| transition.as_str())
2268 .collect::<Vec<_>>();
2269 assert_eq!(
2270 stage_transitions,
2271 vec![
2272 "StageForRunIdle",
2273 "StageForRunAttached",
2274 "StageForRunRunning",
2275 "StageForRunRetired",
2276 "StageForRunStopped"
2277 ]
2278 );
2279 let guard_names = schema
2280 .transitions
2281 .iter()
2282 .find(|transition| transition.name.as_str() == "StageForRunIdle")
2283 .expect("StageForRunIdle transition")
2284 .guards
2285 .iter()
2286 .map(|guard| guard.name.as_str())
2287 .collect::<Vec<_>>();
2288 for required in [
2289 "input_queued",
2290 "input_lane_bound",
2291 "input_sequence_bound",
2292 "input_recovery_lane_bound",
2293 "input_not_run_associated",
2294 "current_run_matches",
2295 ] {
2296 assert!(
2297 guard_names.contains(&required),
2298 "StageForRun command-plan guard expansion must include {required}; got {guard_names:?}"
2299 );
2300 }
2301
2302 let run_commit_plan = schema
2303 .command_plans
2304 .iter()
2305 .find(|plan| plan.name == "AuthorizedRuntimeLoopRunCommit")
2306 .expect("runtime-loop run commit command plan");
2307 let run_commit_transitions = run_commit_plan
2308 .transitions
2309 .iter()
2310 .map(|transition| transition.as_str())
2311 .collect::<Vec<_>>();
2312 for required in [
2313 "RunCompleted",
2314 "RunFailed",
2315 "RunCancelled",
2316 "CommitRunningToIdle",
2317 "CommitRunningToAttached",
2318 "CommitRunningToRetired",
2319 "FailRunningToIdle",
2320 "CancelRunningToIdle",
2321 "RollbackRunRunningToIdle",
2322 ] {
2323 assert!(
2324 run_commit_transitions.contains(&required),
2325 "run-commit command plan must include {required}; got {run_commit_transitions:?}"
2326 );
2327 }
2328 let run_commit_effects = run_commit_plan
2329 .effects
2330 .iter()
2331 .map(|effect| effect.as_str())
2332 .collect::<Vec<_>>();
2333 assert_eq!(
2334 run_commit_effects,
2335 vec!["TurnRunCompleted", "TurnRunFailed", "TurnRunCancelled"]
2336 );
2337 let run_commit_closures = run_commit_plan
2338 .effect_closures
2339 .iter()
2340 .map(|closure| closure.effect.as_str())
2341 .collect::<Vec<_>>();
2342 assert_eq!(
2343 run_commit_closures,
2344 vec!["TurnRunCompleted", "TurnRunFailed", "TurnRunCancelled"]
2345 );
2346 for closure in &run_commit_plan.effect_closures {
2347 assert_eq!(closure.authority_type, "AuthorizedRuntimeLoopRunCommit");
2348 assert_eq!(closure.closure_policy, "RuntimeLoopRunCommitEffect");
2349 assert_eq!(
2350 closure.lifecycle,
2351 vec![
2352 "Authorized",
2353 "Attempted",
2354 "Realized",
2355 "Failed",
2356 "Cancelled",
2357 "Abandoned"
2358 ]
2359 );
2360 }
2361
2362 let adoption_plan = schema
2363 .command_plans
2364 .iter()
2365 .find(|plan| plan.name == "AuthorizedInteractionTerminalOutboxAdoption")
2366 .expect("interaction terminal outbox adoption command plan");
2367 assert_eq!(
2368 adoption_plan
2369 .source_inputs
2370 .iter()
2371 .map(|input| input.as_str())
2372 .collect::<Vec<_>>(),
2373 vec!["AuthorizeInteractionTerminalOutboxAdoption"]
2374 );
2375 assert_eq!(
2376 adoption_plan
2377 .effects
2378 .iter()
2379 .map(|effect| effect.as_str())
2380 .collect::<Vec<_>>(),
2381 vec!["InteractionTerminalOutboxAdoptionAuthorized"]
2382 );
2383 assert_eq!(adoption_plan.effect_closures.len(), 1);
2384 let adoption_closure = &adoption_plan.effect_closures[0];
2385 assert_eq!(
2386 adoption_closure.effect.as_str(),
2387 "InteractionTerminalOutboxAdoptionAuthorized"
2388 );
2389 assert_eq!(
2390 adoption_closure.authority_type,
2391 "AuthorizedInteractionTerminalOutboxAdoption"
2392 );
2393 assert_eq!(
2394 adoption_closure.closure_policy,
2395 "DurableOutboxBindingAdoption"
2396 );
2397 assert_eq!(
2398 adoption_closure.lifecycle,
2399 vec!["Authorized", "Attempted", "Realized", "Failed", "Abandoned"]
2400 );
2401
2402 let completion_closure_plan = schema
2403 .command_plans
2404 .iter()
2405 .find(|plan| plan.name == "AuthorizedRuntimeCompletionResultClosure")
2406 .expect("runtime completion result closure command plan");
2407 assert_eq!(
2408 completion_closure_plan.effects,
2409 vec![EffectVariantId::parse("RuntimeCompletionResultResolved").unwrap()]
2410 );
2411 assert_eq!(completion_closure_plan.effect_closures.len(), 1);
2412 let closure = &completion_closure_plan.effect_closures[0];
2413 assert_eq!(closure.effect.as_str(), "RuntimeCompletionResultResolved");
2414 assert_eq!(closure.authority_type, "RuntimeCompletionResultAuthority");
2415 assert_eq!(closure.closure_policy, "LocalSurfaceResultAlignment");
2416 assert_eq!(
2417 closure.lifecycle,
2418 vec![
2419 "Authorized",
2420 "Attempted",
2421 "Realized",
2422 "Failed",
2423 "Cancelled",
2424 "Abandoned"
2425 ]
2426 );
2427 }
2428
2429 #[test]
2430 fn mob_spawn_command_plan_is_schema_owned() {
2431 let schema = mob_machine();
2432 let command_plan_names = schema
2433 .command_plans
2434 .iter()
2435 .map(|plan| plan.name.as_str())
2436 .collect::<Vec<_>>();
2437 for required in [
2438 "AuthorizedMobSpawnStart",
2439 "CanStartSpawn",
2440 "SpawnStarted",
2441 "SpawnEffect",
2442 "FailSpawn",
2443 ] {
2444 assert!(
2445 command_plan_names.contains(&required),
2446 "mob spawn command plans must include {required}; got {command_plan_names:?}"
2447 );
2448 }
2449 let spawn_plan = schema
2450 .command_plans
2451 .iter()
2452 .find(|plan| plan.name == "AuthorizedMobSpawnStart")
2453 .expect("mob spawn command plan");
2454
2455 assert_eq!(
2456 spawn_plan.authority_type,
2457 "PendingSpawnOperationOwnerAuthorized"
2458 );
2459 assert_eq!(
2460 spawn_plan
2461 .source_signals
2462 .iter()
2463 .map(|signal| signal.as_str())
2464 .collect::<Vec<_>>(),
2465 vec!["StageSpawn", "CompleteSpawn"]
2466 );
2467 assert_eq!(
2468 spawn_plan
2469 .source_inputs
2470 .iter()
2471 .map(|input| input.as_str())
2472 .collect::<Vec<_>>(),
2473 vec!["CancelPendingSpawn"]
2474 );
2475
2476 let transitions = spawn_plan
2477 .transitions
2478 .iter()
2479 .map(|transition| transition.as_str())
2480 .collect::<Vec<_>>();
2481 for required in [
2482 "StageSpawnRunning",
2483 "CompleteSpawnRunning",
2484 "CompleteSpawnLateArrivalRunning",
2485 "CompleteSpawnLateArrivalStopped",
2486 "CompleteSpawnLateArrivalCompleted",
2487 "CompleteSpawnDestroyed",
2488 "CancelPendingSpawnPresentRunning",
2489 "CancelPendingSpawnPresentStopped",
2490 "CancelPendingSpawnPresentCompleted",
2491 "CancelPendingSpawnAbsentRunning",
2492 "CancelPendingSpawnAbsentStopped",
2493 "CancelPendingSpawnAbsentCompleted",
2494 "CancelPendingSpawnDestroyed",
2495 ] {
2496 assert!(
2497 transitions.contains(&required),
2498 "mob spawn command plan must include {required}; got {transitions:?}"
2499 );
2500 }
2501
2502 let effects = spawn_plan
2503 .effects
2504 .iter()
2505 .map(|effect| effect.as_str())
2506 .collect::<Vec<_>>();
2507 assert_eq!(
2508 effects,
2509 vec![
2510 "PendingSpawnOperationOwnerAuthorized",
2511 "ExposePendingSpawn",
2512 "EmitMemberLifecycleNotice"
2513 ]
2514 );
2515 assert_eq!(spawn_plan.effect_closures.len(), 2);
2516 for closure in &spawn_plan.effect_closures {
2517 assert_eq!(
2518 closure.lifecycle,
2519 vec![
2520 "Authorized",
2521 "Attempted",
2522 "Realized",
2523 "Failed",
2524 "Cancelled",
2525 "Abandoned"
2526 ]
2527 );
2528 }
2529 assert!(
2530 spawn_plan
2531 .effect_closures
2532 .iter()
2533 .any(
2534 |closure| closure.effect.as_str() == "PendingSpawnOperationOwnerAuthorized"
2535 && closure.authority_type == "PendingSpawnOperationOwnerAuthorized"
2536 && closure.closure_policy == "LocalPendingSpawnOwner"
2537 ),
2538 "mob spawn plan must declare pending-owner closure metadata"
2539 );
2540 assert!(
2541 spawn_plan
2542 .effect_closures
2543 .iter()
2544 .any(
2545 |closure| closure.effect.as_str() == "EmitMemberLifecycleNotice"
2546 && closure.authority_type == "CompleteSpawn"
2547 && closure.closure_policy == "LocalSpawnCompletion"
2548 ),
2549 "mob spawn plan must declare completion closure metadata"
2550 );
2551 let start_plan = schema
2552 .command_plans
2553 .iter()
2554 .find(|plan| plan.name == "CanStartSpawn")
2555 .expect("CanStartSpawn command plan");
2556 assert_eq!(start_plan.authority_type, "CanStartSpawn");
2557 assert_eq!(
2558 start_plan
2559 .source_signals
2560 .iter()
2561 .map(|signal| signal.as_str())
2562 .collect::<Vec<_>>(),
2563 vec!["StageSpawn"]
2564 );
2565 let started_plan = schema
2566 .command_plans
2567 .iter()
2568 .find(|plan| plan.name == "SpawnStarted")
2569 .expect("SpawnStarted command plan");
2570 assert_eq!(started_plan.authority_type, "SpawnStarted");
2571 assert_eq!(
2572 started_plan
2573 .effects
2574 .iter()
2575 .map(|effect| effect.as_str())
2576 .collect::<Vec<_>>(),
2577 vec!["ExposePendingSpawn"]
2578 );
2579 let spawn_effect_plan = schema
2580 .command_plans
2581 .iter()
2582 .find(|plan| plan.name == "SpawnEffect")
2583 .expect("SpawnEffect command plan");
2584 assert_eq!(spawn_effect_plan.authority_type, "SpawnEffect");
2585 let fail_spawn_plan = schema
2586 .command_plans
2587 .iter()
2588 .find(|plan| plan.name == "FailSpawn")
2589 .expect("FailSpawn command plan");
2590 assert_eq!(fail_spawn_plan.authority_type, "FailSpawn");
2591 assert_eq!(
2592 fail_spawn_plan
2593 .source_inputs
2594 .iter()
2595 .map(|input| input.as_str())
2596 .collect::<Vec<_>>(),
2597 vec!["CancelPendingSpawn"]
2598 );
2599 }
2600
2601 #[test]
2602 fn validate_rejects_struct_binding_with_missing_nested_named_binding() {
2603 let mut schema = meerkat_machine();
2604 schema
2605 .named_types
2606 .retain(|binding| binding.name.as_str() != "ToolSourceKind");
2607
2608 assert_eq!(
2609 schema.validate(),
2610 Err(MachineSchemaError::MissingNamedTypeBinding {
2611 name: "ToolSourceKind".into(),
2612 })
2613 );
2614 }
2615
2616 #[test]
2617 fn validate_rejects_effect_declaring_machine_with_empty_dispositions() {
2618 let mut schema = meerkat_machine();
2624 assert_eq!(schema.validate(), Ok(()));
2625 schema.effect_dispositions.clear();
2626 assert!(
2627 matches!(
2628 schema.validate(),
2629 Err(MachineSchemaError::MissingEffectDisposition { .. })
2630 ),
2631 "empty dispositions on an effect-declaring machine must fail closed"
2632 );
2633 }
2634
2635 #[test]
2636 fn validate_rejects_state_field_without_initializer() {
2637 let mut schema = meerkat_machine();
2644 assert_eq!(schema.validate(), Ok(()));
2645
2646 let dropped = schema
2647 .state
2648 .fields
2649 .first()
2650 .expect("MeerkatMachine has at least one semantic state field")
2651 .name
2652 .as_str()
2653 .to_owned();
2654 schema
2655 .state
2656 .init
2657 .fields
2658 .retain(|initializer| initializer.field.as_str() != dropped);
2659
2660 assert_eq!(
2661 schema.validate(),
2662 Err(MachineSchemaError::MissingInitializer {
2663 field: dropped.clone(),
2664 }),
2665 "a declared state field with no initializer must fail closed (field `{dropped}`)"
2666 );
2667 }
2668
2669 fn replace_register_op_status_update(schema: &mut MachineSchema, status: &str) {
2670 let transition = schema
2671 .transitions
2672 .iter_mut()
2673 .find(|transition| transition.name.as_str() == "RegisterOpAcceptedIdle")
2674 .expect("RegisterOpAcceptedIdle transition");
2675 let update = transition
2676 .updates
2677 .iter_mut()
2678 .find(|update| {
2679 matches!(
2680 update,
2681 Update::MapInsert { field, .. } if field.as_str() == "op_statuses"
2682 )
2683 })
2684 .expect("op_statuses update");
2685 let Update::MapInsert { value, .. } = update else {
2686 panic!("op_statuses update must be a map insert");
2687 };
2688 *value = Expr::NamedVariant {
2689 enum_name: EnumTypeId::parse("OperationStatus").expect("enum type slug"),
2690 variant: EnumVariantId::parse(status).expect("enum variant slug"),
2691 };
2692 }
2693
2694 fn string_enum_binding(name: &str, variants: &[&str]) -> NamedTypeBinding {
2695 NamedTypeBinding {
2696 name: NamedTypeId::parse(name).expect("named type slug"),
2697 rust: RustTypeAtom::StringEnum {
2698 variants: variants
2699 .iter()
2700 .map(|variant| EnumVariantId::parse(*variant).expect("enum variant slug"))
2701 .collect(),
2702 },
2703 }
2704 }
2705
2706 #[test]
2707 fn validate_rejects_unknown_string_enum_named_variant_in_transition_update() {
2708 let mut schema = meerkat_machine();
2709 replace_register_op_status_update(&mut schema, "Launched");
2710
2711 let err = schema
2712 .validate()
2713 .expect_err("unknown OperationStatus variant must be rejected");
2714 let message = err.to_string();
2715 assert!(
2716 message.contains("OperationStatus") && message.contains("Launched"),
2717 "error should identify the invalid string enum variant, got: {message}"
2718 );
2719 }
2720
2721 #[test]
2722 fn validate_rejects_empty_string_enum_named_type_binding() {
2723 let mut schema = meerkat_machine();
2724 schema
2725 .named_types
2726 .push(string_enum_binding("SyntheticStatus", &[]));
2727
2728 let err = schema
2729 .validate()
2730 .expect_err("empty StringEnum bindings must be rejected");
2731 let message = err.to_string();
2732 assert!(
2733 message.contains("SyntheticStatus") && message.contains("at least one variant"),
2734 "error should identify the empty StringEnum binding, got: {message}"
2735 );
2736 }
2737
2738 #[test]
2739 fn validate_rejects_duplicate_string_enum_named_type_variants() {
2740 let mut schema = meerkat_machine();
2741 schema
2742 .named_types
2743 .push(string_enum_binding("SyntheticStatus", &["Ready", "Ready"]));
2744
2745 let err = schema
2746 .validate()
2747 .expect_err("duplicate StringEnum variants must be rejected");
2748 let message = err.to_string();
2749 assert!(
2750 message.contains("SyntheticStatus")
2751 && message.contains("Ready")
2752 && message.contains("duplicate"),
2753 "error should identify the duplicate StringEnum variant, got: {message}"
2754 );
2755 }
2756
2757 #[test]
2758 fn validate_rejects_string_enum_named_type_variant_ident_collisions() {
2759 let mut schema = meerkat_machine();
2760 schema.named_types.push(string_enum_binding(
2761 "SyntheticStatus",
2762 &["foo-bar", "foo_bar"],
2763 ));
2764
2765 let err = schema
2766 .validate()
2767 .expect_err("StringEnum variant Rust identifier collisions must be rejected");
2768 let message = err.to_string();
2769 assert!(
2770 message.contains("SyntheticStatus")
2771 && message.contains("foo-bar")
2772 && message.contains("foo_bar"),
2773 "error should identify colliding StringEnum variants, got: {message}"
2774 );
2775 }
2776
2777 #[test]
2778 fn validate_rejects_enum_type_without_string_enum_binding() {
2779 let mut schema = meerkat_machine();
2780 schema
2781 .named_types
2782 .retain(|binding| binding.name.as_str() != "OperationStatus");
2783
2784 assert_eq!(
2785 schema.validate(),
2786 Err(MachineSchemaError::MissingNamedTypeBinding {
2787 name: "OperationStatus".into(),
2788 })
2789 );
2790 }
2791
2792 #[test]
2793 fn validate_rejects_enum_type_with_unconstrained_string_binding() {
2794 let mut schema = meerkat_machine();
2795 let binding = schema
2796 .named_types
2797 .iter_mut()
2798 .find(|binding| binding.name.as_str() == "OperationStatus")
2799 .expect("OperationStatus binding");
2800 binding.rust = RustTypeAtom::String;
2801
2802 assert_eq!(
2803 schema.validate(),
2804 Err(MachineSchemaError::InvalidStringEnumBinding {
2805 name: "OperationStatus".into(),
2806 reason: "TypeRef::Enum domains must use RustTypeAtom::StringEnum".into(),
2807 })
2808 );
2809 }
2810
2811 #[test]
2812 fn validate_rejects_unbound_named_variant_literal_without_typed_surface_reference() {
2813 let mut schema = meerkat_machine();
2814 schema.invariants.push(InvariantSchema {
2815 name: "synthetic_unbound_enum_literal".to_owned(),
2816 expr: Expr::NamedVariant {
2817 enum_name: EnumTypeId::parse("SyntheticStatus").expect("enum type slug"),
2818 variant: EnumVariantId::parse("Ready").expect("enum variant slug"),
2819 },
2820 });
2821
2822 assert_eq!(
2823 schema.validate(),
2824 Err(MachineSchemaError::MissingStringEnumBinding {
2825 name: "SyntheticStatus".into(),
2826 })
2827 );
2828 }
2829
2830 #[test]
2831 fn validate_rejects_named_variant_literal_with_unconstrained_string_binding() {
2832 let mut schema = meerkat_machine();
2833 schema.invariants.push(InvariantSchema {
2834 name: "synthetic_string_bound_enum_literal".to_owned(),
2835 expr: Expr::NamedVariant {
2836 enum_name: EnumTypeId::parse("AgentRuntimeId").expect("enum type slug"),
2837 variant: EnumVariantId::parse("Ready").expect("enum variant slug"),
2838 },
2839 });
2840
2841 assert_eq!(
2842 schema.validate(),
2843 Err(MachineSchemaError::InvalidStringEnumBinding {
2844 name: "AgentRuntimeId".into(),
2845 reason: "Expr::NamedVariant domains must use RustTypeAtom::StringEnum or RustTypeAtom::TypePathEnum".into(),
2846 })
2847 );
2848 }
2849
2850 #[test]
2851 fn validate_rejects_unknown_type_path_enum_named_variant_literal() {
2852 let mut schema = meerkat_machine();
2853 schema.invariants.push(InvariantSchema {
2854 name: "synthetic_unknown_tool_filter_literal".to_owned(),
2855 expr: Expr::NamedVariant {
2856 enum_name: EnumTypeId::parse("ToolFilter").expect("enum type slug"),
2857 variant: EnumVariantId::parse("Bogus").expect("enum variant slug"),
2858 },
2859 });
2860
2861 assert_eq!(
2862 schema.validate(),
2863 Err(MachineSchemaError::UnknownStringEnumVariant {
2864 enum_name: "ToolFilter".into(),
2865 variant: "Bogus".into(),
2866 })
2867 );
2868 }
2869
2870 #[test]
2871 fn validates_meerkat_machine_without_peer_directory_region() {
2872 let schema = meerkat_machine();
2873
2874 assert!(
2876 !schema
2877 .transitions
2878 .iter()
2879 .any(|transition| transition.name.as_str() == "RecordSendFailedAttached")
2880 );
2881 assert_eq!(schema.validate(), Ok(()));
2882 }
2883
2884 #[test]
2885 fn rejects_unknown_surface_only_inputs() {
2886 let mut schema = meerkat_machine();
2887 schema
2888 .surface_only_inputs
2889 .push(crate::identity::InputVariantId::parse("DoesNotExist").expect("slug"));
2890
2891 assert_eq!(
2892 schema.validate(),
2893 Err(MachineSchemaError::UnknownSurfaceOnlyInputVariant {
2894 variant: "DoesNotExist".into(),
2895 })
2896 );
2897 }
2898
2899 #[test]
2900 fn rejects_surface_only_inputs_with_transitions() {
2901 let mut schema = meerkat_machine();
2902 schema
2903 .surface_only_inputs
2904 .push(crate::identity::InputVariantId::parse("RegisterSession").expect("slug"));
2905 let transition = schema
2906 .transitions
2907 .iter()
2908 .find(|transition| transition.on.variant_str() == "RegisterSession")
2909 .map(|transition| transition.name.as_str().to_owned())
2910 .unwrap_or_default();
2911 assert!(
2912 !transition.is_empty(),
2913 "register session transition should exist"
2914 );
2915
2916 assert_eq!(
2917 schema.validate(),
2918 Err(MachineSchemaError::SurfaceOnlyInputHasTransition {
2919 variant: "RegisterSession".into(),
2920 transition,
2921 })
2922 );
2923 }
2924}