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