Skip to main content

harn_vm/value/
handles.rs

1use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
2use std::sync::Arc;
3
4use parking_lot::Mutex;
5
6use super::{VmError, VmValue};
7
8type VmResourceRelease = Box<dyn FnOnce() -> Result<VmValue, String> + Send + 'static>;
9
10struct VmResourceGuardState {
11    release: Option<VmResourceRelease>,
12    result: Option<Result<VmValue, String>>,
13}
14
15/// A host-owned resource whose cleanup is bound to VM value lifetime.
16///
17/// The explicit [`Self::release`] path returns a typed host receipt. If a
18/// script abandons the value through an exception, cancellation, frame
19/// teardown, or VM drop, `Drop` invokes the same idempotent callback and
20/// discards only the unreportable cleanup result.
21pub struct VmResourceGuardHandle {
22    label: Arc<str>,
23    state: Mutex<VmResourceGuardState>,
24}
25
26impl VmResourceGuardHandle {
27    /// Construct a guard and atomically attach its one-shot release callback.
28    pub fn new(
29        label: impl Into<Arc<str>>,
30        release: impl FnOnce() -> Result<VmValue, String> + Send + 'static,
31    ) -> Self {
32        Self {
33            label: label.into(),
34            state: Mutex::new(VmResourceGuardState {
35                release: Some(Box::new(release)),
36                result: None,
37            }),
38        }
39    }
40
41    /// Stable diagnostic label without exposing resource authority.
42    pub fn label(&self) -> &str {
43        &self.label
44    }
45
46    /// Release exactly once and replay the first typed result to later calls.
47    pub fn release(&self) -> Result<VmValue, VmError> {
48        let mut state = self.state.lock();
49        if let Some(result) = &state.result {
50            return result.clone().map_err(VmError::Runtime);
51        }
52        let release = state
53            .release
54            .take()
55            .expect("resource guard without callback or cached result");
56        let result = release();
57        state.result = Some(result.clone());
58        result.map_err(VmError::Runtime)
59    }
60
61    /// Whether cleanup has already produced its terminal result.
62    pub fn is_released(&self) -> bool {
63        self.state.lock().result.is_some()
64    }
65}
66
67impl std::fmt::Debug for VmResourceGuardHandle {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("VmResourceGuardHandle")
70            .field("label", &self.label)
71            .field("released", &self.is_released())
72            .finish()
73    }
74}
75
76impl Drop for VmResourceGuardHandle {
77    fn drop(&mut self) {
78        let _ = self.release();
79    }
80}
81
82#[cfg(test)]
83mod resource_guard_tests {
84    use std::sync::atomic::{AtomicUsize, Ordering};
85
86    use super::*;
87
88    #[test]
89    fn explicit_release_is_replayed_without_repeating_cleanup() {
90        let calls = Arc::new(AtomicUsize::new(0));
91        let observed = Arc::clone(&calls);
92        let guard = VmResourceGuardHandle::new("fixture", move || {
93            observed.fetch_add(1, Ordering::SeqCst);
94            Ok(VmValue::string("released"))
95        });
96
97        assert_eq!(guard.release().unwrap().display(), "released");
98        assert_eq!(guard.release().unwrap().display(), "released");
99        assert_eq!(calls.load(Ordering::SeqCst), 1);
100        drop(guard);
101        assert_eq!(calls.load(Ordering::SeqCst), 1);
102    }
103
104    #[test]
105    fn drop_runs_abandoned_resource_cleanup() {
106        let calls = Arc::new(AtomicUsize::new(0));
107        let observed = Arc::clone(&calls);
108        let guard = VmResourceGuardHandle::new("fixture", move || {
109            observed.fetch_add(1, Ordering::SeqCst);
110            Ok(VmValue::Nil)
111        });
112
113        drop(guard);
114        assert_eq!(calls.load(Ordering::SeqCst), 1);
115    }
116}
117
118/// The raw join handle type for spawned tasks.
119pub type VmJoinHandle = tokio::task::JoinHandle<Result<(VmValue, String), VmError>>;
120
121/// A spawned async task handle with cancellation support.
122pub struct VmTaskHandle {
123    pub handle: VmJoinHandle,
124    /// Cooperative cancellation token. Set to true to request graceful shutdown.
125    pub cancel_token: Arc<AtomicBool>,
126    /// Runtime-context task id used by the VM scheduler and wait-for graph.
127    pub wait_task_id: String,
128}
129
130/// A channel handle for the VM (uses tokio mpsc).
131#[derive(Debug, Clone)]
132pub struct VmChannelHandle {
133    pub name: Arc<str>,
134    pub sender: Arc<tokio::sync::mpsc::Sender<VmValue>>,
135    pub receiver: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<VmValue>>>,
136    pub close: Arc<VmChannelCloseState>,
137}
138
139#[derive(Debug)]
140pub struct VmChannelCloseState {
141    closed: AtomicBool,
142    signal: tokio::sync::watch::Sender<bool>,
143}
144
145impl VmChannelCloseState {
146    pub(crate) fn open() -> Self {
147        let (signal, _) = tokio::sync::watch::channel(false);
148        Self {
149            closed: AtomicBool::new(false),
150            signal,
151        }
152    }
153
154    pub(crate) fn close(&self) -> bool {
155        if self.closed.swap(true, Ordering::SeqCst) {
156            return false;
157        }
158        self.signal.send_replace(true);
159        true
160    }
161
162    pub(crate) fn is_closed(&self) -> bool {
163        self.closed.load(Ordering::SeqCst)
164    }
165
166    pub(crate) fn subscribe(&self) -> tokio::sync::watch::Receiver<bool> {
167        self.signal.subscribe()
168    }
169}
170
171impl VmChannelHandle {
172    pub(crate) fn close(&self) -> bool {
173        self.close.close()
174    }
175
176    pub(crate) fn is_closed(&self) -> bool {
177        self.close.is_closed()
178    }
179
180    pub(crate) fn subscribe_closed(&self) -> tokio::sync::watch::Receiver<bool> {
181        self.close.subscribe()
182    }
183}
184
185/// An atomic integer handle for the VM.
186#[derive(Debug, Clone)]
187pub struct VmAtomicHandle {
188    pub value: Arc<AtomicI64>,
189}
190
191/// A reproducible random number generator handle.
192#[derive(Clone)]
193pub struct VmRngHandle {
194    pub rng: Arc<Mutex<rand::rngs::StdRng>>,
195}
196
197impl std::fmt::Debug for VmRngHandle {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.write_str("VmRngHandle { .. }")
200    }
201}
202
203/// A host-minted proof-of-execution receipt: the payload of a positive
204/// `Verdict`. Constructed ONLY by the verdict issuance capability
205/// (`harness.verdict.issue`) from the host-owned record of a REAL, unfiltered,
206/// workspace-discovered `run_test` execution — resolved by its opaque
207/// `result_handle`, whose disposition the host froze at execution time.
208/// Issuance reads no caller-supplied filesystem
209/// bytes, so a caller can forge neither the receipt's TYPE (no literal syntax,
210/// no public builtin hands back the bare handle) nor its PROVENANCE (an authored
211/// file has no handle in the execution store). It is refused by the durable
212/// serialization seams, so a positive verdict cannot be minted, forged, or
213/// replayed by asserting scalars or fabricating evidence. The content hash + run
214/// identity close the tamper and cross-run-replay classes: the hash fingerprints
215/// the bytes the host captured, and consumers reject a receipt whose
216/// `execution_scope` differs from the active run.
217#[derive(Debug, Clone)]
218pub struct VmVerdictReceipt {
219    /// Stable identity of the attested execution — the `run_test` `result_handle`
220    /// the host recorded the run under.
221    pub artifact_id: Arc<str>,
222    /// `sha256:HEX` of the output the host captured from the real execution,
223    /// snapshotted when the run was recorded.
224    pub content_hash: Arc<str>,
225    /// Identity of the host-discovered test plan that selected the command.
226    pub plan_id: Arc<str>,
227    /// Hash of the active workspace root the plan was discovered within.
228    pub workspace_hash: Arc<str>,
229    /// Hash of the exact argv selected and executed by the host.
230    pub command_hash: Arc<str>,
231    /// Passing and total checked-unit counts the host COMPUTED from the real
232    /// execution, never a caller scalar. `passed > 0` is required to mint.
233    pub passed: u32,
234    pub total: u32,
235    /// The execution scope that PRODUCED the evidence — captured at `run_test`
236    /// record time, not at receipt-mint time. `verdict_all` rejects receipts
237    /// whose `execution_scope` differ (cross-run replay) AND requires the active
238    /// scope to still equal it, so a receipt cannot be replayed into a later run.
239    pub execution_scope: Arc<str>,
240    /// Optional subject identity (which unit-of-work the evidence attests). Folded
241    /// in when the artifact carries it; when absent it is a NAMED limit (PR body).
242    pub subject: Option<Arc<str>>,
243}
244
245/// A held synchronization permit for mutex/semaphore/gate primitives.
246#[derive(Debug, Clone)]
247pub struct VmSyncPermitHandle {
248    pub(crate) lease: Arc<crate::synchronization::VmSyncLease>,
249}
250
251impl VmSyncPermitHandle {
252    pub(crate) fn release(&self) -> bool {
253        self.lease.release()
254    }
255
256    pub(crate) fn kind(&self) -> &str {
257        self.lease.kind()
258    }
259
260    pub(crate) fn key(&self) -> &str {
261        self.lease.key()
262    }
263
264    pub(crate) fn permits(&self) -> u32 {
265        self.lease.permits()
266    }
267
268    pub(crate) fn is_released(&self) -> bool {
269        self.lease.is_released()
270    }
271
272    pub(crate) fn same_lease(&self, other: &Self) -> bool {
273        Arc::ptr_eq(&self.lease, &other.lease)
274    }
275}
276
277/// A lazy integer range — Python-style. Stores only `(start, end, inclusive)`
278/// so the in-memory footprint is O(1) regardless of the range's length.
279/// `len()`, indexing (`r[k]`), `.contains(x)`, `.first()`, `.last()` are all
280/// O(1); direct iteration walks step-by-step without materializing a list.
281///
282/// Empty-range convention (Python-consistent):
283/// - Inclusive empty when `start > end`.
284/// - Exclusive empty when `start >= end`.
285///
286/// Negative / reversed ranges are NOT supported in v1: `5 to 1` is simply
287/// empty. Authors who want reverse iteration should call `.to_list().reverse()`.
288#[derive(Debug, Clone, Copy)]
289pub struct VmRange {
290    pub start: i64,
291    pub end: i64,
292    pub inclusive: bool,
293}
294
295impl VmRange {
296    /// Number of elements this range yields.
297    ///
298    /// Uses saturating arithmetic so that pathological ranges near
299    /// `i64::MAX`/`i64::MIN` do not panic on overflow. Because a range's
300    /// element count must fit in `i64` the returned length saturates at
301    /// `i64::MAX` for ranges whose width exceeds that (e.g. `i64::MIN to
302    /// i64::MAX` inclusive). Callers that later narrow to `usize` for
303    /// allocation should still guard against huge lengths — see
304    /// `to_vec` / `get` for the indexable-range invariants.
305    pub fn len(&self) -> i64 {
306        if self.inclusive {
307            if self.start > self.end {
308                0
309            } else {
310                self.end.saturating_sub(self.start).saturating_add(1)
311            }
312        } else if self.start >= self.end {
313            0
314        } else {
315            self.end.saturating_sub(self.start)
316        }
317    }
318
319    pub fn is_empty(&self) -> bool {
320        self.len() == 0
321    }
322
323    /// Element at the given 0-based index, bounds-checked.
324    /// Returns `None` when out of bounds or when `start + idx` would
325    /// overflow (which can only happen when `len()` saturated).
326    pub fn get(&self, idx: i64) -> Option<i64> {
327        if idx < 0 || idx >= self.len() {
328            None
329        } else {
330            self.start.checked_add(idx)
331        }
332    }
333
334    /// First element or `None` when empty.
335    pub fn first(&self) -> Option<i64> {
336        if self.is_empty() {
337            None
338        } else {
339            Some(self.start)
340        }
341    }
342
343    /// Last element or `None` when empty.
344    pub fn last(&self) -> Option<i64> {
345        if self.is_empty() {
346            None
347        } else if self.inclusive {
348            Some(self.end)
349        } else {
350            Some(self.end - 1)
351        }
352    }
353
354    /// Whether `v` falls inside the range (O(1)).
355    pub fn contains(&self, v: i64) -> bool {
356        if self.is_empty() {
357            return false;
358        }
359        if self.inclusive {
360            v >= self.start && v <= self.end
361        } else {
362            v >= self.start && v < self.end
363        }
364    }
365
366    /// Materialize to a `Vec<VmValue>` — the explicit escape hatch.
367    ///
368    /// Uses `checked_add` on the per-element index so a range near
369    /// `i64::MAX` stops at the representable bound instead of panicking.
370    /// Callers should still treat a very long range as unwise to
371    /// materialize (the whole point of `VmRange` is to avoid this).
372    pub fn to_vec(&self) -> Vec<VmValue> {
373        let len = self.len();
374        if len <= 0 {
375            return Vec::new();
376        }
377        let cap = len as usize;
378        let mut out = Vec::with_capacity(cap);
379        for i in 0..len {
380            match self.start.checked_add(i) {
381                Some(v) => out.push(VmValue::Int(v)),
382                None => break,
383            }
384        }
385        out
386    }
387}
388
389/// A generator object: lazily produces values via yield.
390/// The generator body runs as a spawned task that sends values through a channel.
391#[derive(Debug, Clone)]
392pub struct VmGenerator {
393    /// Whether the generator has finished (returned or exhausted).
394    pub done: Arc<AtomicBool>,
395    /// Receiver end of the yield channel (generator sends values here).
396    /// Wrapped in a shared async mutex so recv() can be called without holding
397    /// a synchronous iterator-state lock across await points.
398    pub receiver: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Result<VmValue, VmError>>>>,
399}
400
401impl VmGenerator {
402    pub(crate) fn is_done(&self) -> bool {
403        self.done.load(Ordering::Relaxed)
404    }
405
406    pub(crate) fn mark_done(&self) {
407        self.done.store(true, Ordering::Relaxed);
408    }
409}
410
411/// A stream object: lazily produces values from a `gen fn`.
412#[derive(Debug, Clone)]
413pub struct VmStream {
414    /// Whether the stream has finished (returned, thrown, or exhausted).
415    pub done: Arc<AtomicBool>,
416    /// Receiver end of the stream channel.
417    pub receiver: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Result<VmValue, VmError>>>>,
418    /// Optional cancellation hook for host-backed streams.
419    pub cancel: Option<VmStreamCancel>,
420}
421
422impl VmStream {
423    pub(crate) fn is_done(&self) -> bool {
424        self.done.load(Ordering::Relaxed)
425    }
426
427    pub(crate) fn mark_done(&self) {
428        self.done.store(true, Ordering::Relaxed);
429    }
430}
431
432#[derive(Clone)]
433pub struct VmStreamCancel {
434    sender: Arc<tokio::sync::watch::Sender<bool>>,
435}
436
437impl VmStreamCancel {
438    pub fn new() -> Self {
439        let (sender, _receiver) = tokio::sync::watch::channel(false);
440        Self {
441            sender: Arc::new(sender),
442        }
443    }
444
445    pub fn cancel(&self) {
446        let _ = self.sender.send(true);
447    }
448
449    pub fn subscribe(&self) -> tokio::sync::watch::Receiver<bool> {
450        self.sender.subscribe()
451    }
452}
453
454impl Default for VmStreamCancel {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl std::fmt::Debug for VmStreamCancel {
461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462        f.debug_struct("VmStreamCancel")
463            .field("cancelled", &*self.sender.borrow())
464            .finish()
465    }
466}
467
468impl VmStream {
469    pub(crate) fn cancel(&self) {
470        if let Some(cancel) = &self.cancel {
471            cancel.cancel();
472        }
473    }
474}