1use std::mem::size_of;
9use std::sync::Arc;
10
11use crate::determinism::DeterminismMode;
12use crate::effect::EffectHandler;
13use crate::loader::CodeImage;
14use crate::output_condition::OutputConditionPolicy;
15use crate::runtime_contracts::{enforce_vm_runtime_gates, RuntimeContracts, RuntimeGateResult};
16use crate::scheduler::SchedPolicy;
17use crate::vm::{VMConfig, VMError, VM};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum DeterminismCapability {
22 Full,
24 ModuloEffects,
26 ModuloCommutativity,
28 Replay,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SchedulerCapability {
35 Cooperative,
37 RoundRobin,
39 Priority,
41 ProgressAware,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TheoremPackCapabilities {
48 pub determinism: Vec<DeterminismCapability>,
50 pub schedulers: Vec<SchedulerCapability>,
52 pub output_condition_gating: bool,
54}
55
56impl TheoremPackCapabilities {
57 #[must_use]
59 pub fn full() -> Self {
60 Self {
61 determinism: vec![
62 DeterminismCapability::Full,
63 DeterminismCapability::ModuloEffects,
64 DeterminismCapability::ModuloCommutativity,
65 DeterminismCapability::Replay,
66 ],
67 schedulers: vec![
68 SchedulerCapability::Cooperative,
69 SchedulerCapability::RoundRobin,
70 SchedulerCapability::Priority,
71 SchedulerCapability::ProgressAware,
72 ],
73 output_condition_gating: true,
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct CompositionCertificate {
81 pub artifact_id: String,
83 pub link_ok_full: bool,
85 pub theorem_pack: TheoremPackCapabilities,
87 pub runtime_contracts: Option<RuntimeContracts>,
89}
90
91#[derive(Debug, Clone)]
93pub struct ProtocolBundle {
94 pub code: Arc<CodeImage>,
96 pub certificate: CompositionCertificate,
98}
99
100impl ProtocolBundle {
101 #[must_use]
103 pub fn new(code: Arc<CodeImage>, certificate: CompositionCertificate) -> Self {
104 Self { code, certificate }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct MemoryBudget {
111 pub max_bytes_per_coroutine: usize,
113 pub max_bytes_per_session: usize,
115 pub max_incremental_bytes_per_protocol: usize,
117}
118
119impl Default for MemoryBudget {
120 fn default() -> Self {
121 Self {
122 max_bytes_per_coroutine: 64 * 1024,
123 max_bytes_per_session: 256 * 1024,
124 max_incremental_bytes_per_protocol: 128 * 1024,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Default, PartialEq, Eq)]
131pub struct MemoryUsage {
132 pub bytes_per_coroutine: usize,
134 pub bytes_per_session: usize,
136 pub incremental_bytes_per_protocol: usize,
138 pub protocol_count: usize,
140 pub session_count: usize,
142 pub coroutine_count: usize,
144}
145
146#[derive(Debug, thiserror::Error)]
148pub enum CompositionError {
149 #[error("bundle `{artifact_id}` rejected: missing LinkOKFull compatibility evidence")]
151 MissingCompatibilityProof {
152 artifact_id: String,
154 },
155 #[error("bundle `{artifact_id}` rejected: missing required capability `{capability}`")]
157 MissingCapability {
158 artifact_id: String,
160 capability: String,
162 },
163 #[error("bundle `{artifact_id}` rejected: missing VM runtime contracts for advanced mode")]
165 MissingRuntimeContracts {
166 artifact_id: String,
168 },
169 #[error("bundle `{artifact_id}` rejected: memory budget exceeded ({reason})")]
171 BudgetExceeded {
172 artifact_id: String,
174 reason: String,
176 },
177 #[error(transparent)]
179 Vm(#[from] VMError),
180}
181
182#[derive(Debug)]
184pub struct ComposedRuntime {
185 vm: VM,
186 bundles: Vec<ProtocolBundle>,
187 budget: MemoryBudget,
188 usage: MemoryUsage,
189}
190
191impl ComposedRuntime {
192 #[must_use]
194 pub fn new(config: VMConfig, budget: MemoryBudget) -> Self {
195 let vm = VM::new(config);
196 Self {
197 vm,
198 bundles: Vec::new(),
199 budget,
200 usage: MemoryUsage {
201 bytes_per_coroutine: size_of::<crate::coroutine::Coroutine>(),
202 bytes_per_session: size_of::<crate::session::SessionState>(),
203 incremental_bytes_per_protocol: size_of::<Arc<CodeImage>>(),
204 ..MemoryUsage::default()
205 },
206 }
207 }
208
209 pub fn admit_bundle(&mut self, bundle: ProtocolBundle) -> Result<(), CompositionError> {
215 if !bundle.certificate.link_ok_full {
216 return Err(CompositionError::MissingCompatibilityProof {
217 artifact_id: bundle.certificate.artifact_id,
218 });
219 }
220 self.require_capabilities(&bundle)?;
221
222 if self.usage.incremental_bytes_per_protocol
223 > self.budget.max_incremental_bytes_per_protocol
224 {
225 return Err(CompositionError::BudgetExceeded {
226 artifact_id: bundle.certificate.artifact_id,
227 reason: "incremental protocol overhead".to_string(),
228 });
229 }
230
231 self.bundles.push(bundle);
232 self.usage.protocol_count = self.bundles.len();
233 Ok(())
234 }
235
236 pub fn load_bundle_session(&mut self, bundle_idx: usize) -> Result<usize, CompositionError> {
242 let bundle =
243 self.bundles
244 .get(bundle_idx)
245 .ok_or_else(|| CompositionError::BudgetExceeded {
246 artifact_id: format!("bundle/{bundle_idx}"),
247 reason: "bundle index out of range".to_string(),
248 })?;
249
250 let sid = self.vm.load_choreography(&bundle.code)?;
251 self.refresh_usage();
252 self.assert_budget(bundle_idx)?;
253 Ok(sid)
254 }
255
256 pub fn load_bundle_sessions(
262 &mut self,
263 bundle_idx: usize,
264 sessions: usize,
265 ) -> Result<Vec<usize>, CompositionError> {
266 let mut out = Vec::with_capacity(sessions);
267 for _ in 0..sessions {
268 out.push(self.load_bundle_session(bundle_idx)?);
269 }
270 Ok(out)
271 }
272
273 pub fn run(
279 &mut self,
280 handler: &dyn EffectHandler,
281 max_steps: usize,
282 ) -> Result<(), CompositionError> {
283 self.vm.run(handler, max_steps)?;
284 self.refresh_usage();
285 Ok(())
286 }
287
288 #[must_use]
290 pub fn memory_usage(&self) -> &MemoryUsage {
291 &self.usage
292 }
293
294 #[must_use]
296 pub fn bundles(&self) -> &[ProtocolBundle] {
297 &self.bundles
298 }
299
300 #[must_use]
302 pub fn vm(&self) -> &VM {
303 &self.vm
304 }
305
306 fn refresh_usage(&mut self) {
307 self.usage.session_count = self.vm.session_count();
308 self.usage.coroutine_count = self.vm.coroutine_count();
309 self.usage.protocol_count = self.bundles.len();
310 }
311
312 fn assert_budget(&self, bundle_idx: usize) -> Result<(), CompositionError> {
313 let per_coro = self.usage.bytes_per_coroutine;
314 let per_sess = self.usage.bytes_per_session;
315 let artifact_id = self
316 .bundles
317 .get(bundle_idx)
318 .map(|b| b.certificate.artifact_id.clone())
319 .unwrap_or_else(|| format!("bundle/{bundle_idx}"));
320 if per_coro > self.budget.max_bytes_per_coroutine {
321 return Err(CompositionError::BudgetExceeded {
322 artifact_id,
323 reason: "bytes_per_coroutine".to_string(),
324 });
325 }
326 if per_sess > self.budget.max_bytes_per_session {
327 return Err(CompositionError::BudgetExceeded {
328 artifact_id,
329 reason: "bytes_per_session".to_string(),
330 });
331 }
332 Ok(())
333 }
334
335 fn require_capabilities(&self, bundle: &ProtocolBundle) -> Result<(), CompositionError> {
336 let cert = &bundle.certificate;
337 let caps = &cert.theorem_pack;
338 let runtime_contracts = cert.runtime_contracts.as_ref();
339
340 match enforce_vm_runtime_gates(self.vm.config(), runtime_contracts) {
341 RuntimeGateResult::Admitted => {}
342 RuntimeGateResult::RejectedMissingContracts => {
343 return Err(CompositionError::MissingRuntimeContracts {
344 artifact_id: cert.artifact_id.clone(),
345 });
346 }
347 RuntimeGateResult::RejectedUnsupportedDeterminismProfile => {
348 return Err(CompositionError::MissingCapability {
349 artifact_id: cert.artifact_id.clone(),
350 capability: format!(
351 "determinism_profile::{:?}",
352 self.vm.config().determinism_mode
353 ),
354 });
355 }
356 }
357
358 let required_sched = match self.vm.config().sched_policy {
359 SchedPolicy::Cooperative => SchedulerCapability::Cooperative,
360 SchedPolicy::RoundRobin => SchedulerCapability::RoundRobin,
361 SchedPolicy::Priority(_) => SchedulerCapability::Priority,
362 SchedPolicy::ProgressAware => SchedulerCapability::ProgressAware,
363 };
364 if !caps.schedulers.contains(&required_sched) {
365 return Err(CompositionError::MissingCapability {
366 artifact_id: cert.artifact_id.clone(),
367 capability: format!("scheduler::{required_sched:?}"),
368 });
369 }
370
371 let required_det = match self.vm.config().determinism_mode {
372 DeterminismMode::Full => DeterminismCapability::Full,
373 DeterminismMode::ModuloEffects => DeterminismCapability::ModuloEffects,
374 DeterminismMode::ModuloCommutativity => DeterminismCapability::ModuloCommutativity,
375 DeterminismMode::Replay => DeterminismCapability::Replay,
376 };
377 if !caps.determinism.contains(&required_det) {
378 return Err(CompositionError::MissingCapability {
379 artifact_id: cert.artifact_id.clone(),
380 capability: format!("determinism::{required_det:?}"),
381 });
382 }
383 if !matches!(
384 self.vm.config().output_condition_policy,
385 OutputConditionPolicy::Disabled
386 ) && !caps.output_condition_gating
387 {
388 return Err(CompositionError::MissingCapability {
389 artifact_id: cert.artifact_id.clone(),
390 capability: "output_condition_gating".to_string(),
391 });
392 }
393 Ok(())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use std::collections::BTreeMap;
400
401 use telltale_types::{GlobalType, Label, LocalTypeR};
402
403 use super::*;
404 use crate::coroutine::Value;
405
406 #[derive(Debug, Clone, Copy)]
407 struct Noop;
408
409 impl EffectHandler for Noop {
410 fn handle_send(
411 &self,
412 _role: &str,
413 _partner: &str,
414 label: &str,
415 _state: &[Value],
416 ) -> Result<Value, String> {
417 Ok(Value::Str(label.to_string()))
418 }
419
420 fn handle_recv(
421 &self,
422 _role: &str,
423 _partner: &str,
424 _label: &str,
425 _state: &mut Vec<Value>,
426 _payload: &Value,
427 ) -> Result<(), String> {
428 Ok(())
429 }
430
431 fn handle_choose(
432 &self,
433 _role: &str,
434 _partner: &str,
435 labels: &[String],
436 _state: &[Value],
437 ) -> Result<String, String> {
438 labels
439 .first()
440 .cloned()
441 .ok_or_else(|| "no labels available".to_string())
442 }
443
444 fn step(&self, _role: &str, _state: &mut Vec<Value>) -> Result<(), String> {
445 Ok(())
446 }
447 }
448
449 fn image(label: &str) -> Arc<CodeImage> {
450 let mut local_types = BTreeMap::new();
451 local_types.insert(
452 "A".to_string(),
453 LocalTypeR::send("B", Label::new(label), LocalTypeR::End),
454 );
455 local_types.insert(
456 "B".to_string(),
457 LocalTypeR::recv("A", Label::new(label), LocalTypeR::End),
458 );
459 let global = GlobalType::send("A", "B", Label::new(label), GlobalType::End);
460 Arc::new(CodeImage::from_local_types(&local_types, &global))
461 }
462
463 #[test]
464 fn proof_carrying_admission_rejects_missing_link_ok_full() {
465 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
466 let bad = ProtocolBundle::new(
467 image("m"),
468 CompositionCertificate {
469 artifact_id: "cert/bad".to_string(),
470 link_ok_full: false,
471 theorem_pack: TheoremPackCapabilities::full(),
472 runtime_contracts: None,
473 },
474 );
475 assert!(matches!(
476 runtime.admit_bundle(bad),
477 Err(CompositionError::MissingCompatibilityProof { .. })
478 ));
479 }
480
481 #[test]
482 fn immutable_code_artifacts_are_arc_shared() {
483 let shared = image("m");
484 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
485 let b1 = ProtocolBundle::new(
486 Arc::clone(&shared),
487 CompositionCertificate {
488 artifact_id: "cert/1".to_string(),
489 link_ok_full: true,
490 theorem_pack: TheoremPackCapabilities::full(),
491 runtime_contracts: None,
492 },
493 );
494 let b2 = ProtocolBundle::new(
495 Arc::clone(&shared),
496 CompositionCertificate {
497 artifact_id: "cert/2".to_string(),
498 link_ok_full: true,
499 theorem_pack: TheoremPackCapabilities::full(),
500 runtime_contracts: None,
501 },
502 );
503 runtime.admit_bundle(b1).expect("admit b1");
504 runtime.admit_bundle(b2).expect("admit b2");
505 assert!(
506 Arc::strong_count(&shared) >= 3,
507 "bundle admission should keep shared immutable artifacts in Arc"
508 );
509 }
510
511 #[test]
512 fn composed_execution_runs_and_usage_grows_monotonically() {
513 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
514 let b = ProtocolBundle::new(
515 image("m"),
516 CompositionCertificate {
517 artifact_id: "cert/ok".to_string(),
518 link_ok_full: true,
519 theorem_pack: TheoremPackCapabilities::full(),
520 runtime_contracts: None,
521 },
522 );
523 runtime.admit_bundle(b).expect("admit");
524 runtime
525 .load_bundle_sessions(0, 8)
526 .expect("load composed sessions");
527 let usage_before = runtime.memory_usage().clone();
528 assert!(usage_before.session_count >= 8);
529 assert!(usage_before.coroutine_count >= 16);
530 runtime.run(&Noop, 512).expect("composed run");
531 let usage_after = runtime.memory_usage().clone();
532 assert!(usage_after.session_count >= usage_before.session_count);
533 assert!(usage_after.coroutine_count >= usage_before.coroutine_count);
534 }
535
536 #[test]
537 fn admission_rejects_missing_scheduler_profile_capability() {
538 let cfg = VMConfig {
539 sched_policy: SchedPolicy::ProgressAware,
540 ..VMConfig::default()
541 };
542 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
543 let bundle = ProtocolBundle::new(
544 image("m"),
545 CompositionCertificate {
546 artifact_id: "cert/no-sched".to_string(),
547 link_ok_full: true,
548 theorem_pack: TheoremPackCapabilities {
549 determinism: vec![DeterminismCapability::Full],
550 schedulers: vec![SchedulerCapability::Cooperative],
551 output_condition_gating: true,
552 },
553 runtime_contracts: Some(RuntimeContracts::full()),
554 },
555 );
556
557 let err = runtime
558 .admit_bundle(bundle)
559 .expect_err("should reject bundle");
560 assert!(matches!(
561 err,
562 CompositionError::MissingCapability { capability, .. }
563 if capability == "scheduler::ProgressAware"
564 ));
565 }
566
567 #[test]
568 fn admission_rejects_missing_determinism_capability() {
569 let cfg = VMConfig {
570 determinism_mode: DeterminismMode::ModuloCommutativity,
571 ..VMConfig::default()
572 };
573 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
574 let bundle = ProtocolBundle::new(
575 image("m"),
576 CompositionCertificate {
577 artifact_id: "cert/no-det".to_string(),
578 link_ok_full: true,
579 theorem_pack: TheoremPackCapabilities {
580 determinism: vec![DeterminismCapability::Full],
581 schedulers: vec![SchedulerCapability::Cooperative],
582 output_condition_gating: true,
583 },
584 runtime_contracts: Some(RuntimeContracts::full()),
585 },
586 );
587
588 let err = runtime
589 .admit_bundle(bundle)
590 .expect_err("should reject bundle");
591 assert!(matches!(
592 err,
593 CompositionError::MissingCapability { capability, .. }
594 if capability == "determinism::ModuloCommutativity"
595 ));
596 }
597
598 #[test]
599 fn admission_rejects_missing_output_condition_capability() {
600 let cfg = VMConfig {
601 output_condition_policy: OutputConditionPolicy::AllowAll,
602 ..VMConfig::default()
603 };
604 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
605 let bundle = ProtocolBundle::new(
606 image("m"),
607 CompositionCertificate {
608 artifact_id: "cert/no-output-gate".to_string(),
609 link_ok_full: true,
610 theorem_pack: TheoremPackCapabilities {
611 determinism: vec![DeterminismCapability::Full],
612 schedulers: vec![SchedulerCapability::Cooperative],
613 output_condition_gating: false,
614 },
615 runtime_contracts: None,
616 },
617 );
618
619 let err = runtime
620 .admit_bundle(bundle)
621 .expect_err("should reject bundle");
622 assert!(matches!(
623 err,
624 CompositionError::MissingCapability { capability, .. }
625 if capability == "output_condition_gating"
626 ));
627 }
628
629 #[test]
630 fn admission_accepts_when_required_capabilities_present() {
631 let cfg = VMConfig {
632 sched_policy: SchedPolicy::RoundRobin,
633 determinism_mode: DeterminismMode::ModuloEffects,
634 output_condition_policy: OutputConditionPolicy::PredicateAllowList(vec![
635 "vm.observable_output".to_string(),
636 ]),
637 ..VMConfig::default()
638 };
639 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
640 let bundle = ProtocolBundle::new(
641 image("m"),
642 CompositionCertificate {
643 artifact_id: "cert/full".to_string(),
644 link_ok_full: true,
645 theorem_pack: TheoremPackCapabilities::full(),
646 runtime_contracts: Some(RuntimeContracts::full()),
647 },
648 );
649 runtime
650 .admit_bundle(bundle)
651 .expect("bundle should be admitted");
652 }
653
654 #[test]
655 fn admission_accepts_minimal_required_capabilities_without_full_parity() {
656 let cfg = VMConfig::default();
657 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
658 let bundle = ProtocolBundle::new(
659 image("m"),
660 CompositionCertificate {
661 artifact_id: "cert/minimal-required".to_string(),
662 link_ok_full: true,
663 theorem_pack: TheoremPackCapabilities {
664 determinism: vec![DeterminismCapability::Full],
665 schedulers: vec![SchedulerCapability::Cooperative],
666 output_condition_gating: true,
667 },
668 runtime_contracts: None,
669 },
670 );
671 runtime
672 .admit_bundle(bundle)
673 .expect("minimal required capabilities should be sufficient");
674 }
675
676 #[test]
677 fn admission_rejects_advanced_mode_without_runtime_contracts() {
678 let cfg = VMConfig {
679 sched_policy: SchedPolicy::RoundRobin,
680 ..VMConfig::default()
681 };
682 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
683 let bundle = ProtocolBundle::new(
684 image("m"),
685 CompositionCertificate {
686 artifact_id: "cert/no-runtime-contracts".to_string(),
687 link_ok_full: true,
688 theorem_pack: TheoremPackCapabilities::full(),
689 runtime_contracts: None,
690 },
691 );
692 let err = runtime
693 .admit_bundle(bundle)
694 .expect_err("advanced mode should reject missing runtime contracts");
695 assert!(matches!(
696 err,
697 CompositionError::MissingRuntimeContracts { .. }
698 ));
699 }
700
701 #[test]
702 fn admission_rejects_replay_profile_without_mixed_profile_gate() {
703 let cfg = VMConfig {
704 determinism_mode: DeterminismMode::Replay,
705 ..VMConfig::default()
706 };
707 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
708 let mut contracts = RuntimeContracts::full();
709 contracts.can_use_mixed_determinism_profiles = false;
710 let bundle = ProtocolBundle::new(
711 image("m"),
712 CompositionCertificate {
713 artifact_id: "cert/no-mixed-profile-gate".to_string(),
714 link_ok_full: true,
715 theorem_pack: TheoremPackCapabilities::full(),
716 runtime_contracts: Some(contracts),
717 },
718 );
719 let err = runtime
720 .admit_bundle(bundle)
721 .expect_err("replay profile should require mixed-profile gate");
722 assert!(matches!(
723 err,
724 CompositionError::MissingCapability { capability, .. }
725 if capability == "determinism_profile::Replay"
726 ));
727 }
728
729 #[test]
730 fn admission_accepts_replay_profile_with_contracts_and_capability() {
731 let cfg = VMConfig {
732 determinism_mode: DeterminismMode::Replay,
733 ..VMConfig::default()
734 };
735 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
736 let bundle = ProtocolBundle::new(
737 image("m"),
738 CompositionCertificate {
739 artifact_id: "cert/replay-ok".to_string(),
740 link_ok_full: true,
741 theorem_pack: TheoremPackCapabilities::full(),
742 runtime_contracts: Some(RuntimeContracts::full()),
743 },
744 );
745 runtime
746 .admit_bundle(bundle)
747 .expect("replay profile should admit with matching contracts");
748 }
749}