Skip to main content

wdl_engine/
eval.rs

1//! Module for evaluation.
2
3use std::borrow::Cow;
4use std::path::Path;
5use std::sync::Arc;
6use std::sync::atomic::AtomicU8;
7use std::sync::atomic::Ordering;
8
9use anyhow::Result;
10use cloud_copy::TransferEvent;
11use crankshaft::events::Event as CrankshaftEvent;
12use indexmap::IndexMap;
13use tokio::sync::broadcast;
14use tokio_util::sync::CancellationToken;
15use tracing::error;
16use wdl_analysis::Document;
17use wdl_analysis::document::Task;
18use wdl_analysis::types::Type;
19use wdl_ast::Diagnostic;
20use wdl_ast::Span;
21use wdl_ast::SupportedVersion;
22
23use crate::EvaluationPath;
24use crate::GuestPath;
25use crate::HostPath;
26use crate::Outputs;
27use crate::Value;
28use crate::backend::TaskExecutionResult;
29use crate::config::FailureMode;
30use crate::http::Transferer;
31
32mod trie;
33pub mod v1;
34
35/// A name used whenever a file system "root" is mapped.
36///
37/// A root might be a root directory like `/` or `C:\`, but it also might be the root of a URL like `https://example.com`.
38const ROOT_NAME: &str = ".root";
39
40/// A constant to denote that no cancellation has occurred yet.
41const CANCELLATION_STATE_NOT_CANCELED: u8 = 0;
42
43/// A state bit to indicate that we're waiting for executing tasks to
44/// complete.
45///
46/// This bit is mutually exclusive with the `CANCELING` bit.
47const CANCELLATION_STATE_WAITING: u8 = 1;
48
49/// A state bit to denote that we're waiting for executing tasks to cancel.
50///
51/// This bit is mutually exclusive with the `WAITING` bit.
52const CANCELLATION_STATE_CANCELING: u8 = 2;
53
54/// A state bit to denote that cancellation was the result of an error.
55///
56/// This bit will only be set if either the `CANCELING` bit or the `WAITING`
57/// bit are set.
58const CANCELLATION_STATE_ERROR: u8 = 4;
59
60/// The mask to apply to the state for excluding the error bit.
61const CANCELLATION_STATE_MASK: u8 = 0x3;
62
63/// Represents the current state of a [`CancellationContext`].
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CancellationContextState {
66    /// The context has not been canceled yet.
67    NotCanceled,
68    /// The context has been canceled and is waiting for executing tasks to
69    /// complete.
70    Waiting,
71    /// The context has been canceled and is waiting for executing tasks to
72    /// cancel.
73    Canceling,
74}
75
76impl CancellationContextState {
77    /// Gets the context state from a raw state byte.
78    fn from_raw(raw: u8) -> Self {
79        match raw & CANCELLATION_STATE_MASK {
80            CANCELLATION_STATE_NOT_CANCELED => Self::NotCanceled,
81            CANCELLATION_STATE_WAITING => Self::Waiting,
82            CANCELLATION_STATE_CANCELING => Self::Canceling,
83            _ => unreachable!("unexpected cancellation context state"),
84        }
85    }
86
87    /// Updates the context state and returns the new state.
88    ///
89    /// Returns `None` if the update is for an error and there has already been
90    /// a cancellation (i.e. the update was not successful).
91    fn update(mode: FailureMode, error: bool, state: &Arc<AtomicU8>) -> Option<Self> {
92        // Update the provided state with the new state
93        let previous_state = state
94            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |state| {
95                // If updating for an error and there has been a cancellation, bail out
96                if error && state != CANCELLATION_STATE_NOT_CANCELED {
97                    return None;
98                }
99
100                // Otherwise, calculate the new state
101                let mut new_state = match state & CANCELLATION_STATE_MASK {
102                    CANCELLATION_STATE_NOT_CANCELED => match mode {
103                        FailureMode::Slow => CANCELLATION_STATE_WAITING,
104                        FailureMode::Fast => CANCELLATION_STATE_CANCELING,
105                    },
106                    CANCELLATION_STATE_WAITING => CANCELLATION_STATE_CANCELING,
107                    CANCELLATION_STATE_CANCELING => CANCELLATION_STATE_CANCELING,
108                    _ => unreachable!("unexpected cancellation context state"),
109                };
110
111                // Mark the error bit upon error
112                if error {
113                    new_state |= CANCELLATION_STATE_ERROR;
114                }
115
116                // Return the new state along with the old error bit
117                Some(new_state | (state & CANCELLATION_STATE_ERROR))
118            })
119            .ok()?;
120
121        match previous_state & CANCELLATION_STATE_MASK {
122            CANCELLATION_STATE_NOT_CANCELED => match mode {
123                FailureMode::Slow => Some(Self::Waiting),
124                FailureMode::Fast => Some(Self::Canceling),
125            },
126            CANCELLATION_STATE_WAITING => Some(Self::Canceling),
127            CANCELLATION_STATE_CANCELING => Some(Self::Canceling),
128            _ => unreachable!("unexpected cancellation context state"),
129        }
130    }
131}
132
133/// Represents context for cancelling workflow or task evaluation.
134///
135/// Uses a default failure mode of [`Slow`](FailureMode::Slow).
136#[derive(Debug, Clone)]
137pub struct CancellationContext {
138    /// The failure mode for the cancellation context.
139    mode: FailureMode,
140    /// The state of the cancellation context.
141    state: Arc<AtomicU8>,
142    /// The parent context, consulted read-only when folding the effective
143    /// state. `None` for a root context created by [`new`](Self::new).
144    parent: Option<Arc<CancellationContext>>,
145    /// The cancellation token that is canceled upon the first cancellation.
146    first: CancellationToken,
147    /// The cancellation token that is canceled upon the second cancellation
148    /// when the failure mode is "slow" or upon the first cancellation when the
149    /// failure mode is "fast".
150    second: CancellationToken,
151}
152
153impl CancellationContext {
154    /// Constructs a cancellation context for the given [`FailureMode`].
155    ///
156    /// If the provided `mode` is [`Slow`](FailureMode::Slow), the first call to
157    /// [`cancel`](Self::cancel) will wait for currently executing tasks to
158    /// complete; a subsequent call to [`cancel`](Self::cancel) will cancel the
159    /// currently executing tasks.
160    ///
161    /// If the provided `mode` is [`Fast`](FailureMode::Fast), the first call to
162    /// [`cancel`](Self::cancel) will cancel the currently executing tasks.
163    pub fn new(mode: FailureMode) -> Self {
164        Self {
165            mode,
166            state: Arc::new(CANCELLATION_STATE_NOT_CANCELED.into()),
167            parent: None,
168            first: CancellationToken::new(),
169            second: CancellationToken::new(),
170        }
171    }
172
173    /// Creates a new child cancellation context.
174    ///
175    /// The returned [`CancellationContext`] is bound to the parent: both token
176    /// cancellation and the effective [`state`](Self::state) propagate from the
177    /// parent to the child. Cancelling the child, however, never affects the
178    /// parent or its other children.
179    pub fn child(&self, mode: FailureMode) -> Self {
180        Self {
181            mode,
182            state: Arc::new(CANCELLATION_STATE_NOT_CANCELED.into()),
183            parent: Some(Arc::new(self.clone())),
184            first: self.first.child_token(),
185            second: self.second.child_token(),
186        }
187    }
188
189    /// Folds this context's state with its ancestors' into an effective
190    /// `(level, user_initiated)` pair.
191    ///
192    /// `level` is the masked state level, where
193    /// `CANCELLATION_STATE_NOT_CANCELED` < `CANCELLATION_STATE_WAITING` <
194    /// `CANCELLATION_STATE_CANCELING`. `user_initiated` is whether the
195    /// effective cancellation was initiated by the user rather than by an
196    /// error.
197    ///
198    /// The `(level, user_initiated)` pair is always taken from a single
199    /// context rather than mixed across the hierarchy: an ancestor's pair
200    /// wins only when its effective level is strictly greater than this
201    /// context's. On a tie the local cause is authoritative, so a parent
202    /// cancellation that arrives after a child has already entered the same
203    /// level does not relabel the child's cause.
204    fn effective(&self) -> (u8, bool) {
205        let raw = self.state.load(Ordering::SeqCst);
206        let level = raw & CANCELLATION_STATE_MASK;
207        let user =
208            level != CANCELLATION_STATE_NOT_CANCELED && (raw & CANCELLATION_STATE_ERROR == 0);
209
210        match &self.parent {
211            Some(parent) => match parent.effective() {
212                (parent_level, parent_user) if parent_level > level => (parent_level, parent_user),
213                _ => (level, user),
214            },
215            None => (level, user),
216        }
217    }
218
219    /// Gets the effective [`CancellationContextState`] of this
220    /// [`CancellationContext`], folding in any ancestor's cancellation.
221    pub fn state(&self) -> CancellationContextState {
222        CancellationContextState::from_raw(self.effective().0)
223    }
224
225    /// Performs a cancellation.
226    ///
227    /// Returns the current [`CancellationContextState`] which should be checked
228    /// to ensure the desired cancellation occurred.
229    ///
230    /// This method will never return a
231    /// [`CancellationContextState::NotCanceled`] state.
232    #[must_use]
233    pub fn cancel(&self) -> CancellationContextState {
234        let state =
235            CancellationContextState::update(self.mode, false, &self.state).expect("should update");
236
237        match state {
238            CancellationContextState::NotCanceled => panic!("should be canceled"),
239            CancellationContextState::Waiting => self.first.cancel(),
240            CancellationContextState::Canceling => {
241                self.first.cancel();
242                self.second.cancel();
243            }
244        }
245
246        state
247    }
248
249    /// Gets the cancellation token that is canceled upon the first
250    /// cancellation.
251    ///
252    /// The token will be canceled when [`CancellationContext::cancel`] is
253    /// called and the resulting state is [`CancellationContextState::Waiting`]
254    /// or [`CancellationContextState::Canceling`].
255    ///
256    /// Callers should _not_ directly cancel the returned token and instead call
257    /// [`CancellationContext::cancel`].
258    pub fn first(&self) -> CancellationToken {
259        self.first.clone()
260    }
261
262    /// Gets the cancellation token that is canceled upon the second
263    /// cancellation when the failure mode is "slow" or first cancellation when
264    /// the failure mode is "fast".
265    ///
266    /// The token will be canceled when [`CancellationContext::cancel`] is
267    /// called and the resulting state is
268    /// [`CancellationContextState::Canceling`].
269    ///
270    /// Callers should _not_ directly cancel the returned token and instead call
271    /// [`CancellationContext::cancel`].
272    pub fn second(&self) -> CancellationToken {
273        self.second.clone()
274    }
275
276    /// Determines if the user initiated the cancellation, considering any
277    /// ancestor's cancellation.
278    pub fn user_canceled(&self) -> bool {
279        self.effective().1
280    }
281
282    /// Triggers a cancellation as a result of an error.
283    ///
284    /// If the context has already been canceled, this is a no-op.
285    ///
286    /// Otherwise, a cancellation is attempted and an error message is logged
287    /// depending on the current state of the context.
288    pub(crate) fn error(&self, error: &EvaluationError) {
289        if let Some(state) = CancellationContextState::update(self.mode, true, &self.state) {
290            let message: Cow<'_, str> = match error {
291                EvaluationError::Canceled => "evaluation was canceled".into(),
292                EvaluationError::Source(e) => e.diagnostic.message().into(),
293                EvaluationError::Other(e) => format!("{e:#}").into(),
294            };
295
296            match state {
297                CancellationContextState::NotCanceled => unreachable!("should be canceled"),
298                CancellationContextState::Waiting => {
299                    self.first.cancel();
300
301                    error!(
302                        "an evaluation error occurred: waiting for any executing tasks to \
303                         complete: {message}"
304                    );
305                }
306                CancellationContextState::Canceling => {
307                    self.first.cancel();
308                    self.second.cancel();
309
310                    error!(
311                        "an evaluation error occurred: waiting for any executing tasks to cancel: \
312                         {message}"
313                    );
314                }
315            }
316        }
317    }
318}
319
320impl Default for CancellationContext {
321    fn default() -> Self {
322        Self::new(FailureMode::Slow)
323    }
324}
325
326/// Represents an event from the WDL evaluation engine.
327#[derive(Debug, Clone)]
328pub enum EngineEvent {
329    /// A cached task execution result was reused due to a call cache hit.
330    ReusedCachedExecutionResult {
331        /// The id of the task that reused a cached execution result.
332        id: String,
333    },
334    /// A locally running task has been parked by the engine due to insufficient
335    /// resources.
336    TaskParked,
337    /// A locally running task has been unparked by the engine.
338    TaskUnparked {
339        /// Whether or not the task was unparked due to being canceled.
340        canceled: bool,
341    },
342}
343
344/// Represents events that may be sent during WDL evaluation.
345#[derive(Debug, Clone, Default)]
346pub struct Events {
347    /// The WDL engine events channel.
348    ///
349    /// This is `None` when engine events are not enabled.
350    engine: Option<broadcast::Sender<EngineEvent>>,
351    /// The Crankshaft events channel.
352    ///
353    /// This is `None` when Crankshaft events are not enabled.
354    crankshaft: Option<broadcast::Sender<CrankshaftEvent>>,
355    /// The transfer events channel.
356    ///
357    /// This is `None` when transfer events are not enabled.
358    transfer: Option<broadcast::Sender<TransferEvent>>,
359}
360
361impl Events {
362    /// Constructs a new `Events` and enables subscribing to all event channels.
363    pub fn new(capacity: usize) -> Self {
364        Self {
365            engine: Some(broadcast::Sender::new(capacity)),
366            crankshaft: Some(broadcast::Sender::new(capacity)),
367            transfer: Some(broadcast::Sender::new(capacity)),
368        }
369    }
370
371    /// Constructs a new `Events` and disable subscribing to any event channel.
372    pub fn disabled() -> Self {
373        Self::default()
374    }
375
376    /// Subscribes to the WDL engine events channel.
377    ///
378    /// Returns `None` if WDL engine events are not enabled.
379    pub fn subscribe_engine(&self) -> Option<broadcast::Receiver<EngineEvent>> {
380        self.engine.as_ref().map(|s| s.subscribe())
381    }
382
383    /// Subscribes to the Crankshaft events channel.
384    ///
385    /// Returns `None` if Crankshaft events are not enabled.
386    pub fn subscribe_crankshaft(&self) -> Option<broadcast::Receiver<CrankshaftEvent>> {
387        self.crankshaft.as_ref().map(|s| s.subscribe())
388    }
389
390    /// Subscribes to the transfer events channel.
391    ///
392    /// Returns `None` if transfer events are not enabled.
393    pub fn subscribe_transfer(&self) -> Option<broadcast::Receiver<TransferEvent>> {
394        self.transfer.as_ref().map(|s| s.subscribe())
395    }
396
397    /// Gets the sender for the Crankshaft events.
398    pub(crate) fn engine(&self) -> &Option<broadcast::Sender<EngineEvent>> {
399        &self.engine
400    }
401
402    /// Gets the sender for the Crankshaft events.
403    pub(crate) fn crankshaft(&self) -> &Option<broadcast::Sender<CrankshaftEvent>> {
404        &self.crankshaft
405    }
406
407    /// Gets the sender for the transfer events.
408    pub(crate) fn transfer(&self) -> &Option<broadcast::Sender<TransferEvent>> {
409        &self.transfer
410    }
411}
412
413/// Represents the location of a call in an evaluation error.
414#[derive(Debug, Clone)]
415pub struct CallLocation {
416    /// The document containing the call statement.
417    pub document: Document,
418    /// The span of the call statement.
419    pub span: Span,
420}
421
422/// Represents an error that originates from WDL source.
423#[derive(Debug)]
424pub struct SourceError {
425    /// The document originating the diagnostic.
426    pub document: Document,
427    /// The evaluation diagnostic.
428    pub diagnostic: Diagnostic,
429    /// The call backtrace for the error.
430    ///
431    /// An empty backtrace denotes that the error was encountered outside of
432    /// a call.
433    ///
434    /// The call locations are stored as most recent to least recent.
435    pub backtrace: Vec<CallLocation>,
436}
437
438/// Represents an error that may occur when evaluating a workflow or task.
439#[derive(Debug)]
440pub enum EvaluationError {
441    /// Evaluation was canceled.
442    Canceled,
443    /// The error came from WDL source evaluation.
444    Source(Box<SourceError>),
445    /// The error came from another source.
446    Other(anyhow::Error),
447}
448
449impl EvaluationError {
450    /// Creates a new evaluation error from the given document and diagnostic.
451    pub fn new(document: Document, diagnostic: Diagnostic) -> Self {
452        Self::Source(Box::new(SourceError {
453            document,
454            diagnostic,
455            backtrace: Default::default(),
456        }))
457    }
458
459    /// Helper for tests for converting an evaluation error to a string.
460    #[allow(clippy::inherent_to_string)]
461    pub fn to_string(&self) -> String {
462        use std::collections::HashMap;
463
464        use codespan_reporting::diagnostic::Label;
465        use codespan_reporting::diagnostic::LabelStyle;
466        use codespan_reporting::files::SimpleFiles;
467        use codespan_reporting::term;
468        use codespan_reporting::term::Config;
469        use codespan_reporting::term::termcolor::Buffer;
470        use wdl_ast::AstNode;
471
472        match self {
473            Self::Canceled => "evaluation was canceled".to_string(),
474            Self::Source(e) => {
475                let mut files = SimpleFiles::new();
476                let mut map = HashMap::new();
477
478                let file_id = files.add(e.document.path(), e.document.root().text().to_string());
479
480                let diagnostic =
481                    e.diagnostic
482                        .to_codespan(file_id)
483                        .with_labels_iter(e.backtrace.iter().map(|l| {
484                            let id = l.document.id();
485                            let file_id = *map.entry(id).or_insert_with(|| {
486                                files.add(l.document.path(), l.document.root().text().to_string())
487                            });
488
489                            Label {
490                                style: LabelStyle::Secondary,
491                                file_id,
492                                range: l.span.start()..l.span.end(),
493                                message: "called from this location".into(),
494                            }
495                        }));
496
497                let mut buffer = Buffer::no_color();
498                term::emit_to_write_style(&mut buffer, &Config::default(), &files, &diagnostic)
499                    .expect("failed to emit diagnostic");
500
501                String::from_utf8(buffer.into_inner()).expect("should be UTF-8")
502            }
503            Self::Other(e) => format!("{e:#}"),
504        }
505    }
506}
507
508impl From<anyhow::Error> for EvaluationError {
509    fn from(e: anyhow::Error) -> Self {
510        Self::Other(e)
511    }
512}
513
514/// Represents a result from evaluating a workflow or task.
515pub type EvaluationResult<T> = Result<T, EvaluationError>;
516
517/// Represents context to an expression evaluator.
518pub(crate) trait EvaluationContext: Send + Sync {
519    /// Gets the supported version of the document being evaluated.
520    fn version(&self) -> SupportedVersion;
521
522    /// Gets the value of the given name in scope.
523    fn resolve_name(&self, name: &str, span: Span) -> Result<Value, Diagnostic>;
524
525    /// Resolves a type name to a type.
526    fn resolve_type_name(&self, name: &str, span: Span) -> Result<Type, Diagnostic>;
527
528    /// Returns the literal value of an enum variant.
529    fn enum_variant_value(&self, enum_name: &str, variant_name: &str) -> Result<Value, Diagnostic>;
530
531    /// Gets the base directory for the evaluation.
532    ///
533    /// The base directory is what paths are relative to.
534    ///
535    /// For workflow evaluation, the base directory is the document's directory.
536    ///
537    /// For task evaluation, the base directory is the document's directory or
538    /// the task's working directory if the `output` section is being evaluated.
539    fn base_dir(&self) -> &EvaluationPath;
540
541    /// Gets the temp directory for the evaluation.
542    fn temp_dir(&self) -> &Path;
543
544    /// Gets the value to return for a call to the `stdout` function.
545    ///
546    /// This returns `Some` only when evaluating a task's outputs section.
547    fn stdout(&self) -> Option<&Value> {
548        None
549    }
550
551    /// Gets the value to return for a call to the `stderr` function.
552    ///
553    /// This returns `Some` only when evaluating a task's outputs section.
554    fn stderr(&self) -> Option<&Value> {
555        None
556    }
557
558    /// Gets the task associated with the evaluation context.
559    ///
560    /// This returns `Some` only when evaluating a task's hints sections.
561    fn task(&self) -> Option<&Task> {
562        None
563    }
564
565    /// Gets the transferer to use for evaluating expressions.
566    fn transferer(&self) -> &dyn Transferer;
567
568    /// Gets a guest path representation of a host path.
569    ///
570    /// Returns `None` if there is no guest path representation of the host
571    /// path.
572    fn guest_path(&self, path: &HostPath) -> Option<GuestPath> {
573        let _ = path;
574        None
575    }
576
577    /// Gets a host path representation of a guest path.
578    ///
579    /// Returns `None` if there is no host path representation of the guest
580    /// path.
581    fn host_path(&self, path: &GuestPath) -> Option<HostPath> {
582        let _ = path;
583        None
584    }
585
586    /// Notifies the context that a file was created as a result of a call to a
587    /// stdlib function.
588    ///
589    /// A context may map a guest path for the new host path.
590    fn notify_file_created(&mut self, path: &HostPath) -> Result<()> {
591        let _ = path;
592        Ok(())
593    }
594}
595
596/// Represents an index of a scope in a collection of scopes.
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
598struct ScopeIndex(usize);
599
600impl ScopeIndex {
601    /// Constructs a new scope index from a raw index.
602    pub const fn new(index: usize) -> Self {
603        Self(index)
604    }
605}
606
607impl From<usize> for ScopeIndex {
608    fn from(index: usize) -> Self {
609        Self(index)
610    }
611}
612
613impl From<ScopeIndex> for usize {
614    fn from(index: ScopeIndex) -> Self {
615        index.0
616    }
617}
618
619/// Represents an evaluation scope in a WDL document.
620#[derive(Default, Debug)]
621struct Scope {
622    /// The index of the parent scope.
623    ///
624    /// This is `None` for the root scopes.
625    parent: Option<ScopeIndex>,
626    /// The map of names in scope to their values.
627    names: IndexMap<String, Value>,
628}
629
630impl Scope {
631    /// Creates a new scope given the parent scope.
632    pub fn new(parent: ScopeIndex) -> Self {
633        Self {
634            parent: Some(parent),
635            names: Default::default(),
636        }
637    }
638
639    /// Inserts a name into the scope.
640    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
641        let prev = self.names.insert(name.into(), value.into());
642        assert!(prev.is_none(), "conflicting name in scope");
643    }
644
645    /// Iterates over the local names and values in the scope.
646    pub fn local(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
647        self.names.iter().map(|(k, v)| (k.as_str(), v))
648    }
649
650    /// Gets a mutable reference to an existing name in scope.
651    pub(crate) fn get_mut(&mut self, name: &str) -> Option<&mut Value> {
652        self.names.get_mut(name)
653    }
654
655    /// Clears the scope.
656    pub(crate) fn clear(&mut self) {
657        self.parent = None;
658        self.names.clear();
659    }
660
661    /// Sets the scope's parent.
662    pub(crate) fn set_parent(&mut self, parent: ScopeIndex) {
663        self.parent = Some(parent);
664    }
665}
666
667impl From<Scope> for IndexMap<String, Value> {
668    fn from(scope: Scope) -> Self {
669        scope.names
670    }
671}
672
673impl From<Scope> for Outputs {
674    fn from(scope: Scope) -> Self {
675        scope.names.into()
676    }
677}
678
679/// Represents a reference to a scope.
680#[derive(Debug, Clone, Copy)]
681struct ScopeRef<'a> {
682    /// The reference to the scopes collection.
683    scopes: &'a [Scope],
684    /// The index of the scope in the collection.
685    index: ScopeIndex,
686}
687
688impl<'a> ScopeRef<'a> {
689    /// Creates a new scope reference given the scope index.
690    pub fn new(scopes: &'a [Scope], index: impl Into<ScopeIndex>) -> Self {
691        Self {
692            scopes,
693            index: index.into(),
694        }
695    }
696
697    /// Gets the parent scope.
698    ///
699    /// Returns `None` if there is no parent scope.
700    pub fn parent(&self) -> Option<Self> {
701        self.scopes[self.index.0].parent.map(|p| Self {
702            scopes: self.scopes,
703            index: p,
704        })
705    }
706
707    /// Gets the value of a name local to this scope.
708    ///
709    /// Returns `None` if a name local to this scope was not found.
710    pub fn local(&self, name: &str) -> Option<&Value> {
711        self.scopes[self.index.0].names.get(name)
712    }
713
714    /// Lookups a name in the scope.
715    ///
716    /// Returns `None` if the name is not available in the scope.
717    pub fn lookup(&self, name: &str) -> Option<&Value> {
718        let mut current = Some(self.index);
719
720        while let Some(index) = current {
721            if let Some(name) = self.scopes[index.0].names.get(name) {
722                return Some(name);
723            }
724
725            current = self.scopes[index.0].parent;
726        }
727
728        None
729    }
730}
731
732/// Represents an evaluated task.
733///
734/// An evaluated task is one that was executed by a task execution backend.
735///
736/// The evaluated task may have failed as a result of an unacceptable exit code.
737///
738/// Use [`EvaluatedTask::into_outputs`] to get the outputs of the task.
739#[derive(Debug)]
740pub struct EvaluatedTask {
741    /// The underlying task execution result.
742    result: TaskExecutionResult,
743    /// The evaluated outputs of the task.
744    outputs: Outputs,
745    /// Stores the execution error for the evaluated task.
746    ///
747    /// This is `None` when the evaluated task successfully executed.
748    error: Option<EvaluationError>,
749    /// Whether or not the execution result was from the call cache.
750    cached: bool,
751}
752
753impl EvaluatedTask {
754    /// Constructs a new evaluated task.
755    fn new(cached: bool, result: TaskExecutionResult, error: Option<EvaluationError>) -> Self {
756        Self {
757            result,
758            outputs: Default::default(),
759            error,
760            cached,
761        }
762    }
763
764    /// Gets whether or not the evaluated task failed as a result of an
765    /// unacceptable exit code.
766    pub fn failed(&self) -> bool {
767        self.error.is_some()
768    }
769
770    /// Determines whether or not the task execution result was used from the
771    /// call cache.
772    pub fn cached(&self) -> bool {
773        self.cached
774    }
775
776    /// Gets the exit code of the evaluated task.
777    pub fn exit_code(&self) -> i32 {
778        self.result.exit_code
779    }
780
781    /// Gets the working directory of the evaluated task.
782    pub fn work_dir(&self) -> &EvaluationPath {
783        &self.result.work_dir
784    }
785
786    /// Gets the stdout value of the evaluated task.
787    pub fn stdout(&self) -> &Value {
788        &self.result.stdout
789    }
790
791    /// Gets the stderr value of the evaluated task.
792    pub fn stderr(&self) -> &Value {
793        &self.result.stderr
794    }
795
796    /// Converts the evaluated task into its [`Outputs`].
797    ///
798    /// An error is returned if the task failed as a result of an unacceptable
799    /// exit code.
800    pub fn into_outputs(self) -> EvaluationResult<Outputs> {
801        match self.error {
802            Some(e) => Err(e),
803            None => Ok(self.outputs),
804        }
805    }
806}
807
808#[cfg(test)]
809mod test {
810    use super::*;
811
812    #[test]
813    fn cancellation_slow() {
814        let context = CancellationContext::new(FailureMode::Slow);
815        assert_eq!(context.state(), CancellationContextState::NotCanceled);
816
817        // The first cancel should not cancel the fast token
818        assert_eq!(context.cancel(), CancellationContextState::Waiting);
819        assert_eq!(context.state(), CancellationContextState::Waiting);
820        assert!(context.user_canceled());
821        assert!(context.first.is_cancelled());
822        assert!(!context.second.is_cancelled());
823
824        // The second cancel should cancel both tokens
825        assert_eq!(context.cancel(), CancellationContextState::Canceling);
826        assert_eq!(context.state(), CancellationContextState::Canceling);
827        assert!(context.user_canceled());
828        assert!(context.first.is_cancelled());
829        assert!(context.second.is_cancelled());
830
831        // Subsequent cancellations have no effect
832        assert_eq!(context.cancel(), CancellationContextState::Canceling);
833        assert_eq!(context.state(), CancellationContextState::Canceling);
834        assert!(context.user_canceled());
835        assert!(context.first.is_cancelled());
836        assert!(context.second.is_cancelled());
837    }
838
839    #[test]
840    fn cancellation_fast() {
841        let context = CancellationContext::new(FailureMode::Fast);
842        assert_eq!(context.state(), CancellationContextState::NotCanceled);
843
844        // Fail fast should immediately cancel both tokens
845        assert_eq!(context.cancel(), CancellationContextState::Canceling);
846        assert_eq!(context.state(), CancellationContextState::Canceling);
847        assert!(context.user_canceled());
848        assert!(context.first.is_cancelled());
849        assert!(context.second.is_cancelled());
850
851        // Subsequent cancellations have no effect
852        assert_eq!(context.cancel(), CancellationContextState::Canceling);
853        assert_eq!(context.state(), CancellationContextState::Canceling);
854        assert!(context.user_canceled());
855        assert!(context.first.is_cancelled());
856        assert!(context.second.is_cancelled());
857    }
858
859    #[test]
860    fn cancellation_error_slow() {
861        let context = CancellationContext::new(FailureMode::Slow);
862        assert_eq!(context.state(), CancellationContextState::NotCanceled);
863
864        // An error should not cancel the fast token
865        context.error(&EvaluationError::Canceled);
866        assert_eq!(context.state(), CancellationContextState::Waiting);
867        assert!(!context.user_canceled());
868        assert!(context.first.is_cancelled());
869        assert!(!context.second.is_cancelled());
870
871        // A repeated error should not cancel the fast token either
872        context.error(&EvaluationError::Canceled);
873        assert_eq!(context.state(), CancellationContextState::Waiting);
874        assert!(!context.user_canceled());
875        assert!(context.first.is_cancelled());
876        assert!(!context.second.is_cancelled());
877
878        // However, another cancellation will cancel both tokens
879        assert_eq!(context.cancel(), CancellationContextState::Canceling);
880        assert_eq!(context.state(), CancellationContextState::Canceling);
881        assert!(!context.user_canceled());
882        assert!(context.first.is_cancelled());
883        assert!(context.second.is_cancelled());
884    }
885
886    #[test]
887    fn cancellation_error_fast() {
888        let context = CancellationContext::new(FailureMode::Fast);
889        assert_eq!(context.state(), CancellationContextState::NotCanceled);
890
891        // An error should cancel both tokens
892        context.error(&EvaluationError::Canceled);
893        assert_eq!(context.state(), CancellationContextState::Canceling);
894        assert!(!context.user_canceled());
895        assert!(context.first.is_cancelled());
896        assert!(context.second.is_cancelled());
897
898        // A repeated error should not change anything
899        context.error(&EvaluationError::Canceled);
900        assert_eq!(context.state(), CancellationContextState::Canceling);
901        assert!(!context.user_canceled());
902        assert!(context.first.is_cancelled());
903        assert!(context.second.is_cancelled());
904
905        // Neither should another `cancel` call
906        assert_eq!(context.cancel(), CancellationContextState::Canceling);
907        assert_eq!(context.state(), CancellationContextState::Canceling);
908        assert!(!context.user_canceled());
909        assert!(context.first.is_cancelled());
910        assert!(context.second.is_cancelled());
911    }
912
913    #[test]
914    fn cancellation_child() {
915        let context = CancellationContext::new(FailureMode::Fast);
916        assert_eq!(context.state(), CancellationContextState::NotCanceled);
917
918        // Children can have a different failure mode
919        let child = context.child(FailureMode::Slow);
920        assert_eq!(child.state(), CancellationContextState::NotCanceled);
921        assert_eq!(child.cancel(), CancellationContextState::Waiting);
922        assert_eq!(child.cancel(), CancellationContextState::Canceling);
923        assert!(child.user_canceled());
924        assert!(child.first.is_cancelled());
925        assert!(child.second.is_cancelled());
926
927        // Child cancellation doesn't affect the parent
928        assert_eq!(context.state(), CancellationContextState::NotCanceled);
929
930        // But parent cancellation affects the child
931        let child = context.child(FailureMode::Fast);
932        assert_eq!(context.cancel(), CancellationContextState::Canceling);
933        assert!(child.first.is_cancelled());
934        assert!(child.second.is_cancelled());
935    }
936
937    #[test]
938    fn child_inherits_parent_cancellation_state() {
939        let parent = CancellationContext::new(FailureMode::Fast);
940        let child = parent.child(FailureMode::Fast);
941        assert_eq!(child.state(), CancellationContextState::NotCanceled);
942
943        // The user cancels the parent (`Fast` mode cancels in one step).
944        assert_eq!(parent.cancel(), CancellationContextState::Canceling);
945
946        // The child's effective state now reflects the parent's cancellation.
947        assert_eq!(child.state(), CancellationContextState::Canceling);
948        assert!(child.user_canceled());
949
950        // Tokens still propagate from parent to child as before.
951        assert!(child.first.is_cancelled());
952        assert!(child.second.is_cancelled());
953    }
954
955    #[test]
956    fn child_keeps_local_error_cause_on_tie_with_parent() {
957        let parent = CancellationContext::new(FailureMode::Fast);
958        let child = parent.child(FailureMode::Fast);
959
960        // The user cancels the parent.
961        assert_eq!(parent.cancel(), CancellationContextState::Canceling);
962
963        // The child then records a local error, as happens when its aborted
964        // task surfaces `EvaluationError::Canceled`.
965        child.error(&EvaluationError::Canceled);
966
967        // Parent and child both sit at `Canceling`, so the levels tie and the
968        // child's own error cause is authoritative: it does not report a user
969        // cancellation despite the parent's cancellation being user initiated.
970        assert_eq!(child.state(), CancellationContextState::Canceling);
971        assert!(!child.user_canceled());
972    }
973
974    #[test]
975    fn later_parent_cancel_does_not_relabel_child_error() {
976        let parent = CancellationContext::new(FailureMode::Fast);
977        let child = parent.child(FailureMode::Fast);
978
979        // The child errors first and enters `Canceling` on its own.
980        child.error(&EvaluationError::Canceled);
981        assert_eq!(child.state(), CancellationContextState::Canceling);
982        assert!(!child.user_canceled());
983
984        // A later user cancellation of the parent ties at `Canceling`, so it
985        // does not trample the child's already-established error cause.
986        assert_eq!(parent.cancel(), CancellationContextState::Canceling);
987        assert_eq!(child.state(), CancellationContextState::Canceling);
988        assert!(!child.user_canceled());
989    }
990
991    #[test]
992    fn higher_parent_level_escalates_child_and_carries_its_cause() {
993        let parent = CancellationContext::new(FailureMode::Fast);
994        let child = parent.child(FailureMode::Slow);
995
996        // The child errors in `Slow` mode, reaching only `Waiting`.
997        child.error(&EvaluationError::Canceled);
998        assert_eq!(child.state(), CancellationContextState::Waiting);
999        assert!(!child.user_canceled());
1000
1001        // The user cancels the parent in `Fast` mode, reaching `Canceling`.
1002        // The parent's level is strictly greater, so its whole pair wins and
1003        // the child now reports the parent's user cancellation.
1004        assert_eq!(parent.cancel(), CancellationContextState::Canceling);
1005        assert_eq!(child.state(), CancellationContextState::Canceling);
1006        assert!(child.user_canceled());
1007    }
1008
1009    #[test]
1010    fn closest_descendant_wins_tie_across_multiple_levels() {
1011        let root = CancellationContext::new(FailureMode::Fast);
1012        let child = root.child(FailureMode::Fast);
1013        let grandchild = child.child(FailureMode::Fast);
1014
1015        // The user cancels the root, which folds down to the grandchild.
1016        assert_eq!(root.cancel(), CancellationContextState::Canceling);
1017
1018        // The grandchild then errors locally, tying at `Canceling`. The
1019        // closest cause, the grandchild's own error, wins over the ancestor.
1020        grandchild.error(&EvaluationError::Canceled);
1021        assert_eq!(grandchild.state(), CancellationContextState::Canceling);
1022        assert!(!grandchild.user_canceled());
1023    }
1024
1025    #[test]
1026    fn grandchild_inherits_ancestor_cancellation() {
1027        let root = CancellationContext::new(FailureMode::Fast);
1028        let child = root.child(FailureMode::Fast);
1029        let grandchild = child.child(FailureMode::Fast);
1030        assert_eq!(grandchild.state(), CancellationContextState::NotCanceled);
1031
1032        assert_eq!(root.cancel(), CancellationContextState::Canceling);
1033
1034        // Cancellation folds across more than one level.
1035        assert_eq!(grandchild.state(), CancellationContextState::Canceling);
1036        assert!(grandchild.user_canceled());
1037    }
1038
1039    #[test]
1040    fn child_cancellation_does_not_affect_parent_or_siblings() {
1041        let parent = CancellationContext::new(FailureMode::Fast);
1042        let a = parent.child(FailureMode::Fast);
1043        let b = parent.child(FailureMode::Fast);
1044
1045        assert_eq!(a.cancel(), CancellationContextState::Canceling);
1046
1047        // Cancelling one child leaves the parent and the sibling untouched.
1048        assert_eq!(parent.state(), CancellationContextState::NotCanceled);
1049        assert!(!parent.user_canceled());
1050        assert_eq!(b.state(), CancellationContextState::NotCanceled);
1051        assert!(!b.user_canceled());
1052    }
1053}