Skip to main content

dataflow_rs/run/
interp.rs

1//! The interpreter: one `async fn` over a workflow's ops. Nothing else
2//! interprets steps.
3//!
4//! A built-in runs inline with no `.await`; a host call awaits through
5//! [`call_host`](super::host::call_host), and a fan-out through
6//! [`fan_out`](super::fan_out::fan_out), which runs its elements' calls
7//! before it folds them. Every completed call goes through
8//! [`decide`](super::policy::decide), the one definition of failed, rejected
9//! and halted, and then [`record`](super::record::record).
10//!
11//! A loop is two ops. `LoopInit` evaluates `over` once, writes
12//! the counter's `init`, checks the bounds, and in bounds writes the bindings
13//! and falls into the first sweep; `LoopNext` advances the counter and does
14//! the same, jumping back to the sweep's first op. The engine owns the
15//! counter: it lives in the [`Frame`], and the context only gets a copy.
16//!
17//! The run's [`Limits`] are checked every [`CHECK_EVERY`] ops and as each
18//! sweep starts, when one is set. A stop between ops ends the run with
19//! nothing recorded;
20//! one as a sweep starts, after the counter's write, with the engine's step
21//! for that write, so a trace still replays to the message's context.
22//!
23//! A workflow's `WorkflowStarted` goes to the run's observers once, when it
24//! is known to be selected: as it starts when it has no condition, or when
25//! the first evaluation of its condition admits it.
26//!
27//! A traced run's steps that are not a call's are made here: the
28//! workflow's `Skipped` step when its condition refuses it on the first
29//! judgment, one `Skipped` step per call a group's or a task's condition
30//! refuses, the engine's step as a sweep is entered or a loop exits, and,
31//! through the recorder, a workflow's own `Failed` step.
32
33use super::budget::{ConditionErrorPolicy, DispatchKind, TaskBudget};
34use super::cancel::{Limits, Stop};
35use super::error_context::ErrorContext;
36use super::fan_out::fan_out;
37use super::host::call_host;
38use super::message::{AuditEntry, AuditMode};
39use super::observers::Observers;
40use super::policy::{self, Flow};
41use super::record::{self, Completed, TaskIds};
42use super::trace::Capture;
43use crate::base::{
44    AuthoredPath, Change, Error, ErrorKind, ErrorRecord, ErrorSite, ExprCause, Instant, ProgramId,
45    SiteKind, TaskIx, WorkflowIx,
46};
47use crate::def::WorkflowFailurePolicy;
48use crate::expr::path::kind;
49use crate::expr::{Expr, ExprEngine, Path};
50use crate::function::builtins::Builtin;
51use crate::function::{CallLayer, SyncCtx, eval, test};
52use crate::observe::{Event, Selection, StepResult, TraceStep};
53use crate::program::{CallSite, Callee, CompiledWorkflow, CondKind, CondSite, LoopSite, Op};
54use crate::state::{Owner, RunContext};
55use chrono::{DateTime, Utc};
56use datavalue::{DataValue, OwnedDataValue};
57use std::borrow::Cow;
58use std::fmt;
59use std::sync::Arc;
60
61/// How one workflow's ops ended.
62#[derive(Debug)]
63pub(crate) enum Exit {
64    /// The ops ran to their end.
65    Completed,
66    /// A task, a terminal group or a `filter` ended the workflow on purpose.
67    Halted,
68    /// The ops ran to their end, past a failure `on_failure: continue`
69    /// stepped over.
70    Failed,
71    /// A failure stops the run with this error: a task's, under
72    /// `on_failure: fail` in a workflow whose own is `fail`, or a step the
73    /// interpreter refuses.
74    Stopped(Error),
75    /// The run's deadline passed or its token was cancelled: at a check
76    /// between ops, as a sweep starts, or cutting a call short. Only this
77    /// exit makes the run `TimedOut` or `Cancelled`; an `Err` of either kind
78    /// a function returned is a task's failure like any other.
79    Limited(Stop),
80}
81
82/// The loop frame: the sweep the workflow is in.
83#[derive(Debug, Default)]
84pub(crate) struct Frame {
85    /// The counter of the sweep running, stamped on its audit entries:
86    /// `None` until a loop's first sweep, so setup's entries carry none.
87    pub(crate) loop_counter: Option<i64>,
88    /// The sweeps begun: one for a workflow without a loop that ran, and for
89    /// a loop each sweep its condition admitted.
90    pub(crate) sweeps: u32,
91    /// The loop's counter, owned by the engine: the sweep's, or once the loop
92    /// is out of bounds the first counter past them.
93    counter: i64,
94    /// `loop.over`'s elements, evaluated once, after setup. Each sweep moves
95    /// its element out, so the array is never in the run arena, where a
96    /// borrow held across sweeps would forbid every checkpoint.
97    items: Option<Vec<OwnedDataValue>>,
98    /// Whether the workflow's `WorkflowStarted` went out.
99    begun: bool,
100    /// When it went out, in a timed run: where the workflow's elapsed time
101    /// starts.
102    pub(crate) started: Option<Instant>,
103}
104
105impl Frame {
106    /// Whether the ops running are a sweep's: always for a workflow without
107    /// a loop, and for a loop from its first sweep on.
108    fn in_sweep(&self, wf: &CompiledWorkflow) -> bool {
109        wf.loops.is_empty() || self.loop_counter.is_some()
110    }
111}
112
113/// What the interpreter carries through a run, beside the run context: the
114/// run's facts and the message's other parts, and the current workflow's
115/// failure flag and loop frame.
116pub(crate) struct RunEnv<'r, 'p> {
117    /// The program's identity, for a host call's `CallInfo`.
118    pub(crate) program: ProgramId,
119    /// The current workflow's position in the program.
120    pub(crate) workflow_ix: WorkflowIx,
121    /// The program's `TaskIx` of the current workflow's first task.
122    pub(crate) task_base: u32,
123    /// The channel the run was started on.
124    pub(crate) channel: Option<&'r str>,
125    /// The run's routing bucket, at most 99.
126    pub(crate) routing_bucket: Option<u8>,
127    /// The run's layers, outermost first, around every host call.
128    pub(crate) layers: &'r [Arc<dyn CallLayer>],
129    /// The run's deadline and token.
130    pub(crate) limits: Limits<'r>,
131    /// How condition evaluation errors are handled during this run.
132    pub(crate) condition_error_policy: ConditionErrorPolicy,
133    /// Shared task and expression op budget tracker for this run.
134    pub(crate) budget: &'r TaskBudget<'r>,
135    /// The ops the run has begun while a limit is set, across its
136    /// workflows: every [`CHECK_EVERY`]th checks the limits.
137    pub(crate) ops: u32,
138    /// The expression engine the program was compiled with.
139    pub(crate) exprs: &'p ExprEngine,
140    /// The message's payload.
141    pub(crate) payload: &'r OwnedDataValue,
142    /// The message's error list.
143    pub(crate) errors: &'r mut Vec<ErrorRecord>,
144    /// The message's audit trail.
145    pub(crate) audit: &'r mut Vec<AuditEntry>,
146    /// Which audit entries the message keeps.
147    pub(crate) mode: AuditMode,
148    /// Which recorder a call takes, and which changes it captures.
149    pub(crate) capture: CaptureMode,
150    /// What the run reports beyond the message, held by `run_with`.
151    pub(crate) watch: &'r mut Watch<'p>,
152    /// The run's one clock reading.
153    pub(crate) now: DateTime<Utc>,
154    /// The same reading, RFC 3339, for error records: allocated at most once
155    /// per run, on the first error record it stamps, and shared by every
156    /// record of the run.
157    pub(crate) now_text: &'r mut Option<Arc<str>>,
158    /// The changes of the call running, read by the recorder.
159    pub(crate) changes: Vec<Change>,
160    /// The current workflow's id.
161    pub(crate) workflow_id: &'p Arc<str>,
162    /// The current workflow's task ids, by task row.
163    pub(crate) task_ids: &'p [Arc<str>],
164    /// Whether `on_failure: continue` stepped past a failure in it.
165    pub(crate) failed: bool,
166    /// The current workflow's loop frame.
167    pub(crate) frame: Frame,
168}
169
170/// What a run reports beyond the message, which the interpreter reads
171/// rarely: its observers, its trace and its error context, and whether it
172/// reads the clock. One per run, held by `run_with`, so the run's future
173/// carries it once and each workflow's [`RunEnv`] one reference to it.
174pub(crate) struct Watch<'p> {
175    /// The run's observers.
176    pub(crate) observers: Observers<'p>,
177    /// Whether the run reads the clock around each call: an observer of the
178    /// task events asked for `TIMINGS`, or the trace for timings.
179    pub(crate) timed: bool,
180    /// The run's trace, when it is traced. Boxed: the run's future carries
181    /// a pointer.
182    pub(crate) trace: Option<Box<Capture>>,
183    /// Where the recorder mirrors each call's records, when the engine has
184    /// an error context.
185    pub(crate) error_context: Option<&'p ErrorContext>,
186}
187
188/// Which recorder a call takes, and which changes it captures.
189///
190/// An unwatched run has no trace, no error-context mirror and no observer of
191/// the task events, so it never times a call and captures only what its
192/// audit keeps: its calls take the plain recorder unless a change is marked,
193/// and that invariant is this type. A watched run takes the full recorder,
194/// and captures while its audit or its trace wants changes.
195#[derive(Clone, Copy, Debug)]
196pub(crate) enum CaptureMode {
197    /// No trace, no error-context mirror and no task observer: `audit` is
198    /// both whether a call captures changes and whether its audit entry gets
199    /// them.
200    Plain { audit: bool },
201    /// A trace, an error-context mirror or a task observer is attached:
202    /// `audit` is whether the audit entry gets a call's changes, and
203    /// `traced` whether the trace still keeps changes (cleared once its byte
204    /// budget is crossed).
205    Watched { audit: bool, traced: bool },
206}
207
208impl CaptureMode {
209    /// The mode for `watch` and `capture_audit`, computed once as a workflow
210    /// starts.
211    pub(crate) fn new(capture_audit: bool, watch: &Watch<'_>) -> Self {
212        let traced = watch.trace.as_deref().is_some_and(Capture::changes);
213        if watch.trace.is_some() || watch.error_context.is_some() || watch.observers.tasks() {
214            Self::Watched {
215                audit: capture_audit,
216                traced,
217            }
218        } else {
219            debug_assert!(!watch.timed, "only a task observer or a trace times a call");
220            Self::Plain {
221                audit: capture_audit,
222            }
223        }
224    }
225
226    /// Whether a call records its `Change`s: the audit or the trace wants
227    /// them.
228    #[inline(always)]
229    pub(crate) fn captures(self) -> bool {
230        match self {
231            Self::Plain { audit } => audit,
232            Self::Watched { audit, traced } => audit || traced,
233        }
234    }
235
236    /// Whether the audit entry gets a call's `Change`s.
237    #[inline(always)]
238    pub(crate) fn audit(self) -> bool {
239        match self {
240            Self::Plain { audit } | Self::Watched { audit, .. } => audit,
241        }
242    }
243
244    /// Whether the trace keeps changes.
245    #[inline(always)]
246    pub(crate) fn traced(self) -> bool {
247        matches!(self, Self::Watched { traced: true, .. })
248    }
249
250    /// Whether the run is watched beyond the message.
251    #[inline(always)]
252    pub(crate) fn is_watched(self) -> bool {
253        matches!(self, Self::Watched { .. })
254    }
255}
256
257/// How many ops run between two checks of the limits, when one is set.
258///
259/// The interpreter checks a deadline and a cancellation token at every host
260/// call, every fan-out element's start and every sweep, and here: at every
261/// `CHECK_EVERY`th op, so a loop of pure built-ins still stops. The number is
262/// public so a test can derive where a run stops instead of spelling `63`.
263pub const CHECK_EVERY: u32 = 64;
264
265impl<'p> RunEnv<'_, 'p> {
266    /// At an op boundary: the run context's checkpoint when due, then, with
267    /// a limit set, the limits at every 64th op.
268    fn tick(&mut self, rc: &mut RunContext<'_, '_>) -> Result<(), Stop> {
269        rc.tick();
270        if self.limits.is_set() {
271            self.ops = self.ops.wrapping_add(1);
272            if self.ops.is_multiple_of(CHECK_EVERY) {
273                self.limits.check()?;
274            }
275        }
276        Ok(())
277    }
278
279    /// The run's RFC 3339 timestamp, allocated once when first requested.
280    pub(crate) fn now_text(&mut self) -> &Arc<str> {
281        let now = self.now;
282        self.now_text
283            .get_or_insert_with(|| Arc::from(now.to_rfc3339()))
284    }
285
286    /// Whether the trace keeps changes: only then are the engine's writes
287    /// recorded, and their old values read.
288    pub(crate) fn traces_changes(&self) -> bool {
289        self.capture.traced()
290    }
291
292    /// The ids of the current workflow's task at row `task`, as the program
293    /// owns them.
294    pub(crate) fn ids(&self, task: u32) -> TaskIds<'p> {
295        TaskIds {
296            workflow: self.workflow_id,
297            task: &self.task_ids[task as usize],
298        }
299    }
300
301    /// The record of the current workflow's own `error`: its code and
302    /// message, and the workflow's id.
303    pub(crate) fn workflow_record(&self, error: &Error) -> ErrorRecord {
304        let mut record = ErrorRecord::from(error);
305        record.workflow_id = Some(Arc::clone(self.workflow_id));
306        record
307    }
308
309    /// The record of `error` stamped with the current workflow's id, the task
310    /// at row `task`, and the run's timestamp.
311    pub(crate) fn task_error_record(&mut self, task: u32, error: &Error) -> ErrorRecord {
312        let mut record = ErrorRecord::from(error);
313        let ids = self.ids(task);
314        let now = self.now_text();
315        record.stamp(ids.workflow, Some(ids.task), now, None);
316        record
317    }
318
319    /// An [`ErrorSite`] in the current workflow and sweep, at `authored_path`.
320    pub(crate) fn site(&self, kind: SiteKind, authored_path: AuthoredPath) -> ErrorSite {
321        ErrorSite {
322            kind,
323            workflow_id: Arc::clone(self.workflow_id),
324            task_id: None,
325            group_id: None,
326            rule_index: None,
327            mapping_index: None,
328            target_path: None,
329            authored_path: Some(authored_path),
330            loop_counter: self.frame.loop_counter,
331        }
332    }
333
334    /// An [`ErrorSite`] for the task at row `task` in the current workflow
335    /// and sweep, at `authored_path`.
336    pub(crate) fn task_site(
337        &self,
338        kind: SiteKind,
339        task: u32,
340        authored_path: AuthoredPath,
341    ) -> ErrorSite {
342        ErrorSite {
343            task_id: Some(Arc::clone(self.ids(task).task)),
344            ..self.site(kind, authored_path)
345        }
346    }
347
348    /// The current workflow's own step.
349    pub(crate) fn workflow_step(&self, result: StepResult) -> TraceStep {
350        TraceStep::new(self.workflow_ix, self.workflow_id, result)
351    }
352
353    /// A step of the current workflow's task at row `task`, in the sweep
354    /// the frame is in.
355    pub(crate) fn task_step(&self, task: u32, result: StepResult) -> TraceStep {
356        let mut step = self.workflow_step(result);
357        step.task = Some(TaskIx::new(self.task_base + task));
358        step.task_id = Some(Arc::clone(&self.task_ids[task as usize]));
359        step.loop_counter = self.frame.loop_counter;
360        step
361    }
362
363    /// Pushes `step` onto the trace, if the run is traced. Calls record
364    /// their changes from then on only while the audit or the trace wants
365    /// them: a budget crossed leaves only the audit's.
366    pub(crate) fn push_step(&mut self, step: TraceStep) {
367        if let Some(trace) = self.watch.trace.as_deref_mut() {
368            trace.push(step);
369            if let CaptureMode::Watched { traced, .. } = &mut self.capture {
370                *traced = trace.changes();
371            }
372        }
373    }
374}
375
376/// Where a failure `on_failure: continue` steps past goes from `op`.
377fn fail_to(op: Op) -> u32 {
378    match op {
379        Op::Call { fail_to, .. } | Op::FanOut { fail_to, .. } => fail_to,
380        Op::LoopInit { exit_to, .. } => exit_to,
381        Op::Cond { .. } | Op::LoopNext { .. } | Op::Halt => {
382            unreachable!("only a call, a fan-out or a loop's start fails")
383        }
384    }
385}
386
387/// Runs one workflow's ops over the run context: how they ended, and the
388/// workflow's selection.
389///
390/// A workflow is `Ran` unless the first evaluation of its condition refuses
391/// it: false gives `SkippedByCondition`, and an evaluation error
392/// `ConditionFailed`, which jumps as a false one does. A later evaluation,
393/// which ends a loop, records nothing. So a workflow without a condition is
394/// `Ran` when it starts. A group's or a task's condition only jumps.
395///
396/// A workflow without a loop begins its one sweep when it starts, and a loop
397/// each sweep its `LoopInit` or `LoopNext` enters. A workflow condition that
398/// refuses the sweep it guards takes it back: a sweep counts once its
399/// condition admits it, as v3's `sweeps_run`.
400///
401/// The workflow's `WorkflowStarted` goes out as it starts when no workflow
402/// condition judges it first, else once that judgment admits it, and in
403/// any case before it ends `Ran`: a loop whose condition no sweep reached,
404/// or a run stopped before its condition, is `Ran` too.
405pub(crate) async fn run_workflow<'p>(
406    rc: &mut RunContext<'_, 'p>,
407    wf: &'p CompiledWorkflow,
408    env: &mut RunEnv<'_, 'p>,
409) -> (Exit, Selection) {
410    let mut selection = Selection::Ran;
411    // Only a workflow condition's `Cond` judges the selection, and only the
412    // first one evaluated.
413    let mut unjudged = true;
414    env.frame.sweeps = u32::from(wf.loops.is_empty());
415    if !wf.judged_first() {
416        begin(env);
417    }
418    let mut pc = 0;
419    let exit = loop {
420        let Some(&op) = wf.ops.get(pc) else {
421            break if env.failed {
422                Exit::Failed
423            } else {
424                Exit::Completed
425            };
426        };
427        if let Err(e) = env.tick(rc) {
428            break Exit::Limited(e);
429        }
430        let flow = match op {
431            Op::Cond {
432                cond,
433                kind,
434                else_to,
435            } => {
436                let cond_site = &wf.conds[cond as usize];
437                let passed = test(rc, env.exprs, env.budget, &cond_site.expr)
438                    .map_err(|e| attach_cond_site(env, cond_site, kind, e));
439                if env.condition_error_policy == ConditionErrorPolicy::Stop
440                    && let Err(e) = passed
441                {
442                    match kind {
443                        CondKind::Workflow => {
444                            if env.frame.in_sweep(wf) {
445                                env.frame.sweeps -= 1;
446                            }
447                            if std::mem::take(&mut unjudged) {
448                                selection = Selection::ConditionFailed(e.clone());
449                            }
450                            record::workflow_failure(env, &e);
451                        }
452                        CondKind::Group => {
453                            record::workflow_failure(env, &e);
454                        }
455                        CondKind::Task => {
456                            if let Some(Op::Call { site, .. } | Op::FanOut { site, .. }) =
457                                wf.ops.get(pc + 1)
458                            {
459                                record::task_condition_failure(
460                                    env,
461                                    wf.calls[*site as usize].task,
462                                    &e,
463                                );
464                            } else {
465                                record::workflow_failure(env, &e);
466                            }
467                        }
468                    }
469                    break Exit::Stopped(e);
470                }
471                let admitted = matches!(passed, Ok(true));
472                if kind == CondKind::Workflow {
473                    if !admitted && env.frame.in_sweep(wf) {
474                        env.frame.sweeps -= 1;
475                    }
476                    if std::mem::take(&mut unjudged) {
477                        selection = match passed {
478                            Ok(true) => {
479                                begin(env);
480                                Selection::Ran
481                            }
482                            Ok(false) => {
483                                skipped_workflow(env, None);
484                                Selection::SkippedByCondition
485                            }
486                            Err(e) => {
487                                skipped_workflow(env, Some(&e));
488                                Selection::ConditionFailed(e)
489                            }
490                        };
491                    }
492                } else if !admitted && env.watch.trace.is_some() {
493                    skipped_calls(env, wf, kind, pc, else_to, passed.as_ref().err());
494                }
495                if admitted {
496                    Flow::Next
497                } else {
498                    Flow::Jump(else_to)
499                }
500            }
501            Op::Call { site, .. } => {
502                let site = &wf.calls[site as usize];
503                match &site.callee {
504                    Callee::Builtin(builtin) => call(rc, env, wf, site, builtin),
505                    Callee::Host(bound) => call_host(rc, env, wf, site, &**bound).await,
506                }
507            }
508            Op::FanOut { site, fan, .. } => {
509                let site = &wf.calls[site as usize];
510                let Callee::Host(bound) = &site.callee else {
511                    unreachable!("compile_one checks that a fan-out calls a host function")
512                };
513                // Boxed: a fan-out's state stays out of the run's future,
514                // which a host spawns once per message.
515                Box::pin(fan_out(rc, env, wf, site, &wf.fans[fan as usize], &**bound)).await
516            }
517            Op::LoopInit { lp, exit_to } => loop_init(rc, env, &wf.loops[lp as usize], exit_to),
518            Op::LoopNext {
519                lp,
520                body_to,
521                exit_to,
522            } => loop_next(rc, env, &wf.loops[lp as usize], body_to, exit_to),
523            Op::Halt => Flow::Halt,
524        };
525        pc = match flow {
526            Flow::Next => pc + 1,
527            Flow::Jump(to) => to as usize,
528            Flow::Halt => break Exit::Halted,
529            Flow::Stop(e) => break Exit::Limited(e),
530            Flow::Abort(e) => break Exit::Stopped(e),
531            Flow::Fail(e) => match wf.on_failure {
532                WorkflowFailurePolicy::Continue => {
533                    env.failed = true;
534                    fail_to(op) as usize
535                }
536                WorkflowFailurePolicy::Fail => break Exit::Stopped(e),
537            },
538        };
539    };
540    if matches!(selection, Selection::Ran) {
541        begin(env);
542    }
543    (exit, selection)
544}
545
546/// The workflow's `WorkflowStarted`, once, and in a timed run the clock
547/// reading its elapsed time starts from.
548fn begin(env: &mut RunEnv<'_, '_>) {
549    if !env.watch.observers.workflows() || std::mem::replace(&mut env.frame.begun, true) {
550        return;
551    }
552    // Only an observer of the workflow events with `TIMINGS` times a
553    // workflow: a trace's timings are its calls'.
554    if env.watch.observers.timed_workflows() {
555        env.frame.started = Some(record::now());
556    }
557    env.watch.observers.emit(Event::WorkflowStarted {
558        workflow: env.workflow_ix,
559        id: env.workflow_id,
560    });
561}
562
563/// The workflow's own `Skipped` step, for a condition that refused it on
564/// its first judgment: when it failed to evaluate, with the record of
565/// the error it failed with and the workflow's id, so the step is told
566/// apart from a plain `false`. The message never gets that record, so the
567/// step's is the only one.
568fn skipped_workflow(env: &mut RunEnv<'_, '_>, error: Option<&Error>) {
569    if env.watch.trace.is_none() {
570        return;
571    }
572    let mut step = env.workflow_step(StepResult::Skipped);
573    if let Some(e) = error {
574        step.errors.push(env.workflow_record(e));
575    }
576    env.push_step(step);
577}
578
579/// Attaches an [`ErrorSite`] to a condition evaluation error.
580fn attach_cond_site(
581    env: &RunEnv<'_, '_>,
582    cond_site: &CondSite,
583    kind: CondKind,
584    error: Error,
585) -> Error {
586    let site_kind = match kind {
587        CondKind::Workflow => SiteKind::WorkflowCondition,
588        CondKind::Group => SiteKind::GroupCondition,
589        CondKind::Task => SiteKind::TaskCondition,
590    };
591    let step_arc = cond_site.step.as_deref().map(Arc::<str>::from);
592    let (task_id, group_id) = match kind {
593        CondKind::Workflow => (None, None),
594        CondKind::Group => (None, step_arc),
595        CondKind::Task => (step_arc, None),
596    };
597    error.with_site(ErrorSite {
598        task_id,
599        group_id,
600        ..env.site(site_kind, cond_site.path.clone())
601    })
602}
603
604/// The `Skipped` steps of what a group's or a task's condition at `pc`
605/// refused: one per call in the group's span, `pc + 1 .. else_to`, or the
606/// task's one call, at `pc + 1`, in the sweep the frame is in.
607fn skipped_calls(
608    env: &mut RunEnv<'_, '_>,
609    wf: &CompiledWorkflow,
610    kind: CondKind,
611    pc: usize,
612    else_to: u32,
613    error: Option<&Error>,
614) {
615    let span = match kind {
616        CondKind::Group => wf.ops.get(pc + 1..else_to as usize),
617        CondKind::Task => wf.ops.get(pc + 1..pc + 2),
618        CondKind::Workflow => None,
619    };
620    for op in span.unwrap_or_default() {
621        if let Op::Call { site, .. } | Op::FanOut { site, .. } = *op {
622            let task = wf.calls[site as usize].task;
623            let mut step = env.task_step(task, StepResult::Skipped);
624            if let Some(e) = error {
625                step.errors.push(env.task_error_record(task, e));
626            }
627            env.push_step(step);
628        }
629    }
630}
631
632/// `LoopInit`: `over`, once, then the counter's `init` and the first sweep.
633///
634/// An `over` that does not give an array fails the workflow before the
635/// counter is written: one record, whatever the workflow's policy,
636/// which then decides; under `continue` the failure goes to the loop's exit.
637fn loop_init<'p>(
638    rc: &mut RunContext<'_, 'p>,
639    env: &mut RunEnv<'_, 'p>,
640    site: &'p LoopSite,
641    exit_to: u32,
642) -> Flow {
643    if let Some(over) = &site.over {
644        let over_site = ErrorSite {
645            loop_counter: None,
646            ..env.site(
647                SiteKind::LoopOver,
648                AuthoredPath::root().join("loop").join("over"),
649            )
650        };
651        match elements(
652            rc,
653            env.exprs,
654            env.budget,
655            over,
656            Over::Loop(env.workflow_id),
657            over_site,
658        ) {
659            // A loop moves each element out: its own copy.
660            Ok(items) => env.frame.items = Some(items.into_owned()),
661            Err(e) => {
662                record::workflow_failure(env, &e);
663                return if *e.kind() == ErrorKind::Budget && env.budget.is_run_limited() {
664                    Flow::Abort(e)
665                } else {
666                    Flow::Fail(e)
667                };
668            }
669        }
670    }
671    env.frame.counter = site.init;
672    enter(rc, env, site, Flow::Next, exit_to)
673}
674
675/// `LoopNext`: the counter advanced by the increment, then the next sweep at
676/// `body_to`. The advance saturates, so a counter pinned at `i64::MAX` is out
677/// of bounds for every `max`, as in v3.
678fn loop_next<'p>(
679    rc: &mut RunContext<'_, 'p>,
680    env: &mut RunEnv<'_, 'p>,
681    site: &'p LoopSite,
682    body_to: u32,
683    exit_to: u32,
684) -> Flow {
685    env.frame.counter = env.frame.counter.saturating_add(site.increment);
686    enter(rc, env, site, Flow::Jump(body_to), exit_to)
687}
688
689/// The sweep at the frame's counter, in this order: the counter written,
690/// then the bounds, `counter < max` and `counter < len(over)`, then in bounds
691/// the run's limits, then `over[counter]` moved into `as` and `{}` into
692/// `scratch`, before the sweep's condition. Out of bounds the loop exits,
693/// leaving the first counter past them in the context; past a limit the run
694/// stops, the sweep not begun.
695///
696/// None of these writes records a `Change` for the audit. A traced run
697/// gets the engine's step for them, `task: None` with the counter written,
698/// as each sweep is entered, as the loop exits on a counter it wrote, and as
699/// a limit stops the run after the counter's write, with each
700/// write's `Change` while the trace keeps changes.
701fn enter<'p>(
702    rc: &mut RunContext<'_, 'p>,
703    env: &mut RunEnv<'_, 'p>,
704    site: &'p LoopSite,
705    sweep: Flow,
706    exit_to: u32,
707) -> Flow {
708    let traced = env.traces_changes();
709    let mut changes = Vec::new();
710    let counter = env.frame.counter;
711    if let Some(path) = &site.counter {
712        engine_write(
713            rc,
714            path,
715            traced,
716            &mut changes,
717            || OwnedDataValue::from_i64(counter),
718            |_| DataValue::from_i64(counter),
719        );
720    }
721    // The element's index, when the loop is over an array.
722    let element = match &env.frame.items {
723        None => None,
724        Some(items) => usize::try_from(counter).ok().filter(|&i| i < items.len()),
725    };
726    let ends = if counter >= site.max || (env.frame.items.is_some() && element.is_none()) {
727        Some(Flow::Jump(exit_to))
728    } else {
729        // Only the counter is written by now: its step, so a stopped run's
730        // trace replays to its context.
731        env.limits.check().err().map(Flow::Stop)
732    };
733    if let Some(flow) = ends {
734        if site.counter.is_some() {
735            sweep_step(env, counter, changes);
736        }
737        return flow;
738    }
739    let _guard = match env.budget.charge_dispatch(DispatchKind::LoopStep {
740        workflow_id: env.workflow_id,
741        iteration: u64::from(env.frame.sweeps),
742    }) {
743        Ok(guard) => guard,
744        Err(e) => {
745            if site.counter.is_some() {
746                sweep_step(env, counter, changes);
747            }
748            let e = e.with_site(ErrorSite {
749                loop_counter: Some(counter),
750                ..env.site(SiteKind::LoopOver, AuthoredPath::root().join("loop"))
751            });
752            record::workflow_failure(env, &e);
753            return Flow::Abort(e);
754        }
755    };
756    if let Some(item) = element.and_then(|i| env.frame.items.as_mut()?.get_mut(i)) {
757        // Moved, not cloned: `increment >= 1` visits no element twice.
758        let item = std::mem::take(item);
759        if let Some(path) = &site.r#as {
760            rc.set_recorded(path, item, None, traced.then_some(&mut changes));
761        }
762    }
763    if let Some(path) = &site.scratch {
764        engine_write(
765            rc,
766            path,
767            traced,
768            &mut changes,
769            || OwnedDataValue::Object(Vec::new()),
770            |_| DataValue::Object(&[]),
771        );
772    }
773    env.frame.loop_counter = Some(counter);
774    env.frame.sweeps = env.frame.sweeps.saturating_add(1);
775    sweep_step(env, counter, changes);
776    sweep
777}
778
779/// An engine write at `path` ([`RunContext::engine_set`]), with its
780/// `Change` onto `changes` while the trace keeps changes.
781fn engine_write<'p>(
782    rc: &mut RunContext<'_, 'p>,
783    path: &Path,
784    traced: bool,
785    changes: &mut Vec<Change>,
786    owned: impl FnOnce() -> OwnedDataValue,
787    view: impl for<'q> FnOnce(&'q Owner<'p>) -> DataValue<'q>,
788) {
789    let old = traced.then(|| rc.get_or_null(path));
790    if let Some(old) = old {
791        let new = owned();
792        let new_clone = new.clone();
793        rc.engine_set(path, || new_clone, view);
794        changes.push(Change::write(Arc::clone(path.dotted()), old, new));
795    } else {
796        rc.engine_set(path, owned, view);
797    }
798}
799
800/// The engine's step for a sweep entered or a loop exited at `counter`,
801/// with its writes' `changes`, when the run is traced.
802fn sweep_step(env: &mut RunEnv<'_, '_>, counter: i64, changes: Vec<Change>) {
803    if env.watch.trace.is_none() {
804        return;
805    }
806    let mut step = env.workflow_step(StepResult::Executed);
807    step.changes = changes;
808    step.loop_counter = Some(counter);
809    env.push_step(step);
810}
811
812/// Whose `over` is evaluated, which its errors name.
813#[derive(Clone, Copy, Debug)]
814pub(super) enum Over<'a> {
815    /// A workflow's `loop.over`, by the workflow's id.
816    Loop(&'a str),
817    /// A task's `for_each.over`, by the task's id.
818    FanOut(&'a str),
819}
820
821impl fmt::Display for Over<'_> {
822    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823        match self {
824            Self::Loop(id) => write!(f, "Workflow {id}: loop.over"),
825            Self::FanOut(id) => write!(f, "Task {id}: for_each.over"),
826        }
827    }
828}
829
830/// An `over`'s elements: a loop's or a fan-out's, by one rule; a folded
831/// constant's lent. A value that is not an array is [`ErrorKind::Eval`], as
832/// a wrong-typed result is everywhere else; an evaluation error keeps its
833/// own kind, with v3's text less v3's wrapper.
834pub(super) fn elements<'e>(
835    rc: &mut RunContext<'_, '_>,
836    exprs: &ExprEngine,
837    budget: &TaskBudget<'_>,
838    over: &'e Expr,
839    owner: Over<'_>,
840    site: ErrorSite,
841) -> Result<Cow<'e, [OwnedDataValue]>, Error> {
842    let value = eval(rc, exprs, budget, over).map_err(|e| {
843        let msg = format!("{owner} failed to evaluate: {}", e.record_text());
844        e.with_message(msg).with_site(site.clone())
845    })?;
846    match value {
847        Cow::Owned(OwnedDataValue::Array(items)) => Ok(Cow::Owned(items)),
848        Cow::Borrowed(OwnedDataValue::Array(items)) => Ok(Cow::Borrowed(items)),
849        other => Err(Error::new(
850            ErrorKind::Eval,
851            format!("{owner} must evaluate to an array, got {}", kind(&other)),
852        )
853        .with_expr_cause(ExprCause::type_mismatch(None))
854        .with_site(site)),
855    }
856}
857
858/// One built-in call: the call, timed in a timed run, the decision, the
859/// recorder; the flow.
860fn call<'p>(
861    rc: &mut RunContext<'_, 'p>,
862    env: &mut RunEnv<'_, 'p>,
863    wf: &'p CompiledWorkflow,
864    site: &'p CallSite,
865    builtin: &'p Builtin,
866) -> Flow {
867    let errors_before = env.errors.len();
868    let meta = &wf.tasks[site.task as usize];
869    let guard = match env.budget.charge_dispatch(DispatchKind::InlineBuiltin {
870        workflow_id: &wf.id,
871        task_id: &meta.id,
872        function: &meta.function,
873    }) {
874        Ok(guard) => guard,
875        Err(e) => {
876            let e = e.with_site(env.task_site(SiteKind::Task, site.task, meta.path.clone()));
877            let decision = policy::forced_abort(env.ids(site.task).task, e);
878            let done = Completed::new(env, site.task, None, None, errors_before, false);
879            return record::record(rc, env, wf, done, decision);
880        }
881    };
882    // An unwatched run neither traces nor times a call.
883    let (traced, started) = if env.capture.is_watched() {
884        (env.traces_changes(), env.watch.timed.then(record::now))
885    } else {
886        (false, None)
887    };
888    // Lent only to a call whose trace keeps changes.
889    let mapping_ends = if traced {
890        env.watch.trace.as_deref_mut().map(Capture::mapping_ends)
891    } else {
892        None
893    };
894    let mut cx = SyncCtx {
895        rc: &mut *rc,
896        exprs: env.exprs,
897        budget: env.budget,
898        condition_error_policy: env.condition_error_policy,
899        payload: env.payload,
900        workflow_id: env.workflow_id,
901        task_id: &env.task_ids[site.task as usize],
902        task_path: &meta.path,
903        loop_counter: env.frame.loop_counter,
904        capture: env.capture.captures(),
905        traced,
906        changes: &mut env.changes,
907        marked: false,
908        mapping_ends,
909        errors: &mut *env.errors,
910        now: env.now,
911        now_text: &mut *env.now_text,
912    };
913    let result = builtin.call(&mut cx);
914    let mut marked = cx.marked;
915    drop(guard);
916    let elapsed = started.map(record::since);
917    // An `Err` records no changes for the audit, as v3's recorder drops
918    // them. Its writes stand, so a trace that keeps changes keeps them, for
919    // itself alone: the call's `Failed` or `Executed` step carries them.
920    if result.is_err() {
921        if traced {
922            for change in &mut env.changes {
923                change.trace_only = true;
924            }
925            marked = true;
926        } else {
927            env.changes.clear();
928        }
929    }
930    let decision = match result {
931        Err(mut e) => {
932            if e.site().is_none() {
933                e = e.with_site(env.task_site(SiteKind::Task, site.task, meta.path.clone()));
934            }
935            let force_stop = (env.condition_error_policy == ConditionErrorPolicy::Stop
936                && e.site()
937                    .is_some_and(|s| s.kind == SiteKind::FilterCondition))
938                || (*e.kind() == ErrorKind::Budget && env.budget.is_run_limited());
939            if force_stop {
940                policy::forced_abort(env.ids(site.task).task, e)
941            } else {
942                policy::decide(site.policy, env.ids(site.task).task, Err(e))
943            }
944        }
945        Ok(outcome) => policy::decide(site.policy, env.ids(site.task).task, Ok(outcome)),
946    };
947    let done = Completed::new(env, site.task, None, elapsed, errors_before, marked);
948    record::record(rc, env, wf, done, decision)
949}