1use core::fmt;
10
11use crate::model::{InputBinding, OutputBinding};
12use crate::{AuthorizedPlan, Effect, ExecutionRealm, GraphId, NodeId, Partiality, PlanId, StepId};
13
14pub const MAX_STATIC_STEP_INPUTS: usize = 32;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct McuArenaRequirements {
21 pub byte_arena: u64,
22 pub value_slots: usize,
23 pub invocation_slots: usize,
24 pub max_step_inputs: usize,
25 pub max_step_outputs: usize,
26 pub attempt_slots: usize,
27 pub terminal_slots: usize,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum McuPlanError {
32 WrongRealm(ExecutionRealm),
33 UnsupportedEffect(crate::StepId, Effect),
34 UnboundedPartialOutput(crate::StepId),
35 HostResource(crate::StepId),
36 StatefulPlan,
37 HierarchicalPlan(crate::StepId),
38 SizeOverflow,
39}
40
41impl fmt::Display for McuPlanError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{self:?}")
44 }
45}
46
47#[cfg(feature = "std")]
48impl std::error::Error for McuPlanError {}
49
50impl AuthorizedPlan {
51 pub fn mcu_arena_requirements(&self) -> Result<McuArenaRequirements, McuPlanError> {
54 if self.realm != ExecutionRealm::McuAot {
55 return Err(McuPlanError::WrongRealm(self.realm));
56 }
57 if self.persistent_state_bytes != 0 || !self.feedback.is_empty() || self.session.is_some() {
58 return Err(McuPlanError::StatefulPlan);
59 }
60 let mut max_step_inputs = 0usize;
61 let mut max_step_outputs = 0usize;
62 let mut terminal_slots = 0usize;
63 for step in &self.nodes {
64 if !matches!(step.effect, Effect::Pure | Effect::Idempotent) {
65 return Err(McuPlanError::UnsupportedEffect(step.id, step.effect));
66 }
67 if step.partiality != Partiality::Atomic {
68 return Err(McuPlanError::UnboundedPartialOutput(step.id));
69 }
70 if step.resources.threads != 1 || step.resources.device.is_some() {
71 return Err(McuPlanError::HostResource(step.id));
72 }
73 if step.state.scope != crate::StateScope::Stateless {
74 return Err(McuPlanError::StatefulPlan);
75 }
76 if !step.subgraph_path.is_empty() {
77 return Err(McuPlanError::HierarchicalPlan(step.id));
78 }
79 max_step_inputs = max_step_inputs.max(step.input_bindings.len());
80 max_step_outputs = max_step_outputs.max(step.output_bindings.len());
81 terminal_slots = terminal_slots
82 .checked_add(
83 step.output_bindings
84 .iter()
85 .filter(|binding| matches!(binding, crate::OutputBinding::Terminal))
86 .count(),
87 )
88 .ok_or(McuPlanError::SizeOverflow)?;
89 }
90 Ok(McuArenaRequirements {
91 byte_arena: self.peak_bytes,
92 value_slots: self.buffers.len(),
93 invocation_slots: self.invocation_ports.len(),
94 max_step_inputs,
95 max_step_outputs,
96 attempt_slots: self.nodes.len(),
97 terminal_slots,
98 })
99 }
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum StaticExecutionError {
107 WrongRealm(ExecutionRealm),
109 NotFirmwareSubset(McuPlanError),
111 ArenaTooSmall,
113 FanInTooWide(StepId),
115 NonDenseBuffers,
117 MissingBuffer(StepId),
119 MissingInvocation(StepId),
121 OutputArity(StepId),
123 KernelFault(StepId),
125}
126
127impl fmt::Display for StaticExecutionError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "{self:?}")
130 }
131}
132
133#[cfg(feature = "std")]
134impl std::error::Error for StaticExecutionError {}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub struct StaticReceipt {
140 pub invocation_id: [u8; 32],
141 pub graph_id: GraphId,
142 pub plan_id: PlanId,
143 pub realm: ExecutionRealm,
144 pub completed_steps: u32,
145 pub terminal_values: usize,
146 pub last_step: Option<NodeId>,
147}
148
149pub struct StaticArenas<'a, V> {
153 pub values: &'a mut [Option<V>],
155 pub terminals: &'a mut [Option<V>],
157 pub output_scratch: &'a mut [V],
159 pub invocation: &'a [Option<V>],
161}
162
163pub trait StaticKernel {
166 type Value: Clone;
167
168 fn execute(
172 &mut self,
173 node: &crate::CompiledNode,
174 inputs: &[Option<&Self::Value>],
175 outputs: &mut [Self::Value],
176 ) -> Result<(), StaticExecutionError>;
177}
178
179pub struct StaticExecutor;
185
186impl StaticExecutor {
187 pub fn execute<V, K>(
197 plan: &AuthorizedPlan,
198 requirements: &McuArenaRequirements,
199 invocation_id: [u8; 32],
200 arenas: &mut StaticArenas<'_, V>,
201 kernel: &mut K,
202 ) -> Result<StaticReceipt, StaticExecutionError>
203 where
204 V: Clone + Default,
205 K: StaticKernel<Value = V>,
206 {
207 let values = &mut *arenas.values;
210 let terminals = &mut *arenas.terminals;
211 let output_scratch = &mut *arenas.output_scratch;
212 let invocation: &[Option<V>] = arenas.invocation;
213 if plan.realm != ExecutionRealm::McuAot {
214 return Err(StaticExecutionError::WrongRealm(plan.realm));
215 }
216 let checked = plan
219 .mcu_arena_requirements()
220 .map_err(StaticExecutionError::NotFirmwareSubset)?;
221 if &checked != requirements {
222 return Err(StaticExecutionError::NotFirmwareSubset(
223 McuPlanError::SizeOverflow,
224 ));
225 }
226 if values.len() < requirements.value_slots
227 || terminals.len() < requirements.terminal_slots
228 || output_scratch.len() < requirements.max_step_outputs
229 || invocation.len() < requirements.invocation_slots
230 {
231 return Err(StaticExecutionError::ArenaTooSmall);
232 }
233 if requirements.max_step_inputs > MAX_STATIC_STEP_INPUTS {
234 return Err(StaticExecutionError::FanInTooWide(
236 plan.nodes
237 .iter()
238 .find(|step| step.input_bindings.len() > MAX_STATIC_STEP_INPUTS)
239 .map_or(StepId(0), |step| step.id),
240 ));
241 }
242 for (index, buffer) in plan.buffers.iter().enumerate() {
245 if buffer.id.0 as usize != index {
246 return Err(StaticExecutionError::NonDenseBuffers);
247 }
248 }
249 for slot in values.iter_mut().take(requirements.value_slots) {
252 *slot = None;
253 }
254 let mut terminal_cursor = 0usize;
255 let mut completed_steps = 0u32;
256 let mut last_step = None;
257
258 for step in &plan.nodes {
259 let mut gathered: [Option<&V>; MAX_STATIC_STEP_INPUTS] =
262 [const { None }; MAX_STATIC_STEP_INPUTS];
263 let input_count = step.input_bindings.len();
264 {
265 for (slot, binding) in gathered.iter_mut().zip(&step.input_bindings) {
266 *slot = match binding {
267 InputBinding::Absent => None,
268 InputBinding::Buffer(buffer) => {
269 let value = values
270 .get(buffer.0 as usize)
271 .and_then(Option::as_ref)
272 .ok_or(StaticExecutionError::MissingBuffer(step.id))?;
273 Some(value)
274 }
275 InputBinding::Invocation(port) => {
276 let value = invocation
277 .get(*port as usize)
278 .and_then(Option::as_ref)
279 .ok_or(StaticExecutionError::MissingInvocation(step.id))?;
280 Some(value)
281 }
282 InputBinding::Feedback(_) => {
283 return Err(StaticExecutionError::NotFirmwareSubset(
286 McuPlanError::StatefulPlan,
287 ));
288 }
289 };
290 }
291 let output_count = step.output_bindings.len();
292 let outputs = &mut output_scratch[..output_count];
293 kernel
294 .execute(step, &gathered[..input_count], outputs)
295 .map_err(|_| StaticExecutionError::KernelFault(step.id))?;
296 }
297 for (index, binding) in step.output_bindings.iter().enumerate() {
301 let produced = output_scratch
302 .get(index)
303 .ok_or(StaticExecutionError::OutputArity(step.id))?
304 .clone();
305 match binding {
306 OutputBinding::Buffer(buffer) => {
307 let slot = values
308 .get_mut(buffer.0 as usize)
309 .ok_or(StaticExecutionError::MissingBuffer(step.id))?;
310 *slot = Some(produced);
311 }
312 OutputBinding::Terminal => {
313 let slot = terminals
314 .get_mut(terminal_cursor)
315 .ok_or(StaticExecutionError::ArenaTooSmall)?;
316 *slot = Some(produced);
317 terminal_cursor += 1;
318 }
319 }
320 }
321 for binding in &step.input_bindings {
324 if let InputBinding::Buffer(buffer) = binding
325 && plan
326 .buffers
327 .get(buffer.0 as usize)
328 .is_some_and(|plan_buffer| plan_buffer.last_consumer == step.id)
329 && let Some(slot) = values.get_mut(buffer.0 as usize)
330 {
331 *slot = None;
332 }
333 }
334 completed_steps += 1;
335 last_step = step.semantic_nodes.last().copied().or(last_step);
336 }
337
338 Ok(StaticReceipt {
339 invocation_id,
340 graph_id: plan.graph_id,
341 plan_id: plan.plan_id,
342 realm: plan.realm,
343 completed_steps,
344 terminal_values: terminal_cursor,
345 last_step,
346 })
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use alloc::collections::BTreeMap;
353 use alloc::vec;
354
355 use crate::{
356 AuthorizedPlan, CompiledNode, CompiledPlan, Determinism, FailureContract, GraphId,
357 ImplementationId, KernelId, NodeId, NodeTypeRef, OutputBinding, Partiality, PlanId,
358 ResourceEnvelope, StepId,
359 };
360
361 use super::*;
362
363 #[test]
364 fn authorized_mcu_plan_exposes_exact_fixed_arena_shape() {
365 let mut plan = CompiledPlan {
366 schema_version: 3,
367 graph_id: GraphId([1; 32]),
368 plan_id: PlanId([0; 32]),
369 realm: ExecutionRealm::McuAot,
370 order: vec![NodeId(0)],
371 nodes: vec![CompiledNode {
372 id: StepId(0),
373 semantic_nodes: vec![NodeId(0)],
374 semantic_types: vec![NodeTypeRef {
375 type_name: "test".into(),
376 version: 1,
377 }],
378 semantic_configs: vec![BTreeMap::new()],
379 kernel: KernelId(0),
380 implementation_id: ImplementationId([2; 32]),
381 resources: ResourceEnvelope::bounded(0, 0, 1),
382 determinism: Determinism::BitExact,
383 lowering: "static".into(),
384 conversion: None,
385 input_ports: vec![],
386 output_ports: vec!["out".into()],
387 input_contracts: vec![],
388 output_contracts: vec![crate::CompiledPortContract::opaque(
389 "out",
390 "test",
391 crate::Layout::Canonical,
392 1,
393 )],
394 input_bindings: vec![],
395 output_bindings: vec![OutputBinding::Terminal],
396 partiality: Partiality::Atomic,
397 failure: FailureContract { domains: vec![] },
398 effect: Effect::Pure,
399 retry_limit: 0,
400 state: crate::StateContract::stateless(),
401 subgraph_path: vec![],
402 }],
403 buffers: vec![],
404 feedback: vec![],
405 invocation_ports: vec![],
406 propagated_proofs: vec![],
407 propagated_policy: vec![],
408 resulting_fidelity: u16::MAX,
409 peak_bytes: 128,
410 persistent_state_bytes: 0,
411 session: None,
412 };
413 plan.plan_id = PlanId(crate::compile::hash_plan(&plan));
414 let requirements = AuthorizedPlan::new(plan).mcu_arena_requirements().unwrap();
415 assert_eq!(requirements.byte_arena, 128);
416 assert_eq!(requirements.attempt_slots, 1);
417 assert_eq!(requirements.terminal_slots, 1);
418 }
419
420 fn firmware_node(
421 id: u32,
422 inputs: vec::Vec<crate::BufferId>,
423 outputs: vec::Vec<crate::BufferId>,
424 ) -> CompiledNode {
425 let input_count = inputs.len();
426 let output_count = outputs.len().max(1);
427 CompiledNode {
428 id: StepId(id),
429 semantic_nodes: vec![NodeId(id)],
430 semantic_types: vec![NodeTypeRef {
431 type_name: "test".into(),
432 version: 1,
433 }],
434 semantic_configs: vec![BTreeMap::new()],
435 kernel: KernelId(id),
436 implementation_id: ImplementationId([id as u8 + 1; 32]),
437 resources: ResourceEnvelope::bounded(0, 0, 1),
438 determinism: Determinism::BitExact,
439 lowering: "static".into(),
440 conversion: None,
441 input_ports: (0..input_count).map(|i| format!("in-{i}")).collect(),
442 output_ports: if outputs.is_empty() {
443 vec!["out".into()]
444 } else {
445 (0..outputs.len()).map(|i| format!("out-{i}")).collect()
446 },
447 input_contracts: (0..input_count)
448 .map(|i| {
449 crate::CompiledPortContract::opaque(
450 format!("in-{i}"),
451 "test",
452 crate::Layout::Canonical,
453 4,
454 )
455 })
456 .collect(),
457 output_contracts: (0..output_count)
458 .map(|i| {
459 crate::CompiledPortContract::opaque(
460 if output_count == 1 {
461 "out".into()
462 } else {
463 format!("out-{i}")
464 },
465 "test",
466 crate::Layout::Canonical,
467 4,
468 )
469 })
470 .collect(),
471 input_bindings: inputs
472 .into_iter()
473 .map(crate::model::InputBinding::Buffer)
474 .collect(),
475 output_bindings: if outputs.is_empty() {
476 vec![OutputBinding::Terminal]
477 } else {
478 outputs.into_iter().map(OutputBinding::Buffer).collect()
479 },
480 partiality: Partiality::Atomic,
481 failure: FailureContract { domains: vec![] },
482 effect: Effect::Pure,
483 retry_limit: 0,
484 state: crate::StateContract::stateless(),
485 subgraph_path: vec![],
486 }
487 }
488
489 struct CountingKernel;
490
491 impl super::StaticKernel for CountingKernel {
492 type Value = u32;
493
494 fn execute(
495 &mut self,
496 _node: &CompiledNode,
497 inputs: &[Option<&u32>],
498 outputs: &mut [u32],
499 ) -> Result<(), super::StaticExecutionError> {
500 let value = inputs.iter().flatten().map(|value| **value).sum::<u32>() + 1;
501 for slot in outputs.iter_mut() {
502 *slot = value;
503 }
504 Ok(())
505 }
506 }
507
508 fn firmware_buffer(id: u32, producer: u32, last_consumer: u32) -> crate::BufferPlan {
509 crate::BufferPlan {
510 id: crate::BufferId(id),
511 layout: crate::Layout::Canonical,
512 capacity_bytes: 4,
513 producer: StepId(producer),
514 consumers: vec![StepId(last_consumer)],
515 last_consumer: StepId(last_consumer),
516 aliases: None,
517 }
518 }
519
520 #[test]
521 fn static_executor_runs_the_firmware_subset_over_caller_owned_arenas() {
522 let mut plan = CompiledPlan {
523 schema_version: 3,
524 graph_id: GraphId([7; 32]),
525 plan_id: PlanId([0; 32]),
526 realm: ExecutionRealm::McuAot,
527 order: vec![NodeId(0), NodeId(1), NodeId(2)],
528 nodes: vec![
529 firmware_node(0, vec![], vec![crate::BufferId(0)]),
530 firmware_node(1, vec![crate::BufferId(0)], vec![crate::BufferId(1)]),
531 firmware_node(2, vec![crate::BufferId(1)], vec![]),
532 ],
533 buffers: vec![firmware_buffer(0, 0, 1), firmware_buffer(1, 1, 2)],
534 feedback: vec![],
535 invocation_ports: vec![],
536 propagated_proofs: vec![],
537 propagated_policy: vec![],
538 resulting_fidelity: u16::MAX,
539 peak_bytes: 8,
540 persistent_state_bytes: 0,
541 session: None,
542 };
543 plan.plan_id = PlanId(crate::compile::hash_plan(&plan));
544 let plan = AuthorizedPlan::new(plan);
545 let requirements = plan.mcu_arena_requirements().unwrap();
546
547 let mut values: vec::Vec<Option<u32>> = vec![None; requirements.value_slots];
548 let mut terminals: vec::Vec<Option<u32>> = vec![None; requirements.terminal_slots];
549 let mut output_scratch: vec::Vec<u32> = vec![0; requirements.max_step_outputs.max(1)];
550 let invocation: vec::Vec<Option<u32>> = vec![None; requirements.invocation_slots];
551 let mut arenas = super::StaticArenas {
552 values: &mut values,
553 terminals: &mut terminals,
554 output_scratch: &mut output_scratch,
555 invocation: &invocation,
556 };
557
558 let receipt = super::StaticExecutor::execute(
559 &plan,
560 &requirements,
561 [9; 32],
562 &mut arenas,
563 &mut CountingKernel,
564 )
565 .unwrap();
566
567 assert_eq!(terminals[0], Some(3));
570 assert_eq!(receipt.completed_steps, 3);
571 assert_eq!(receipt.terminal_values, 1);
572 assert_eq!(receipt.graph_id, plan.graph_id);
573 assert_eq!(receipt.plan_id, plan.plan_id);
574 assert_eq!(receipt.realm, ExecutionRealm::McuAot);
575 assert_eq!(receipt.last_step, Some(NodeId(2)));
576 assert!(values.iter().all(Option::is_none));
578 }
579
580 #[test]
581 fn static_executor_rejects_a_host_realm_plan() {
582 let mut plan = CompiledPlan {
583 schema_version: 3,
584 graph_id: GraphId([7; 32]),
585 plan_id: PlanId([0; 32]),
586 realm: ExecutionRealm::HostStream,
587 order: vec![NodeId(0)],
588 nodes: vec![firmware_node(0, vec![], vec![])],
589 buffers: vec![],
590 feedback: vec![],
591 invocation_ports: vec![],
592 propagated_proofs: vec![],
593 propagated_policy: vec![],
594 resulting_fidelity: u16::MAX,
595 peak_bytes: 0,
596 persistent_state_bytes: 0,
597 session: None,
598 };
599 plan.plan_id = PlanId(crate::compile::hash_plan(&plan));
600 let plan = AuthorizedPlan::new(plan);
601 let requirements = McuArenaRequirements {
602 byte_arena: 0,
603 value_slots: 0,
604 invocation_slots: 0,
605 max_step_inputs: 0,
606 max_step_outputs: 1,
607 attempt_slots: 1,
608 terminal_slots: 1,
609 };
610 let mut values: vec::Vec<Option<u32>> = vec![];
611 let mut terminals: vec::Vec<Option<u32>> = vec![None];
612 let mut output_scratch: vec::Vec<u32> = vec![0];
613 let invocation: vec::Vec<Option<u32>> = vec![];
614 let mut arenas = super::StaticArenas {
615 values: &mut values,
616 terminals: &mut terminals,
617 output_scratch: &mut output_scratch,
618 invocation: &invocation,
619 };
620 let error = super::StaticExecutor::execute(
621 &plan,
622 &requirements,
623 [0; 32],
624 &mut arenas,
625 &mut CountingKernel,
626 )
627 .unwrap_err();
628 assert_eq!(
629 error,
630 super::StaticExecutionError::WrongRealm(ExecutionRealm::HostStream)
631 );
632 }
633}