Skip to main content

lattice_protocol/
cancel.rs

1//! Cooperative cancellation primitive (DESIGN.md §5.2.5).
2//!
3//! Lives at the protocol layer so every other crate can poll it
4//! without taking a dependency on grammar. The grammar crate re-
5//! exports this type as `CancellationToken` and adds the
6//! `CommandError::Cancelled`-aware `check()` short-circuit on top.
7//!
8//! The token is a clone-cheap `Arc<AtomicBool>`. Cancelling costs
9//! one atomic store; polling is one atomic load. Evaluators
10//! (motions, operators, search loops, ex commands) poll on a
11//! regular cadence -- typically once per inner-loop iteration --
12//! and bail with the appropriate domain error on a flipped token.
13//!
14//! # Sources of cancellation
15//!
16//! - **User Esc.** While an evaluator runs on the document actor,
17//!   the user can interrupt by pressing Esc. The TUI input loop
18//!   flips the token of the in-flight invocation.
19//! - **Deadline timer** (NOT YET WIRED). Per `LatencyClass` budget.
20//! - **Supersede** (NOT YET WIRED). A newer same-event request
21//!   invalidates the in-flight one.
22//!
23//! # Default token
24//!
25//! [`CancellationToken::never`] returns a token that callers have
26//! no handle to flip. Use it when the call site doesn't drive
27//! cancellation but the API requires a token.
28
29use std::sync::Arc;
30use std::sync::atomic::{AtomicBool, Ordering};
31
32/// Cooperative cancellation handle. Cheap to clone (one Arc bump);
33/// safe to share across threads / tasks.
34///
35/// Every clone shares one flag: cancelling any clone is observed by all of
36/// them, and a cancelled token never resets. Cancellation is a request, not
37/// preemption — the work only stops where it polls [`Self::is_cancelled`].
38///
39/// # Examples
40///
41/// ```
42/// use lattice_protocol::CancellationToken;
43///
44/// let token = CancellationToken::new();
45/// let worker_copy = token.clone(); // handed to the evaluator
46///
47/// let mut steps = 0;
48/// for i in 0..1_000 {
49///     if worker_copy.is_cancelled() {
50///         break; // bail with the domain's "cancelled" error
51///     }
52///     steps += 1;
53///     if i == 9 {
54///         token.cancel(); // e.g. the user pressed Esc
55///     }
56/// }
57/// assert_eq!(steps, 10);
58/// assert!(worker_copy.is_cancelled());
59/// ```
60#[derive(Debug, Clone, Default)]
61pub struct CancellationToken {
62    flag: Arc<AtomicBool>,
63}
64
65impl CancellationToken {
66    /// Build a fresh, un-cancelled token.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Build a token that can never be cancelled. Useful as a
72    /// default for callers that don't drive cancellation.
73    pub fn never() -> Self {
74        Self::default()
75    }
76
77    /// Flip the token. Subsequent `is_cancelled` calls (on this
78    /// clone or any other) will observe the cancellation.
79    pub fn cancel(&self) {
80        // Release so any state the canceller wrote before flipping
81        // is visible to readers that observe the flag.
82        self.flag.store(true, Ordering::Release);
83    }
84
85    /// Cheap (one atomic load) check. Returns `true` iff the
86    /// token has been cancelled by some clone.
87    pub fn is_cancelled(&self) -> bool {
88        self.flag.load(Ordering::Acquire)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    #![allow(clippy::unwrap_used, clippy::panic)]
95    use super::*;
96
97    #[test]
98    fn fresh_token_is_not_cancelled() {
99        let t = CancellationToken::new();
100        assert!(!t.is_cancelled());
101    }
102
103    #[test]
104    fn cancel_flips_observed_state() {
105        let t = CancellationToken::new();
106        t.cancel();
107        assert!(t.is_cancelled());
108    }
109
110    #[test]
111    fn clone_observes_same_state() {
112        let t = CancellationToken::new();
113        let t2 = t.clone();
114        t.cancel();
115        assert!(t2.is_cancelled());
116    }
117
118    #[test]
119    fn cancel_via_clone_observed_by_original() {
120        let t = CancellationToken::new();
121        let t2 = t.clone();
122        t2.cancel();
123        assert!(t.is_cancelled());
124    }
125
126    #[test]
127    fn never_token_starts_uncancelled() {
128        let t = CancellationToken::never();
129        assert!(!t.is_cancelled());
130    }
131}