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 use crate::effect::{EffectFailure, EffectResult};
406
407 #[derive(Debug, Clone, Copy)]
408 struct Noop;
409
410 impl EffectHandler for Noop {
411 fn handle_send(
412 &self,
413 _role: &str,
414 _partner: &str,
415 label: &str,
416 _state: &[Value],
417 ) -> EffectResult<Value> {
418 EffectResult::success(Value::Str(label.to_string()))
419 }
420
421 fn handle_recv(
422 &self,
423 _role: &str,
424 _partner: &str,
425 _label: &str,
426 _state: &mut Vec<Value>,
427 _payload: &Value,
428 ) -> EffectResult<()> {
429 EffectResult::success(())
430 }
431
432 fn handle_choose(
433 &self,
434 _role: &str,
435 _partner: &str,
436 labels: &[String],
437 _state: &[Value],
438 ) -> EffectResult<String> {
439 match labels.first().cloned() {
440 Some(label) => EffectResult::success(label),
441 None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
442 }
443 }
444
445 fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
446 EffectResult::success(())
447 }
448 }
449
450 fn image(label: &str) -> Arc<CodeImage> {
451 let mut local_types = BTreeMap::new();
452 local_types.insert(
453 "A".to_string(),
454 LocalTypeR::send("B", Label::new(label), LocalTypeR::End),
455 );
456 local_types.insert(
457 "B".to_string(),
458 LocalTypeR::recv("A", Label::new(label), LocalTypeR::End),
459 );
460 let global = GlobalType::send("A", "B", Label::new(label), GlobalType::End);
461 Arc::new(CodeImage::from_local_types(&local_types, &global))
462 }
463
464 #[test]
465 fn proof_carrying_admission_rejects_missing_link_ok_full() {
466 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
467 let bad = ProtocolBundle::new(
468 image("m"),
469 CompositionCertificate {
470 artifact_id: "cert/bad".to_string(),
471 link_ok_full: false,
472 theorem_pack: TheoremPackCapabilities::full(),
473 runtime_contracts: None,
474 },
475 );
476 assert!(matches!(
477 runtime.admit_bundle(bad),
478 Err(CompositionError::MissingCompatibilityProof { .. })
479 ));
480 }
481
482 #[test]
483 fn immutable_code_artifacts_are_arc_shared() {
484 let shared = image("m");
485 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
486 let b1 = ProtocolBundle::new(
487 Arc::clone(&shared),
488 CompositionCertificate {
489 artifact_id: "cert/1".to_string(),
490 link_ok_full: true,
491 theorem_pack: TheoremPackCapabilities::full(),
492 runtime_contracts: None,
493 },
494 );
495 let b2 = ProtocolBundle::new(
496 Arc::clone(&shared),
497 CompositionCertificate {
498 artifact_id: "cert/2".to_string(),
499 link_ok_full: true,
500 theorem_pack: TheoremPackCapabilities::full(),
501 runtime_contracts: None,
502 },
503 );
504 runtime.admit_bundle(b1).expect("admit b1");
505 runtime.admit_bundle(b2).expect("admit b2");
506 assert!(
507 Arc::strong_count(&shared) >= 3,
508 "bundle admission should keep shared immutable artifacts in Arc"
509 );
510 }
511
512 #[test]
513 fn composed_execution_runs_and_usage_grows_monotonically() {
514 let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
515 let b = ProtocolBundle::new(
516 image("m"),
517 CompositionCertificate {
518 artifact_id: "cert/ok".to_string(),
519 link_ok_full: true,
520 theorem_pack: TheoremPackCapabilities::full(),
521 runtime_contracts: None,
522 },
523 );
524 runtime.admit_bundle(b).expect("admit");
525 runtime
526 .load_bundle_sessions(0, 8)
527 .expect("load composed sessions");
528 let usage_before = runtime.memory_usage().clone();
529 assert!(usage_before.session_count >= 8);
530 assert!(usage_before.coroutine_count >= 16);
531 runtime.run(&Noop, 512).expect("composed run");
532 let usage_after = runtime.memory_usage().clone();
533 assert!(usage_after.session_count >= usage_before.session_count);
534 assert!(usage_after.coroutine_count >= usage_before.coroutine_count);
535 }
536
537 #[test]
538 fn admission_rejects_missing_scheduler_profile_capability() {
539 let cfg = VMConfig {
540 sched_policy: SchedPolicy::ProgressAware,
541 ..VMConfig::default()
542 };
543 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
544 let bundle = ProtocolBundle::new(
545 image("m"),
546 CompositionCertificate {
547 artifact_id: "cert/no-sched".to_string(),
548 link_ok_full: true,
549 theorem_pack: TheoremPackCapabilities {
550 determinism: vec![DeterminismCapability::Full],
551 schedulers: vec![SchedulerCapability::Cooperative],
552 output_condition_gating: true,
553 },
554 runtime_contracts: Some(RuntimeContracts::full()),
555 },
556 );
557
558 let err = runtime
559 .admit_bundle(bundle)
560 .expect_err("should reject bundle");
561 assert!(matches!(
562 err,
563 CompositionError::MissingCapability { capability, .. }
564 if capability == "scheduler::ProgressAware"
565 ));
566 }
567
568 #[test]
569 fn admission_rejects_missing_determinism_capability() {
570 let cfg = VMConfig {
571 determinism_mode: DeterminismMode::ModuloCommutativity,
572 ..VMConfig::default()
573 };
574 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
575 let bundle = ProtocolBundle::new(
576 image("m"),
577 CompositionCertificate {
578 artifact_id: "cert/no-det".to_string(),
579 link_ok_full: true,
580 theorem_pack: TheoremPackCapabilities {
581 determinism: vec![DeterminismCapability::Full],
582 schedulers: vec![SchedulerCapability::Cooperative],
583 output_condition_gating: true,
584 },
585 runtime_contracts: Some(RuntimeContracts::full()),
586 },
587 );
588
589 let err = runtime
590 .admit_bundle(bundle)
591 .expect_err("should reject bundle");
592 assert!(matches!(
593 err,
594 CompositionError::MissingCapability { capability, .. }
595 if capability == "determinism::ModuloCommutativity"
596 ));
597 }
598
599 #[test]
600 fn admission_rejects_missing_output_condition_capability() {
601 let cfg = VMConfig {
602 output_condition_policy: OutputConditionPolicy::AllowAll,
603 ..VMConfig::default()
604 };
605 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
606 let bundle = ProtocolBundle::new(
607 image("m"),
608 CompositionCertificate {
609 artifact_id: "cert/no-output-gate".to_string(),
610 link_ok_full: true,
611 theorem_pack: TheoremPackCapabilities {
612 determinism: vec![DeterminismCapability::Full],
613 schedulers: vec![SchedulerCapability::Cooperative],
614 output_condition_gating: false,
615 },
616 runtime_contracts: None,
617 },
618 );
619
620 let err = runtime
621 .admit_bundle(bundle)
622 .expect_err("should reject bundle");
623 assert!(matches!(
624 err,
625 CompositionError::MissingCapability { capability, .. }
626 if capability == "output_condition_gating"
627 ));
628 }
629
630 #[test]
631 fn admission_accepts_when_required_capabilities_present() {
632 let cfg = VMConfig {
633 sched_policy: SchedPolicy::RoundRobin,
634 determinism_mode: DeterminismMode::ModuloEffects,
635 output_condition_policy: OutputConditionPolicy::PredicateAllowList(vec![
636 "vm.observable_output".to_string(),
637 ]),
638 ..VMConfig::default()
639 };
640 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
641 let bundle = ProtocolBundle::new(
642 image("m"),
643 CompositionCertificate {
644 artifact_id: "cert/full".to_string(),
645 link_ok_full: true,
646 theorem_pack: TheoremPackCapabilities::full(),
647 runtime_contracts: Some(RuntimeContracts::full()),
648 },
649 );
650 runtime
651 .admit_bundle(bundle)
652 .expect("bundle should be admitted");
653 }
654
655 #[test]
656 fn admission_accepts_minimal_required_capabilities_without_full_parity() {
657 let cfg = VMConfig::default();
658 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
659 let bundle = ProtocolBundle::new(
660 image("m"),
661 CompositionCertificate {
662 artifact_id: "cert/minimal-required".to_string(),
663 link_ok_full: true,
664 theorem_pack: TheoremPackCapabilities {
665 determinism: vec![DeterminismCapability::Full],
666 schedulers: vec![SchedulerCapability::Cooperative],
667 output_condition_gating: true,
668 },
669 runtime_contracts: None,
670 },
671 );
672 runtime
673 .admit_bundle(bundle)
674 .expect("minimal required capabilities should be sufficient");
675 }
676
677 #[test]
678 fn admission_rejects_advanced_mode_without_runtime_contracts() {
679 let cfg = VMConfig {
680 sched_policy: SchedPolicy::RoundRobin,
681 ..VMConfig::default()
682 };
683 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
684 let bundle = ProtocolBundle::new(
685 image("m"),
686 CompositionCertificate {
687 artifact_id: "cert/no-runtime-contracts".to_string(),
688 link_ok_full: true,
689 theorem_pack: TheoremPackCapabilities::full(),
690 runtime_contracts: None,
691 },
692 );
693 let err = runtime
694 .admit_bundle(bundle)
695 .expect_err("advanced mode should reject missing runtime contracts");
696 assert!(matches!(
697 err,
698 CompositionError::MissingRuntimeContracts { .. }
699 ));
700 }
701
702 #[test]
703 fn admission_rejects_replay_profile_without_mixed_profile_gate() {
704 let cfg = VMConfig {
705 determinism_mode: DeterminismMode::Replay,
706 ..VMConfig::default()
707 };
708 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
709 let mut contracts = RuntimeContracts::full();
710 contracts.can_use_mixed_determinism_profiles = false;
711 let bundle = ProtocolBundle::new(
712 image("m"),
713 CompositionCertificate {
714 artifact_id: "cert/no-mixed-profile-gate".to_string(),
715 link_ok_full: true,
716 theorem_pack: TheoremPackCapabilities::full(),
717 runtime_contracts: Some(contracts),
718 },
719 );
720 let err = runtime
721 .admit_bundle(bundle)
722 .expect_err("replay profile should require mixed-profile gate");
723 assert!(matches!(
724 err,
725 CompositionError::MissingCapability { capability, .. }
726 if capability == "determinism_profile::Replay"
727 ));
728 }
729
730 #[test]
731 fn admission_accepts_replay_profile_with_contracts_and_capability() {
732 let cfg = VMConfig {
733 determinism_mode: DeterminismMode::Replay,
734 ..VMConfig::default()
735 };
736 let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
737 let bundle = ProtocolBundle::new(
738 image("m"),
739 CompositionCertificate {
740 artifact_id: "cert/replay-ok".to_string(),
741 link_ok_full: true,
742 theorem_pack: TheoremPackCapabilities::full(),
743 runtime_contracts: Some(RuntimeContracts::full()),
744 },
745 );
746 runtime
747 .admit_bundle(bundle)
748 .expect("replay profile should admit with matching contracts");
749 }
750}