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}