Skip to main content

oxdock_parser/
value.rs

1//! Value-word core: every DSL value is a fixed-size word (a [`TypeDescriptor`]
2//! vtable pointer plus a 64-bit [`ValuePayload`]) interpreted through that
3//! vtable.
4//!
5//! There is exactly one representation for every type. Payloads that fit in
6//! 64 bits (integers, floats, booleans, handles, and host scalars annotated
7//! `#[oxdock_type(inline)]`) ride directly in the payload; everything else
8//! rides behind a thin pointer to either an owned `Box<T>` (exclusive heaps:
9//! `STRING`, `PATH`, `DURATION`, `PIPE`, most host types) or a shared
10//! `Arc<T>` (shared heaps: `LIST`, `MAP`, and host types annotated
11//! `#[oxdock_type(shared)]`). The vtable owns the lifecycle (`clone`, `drop`)
12//! and operations (`eq`, `fmt`), so `Clone`/`Drop`/`PartialEq`/`Display` on
13//! [`Value`] delegate instead of matching. There are no dynamic trait
14//! objects anywhere in this path: every hook is a monomorphic function
15//! pointer reached directly, with no table lookup and no lock.
16//!
17//! Descriptors are canonical singletons: each `#[oxdock_type]` struct gets
18//! one `&'static TypeDescriptor` (built at compile time, shared by every
19//! word of that type), so words carry their own vtable and no registry of
20//! any kind exists. The twelve startup types (`INT`, `FLOAT`, `STRING`, `BOOL`,
21//! `LIST`, `MAP`, `PATH`, `DURATION`, `PIPE`, `HANDLE`, `SEMAPHORE`, `PERMIT`)
22//! are ordinary Rust structs annotated with `#[oxdock_type]`, exactly as host
23//! types are. Name directories (which descriptor answers for `"TAG"`) live per execution
24//! state in `oxdock-core`, never here: this module knows types, not names.
25//!
26//! Ownership discipline (load-bearing, Miri-verified in
27//! `crates/oxdock-core/tests/miri_value_words.rs`):
28//!
29//! - Exclusive heap [`Value`]s own their box exactly once. `clone` allocates
30//!   a new box; `drop` frees it. No sharing, no aliasing. Because each box
31//!   holds a concrete sized `T`, its pointer is thin: no double-boxing, no
32//!   fat pointer casts, no metadata to lose.
33//! - Shared heap [`Value`]s (`LIST`, `MAP`) co-own an `Arc<T>` buffer.
34//!   `clone` bumps the strong count in `O(1)` with no allocation; `drop`
35//!   releases one count and frees only the final word's drop. Because the
36//!   DSL exposes no interior mutability, aliases, or reference syntax,
37//!   container graphs are strictly acyclic trees, so refcounting reclaims
38//!   deterministically with no tracing collector. Mutable access goes only
39//!   through [`Value::read_heap_mut`], which detaches (clones the buffer)
40//!   whenever the strong count exceeds 1, so a writer always exclusively
41//!   owns a private buffer and clones never observe each other's writes.
42//!   Deriving `&mut` from a payload any other way is unsound.
43//! - Pointer casts are always `Box::into_raw` / `Box::from_raw` (exclusive)
44//!   or `Arc::into_raw` / `Arc::from_raw` plus `Arc::increment_strong_count`
45//!   (shared) round trips on the same concrete payload type, which preserves
46//!   provenance. Inline words never touch the pointer domain; heap words
47//!   never touch the integer domain.
48//! - Minting a word with a descriptor built for a different Rust type
49//!   misdirects the vtable and is unsound. The `mint_*` constructors
50//!   document this contract; hosts mint through the payload type's own
51//!   `OxDockType::descriptor()`, which cannot mismatch by construction.
52
53use std::collections::BTreeMap;
54use std::fmt;
55use std::time::Duration;
56
57use oxdock_func_macro::oxdock_type;
58use oxdock_pipe::{PipeHandle, new_handle_in_task};
59
60/// Anchor of a type's reference section, derived from its name the way the
61/// Markdown slugger derives it from the doc title.
62pub fn type_anchor(name: &str) -> String {
63    format!("value-type-{}", name.to_lowercase())
64}
65
66/// Canonical descriptors of the twelve startup types, in a fixed order, for
67/// seeding per-state name directories and static rendering (docs-gen).
68/// Each entry is the payload struct's own singleton: no table, no lock.
69pub fn startup_descriptors() -> [(&'static str, &'static TypeDescriptor); 12] {
70    [
71        ("INT", IntValue::descriptor()),
72        ("FLOAT", FloatValue::descriptor()),
73        ("STRING", StringValue::descriptor()),
74        ("BOOL", BoolValue::descriptor()),
75        ("LIST", ListValue::descriptor()),
76        ("MAP", MapValue::descriptor()),
77        ("PATH", PathValue::descriptor()),
78        ("DURATION", DurationValue::descriptor()),
79        ("PIPE", PipeValue::descriptor()),
80        ("HANDLE", HandleValue::descriptor()),
81        ("SEMAPHORE", SemaphoreValue::descriptor()),
82        ("PERMIT", SemaphorePermit::descriptor()),
83    ]
84}
85
86// ---------------------------------------------------------------------------
87// Payload structs for the startup-registered types. Each carries
88// `#[oxdock_type]` so its descriptor derives from the same macro hosts use;
89// `inline` selects the zero-allocation payload path, exactly as for host
90// scalars. Private: hosts never name these types; they observe them through
91// the word accessors below.
92// ---------------------------------------------------------------------------
93
94/// 64-bit signed integer, e.g. an exit code.
95#[oxdock_type(crate_path = "::oxdock_parser", name = "INT", inline)]
96#[derive(Debug, Clone, Copy, PartialEq)]
97struct IntValue(pub i64);
98
99/// 64-bit float, e.g. a ratio.
100#[oxdock_type(crate_path = "::oxdock_parser", name = "FLOAT", inline)]
101#[derive(Debug, Clone, Copy, PartialEq)]
102struct FloatValue(pub f64);
103
104/// Arbitrary text. Quotes keep exact bytes, lone `$var` evaluates, `{{ ... }}` interpolates.
105#[oxdock_type(
106    crate_path = "::oxdock_parser",
107    name = "STRING",
108    summary = "Arbitrary text."
109)]
110#[derive(Debug, Clone, PartialEq)]
111struct StringValue(pub String);
112
113/// Boolean `true` or `false`.
114#[oxdock_type(crate_path = "::oxdock_parser", name = "BOOL", inline)]
115#[derive(Debug, Clone, Copy, PartialEq)]
116struct BoolValue(pub bool);
117
118/// Ordered list of values. Shared heap: cloning bumps a refcount.
119#[oxdock_type(crate_path = "::oxdock_parser", name = "LIST", shared)]
120#[derive(Debug, Clone, PartialEq)]
121struct ListValue(pub Vec<Value>);
122
123/// String-keyed map of values. Shared heap: cloning bumps a refcount.
124#[oxdock_type(crate_path = "::oxdock_parser", name = "MAP", shared)]
125#[derive(Debug, Clone, PartialEq)]
126struct MapValue(pub BTreeMap<String, Value>);
127
128/// Workspace path, resolved against cwd and guarded against escape.
129#[oxdock_type(crate_path = "::oxdock_parser", name = "PATH")]
130#[derive(Debug, Clone, PartialEq)]
131#[allow(clippy::disallowed_types)]
132struct PathValue(#[allow(clippy::disallowed_types)] pub std::path::PathBuf);
133
134/// Positive time span: `500ms`, `10s`, `2m`, `1h`; bare number means seconds.
135#[oxdock_type(
136    crate_path = "::oxdock_parser",
137    name = "DURATION",
138    summary = "Positive time span."
139)]
140#[derive(Debug, Clone, PartialEq)]
141struct DurationValue(pub Duration);
142
143/// Anonymous pipe handle. The backend materializes lazily on first
144/// binding (never eagerly at declaration), so the choice always has full
145/// usage context. Cloning shares the backend (explicit-sharing fan-out);
146/// equality is handle identity, never byte comparison.
147#[oxdock_type(
148    crate_path = "::oxdock_parser",
149    name = "PIPE",
150    summary = "Anonymous pipe handle.",
151    shared
152)]
153#[derive(Debug, Clone)]
154struct PipeValue(pub PipeHandle);
155
156impl PartialEq for PipeValue {
157    /// Handle identity: two words name the same channel iff they share
158    /// the cell. Never compares bytes (backends may be unbound, and
159    /// locking two cells in `eq` risks ordering deadlocks).
160    fn eq(&self, other: &Self) -> bool {
161        self.0.ptr_eq(&other.0)
162    }
163}
164
165/// Background ASYNC task handle for AWAIT/CANCEL.
166#[oxdock_type(crate_path = "::oxdock_parser", name = "HANDLE", inline)]
167#[derive(Debug, Clone, Copy, PartialEq)]
168struct HandleValue(pub u64);
169
170/// Shared admission-control counter backing `SEMAPHORE` words. Clones
171/// share the backend, so every word naming one semaphore observes the
172/// same count. Lock-free atomics throughout: no mutex exists to poison,
173/// so `Drop` paths stay infallible on every path including unwinding.
174/// No waiter ever sleeps on the counter, so the non-blocking acquire
175/// adds zero wedge surface.
176#[derive(Debug)]
177pub struct SemaphoreState {
178    max: usize,
179    held: std::sync::atomic::AtomicUsize,
180}
181
182impl SemaphoreState {
183    fn new(max: usize) -> Self {
184        Self {
185            max,
186            held: std::sync::atomic::AtomicUsize::new(0),
187        }
188    }
189
190    /// Non-blocking acquire: true and counted when a permit was free.
191    /// Compare-and-swap loop, so only a winning CAS claims a slot.
192    pub fn try_acquire(&self) -> bool {
193        use std::sync::atomic::Ordering;
194        let mut current = self.held.load(Ordering::Acquire);
195        loop {
196            if current >= self.max {
197                return false;
198            }
199            match self.held.compare_exchange_weak(
200                current,
201                current + 1,
202                Ordering::AcqRel,
203                Ordering::Acquire,
204            ) {
205                Ok(_) => return true,
206                Err(actual) => current = actual,
207            }
208        }
209    }
210
211    /// Release one permit. Saturating and infallible: exactly-once
212    /// last-drop release (see `PermitInner`) keeps this exact, and the
213    /// floor (never a wrap, never a panic) is the backstop.
214    pub fn release(&self) {
215        use std::sync::atomic::Ordering;
216        let _ = self
217            .held
218            .try_update(Ordering::AcqRel, Ordering::Acquire, |held| {
219                held.checked_sub(1)
220            });
221    }
222
223    /// Permits currently free. Exact at read time, stale the instant the
224    /// caller acts on it: observability only, never admission input.
225    pub fn available(&self) -> usize {
226        self.max
227            .saturating_sub(self.held.load(std::sync::atomic::Ordering::Acquire))
228    }
229}
230
231/// Counting semaphore for admission control. Cloning shares the backend
232/// (explicit-sharing fan-out); equality is handle identity, never the
233/// count.
234#[oxdock_type(
235    crate_path = "::oxdock_parser",
236    name = "SEMAPHORE",
237    summary = "Counting semaphore for admission control.",
238    shared
239)]
240#[derive(Debug, Clone)]
241struct SemaphoreValue(pub std::sync::Arc<SemaphoreState>);
242
243impl PartialEq for SemaphoreValue {
244    /// Handle identity: two words name the same semaphore iff they share
245    /// the backend. Never compares counts (a racing acquire would make
246    /// equality nondeterministic).
247    fn eq(&self, other: &Self) -> bool {
248        std::sync::Arc::ptr_eq(&self.0, &other.0)
249    }
250}
251
252/// Last-drop releaser for one acquired permit. Lives behind the permit
253/// word's `Arc`: when the final permit clone drops, this drops and hands
254/// the permit back exactly once. No manual release exists, so worker
255/// return (clean, error, panic, cooperative cancel) releases through
256/// ordinary frame teardown with no DSL cleanup code.
257#[derive(Debug)]
258pub struct PermitInner {
259    sem: std::sync::Arc<SemaphoreState>,
260}
261
262impl Drop for PermitInner {
263    fn drop(&mut self) {
264        self.sem.release();
265    }
266}
267
268/// Opaque admission permit minted by `SEMAPHORE_TRY_ACQUIRE`. Cloning
269/// shares the release obligation (first drops release nothing, the last
270/// releases once); equality is handle identity.
271#[oxdock_type(
272    crate_path = "::oxdock_parser",
273    name = "PERMIT",
274    summary = "Opaque admission permit; last drop releases it.",
275    shared
276)]
277#[derive(Debug, Clone)]
278struct SemaphorePermit(pub std::sync::Arc<PermitInner>);
279
280impl PartialEq for SemaphorePermit {
281    /// Handle identity: two words name the same permit iff they share
282    /// the releaser.
283    fn eq(&self, other: &Self) -> bool {
284        std::sync::Arc::ptr_eq(&self.0, &other.0)
285    }
286}
287
288impl fmt::Display for IntValue {
289    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
290        write!(f, "{}", self.0)
291    }
292}
293
294impl fmt::Display for FloatValue {
295    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
296        write!(f, "{}", self.0)
297    }
298}
299
300impl fmt::Display for BoolValue {
301    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
302        write!(f, "{}", self.0)
303    }
304}
305
306impl fmt::Display for HandleValue {
307    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308        write!(f, "task#{}", self.0)
309    }
310}
311
312impl fmt::Display for StringValue {
313    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
314        write!(f, "\"{}\"", self.0)
315    }
316}
317
318impl fmt::Display for ListValue {
319    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
320        write!(f, "[")?;
321        for (i, item) in self.0.iter().enumerate() {
322            if i > 0 {
323                write!(f, ", ")?;
324            }
325            write!(f, "{}", item)?;
326        }
327        write!(f, "]")
328    }
329}
330
331impl fmt::Display for MapValue {
332    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
333        write!(f, "{{")?;
334        for (i, (k, v)) in self.0.iter().enumerate() {
335            if i > 0 {
336                write!(f, ", ")?;
337            }
338            write!(f, "{}: {}", k, v)?;
339        }
340        write!(f, "}}")
341    }
342}
343
344impl fmt::Display for DurationValue {
345    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
346        write!(f, "{}", crate::command::format_duration(&self.0))
347    }
348}
349
350impl fmt::Display for PathValue {
351    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
352        write!(f, "{}", self.0.display())
353    }
354}
355
356impl fmt::Display for PipeValue {
357    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
358        write!(f, "<pipe>")
359    }
360}
361
362impl fmt::Display for SemaphoreValue {
363    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
364        write!(f, "<semaphore>")
365    }
366}
367
368impl fmt::Display for SemaphorePermit {
369    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
370        write!(f, "<permit>")
371    }
372}
373
374/// Payload half of a [`Value`] word: either the value's bytes inline or a
375/// thin pointer to an owned `Box<T>` (exclusive heaps) or a shared `Arc<T>`
376/// (shared heaps), as the type's descriptor dictates.
377/// Inline and pointer domains never mix for a given [`TypeDescriptor`].
378///
379/// Fields are private so safe code cannot forge payloads: every payload
380/// enters a word through [`store_inline`] (inline bytes) or the
381/// [`Value::mint_heap`] / [`Value::mint_heap_shared`] / [`Value::mint_inline`]
382/// choke points, where the `Send + Sync + 'static` bounds are enforced.
383/// External code observes payload bits through [`Value::inline_bits`] and
384/// [`Value::heap_ptr`].
385#[repr(C)]
386#[derive(Clone, Copy)]
387pub union ValuePayload {
388    as_u64: u64,
389    as_ptr: *mut (),
390}
391
392// Raw pointers are not `Send`/`Sync`, so both are implemented by hand.
393// Soundness: fields are private, so every exclusive heap [`Value`] owns its
394// box exactly once (mint allocates, `clone` allocates, `drop` frees),
395// shared heap [`Value`]s co-own their `Arc` buffer (mint allocates with
396// count 1, `clone` bumps, `drop` releases), payloads are never mutably
397// aliased, no vtable hook writes through a shared reference, and heap
398// contents are `Send + Sync` by construction (enforced at the `mint_*`
399// choke points, the only construction path; `Arc<T>` itself is `Send + Sync`
400// exactly when `T` is, which the same bounds guarantee).
401unsafe impl Send for ValuePayload {}
402unsafe impl Sync for ValuePayload {}
403
404/// A DSL value: a [`TypeDescriptor`] vtable pointer plus a [`ValuePayload`].
405/// Fixed size (128 bits on 64-bit targets). Lifecycle and operations call
406/// the vtable directly, with no table lookup and no lock; see the module
407/// docs for the ownership discipline.
408//
409// Fields are private so safe code cannot forge words with dangling
410// pointers: construction flows through [`Value::mint_inline`],
411// [`Value::mint_heap`], or the typed constructors below, and typed reads
412// go through [`Value::read_inline`] / [`Value::read_heap`].
413// `Send`/`Sync` follow from the payload impls above plus shared references.
414unsafe impl Send for Value {}
415unsafe impl Sync for Value {}
416#[repr(C)]
417pub struct Value {
418    vtable: &'static TypeDescriptor,
419    payload: ValuePayload,
420}
421
422impl Value {
423    /// The word's canonical descriptor singleton: the vtable backing its
424    /// lifecycle and operations.
425    pub fn descriptor(&self) -> &'static TypeDescriptor {
426        self.vtable
427    }
428
429    /// The word's registered type name (the descriptor's name).
430    pub fn type_name(&self) -> &'static str {
431        self.vtable.name
432    }
433
434    /// Raw payload bits, copied out. Meaningful for inline words (the
435    /// value's bytes); for heap words these are the box pointer's bits.
436    pub fn inline_bits(&self) -> u64 {
437        unsafe { self.payload.as_u64 }
438    }
439
440    /// Heap box (exclusive) or buffer (shared) pointer, copied out. Only
441    /// meaningful for heap words; never dereferenced here. Reading (not
442    /// dereferencing) is safe.
443    pub fn heap_ptr(&self) -> *mut () {
444        unsafe { self.payload.as_ptr }
445    }
446
447    /// Mint an inline word: memcpy the value's bytes into the payload.
448    /// Zero allocation. The descriptor must be the payload type's own
449    /// `OxDockType::descriptor()`; mismatching them misdirects the vtable
450    /// and is unsound.
451    pub fn mint_inline<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
452    where
453        T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
454    {
455        Self {
456            vtable: descriptor,
457            payload: store_inline(value),
458        }
459    }
460
461    /// Mint an exclusive heap word: move the value into an owned `Box<T>`
462    /// behind a thin pointer. One box allocation. The descriptor must be the
463    /// payload type's own `OxDockType::descriptor()`; mismatching them
464    /// misdirects the vtable and is unsound.
465    pub fn mint_heap<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
466    where
467        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
468    {
469        Self {
470            vtable: descriptor,
471            payload: ValuePayload {
472                as_ptr: Box::into_raw(Box::new(value)) as *mut (),
473            },
474        }
475    }
476
477    /// Mint a shared heap word: move the value into a reference-counted
478    /// `Arc<T>` behind a thin pointer. One allocation; later clones bump the
479    /// strong count instead of copying. The descriptor must be the payload
480    /// type's own `OxDockType::descriptor()` built for the shared path
481    /// (`#[oxdock_type(shared)]`); mismatching them misdirects the vtable
482    /// and is unsound.
483    pub fn mint_heap_shared<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
484    where
485        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
486    {
487        Self {
488            vtable: descriptor,
489            payload: ValuePayload {
490                as_ptr: std::sync::Arc::into_raw(std::sync::Arc::new(value)) as *mut (),
491            },
492        }
493    }
494
495    /// Read an inline word back out. Returns `None` when the word carries
496    /// a different descriptor; the load itself is infallible for a word
497    /// minted for `T`.
498    pub fn read_inline<T>(&self, expected: &'static TypeDescriptor) -> Option<T>
499    where
500        T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
501    {
502        if !std::ptr::eq(self.vtable, expected) {
503            return None;
504        }
505        Some(unsafe { load_inline::<T>(self.payload) })
506    }
507
508    /// Borrow a heap word's concrete value. Returns `None` when the word
509    /// carries a different descriptor.
510    pub fn read_heap<T>(&self, expected: &'static TypeDescriptor) -> Option<&T>
511    where
512        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
513    {
514        if !std::ptr::eq(self.vtable, expected) {
515            return None;
516        }
517        Some(unsafe { &*(self.payload.as_ptr as *const T) })
518    }
519
520    /// Borrow a heap word's concrete value mutably, detaching shared buffers
521    /// first (copy-on-write). Returns `None` when the word carries a
522    /// different descriptor. This is the only sound way to obtain `&mut`
523    /// access to a heap payload: exclusive heaps hand out their box
524    /// directly, shared heaps clone-then-hand-out when the strong count
525    /// exceeds 1 and mutate in place otherwise. Panics when called with an
526    /// inline descriptor, which has no heap buffer.
527    pub fn read_heap_mut<T>(&mut self, expected: &'static TypeDescriptor) -> Option<&mut T>
528    where
529        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
530    {
531        if !std::ptr::eq(self.vtable, expected) {
532            return None;
533        }
534        Some(unsafe { &mut *((self.vtable.unshare)(&mut self.payload) as *mut T) })
535    }
536
537    /// Construct an integer word (inline, zero allocation).
538    pub fn int(n: i64) -> Self {
539        Self::mint_inline(IntValue::descriptor(), IntValue(n))
540    }
541
542    /// Construct a float word (inline, zero allocation).
543    pub fn float(f: f64) -> Self {
544        Self::mint_inline(FloatValue::descriptor(), FloatValue(f))
545    }
546
547    /// Construct a boolean word (inline, zero allocation).
548    pub fn bool(b: bool) -> Self {
549        Self::mint_inline(BoolValue::descriptor(), BoolValue(b))
550    }
551
552    /// Construct a task-handle word (inline, zero allocation).
553    pub fn handle(id: u64) -> Self {
554        Self::mint_inline(HandleValue::descriptor(), HandleValue(id))
555    }
556
557    /// Construct a string word.
558    pub fn string(s: String) -> Self {
559        Self::mint_heap(StringValue::descriptor(), StringValue(s))
560    }
561
562    /// Construct a list word (shared heap: clones share the buffer).
563    pub fn list(items: Vec<Value>) -> Self {
564        Self::mint_heap_shared(ListValue::descriptor(), ListValue(items))
565    }
566
567    /// Construct a map word (shared heap: clones share the buffer).
568    pub fn map(entries: BTreeMap<String, Value>) -> Self {
569        Self::mint_heap_shared(MapValue::descriptor(), MapValue(entries))
570    }
571
572    /// Construct a path word.
573    #[allow(clippy::disallowed_types)]
574    pub fn path(p: std::path::PathBuf) -> Self {
575        Self::mint_heap(PathValue::descriptor(), PathValue(p))
576    }
577
578    /// Construct a duration word.
579    pub fn duration(d: Duration) -> Self {
580        Self::mint_heap(DurationValue::descriptor(), DurationValue(d))
581    }
582
583    /// Construct a fresh unbound pipe handle (`LET $p: PIPE`, host
584    /// `new_pipe()`). Materializes lazily on first binding.
585    pub fn pipe_fresh() -> Self {
586        Self::pipe_fresh_in_task(0)
587    }
588
589    /// Construct a fresh unbound pipe handle declared by `task_id` (`0` =
590    /// root flow). The id travels with every clone so promotion checks
591    /// always see the declaration origin.
592    pub fn pipe_fresh_in_task(task_id: u64) -> Self {
593        Self::mint_heap_shared(
594            PipeValue::descriptor(),
595            PipeValue(new_handle_in_task(task_id)),
596        )
597    }
598
599    /// Wrap an existing handle as a `PIPE` word. Clones share the backend.
600    pub fn pipe_handle(handle: PipeHandle) -> Self {
601        Self::mint_heap_shared(PipeValue::descriptor(), PipeValue(handle))
602    }
603
604    /// Construct a semaphore word admitting at most `max` concurrent
605    /// holders. Every clone names the same backend.
606    pub fn semaphore(max: usize) -> Self {
607        Self::mint_heap_shared(
608            SemaphoreValue::descriptor(),
609            SemaphoreValue(std::sync::Arc::new(SemaphoreState::new(max))),
610        )
611    }
612
613    /// Mint a `PERMIT` word bound to `sem`. The permit returns to the
614    /// semaphore when the last clone of the word drops.
615    pub fn permit(sem: &std::sync::Arc<SemaphoreState>) -> Self {
616        Self::mint_heap_shared(
617            SemaphorePermit::descriptor(),
618            SemaphorePermit(std::sync::Arc::new(PermitInner {
619                sem: std::sync::Arc::clone(sem),
620            })),
621        )
622    }
623
624    /// Borrow the semaphore backend out of a `SEMAPHORE` word. Returns
625    /// `None` for non-`SEMAPHORE` words. The clone shares the backend.
626    pub fn as_semaphore(&self) -> Option<std::sync::Arc<SemaphoreState>> {
627        self.read_heap::<SemaphoreValue>(SemaphoreValue::descriptor())
628            .map(|v| std::sync::Arc::clone(&v.0))
629    }
630
631    /// Borrow the permit backend out of a `PERMIT` word. Returns `None`
632    /// for non-`PERMIT` words. The clone shares the backend.
633    pub fn as_permit(&self) -> Option<std::sync::Arc<PermitInner>> {
634        self.read_heap::<SemaphorePermit>(SemaphorePermit::descriptor())
635            .map(|v| std::sync::Arc::clone(&v.0))
636    }
637
638    /// Read an integer payload. Returns `None` for non-`INT` words.
639    pub fn as_i64(&self) -> Option<i64> {
640        self.read_inline::<IntValue>(IntValue::descriptor())
641            .map(|v| v.0)
642    }
643
644    /// Read a float payload. Returns `None` for non-`FLOAT` words.
645    pub fn as_f64(&self) -> Option<f64> {
646        self.read_inline::<FloatValue>(FloatValue::descriptor())
647            .map(|v| v.0)
648    }
649
650    /// Read a boolean payload. Returns `None` for non-`BOOL` words.
651    pub fn as_bool(&self) -> Option<bool> {
652        self.read_inline::<BoolValue>(BoolValue::descriptor())
653            .map(|v| v.0)
654    }
655
656    /// Read a task-handle payload. Returns `None` for non-`HANDLE` words.
657    pub fn as_handle(&self) -> Option<u64> {
658        self.read_inline::<HandleValue>(HandleValue::descriptor())
659            .map(|v| v.0)
660    }
661
662    /// Borrow a string payload. Returns `None` for non-`STRING` words.
663    pub fn as_str(&self) -> Option<&str> {
664        self.read_heap::<StringValue>(StringValue::descriptor())
665            .map(|v| v.0.as_str())
666    }
667
668    /// Borrow a list payload. Returns `None` for non-`LIST` words.
669    pub fn as_list(&self) -> Option<&Vec<Value>> {
670        self.read_heap::<ListValue>(ListValue::descriptor())
671            .map(|v| &v.0)
672    }
673
674    /// Borrow a list payload mutably, detaching the shared buffer first when
675    /// clones exist. Returns `None` for non-`LIST` words. This is the choke
676    /// point every future in-place container mutation must go through.
677    pub fn as_list_mut(&mut self) -> Option<&mut Vec<Value>> {
678        self.read_heap_mut::<ListValue>(ListValue::descriptor())
679            .map(|v| &mut v.0)
680    }
681
682    /// Borrow a map payload. Returns `None` for non-`MAP` words.
683    pub fn as_map(&self) -> Option<&BTreeMap<String, Value>> {
684        self.read_heap::<MapValue>(MapValue::descriptor())
685            .map(|v| &v.0)
686    }
687
688    /// Borrow a map payload mutably, detaching the shared buffer first when
689    /// clones exist. Returns `None` for non-`MAP` words. This is the choke
690    /// point every future in-place container mutation must go through.
691    pub fn as_map_mut(&mut self) -> Option<&mut BTreeMap<String, Value>> {
692        self.read_heap_mut::<MapValue>(MapValue::descriptor())
693            .map(|v| &mut v.0)
694    }
695
696    /// Clone the pipe handle out of a `PIPE` word. Returns `None` for
697    /// non-`PIPE` words. The clone shares the backend cell.
698    pub fn as_pipe_handle(&self) -> Option<PipeHandle> {
699        self.read_heap::<PipeValue>(PipeValue::descriptor())
700            .map(|v| v.0.clone())
701    }
702
703    /// Read a duration payload. Returns `None` for non-`DURATION` words.
704    pub fn as_duration(&self) -> Option<Duration> {
705        self.read_heap::<DurationValue>(DurationValue::descriptor())
706            .map(|v| v.0)
707    }
708
709    /// Borrow a path payload. Returns `None` for non-`PATH` words.
710    #[allow(clippy::disallowed_types)]
711    pub fn as_path(&self) -> Option<&std::path::Path> {
712        self.read_heap::<PathValue>(PathValue::descriptor())
713            .map(|v| v.0.as_path())
714    }
715}
716
717impl Clone for Value {
718    fn clone(&self) -> Self {
719        let payload = unsafe { (self.vtable.clone)(self.payload) };
720        Self {
721            vtable: self.vtable,
722            payload,
723        }
724    }
725}
726
727impl Drop for Value {
728    fn drop(&mut self) {
729        unsafe { (self.vtable.drop)(self.payload) };
730    }
731}
732
733impl PartialEq for Value {
734    fn eq(&self, other: &Self) -> bool {
735        if !std::ptr::eq(self.vtable, other.vtable) {
736            return false;
737        }
738        unsafe { (self.vtable.eq)(self.payload, other.payload) }
739    }
740}
741
742impl fmt::Debug for Value {
743    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
744        write!(f, "{}(", self.vtable.name)?;
745        unsafe { (self.vtable.fmt)(self.payload, f) }?;
746        write!(f, ")")
747    }
748}
749
750impl fmt::Display for Value {
751    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
752        unsafe { (self.vtable.fmt)(self.payload, f) }
753    }
754}
755
756/// Export hook for a DSL payload type, implemented by `#[oxdock_type]` on
757/// the payload struct itself. The canonical descriptor singleton backs
758/// every word of the type; user code never names a generated symbol.
759pub trait OxDockType {
760    /// The canonical descriptor deriving from the struct's name plus doc
761    /// comments. The same reference every call: pointer-compare words
762    /// against it.
763    fn descriptor() -> &'static TypeDescriptor;
764}
765
766/// Vtable for one type: lifecycle plus operations. All hooks are plain
767/// function pointers (never closures) so descriptors stay `Copy` and the
768/// global table hands them out by value. Every hook documents the payload
769/// domain it expects; calling one with a foreign payload is unsound, and
770/// every call site is a single choke point reviewed with the layout.
771///
772/// `unshare` is the copy-on-write gate: it rewrites the payload to a
773/// uniquely owned buffer when necessary and returns a mutable pointer the
774/// caller exclusively owns. Mutation must always go through
775/// [`Value::read_heap_mut`]; deriving `&mut` from a payload any other way
776/// is unsound for shared heaps.
777#[derive(Clone, Copy)]
778pub struct TypeDescriptor {
779    pub name: &'static str,
780    pub summary: &'static str,
781    pub docs: &'static str,
782    pub clone: unsafe fn(ValuePayload) -> ValuePayload,
783    pub drop: unsafe fn(ValuePayload),
784    pub eq: unsafe fn(ValuePayload, ValuePayload) -> bool,
785    pub fmt: unsafe fn(ValuePayload, &mut fmt::Formatter<'_>) -> fmt::Result,
786    pub unshare: unsafe fn(&mut ValuePayload) -> *mut (),
787}
788
789// ---------------------------------------------------------------------------
790// Payload adapters: one inline set, one exclusive-heap set, and one shared-
791// heap set drive `clone`/`drop`/`eq`/`fmt`/`unshare` for every type through
792// monomorphic function pointers. These are `pub` solely so `#[oxdock_type]`-
793// generated descriptors can name them; hosts never call them directly.
794// ---------------------------------------------------------------------------
795
796/// Copy a `Copy` value's bytes into a payload. Panics when `T` exceeds 64
797/// bits: such types must use the heap path.
798pub fn store_inline<T>(value: T) -> ValuePayload
799where
800    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
801{
802    assert!(
803        std::mem::size_of::<T>() <= 8,
804        "inline payloads hold at most 64 bits"
805    );
806    let mut bits: u64 = 0;
807    unsafe {
808        std::ptr::copy_nonoverlapping(
809            &value as *const T as *const u8,
810            &mut bits as *mut u64 as *mut u8,
811            std::mem::size_of::<T>(),
812        );
813    }
814    // No `mem::forget`: `T: Copy` has no finalizer, so the source needs no
815    // suppression after its bytes are copied out.
816    ValuePayload { as_u64: bits }
817}
818
819/// Reconstruct a `Copy` value from an inline payload.
820///
821/// # Safety
822/// The payload must hold bytes stored by [`store_inline`] for `T`.
823pub unsafe fn load_inline<T>(payload: ValuePayload) -> T
824where
825    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
826{
827    // (Spelled as an explicit gate because `debug_assert!` expands to the
828    // banned `cfg!` macro.)
829    #[cfg(debug_assertions)]
830    if std::mem::size_of::<T>() > 8 {
831        panic!("inline payloads hold at most 64 bits");
832    }
833    let mut value = std::mem::MaybeUninit::<T>::uninit();
834    unsafe {
835        std::ptr::copy_nonoverlapping(
836            &payload.as_u64 as *const u64 as *const u8,
837            value.as_mut_ptr() as *mut u8,
838            std::mem::size_of::<T>(),
839        );
840        value.assume_init()
841    }
842}
843
844/// Inline `clone`: payloads are plain bytes.
845///
846/// # Safety
847/// The payload must hold inline bytes (never a live pointer).
848pub unsafe fn clone_copy(payload: ValuePayload) -> ValuePayload {
849    payload
850}
851
852/// Inline `drop`: nothing owns anything.
853///
854/// # Safety
855/// The payload must hold inline bytes (never a live pointer).
856pub unsafe fn drop_noop(_payload: ValuePayload) {}
857
858/// Inline `eq`: reconstruct both sides and compare.
859///
860/// # Safety
861/// Both payloads must hold bytes stored by [`store_inline`] for `T`.
862pub unsafe fn eq_inline<T>(a: ValuePayload, b: ValuePayload) -> bool
863where
864    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
865{
866    unsafe { load_inline::<T>(a) == load_inline::<T>(b) }
867}
868
869/// Inline `fmt`: reconstruct and render.
870///
871/// # Safety
872/// The payload must hold bytes stored by [`store_inline`] for `T`.
873pub unsafe fn fmt_inline<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
874where
875    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
876{
877    write!(f, "{}", unsafe { load_inline::<T>(payload) })
878}
879
880/// Heap `clone`: deep-copy the box.
881///
882/// # Safety
883/// The payload must own a `Box<T>` exactly once.
884pub unsafe fn clone_boxed<T>(payload: ValuePayload) -> ValuePayload
885where
886    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
887{
888    let source = unsafe { &*(payload.as_ptr as *const T) };
889    ValuePayload {
890        as_ptr: Box::into_raw(Box::new(source.clone())) as *mut (),
891    }
892}
893/// Heap `drop`: free the box.
894///
895/// # Safety
896/// The payload must own a `Box<T>` exactly once; it must never be used
897/// again afterwards.
898pub unsafe fn drop_boxed<T>(payload: ValuePayload)
899where
900    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
901{
902    drop(unsafe { Box::from_raw(payload.as_ptr as *mut T) });
903}
904
905/// Heap `eq`: compare the boxed values.
906///
907/// # Safety
908/// Both payloads must own a `Box<T>` exactly once.
909pub unsafe fn eq_boxed<T>(a: ValuePayload, b: ValuePayload) -> bool
910where
911    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
912{
913    let left = unsafe { &*(a.as_ptr as *const T) };
914    let right = unsafe { &*(b.as_ptr as *const T) };
915    left == right
916}
917
918/// Heap `fmt`: render the boxed value.
919///
920/// # Safety
921/// The payload must own a `Box<T>` exactly once.
922pub unsafe fn fmt_boxed<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
923where
924    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
925{
926    let value = unsafe { &*(payload.as_ptr as *const T) };
927    write!(f, "{value}")
928}
929
930/// Shared-heap `clone`: bump the `Arc` strong count, sharing the buffer.
931/// `O(1)` with no allocation.
932///
933/// # Safety
934/// The payload must co-own an `Arc<T>` buffer minted by
935/// [`Value::mint_heap_shared`] and cloned only through this hook, so one
936/// outstanding strong count exists per live word.
937pub unsafe fn clone_shared<T>(payload: ValuePayload) -> ValuePayload
938where
939    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
940{
941    unsafe { std::sync::Arc::increment_strong_count(payload.as_ptr as *const T) };
942    payload
943}
944
945/// Shared-heap `drop`: release one `Arc` strong count, freeing the buffer
946/// only when the final word drops.
947///
948/// # Safety
949/// The payload must co-own an `Arc<T>` buffer; it must never be used again
950/// afterwards.
951pub unsafe fn drop_shared<T>(payload: ValuePayload)
952where
953    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
954{
955    drop(unsafe { std::sync::Arc::from_raw(payload.as_ptr as *const T) });
956}
957
958/// Shared-heap `eq`: compare the shared values.
959///
960/// # Safety
961/// Both payloads must co-own an `Arc<T>` buffer.
962pub unsafe fn eq_shared<T>(a: ValuePayload, b: ValuePayload) -> bool
963where
964    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
965{
966    let left = unsafe { &*(a.as_ptr as *const T) };
967    let right = unsafe { &*(b.as_ptr as *const T) };
968    left == right
969}
970
971/// Shared-heap `fmt`: render the shared value.
972///
973/// # Safety
974/// The payload must co-own an `Arc<T>` buffer.
975pub unsafe fn fmt_shared<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
976where
977    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
978{
979    let value = unsafe { &*(payload.as_ptr as *const T) };
980    write!(f, "{value}")
981}
982
983/// Inline `unshare`: inline words hold bytes, not a heap buffer, so there
984/// is nothing to hand out mutably. Panics: reaching this hook means
985/// [`Value::read_heap_mut`] was called with an inline descriptor, a caller
986/// bug (mirrors [`store_inline`]'s size assert).
987///
988/// # Safety
989/// The payload must hold inline bytes (never a live pointer).
990pub unsafe fn unshare_inline(payload: &mut ValuePayload) -> *mut () {
991    let _ = payload;
992    panic!("inline words have no heap buffer to unshare");
993}
994
995/// Exclusive-heap `unshare`: the box is already uniquely owned, so the
996/// payload is returned unchanged with no allocation.
997///
998/// # Safety
999/// The payload must own a `Box<T>` exactly once. The returned pointer must
1000/// only be written through while this word stays the sole owner.
1001pub unsafe fn unshare_boxed<T>(payload: &mut ValuePayload) -> *mut ()
1002where
1003    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
1004{
1005    unsafe { payload.as_ptr }
1006}
1007
1008/// Shared-heap `unshare`: detach on write. When the strong count is 1 the
1009/// payload is returned unchanged (in-place, no allocation); otherwise the
1010/// buffer is cloned, the word is rewritten to the private buffer, and the
1011/// other clones keep the original. Either way the returned pointer addresses
1012/// a buffer this word uniquely owns.
1013///
1014/// # Safety
1015/// The payload must co-own an `Arc<T>` buffer minted by
1016/// [`Value::mint_heap_shared`] with one outstanding strong count per live
1017/// word. The returned pointer must only be written through while this word
1018/// stays the sole owner of its (possibly fresh) buffer.
1019pub unsafe fn unshare_shared<T>(payload: &mut ValuePayload) -> *mut ()
1020where
1021    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
1022{
1023    let raw = unsafe { payload.as_ptr } as *const T;
1024    let mut shared = unsafe { std::sync::Arc::from_raw(raw) };
1025    let unique = std::sync::Arc::make_mut(&mut shared);
1026    let out = unique as *mut T;
1027    payload.as_ptr = std::sync::Arc::into_raw(shared) as *mut ();
1028    out as *mut ()
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034
1035    #[test]
1036    fn semaphore_counts_exactly_to_cap() {
1037        let sem = Value::semaphore(2);
1038        let inner = sem.as_semaphore().expect("SEMAPHORE word");
1039        assert_eq!(inner.available(), 2);
1040        assert!(inner.try_acquire());
1041        assert_eq!(inner.available(), 1);
1042        assert!(inner.try_acquire());
1043        assert_eq!(inner.available(), 0);
1044        assert!(!inner.try_acquire());
1045        inner.release();
1046        assert_eq!(inner.available(), 1);
1047    }
1048
1049    #[test]
1050    fn semaphore_words_share_one_backend_by_identity() {
1051        let first = Value::semaphore(1);
1052        let alias = first.clone();
1053        assert_eq!(&alias, &first);
1054        assert_eq!(format!("{first}"), "<semaphore>");
1055        // One acquire through either word exhausts the shared count.
1056        assert!(first.as_semaphore().expect("backend").try_acquire());
1057        assert!(!alias.as_semaphore().expect("backend").try_acquire());
1058        // Distinct declarations never alias; non-words read as absent.
1059        assert_ne!(Value::semaphore(1), first);
1060        assert!(Value::int(1).as_semaphore().is_none());
1061    }
1062
1063    #[test]
1064    fn permit_last_drop_releases_exactly_once() {
1065        let sem = Value::semaphore(1);
1066        let inner = sem.as_semaphore().expect("backend");
1067        assert!(inner.try_acquire());
1068        let first = Value::permit(&inner);
1069        assert_eq!(format!("{first}"), "<permit>");
1070        let second = first.clone();
1071        assert_eq!(&first, &second);
1072        drop(first);
1073        assert_eq!(inner.available(), 0);
1074        drop(second);
1075        assert_eq!(inner.available(), 1);
1076    }
1077
1078    #[test]
1079    fn permit_releases_when_holder_panics() {
1080        let sem = Value::semaphore(1);
1081        let inner = sem.as_semaphore().expect("backend");
1082        let worker = {
1083            let inner = std::sync::Arc::clone(&inner);
1084            std::thread::spawn(move || {
1085                assert!(inner.try_acquire());
1086                let _permit = Value::permit(&inner);
1087                panic!("worker fails holding the permit");
1088            })
1089        };
1090        assert!(worker.join().is_err());
1091        assert_eq!(inner.available(), 1);
1092    }
1093
1094    #[test]
1095    fn semaphore_holds_cap_under_contention() {
1096        use std::sync::atomic::Ordering;
1097        let sem = Value::semaphore(4);
1098        let inner = sem.as_semaphore().expect("backend");
1099        let holders = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1100        let peak = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1101        let mut threads = Vec::new();
1102        for _ in 0..8 {
1103            let inner = std::sync::Arc::clone(&inner);
1104            let holders = std::sync::Arc::clone(&holders);
1105            let peak = std::sync::Arc::clone(&peak);
1106            threads.push(std::thread::spawn(move || {
1107                let mut acquired = 0;
1108                while acquired < 25 {
1109                    if inner.try_acquire() {
1110                        {
1111                            // Release rides the permit word's drop: the
1112                            // scope exit below hands the permit back.
1113                            let _permit = Value::permit(&inner);
1114                            let current = holders.fetch_add(1, Ordering::SeqCst) + 1;
1115                            peak.fetch_max(current, Ordering::SeqCst);
1116                            std::thread::yield_now();
1117                            holders.fetch_sub(1, Ordering::SeqCst);
1118                        }
1119                        acquired += 1;
1120                    } else {
1121                        std::thread::yield_now();
1122                    }
1123                }
1124            }));
1125        }
1126        for thread in threads {
1127            thread.join().expect("worker joins");
1128        }
1129        // 200 acquisitions, never more than 4 inside at once, every
1130        // permit handed back: exact accounting under contention.
1131        assert!(peak.load(Ordering::SeqCst) <= 4);
1132        assert_eq!(holders.load(Ordering::SeqCst), 0);
1133        assert_eq!(inner.available(), 4);
1134    }
1135}