Skip to main content

sim_lib_machine/
driver.rs

1use sim_kernel::ContentId;
2use sim_lib_control::{CleanupStack, Unwind, WorkLimit};
3
4use crate::{
5    AdmissionPolicy, CodeCursor, Frame, FrameStack, FrameStackError, InstructionPolicy,
6    MachineDescription, MachinePermit, ManagedRootSource, RootScanError, RootSnapshot,
7    SourceLocation, ValueWidthPolicy,
8};
9
10/// The cursor access needed by the neutral driver.
11///
12/// Consumers may use [`Frame`] or provide a distinct activation record.
13pub trait MachineFrame {
14    /// Returns the instruction to execute next.
15    fn cursor(&self) -> CodeCursor;
16
17    /// Selects the next validated instruction boundary.
18    fn set_cursor(&mut self, cursor: CodeCursor);
19}
20
21impl<P, K, R, H> MachineFrame for Frame<P, K, R, H>
22where
23    P: ValueWidthPolicy,
24{
25    fn cursor(&self) -> CodeCursor {
26        self.cursor()
27    }
28
29    fn set_cursor(&mut self, cursor: CodeCursor) {
30        self.set_cursor(cursor);
31    }
32}
33
34/// The explicit result of executing exactly one decoded instruction.
35pub enum StepOutcome<F, R, A, Y, I> {
36    /// Continue in the current frame at the supplied validated cursor.
37    Continue(CodeCursor),
38    /// Push a new guest frame. This is data interpreted by the loop, not a Rust call.
39    Call(F),
40    /// Pop the current guest frame and carry consumer-defined return data.
41    Return(R),
42    /// Stop with a consumer-defined abrupt outcome.
43    Raise(A),
44    /// Cooperatively yield consumer-defined state.
45    Yield(Y),
46    /// Stop for a consumer-defined interrupt.
47    Interrupt(I),
48}
49
50/// Result type returned by one consumer instruction policy invocation.
51pub type PolicyStep<F, R, A, Y, I, E> = Result<StepOutcome<F, R, A, Y, I>, E>;
52
53/// Stable classification recorded for each charged instruction.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum StepKind {
56    /// Ordinary continuation.
57    Continue,
58    /// Guest-frame push.
59    Call,
60    /// Guest-frame pop.
61    Return,
62    /// Abrupt outcome.
63    Raise,
64    /// Cooperative yield.
65    Yield,
66    /// Interrupt.
67    Interrupt,
68}
69
70/// Deterministic evidence for the exact instruction work performed by one drive.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct WorkReceipt<Id> {
73    steps: Vec<(Id, StepKind)>,
74}
75
76impl<Id> WorkReceipt<Id> {
77    /// Returns the exact amount of charged instruction work.
78    pub fn charged(&self) -> usize {
79        self.steps.len()
80    }
81
82    /// Returns ordered instruction identities and their control outcomes.
83    pub fn steps(&self) -> &[(Id, StepKind)] {
84        &self.steps
85    }
86
87    /// Appends later work to this receipt prefix without changing its order.
88    pub fn append(&mut self, later: Self) {
89        self.steps.extend(later.steps);
90    }
91}
92
93/// Abrupt terminal reasons carried through the control organ's unwind vocabulary.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub enum MachineAbrupt<A, I> {
96    /// No prepared protected region handled a raised value.
97    Raise(A),
98    /// Execution stopped at an interruption checkpoint.
99    Interrupt(I),
100    /// The bounded drive consumed its complete allowance.
101    BudgetExhausted,
102    /// Admission, frame, or instruction execution failed closed.
103    Fault,
104}
105
106/// Control-organ reason delivered exactly once to registered machine cleanups.
107pub type MachineUnwind<R, A, I> = Unwind<R, (), (), MachineAbrupt<A, I>>;
108
109/// Content-bound evidence for a suspended continuation and its receipt prefix.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct ContinuationEvidence<Id> {
112    content_id: ContentId,
113    receipt: WorkReceipt<Id>,
114}
115
116impl<Id> ContinuationEvidence<Id> {
117    /// Returns the admitted program identity to which this continuation belongs.
118    pub fn content_id(&self) -> &ContentId {
119        &self.content_id
120    }
121
122    /// Returns all work observed before suspension.
123    pub fn receipt(&self) -> &WorkReceipt<Id> {
124        &self.receipt
125    }
126}
127
128/// Owned resumable machine state; construction requires an admitted permit.
129#[derive(Debug)]
130pub struct MachineCheckpoint<F, Id> {
131    frames: FrameStack<F>,
132    evidence: ContinuationEvidence<Id>,
133}
134
135impl<F, Id> MachineCheckpoint<F, Id> {
136    /// Binds suspended frames and their ordered receipt prefix to admitted content.
137    pub fn new(frames: FrameStack<F>, permit: &MachinePermit, receipt: WorkReceipt<Id>) -> Self {
138        Self {
139            frames,
140            evidence: ContinuationEvidence {
141                content_id: permit.content_id().clone(),
142                receipt,
143            },
144        }
145    }
146
147    /// Returns the immutable continuation evidence.
148    pub fn evidence(&self) -> &ContinuationEvidence<Id> {
149        &self.evidence
150    }
151
152    /// Resumes only when the supplied permit admits the checkpoint's exact content.
153    pub fn resume(self, permit: &MachinePermit) -> Result<(FrameStack<F>, WorkReceipt<Id>), Self> {
154        if self.evidence.content_id == *permit.content_id() {
155            Ok((self.frames, self.evidence.receipt))
156        } else {
157            Err(self)
158        }
159    }
160}
161
162/// Consumer semantics invoked once per charged instruction.
163///
164/// A WebAssembly engine or a bytecode-independent workflow machine can supply
165/// this policy. The callback cannot run until admission identity is checked.
166pub trait InstructionDriverPolicy<P, F>
167where
168    P: InstructionPolicy,
169{
170    /// Return transfer data.
171    type Return;
172    /// Abrupt outcome.
173    type Abrupt;
174    /// Cooperative yield data.
175    type Yield;
176    /// Interrupt data.
177    type Interrupt;
178    /// Instruction failure.
179    type Fault;
180
181    /// Executes one instruction policy and returns an explicit control outcome.
182    #[allow(
183        clippy::type_complexity,
184        reason = "the explicit outcome channels are the driver contract"
185    )]
186    fn step(
187        &mut self,
188        instruction: &P::Instruction,
189        frame: &mut F,
190    ) -> PolicyStep<F, Self::Return, Self::Abrupt, Self::Yield, Self::Interrupt, Self::Fault>;
191}
192
193/// An instruction failure paired with its stable identity and source location.
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct LocatedFault<Id, E> {
196    /// Stable identity of the failing instruction.
197    pub instruction: Id,
198    /// Exact prepared source location of the failing instruction.
199    pub location: SourceLocation,
200    /// Consumer-defined failure.
201    pub fault: E,
202}
203
204/// A completed or suspended iterative drive.
205pub enum DriveOutcome<Id, R, A, Y, I> {
206    /// Work was exhausted with explicit guest frames still runnable.
207    Continue(WorkReceipt<Id>),
208    /// The outermost guest frame returned.
209    Return(R, WorkReceipt<Id>),
210    /// An instruction raised an abrupt outcome.
211    Raise(A, WorkReceipt<Id>),
212    /// An instruction cooperatively yielded.
213    Yield(Y, WorkReceipt<Id>),
214    /// An instruction requested interruption.
215    Interrupt(I, WorkReceipt<Id>),
216}
217
218/// A refusal produced by the iterative driver.
219#[derive(Clone, Debug, PartialEq, Eq)]
220pub enum DriveError<Id, E> {
221    /// The permit does not bind the exact supplied description.
222    PermitMismatch,
223    /// No frame exists to execute.
224    EmptyFrames,
225    /// A guest call exceeded the explicit frame budget.
226    FrameLimit(FrameStackError),
227    /// Instruction policy failed at a prepared source location.
228    Fault(LocatedFault<Id, E>),
229}
230
231/// Result type returned by one bounded drive operation.
232pub type DriveResult<Id, R, A, Y, I, E> = Result<DriveOutcome<Id, R, A, Y, I>, DriveError<Id, E>>;
233
234/// A refusal from root publication or instruction execution during safepoint driving.
235#[derive(Clone, Debug, PartialEq, Eq)]
236pub enum SafepointDriveError<Id, E, S> {
237    /// The complete root view exceeded its explicit budget.
238    RootScan(RootScanError),
239    /// The consumer rejected a complete root view.
240    Safepoint(S),
241    /// Ordinary machine driving failed.
242    Drive(DriveError<Id, E>),
243}
244
245/// Bounded iterative instruction driver.
246pub struct Driver<D> {
247    policy: D,
248}
249
250impl<D> Driver<D> {
251    /// Creates a driver around consumer-owned instruction semantics.
252    pub fn new(policy: D) -> Self {
253        Self { policy }
254    }
255
256    /// Drives at most `work` instructions without recursive guest execution.
257    #[allow(
258        clippy::type_complexity,
259        reason = "the explicit outcome channels are the driver contract"
260    )]
261    pub fn drive<P, M, A, F>(
262        &mut self,
263        description: &MachineDescription<'_, P, M>,
264        permit: &MachinePermit,
265        frames: &mut FrameStack<F>,
266        work: WorkLimit,
267    ) -> DriveResult<P::InstructionId, D::Return, D::Abrupt, D::Yield, D::Interrupt, D::Fault>
268    where
269        P: InstructionPolicy,
270        P::InstructionId: Copy + Eq + Ord,
271        A: AdmissionPolicy<P, M>,
272        F: MachineFrame,
273        D: InstructionDriverPolicy<P, F>,
274    {
275        // This identity check deliberately precedes every access to `self.policy`.
276        if !permit.accepts::<P, M, A>(description) {
277            return Err(DriveError::PermitMismatch);
278        }
279        if frames.current().is_none() {
280            return Err(DriveError::EmptyFrames);
281        }
282
283        let mut steps = Vec::with_capacity(work.0);
284        for _ in 0..work.0 {
285            let cursor = frames.current().ok_or(DriveError::EmptyFrames)?.cursor();
286            let located = description.code().instruction(cursor);
287            let id = *located.id();
288            let location = located.location().clone();
289            let outcome = self
290                .policy
291                .step(
292                    located.instruction(),
293                    frames.current_mut().expect("frame was checked"),
294                )
295                .map_err(|fault| {
296                    DriveError::Fault(LocatedFault {
297                        instruction: id,
298                        location,
299                        fault,
300                    })
301                })?;
302            match outcome {
303                StepOutcome::Continue(next) => {
304                    steps.push((id, StepKind::Continue));
305                    frames
306                        .current_mut()
307                        .expect("current frame remains")
308                        .set_cursor(next);
309                }
310                StepOutcome::Call(frame) => {
311                    steps.push((id, StepKind::Call));
312                    frames.push(frame).map_err(DriveError::FrameLimit)?;
313                }
314                StepOutcome::Return(value) => {
315                    steps.push((id, StepKind::Return));
316                    frames.pop();
317                    if frames.current().is_none() {
318                        return Ok(DriveOutcome::Return(value, WorkReceipt { steps }));
319                    }
320                }
321                StepOutcome::Raise(value) => {
322                    steps.push((id, StepKind::Raise));
323                    return Ok(DriveOutcome::Raise(value, WorkReceipt { steps }));
324                }
325                StepOutcome::Yield(value) => {
326                    steps.push((id, StepKind::Yield));
327                    return Ok(DriveOutcome::Yield(value, WorkReceipt { steps }));
328                }
329                StepOutcome::Interrupt(value) => {
330                    steps.push((id, StepKind::Interrupt));
331                    return Ok(DriveOutcome::Interrupt(value, WorkReceipt { steps }));
332                }
333            }
334        }
335        Ok(DriveOutcome::Continue(WorkReceipt { steps }))
336    }
337
338    /// Drives one bounded slice, publishing a complete root view immediately
339    /// before every prepared instruction marked as a safepoint.
340    ///
341    /// The observer owns policy: this organ neither selects nor names a
342    /// reclamation implementation. Suspended caller frames remain in `frames`
343    /// and are therefore included in every view.
344    #[allow(
345        clippy::type_complexity,
346        reason = "the explicit outcome channels are the driver contract"
347    )]
348    pub fn drive_with_safepoints<P, M, A, F, S>(
349        &mut self,
350        description: &MachineDescription<'_, P, M>,
351        permit: &MachinePermit,
352        frames: &mut FrameStack<F>,
353        work: WorkLimit,
354        root_budget: WorkLimit,
355        mut observe: impl FnMut(&RootSnapshot) -> Result<(), S>,
356    ) -> Result<
357        DriveOutcome<P::InstructionId, D::Return, D::Abrupt, D::Yield, D::Interrupt>,
358        SafepointDriveError<P::InstructionId, D::Fault, S>,
359    >
360    where
361        P: InstructionPolicy,
362        P::InstructionId: Copy + Eq + Ord,
363        A: AdmissionPolicy<P, M>,
364        F: MachineFrame + ManagedRootSource,
365        D: InstructionDriverPolicy<P, F>,
366    {
367        if !permit.accepts::<P, M, A>(description) {
368            return Err(SafepointDriveError::Drive(DriveError::PermitMismatch));
369        }
370        if frames.current().is_none() {
371            return Err(SafepointDriveError::Drive(DriveError::EmptyFrames));
372        }
373
374        let mut combined = WorkReceipt { steps: Vec::new() };
375        for _ in 0..work.0 {
376            let cursor = frames
377                .current()
378                .ok_or(SafepointDriveError::Drive(DriveError::EmptyFrames))?
379                .cursor();
380            if description.code().instruction(cursor).is_safepoint() {
381                let roots = RootSnapshot::scan(frames, root_budget)
382                    .map_err(SafepointDriveError::RootScan)?;
383                observe(&roots).map_err(SafepointDriveError::Safepoint)?;
384            }
385            let outcome = self
386                .drive::<P, M, A, F>(description, permit, frames, WorkLimit(1))
387                .map_err(SafepointDriveError::Drive)?;
388            match outcome {
389                DriveOutcome::Continue(receipt) => combined.append(receipt),
390                DriveOutcome::Return(value, receipt) => {
391                    combined.append(receipt);
392                    return Ok(DriveOutcome::Return(value, combined));
393                }
394                DriveOutcome::Raise(value, receipt) => {
395                    combined.append(receipt);
396                    return Ok(DriveOutcome::Raise(value, combined));
397                }
398                DriveOutcome::Yield(value, receipt) => {
399                    combined.append(receipt);
400                    return Ok(DriveOutcome::Yield(value, combined));
401                }
402                DriveOutcome::Interrupt(value, receipt) => {
403                    combined.append(receipt);
404                    return Ok(DriveOutcome::Interrupt(value, combined));
405                }
406            }
407        }
408        Ok(DriveOutcome::Continue(combined))
409    }
410
411    /// Drives through prepared protected regions, selecting the innermost handler.
412    ///
413    /// A handled raise remains visible in the receipt, then execution continues at
414    /// the validated handler boundary. The abrupt value remains consumer-owned;
415    /// policies place it in their frame's handler state before returning `Raise`.
416    #[allow(
417        clippy::type_complexity,
418        reason = "matches the explicit driver channels"
419    )]
420    pub fn drive_protected<P, M, A, F>(
421        &mut self,
422        description: &MachineDescription<'_, P, M>,
423        permit: &MachinePermit,
424        frames: &mut FrameStack<F>,
425        work: WorkLimit,
426    ) -> DriveResult<P::InstructionId, D::Return, D::Abrupt, D::Yield, D::Interrupt, D::Fault>
427    where
428        P: InstructionPolicy,
429        P::InstructionId: Copy + Eq + Ord,
430        A: AdmissionPolicy<P, M>,
431        F: MachineFrame,
432        D: InstructionDriverPolicy<P, F>,
433    {
434        let mut combined = WorkReceipt { steps: Vec::new() };
435        let mut left = work.0;
436        while left > 0 {
437            let cursor = frames.current().ok_or(DriveError::EmptyFrames)?.cursor();
438            let outcome = self.drive::<P, M, A, F>(description, permit, frames, WorkLimit(1))?;
439            match outcome {
440                DriveOutcome::Raise(value, receipt) => {
441                    left -= receipt.charged();
442                    combined.append(receipt);
443                    if let Some(region) = description.code().innermost_protected_region(cursor) {
444                        frames
445                            .current_mut()
446                            .expect("raising frame remains")
447                            .set_cursor(region.handler);
448                    } else {
449                        return Ok(DriveOutcome::Raise(value, combined));
450                    }
451                }
452                DriveOutcome::Continue(receipt) => {
453                    left -= receipt.charged();
454                    combined.append(receipt);
455                    if left == 0 {
456                        return Ok(DriveOutcome::Continue(combined));
457                    }
458                }
459                DriveOutcome::Return(value, receipt) => {
460                    combined.append(receipt);
461                    return Ok(DriveOutcome::Return(value, combined));
462                }
463                DriveOutcome::Yield(value, receipt) => {
464                    combined.append(receipt);
465                    return Ok(DriveOutcome::Yield(value, combined));
466                }
467                DriveOutcome::Interrupt(value, receipt) => {
468                    combined.append(receipt);
469                    return Ok(DriveOutcome::Interrupt(value, combined));
470                }
471            }
472        }
473        Ok(DriveOutcome::Continue(combined))
474    }
475
476    /// Runs a protected drive and unwinds registered cleanups on every abrupt or
477    /// terminal path. Yield is a suspension and therefore retains its dynamic extent.
478    #[allow(
479        clippy::type_complexity,
480        reason = "matches the explicit driver channels"
481    )]
482    pub fn drive_with_cleanup<P, M, A, F>(
483        &mut self,
484        description: &MachineDescription<'_, P, M>,
485        permit: &MachinePermit,
486        frames: &mut FrameStack<F>,
487        work: WorkLimit,
488        cleanups: CleanupStack<MachineUnwind<D::Return, D::Abrupt, D::Interrupt>>,
489    ) -> DriveResult<P::InstructionId, D::Return, D::Abrupt, D::Yield, D::Interrupt, D::Fault>
490    where
491        P: InstructionPolicy,
492        P::InstructionId: Copy + Eq + Ord,
493        A: AdmissionPolicy<P, M>,
494        F: MachineFrame,
495        D: InstructionDriverPolicy<P, F>,
496        D::Return: Clone,
497        D::Abrupt: Clone,
498        D::Interrupt: Clone,
499    {
500        let outcome = match self.drive_protected::<P, M, A, F>(description, permit, frames, work) {
501            Ok(outcome) => outcome,
502            Err(error) => {
503                cleanups.unwind(Unwind::Exception(MachineAbrupt::Fault));
504                return Err(error);
505            }
506        };
507        match &outcome {
508            DriveOutcome::Return(value, _) => {
509                cleanups.unwind(Unwind::Return(value.clone()));
510            }
511            DriveOutcome::Raise(value, _) => {
512                cleanups.unwind(Unwind::Exception(MachineAbrupt::Raise(value.clone())));
513            }
514            DriveOutcome::Interrupt(value, _) => {
515                cleanups.unwind(Unwind::Exception(MachineAbrupt::Interrupt(value.clone())));
516            }
517            DriveOutcome::Continue(_) => {
518                cleanups.unwind(Unwind::Exception(MachineAbrupt::BudgetExhausted));
519            }
520            DriveOutcome::Yield(_, _) => return Ok(outcome),
521        }
522        Ok(outcome)
523    }
524}