Skip to main content

harn_vm/value/
handles.rs

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