Skip to main content

blut_graph_core/
mcu.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Allocation-free execution sizing for statically linked MCU executors.
3//!
4//! Graph compilation and AOT authorization may allocate on a host. Once an
5//! authorized MCU plan is installed, firmware uses these exact requirements to
6//! provision caller-owned arenas; the execution loop never needs to grow a
7//! collection or discover an undeclared bound.
8
9use core::fmt;
10
11use crate::model::{InputBinding, OutputBinding};
12use crate::{AuthorizedPlan, Effect, ExecutionRealm, GraphId, NodeId, Partiality, PlanId, StepId};
13
14/// Maximum physical fan-in a statically linked MCU step may declare. The
15/// firmware executor gathers input references on the stack, so this bound keeps
16/// the gather buffer alloc-free. Compilation already caps fan-in far below this.
17pub 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    /// Validate the firmware execution subset and return exact fixed-arena
52    /// dimensions. This method performs no allocation.
53    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/// A structured fault raised by the statically linked MCU executor. Every
103/// variant is a bounded-arena or contract violation; the executor never
104/// allocates and never panics on well-formed firmware plans.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum StaticExecutionError {
107    /// The plan was authorized for a different realm than `McuAot`.
108    WrongRealm(ExecutionRealm),
109    /// The plan is not a valid firmware subset (stateful, hierarchical, …).
110    NotFirmwareSubset(McuPlanError),
111    /// A caller-owned arena was smaller than the plan's exact requirement.
112    ArenaTooSmall,
113    /// A step declares more physical inputs than [`MAX_STATIC_STEP_INPUTS`].
114    FanInTooWide(StepId),
115    /// Buffer identities are not the dense `0..value_slots` the executor indexes.
116    NonDenseBuffers,
117    /// A step read a buffer that no prior step in topological order produced.
118    MissingBuffer(StepId),
119    /// A step read an invocation input the caller did not supply.
120    MissingInvocation(StepId),
121    /// The kernel wrote a different output count than the step declares.
122    OutputArity(StepId),
123    /// The bound kernel reported a fault for this step.
124    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/// A compact, `Copy`, allocation-free execution receipt. Firmware records only
137/// bounded scalars; the host reconstructs full attempt detail from the plan.
138#[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
149/// Caller-owned execution arenas for [`StaticExecutor::execute`]. Firmware
150/// provisions each slice once from [`McuArenaRequirements`]; the executor grows
151/// none of them.
152pub struct StaticArenas<'a, V> {
153    /// One live-buffer slot per plan buffer (`value_slots`).
154    pub values: &'a mut [Option<V>],
155    /// Collected unconnected outputs (`terminal_slots`).
156    pub terminals: &'a mut [Option<V>],
157    /// Per-step output workspace, reused each step (`max_step_outputs`).
158    pub output_scratch: &'a mut [V],
159    /// One value per invocation port (`invocation_slots`).
160    pub invocation: &'a [Option<V>],
161}
162
163/// A statically linked firmware kernel. Unlike [`crate::KernelExecutor`], it
164/// writes into a caller-owned output slice and never allocates.
165pub trait StaticKernel {
166    type Value: Clone;
167
168    /// Execute one step from immutable input references, writing exactly
169    /// `outputs.len()` values into `outputs` (already sized to the step's
170    /// physical output arity). Returning `Err` aborts the plan.
171    fn execute(
172        &mut self,
173        node: &crate::CompiledNode,
174        inputs: &[Option<&Self::Value>],
175        outputs: &mut [Self::Value],
176    ) -> Result<(), StaticExecutionError>;
177}
178
179/// The distinct, allocation-free execution engine for authorized firmware
180/// plans. It consumes the exact [`McuArenaRequirements`] and executes over
181/// caller-owned arenas, growing no collection and touching no allocator. This
182/// is the firmware counterpart to the host [`crate::PlanExecutor`]; both drive
183/// the same canonical [`AuthorizedPlan`] to an identity-stable receipt.
184pub struct StaticExecutor;
185
186impl StaticExecutor {
187    /// Execute `plan` on `McuAot` over caller-owned arenas.
188    ///
189    /// * `values` holds one live-buffer slot per plan buffer (`value_slots`).
190    /// * `terminals` collects unconnected outputs (`terminal_slots`).
191    /// * `output_scratch` is reused per step (`max_step_outputs`).
192    /// * `invocation` supplies one value per invocation port (`invocation_slots`).
193    ///
194    /// All four are sized by [`AuthorizedPlan::mcu_arena_requirements`]. The
195    /// method performs no allocation.
196    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        // Disjoint field reborrows keep the executor body allocation- and
208        // alias-free while presenting one bundled arena argument.
209        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        // Re-validate the firmware subset from the plan itself; never trust the
217        // caller-supplied requirements without binding them to this plan.
218        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            // The plan's widest step exceeds the stack gather bound.
235            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        // The executor indexes buffers by position; require dense identities so
243        // no lookup map (and thus no allocation) is ever needed.
244        for (index, buffer) in plan.buffers.iter().enumerate() {
245            if buffer.id.0 as usize != index {
246                return Err(StaticExecutionError::NonDenseBuffers);
247            }
248        }
249        // A produced-buffer bitmap over the fixed value arena, tracked without
250        // allocation by reusing `Option::is_some` on the slots themselves.
251        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            // Gather immutable input references on the stack. Scoped so the
260            // borrow of `values` ends before outputs are written back.
261            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                            // `mcu_arena_requirements` already rejects stateful
284                            // plans; feedback can never reach a firmware step.
285                            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            // Write results back into the fixed arenas. `output_scratch` is a
298            // separate slice, so this mutable borrow of `values` does not alias
299            // the input gather above.
300            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            // Release buffers whose last consumer is this step, mirroring the
322            // host executor's liveness rule so peak occupancy stays bounded.
323            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        // source=1 -> process=2 -> sink=3 (terminal), identical to the host
568        // reference executor over the same canonical plan.
569        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        // Liveness release: no buffer slot remains occupied after the run.
577        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}