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 ten startup types (`INT`, `FLOAT`, `STRING`, `BOOL`,
21//! `LIST`, `MAP`, `PATH`, `DURATION`, `PIPE`, `HANDLE`) are ordinary Rust
22//! structs annotated with `#[oxdock_type]`, exactly as host types are. Name
23//! 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;
58
59/// Anchor of a type's reference section, derived from its name the way the
60/// Markdown slugger derives it from the doc title.
61pub fn type_anchor(name: &str) -> String {
62    format!("value-type-{}", name.to_lowercase())
63}
64
65/// Canonical descriptors of the ten startup types, in a fixed order, for
66/// seeding per-state name directories and static rendering (docs-gen).
67/// Each entry is the payload struct's own singleton: no table, no lock.
68pub fn startup_descriptors() -> [(&'static str, &'static TypeDescriptor); 10] {
69    [
70        ("INT", IntValue::descriptor()),
71        ("FLOAT", FloatValue::descriptor()),
72        ("STRING", StringValue::descriptor()),
73        ("BOOL", BoolValue::descriptor()),
74        ("LIST", ListValue::descriptor()),
75        ("MAP", MapValue::descriptor()),
76        ("PATH", PathValue::descriptor()),
77        ("DURATION", DurationValue::descriptor()),
78        ("PIPE", PipeValue::descriptor()),
79        ("HANDLE", HandleValue::descriptor()),
80    ]
81}
82
83// ---------------------------------------------------------------------------
84// Payload structs for the startup-registered types. Each carries
85// `#[oxdock_type]` so its descriptor derives from the same macro hosts use;
86// `inline` selects the zero-allocation payload path, exactly as for host
87// scalars. Private: hosts never name these types; they observe them through
88// the word accessors below.
89// ---------------------------------------------------------------------------
90
91/// 64-bit signed integer, e.g. an exit code.
92#[oxdock_type(crate_path = "::oxdock_parser", name = "INT", inline)]
93#[derive(Debug, Clone, Copy, PartialEq)]
94struct IntValue(pub i64);
95
96/// 64-bit float, e.g. a ratio.
97#[oxdock_type(crate_path = "::oxdock_parser", name = "FLOAT", inline)]
98#[derive(Debug, Clone, Copy, PartialEq)]
99struct FloatValue(pub f64);
100
101/// Arbitrary text. Quotes keep exact bytes, lone `$var` evaluates, `{{ ... }}` interpolates.
102#[oxdock_type(
103    crate_path = "::oxdock_parser",
104    name = "STRING",
105    summary = "Arbitrary text."
106)]
107#[derive(Debug, Clone, PartialEq)]
108struct StringValue(pub String);
109
110/// Boolean `true` or `false`.
111#[oxdock_type(crate_path = "::oxdock_parser", name = "BOOL", inline)]
112#[derive(Debug, Clone, Copy, PartialEq)]
113struct BoolValue(pub bool);
114
115/// Ordered list of values. Shared heap: cloning bumps a refcount.
116#[oxdock_type(crate_path = "::oxdock_parser", name = "LIST", shared)]
117#[derive(Debug, Clone, PartialEq)]
118struct ListValue(pub Vec<Value>);
119
120/// String-keyed map of values. Shared heap: cloning bumps a refcount.
121#[oxdock_type(crate_path = "::oxdock_parser", name = "MAP", shared)]
122#[derive(Debug, Clone, PartialEq)]
123struct MapValue(pub BTreeMap<String, Value>);
124
125/// Workspace path, resolved against cwd and guarded against escape.
126#[oxdock_type(crate_path = "::oxdock_parser", name = "PATH")]
127#[derive(Debug, Clone, PartialEq)]
128#[allow(clippy::disallowed_types)]
129struct PathValue(#[allow(clippy::disallowed_types)] pub std::path::PathBuf);
130
131/// Positive time span: `500ms`, `10s`, `2m`, `1h`; bare number means seconds.
132#[oxdock_type(
133    crate_path = "::oxdock_parser",
134    name = "DURATION",
135    summary = "Positive time span."
136)]
137#[derive(Debug, Clone, PartialEq)]
138struct DurationValue(pub Duration);
139
140/// Named script pipe. Validity is checked against the pipe registry at coercion time.
141#[oxdock_type(
142    crate_path = "::oxdock_parser",
143    name = "PIPE",
144    summary = "Named script pipe."
145)]
146#[derive(Debug, Clone, PartialEq)]
147struct PipeValue(pub String);
148
149/// Background ASYNC task handle for AWAIT/CANCEL.
150#[oxdock_type(crate_path = "::oxdock_parser", name = "HANDLE", inline)]
151#[derive(Debug, Clone, Copy, PartialEq)]
152struct HandleValue(pub u64);
153
154impl fmt::Display for IntValue {
155    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156        write!(f, "{}", self.0)
157    }
158}
159
160impl fmt::Display for FloatValue {
161    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
162        write!(f, "{}", self.0)
163    }
164}
165
166impl fmt::Display for BoolValue {
167    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168        write!(f, "{}", self.0)
169    }
170}
171
172impl fmt::Display for HandleValue {
173    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174        write!(f, "task#{}", self.0)
175    }
176}
177
178impl fmt::Display for StringValue {
179    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
180        write!(f, "\"{}\"", self.0)
181    }
182}
183
184impl fmt::Display for ListValue {
185    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186        write!(f, "[")?;
187        for (i, item) in self.0.iter().enumerate() {
188            if i > 0 {
189                write!(f, ", ")?;
190            }
191            write!(f, "{}", item)?;
192        }
193        write!(f, "]")
194    }
195}
196
197impl fmt::Display for MapValue {
198    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
199        write!(f, "{{")?;
200        for (i, (k, v)) in self.0.iter().enumerate() {
201            if i > 0 {
202                write!(f, ", ")?;
203            }
204            write!(f, "{}: {}", k, v)?;
205        }
206        write!(f, "}}")
207    }
208}
209
210impl fmt::Display for DurationValue {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(f, "{}", crate::command::format_duration(&self.0))
213    }
214}
215
216impl fmt::Display for PathValue {
217    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218        write!(f, "{}", self.0.display())
219    }
220}
221
222impl fmt::Display for PipeValue {
223    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
224        write!(f, "pipe:{}", self.0)
225    }
226}
227
228/// Payload half of a [`Value`] word: either the value's bytes inline or a
229/// thin pointer to an owned `Box<T>` (exclusive heaps) or a shared `Arc<T>`
230/// (shared heaps), as the type's descriptor dictates.
231/// Inline and pointer domains never mix for a given [`TypeDescriptor`].
232///
233/// Fields are private so safe code cannot forge payloads: every payload
234/// enters a word through [`store_inline`] (inline bytes) or the
235/// [`Value::mint_heap`] / [`Value::mint_heap_shared`] / [`Value::mint_inline`]
236/// choke points, where the `Send + Sync + 'static` bounds are enforced.
237/// External code observes payload bits through [`Value::inline_bits`] and
238/// [`Value::heap_ptr`].
239#[repr(C)]
240#[derive(Clone, Copy)]
241pub union ValuePayload {
242    as_u64: u64,
243    as_ptr: *mut (),
244}
245
246// Raw pointers are not `Send`/`Sync`, so both are implemented by hand.
247// Soundness: fields are private, so every exclusive heap [`Value`] owns its
248// box exactly once (mint allocates, `clone` allocates, `drop` frees),
249// shared heap [`Value`]s co-own their `Arc` buffer (mint allocates with
250// count 1, `clone` bumps, `drop` releases), payloads are never mutably
251// aliased, no vtable hook writes through a shared reference, and heap
252// contents are `Send + Sync` by construction (enforced at the `mint_*`
253// choke points, the only construction path; `Arc<T>` itself is `Send + Sync`
254// exactly when `T` is, which the same bounds guarantee).
255unsafe impl Send for ValuePayload {}
256unsafe impl Sync for ValuePayload {}
257
258/// A DSL value: a [`TypeDescriptor`] vtable pointer plus a [`ValuePayload`].
259/// Fixed size (128 bits on 64-bit targets). Lifecycle and operations call
260/// the vtable directly, with no table lookup and no lock; see the module
261/// docs for the ownership discipline.
262//
263// Fields are private so safe code cannot forge words with dangling
264// pointers: construction flows through [`Value::mint_inline`],
265// [`Value::mint_heap`], or the typed constructors below, and typed reads
266// go through [`Value::read_inline`] / [`Value::read_heap`].
267// `Send`/`Sync` follow from the payload impls above plus shared references.
268unsafe impl Send for Value {}
269unsafe impl Sync for Value {}
270#[repr(C)]
271pub struct Value {
272    vtable: &'static TypeDescriptor,
273    payload: ValuePayload,
274}
275
276impl Value {
277    /// The word's canonical descriptor singleton: the vtable backing its
278    /// lifecycle and operations.
279    pub fn descriptor(&self) -> &'static TypeDescriptor {
280        self.vtable
281    }
282
283    /// The word's registered type name (the descriptor's name).
284    pub fn type_name(&self) -> &'static str {
285        self.vtable.name
286    }
287
288    /// Raw payload bits, copied out. Meaningful for inline words (the
289    /// value's bytes); for heap words these are the box pointer's bits.
290    pub fn inline_bits(&self) -> u64 {
291        unsafe { self.payload.as_u64 }
292    }
293
294    /// Heap box (exclusive) or buffer (shared) pointer, copied out. Only
295    /// meaningful for heap words; never dereferenced here. Reading (not
296    /// dereferencing) is safe.
297    pub fn heap_ptr(&self) -> *mut () {
298        unsafe { self.payload.as_ptr }
299    }
300
301    /// Mint an inline word: memcpy the value's bytes into the payload.
302    /// Zero allocation. The descriptor must be the payload type's own
303    /// `OxDockType::descriptor()`; mismatching them misdirects the vtable
304    /// and is unsound.
305    pub fn mint_inline<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
306    where
307        T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
308    {
309        Self {
310            vtable: descriptor,
311            payload: store_inline(value),
312        }
313    }
314
315    /// Mint an exclusive heap word: move the value into an owned `Box<T>`
316    /// behind a thin pointer. One box allocation. The descriptor must be the
317    /// payload type's own `OxDockType::descriptor()`; mismatching them
318    /// misdirects the vtable and is unsound.
319    pub fn mint_heap<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
320    where
321        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
322    {
323        Self {
324            vtable: descriptor,
325            payload: ValuePayload {
326                as_ptr: Box::into_raw(Box::new(value)) as *mut (),
327            },
328        }
329    }
330
331    /// Mint a shared heap word: move the value into a reference-counted
332    /// `Arc<T>` behind a thin pointer. One allocation; later clones bump the
333    /// strong count instead of copying. The descriptor must be the payload
334    /// type's own `OxDockType::descriptor()` built for the shared path
335    /// (`#[oxdock_type(shared)]`); mismatching them misdirects the vtable
336    /// and is unsound.
337    pub fn mint_heap_shared<T>(descriptor: &'static TypeDescriptor, value: T) -> Self
338    where
339        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
340    {
341        Self {
342            vtable: descriptor,
343            payload: ValuePayload {
344                as_ptr: std::sync::Arc::into_raw(std::sync::Arc::new(value)) as *mut (),
345            },
346        }
347    }
348
349    /// Read an inline word back out. Returns `None` when the word carries
350    /// a different descriptor; the load itself is infallible for a word
351    /// minted for `T`.
352    pub fn read_inline<T>(&self, expected: &'static TypeDescriptor) -> Option<T>
353    where
354        T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
355    {
356        if !std::ptr::eq(self.vtable, expected) {
357            return None;
358        }
359        Some(unsafe { load_inline::<T>(self.payload) })
360    }
361
362    /// Borrow a heap word's concrete value. Returns `None` when the word
363    /// carries a different descriptor.
364    pub fn read_heap<T>(&self, expected: &'static TypeDescriptor) -> Option<&T>
365    where
366        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
367    {
368        if !std::ptr::eq(self.vtable, expected) {
369            return None;
370        }
371        Some(unsafe { &*(self.payload.as_ptr as *const T) })
372    }
373
374    /// Borrow a heap word's concrete value mutably, detaching shared buffers
375    /// first (copy-on-write). Returns `None` when the word carries a
376    /// different descriptor. This is the only sound way to obtain `&mut`
377    /// access to a heap payload: exclusive heaps hand out their box
378    /// directly, shared heaps clone-then-hand-out when the strong count
379    /// exceeds 1 and mutate in place otherwise. Panics when called with an
380    /// inline descriptor, which has no heap buffer.
381    pub fn read_heap_mut<T>(&mut self, expected: &'static TypeDescriptor) -> Option<&mut T>
382    where
383        T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
384    {
385        if !std::ptr::eq(self.vtable, expected) {
386            return None;
387        }
388        Some(unsafe { &mut *((self.vtable.unshare)(&mut self.payload) as *mut T) })
389    }
390
391    /// Construct an integer word (inline, zero allocation).
392    pub fn int(n: i64) -> Self {
393        Self::mint_inline(IntValue::descriptor(), IntValue(n))
394    }
395
396    /// Construct a float word (inline, zero allocation).
397    pub fn float(f: f64) -> Self {
398        Self::mint_inline(FloatValue::descriptor(), FloatValue(f))
399    }
400
401    /// Construct a boolean word (inline, zero allocation).
402    pub fn bool(b: bool) -> Self {
403        Self::mint_inline(BoolValue::descriptor(), BoolValue(b))
404    }
405
406    /// Construct a task-handle word (inline, zero allocation).
407    pub fn handle(id: u64) -> Self {
408        Self::mint_inline(HandleValue::descriptor(), HandleValue(id))
409    }
410
411    /// Construct a string word.
412    pub fn string(s: String) -> Self {
413        Self::mint_heap(StringValue::descriptor(), StringValue(s))
414    }
415
416    /// Construct a list word (shared heap: clones share the buffer).
417    pub fn list(items: Vec<Value>) -> Self {
418        Self::mint_heap_shared(ListValue::descriptor(), ListValue(items))
419    }
420
421    /// Construct a map word (shared heap: clones share the buffer).
422    pub fn map(entries: BTreeMap<String, Value>) -> Self {
423        Self::mint_heap_shared(MapValue::descriptor(), MapValue(entries))
424    }
425
426    /// Construct a path word.
427    #[allow(clippy::disallowed_types)]
428    pub fn path(p: std::path::PathBuf) -> Self {
429        Self::mint_heap(PathValue::descriptor(), PathValue(p))
430    }
431
432    /// Construct a duration word.
433    pub fn duration(d: Duration) -> Self {
434        Self::mint_heap(DurationValue::descriptor(), DurationValue(d))
435    }
436
437    /// Construct a pipe-name word.
438    pub fn pipe(name: String) -> Self {
439        Self::mint_heap(PipeValue::descriptor(), PipeValue(name))
440    }
441
442    /// Read an integer payload. Returns `None` for non-`INT` words.
443    pub fn as_i64(&self) -> Option<i64> {
444        self.read_inline::<IntValue>(IntValue::descriptor())
445            .map(|v| v.0)
446    }
447
448    /// Read a float payload. Returns `None` for non-`FLOAT` words.
449    pub fn as_f64(&self) -> Option<f64> {
450        self.read_inline::<FloatValue>(FloatValue::descriptor())
451            .map(|v| v.0)
452    }
453
454    /// Read a boolean payload. Returns `None` for non-`BOOL` words.
455    pub fn as_bool(&self) -> Option<bool> {
456        self.read_inline::<BoolValue>(BoolValue::descriptor())
457            .map(|v| v.0)
458    }
459
460    /// Read a task-handle payload. Returns `None` for non-`HANDLE` words.
461    pub fn as_handle(&self) -> Option<u64> {
462        self.read_inline::<HandleValue>(HandleValue::descriptor())
463            .map(|v| v.0)
464    }
465
466    /// Borrow a string payload. Returns `None` for non-`STRING` words.
467    pub fn as_str(&self) -> Option<&str> {
468        self.read_heap::<StringValue>(StringValue::descriptor())
469            .map(|v| v.0.as_str())
470    }
471
472    /// Borrow a list payload. Returns `None` for non-`LIST` words.
473    pub fn as_list(&self) -> Option<&Vec<Value>> {
474        self.read_heap::<ListValue>(ListValue::descriptor())
475            .map(|v| &v.0)
476    }
477
478    /// Borrow a list payload mutably, detaching the shared buffer first when
479    /// clones exist. Returns `None` for non-`LIST` words. This is the choke
480    /// point every future in-place container mutation must go through.
481    pub fn as_list_mut(&mut self) -> Option<&mut Vec<Value>> {
482        self.read_heap_mut::<ListValue>(ListValue::descriptor())
483            .map(|v| &mut v.0)
484    }
485
486    /// Borrow a map payload. Returns `None` for non-`MAP` words.
487    pub fn as_map(&self) -> Option<&BTreeMap<String, Value>> {
488        self.read_heap::<MapValue>(MapValue::descriptor())
489            .map(|v| &v.0)
490    }
491
492    /// Borrow a map payload mutably, detaching the shared buffer first when
493    /// clones exist. Returns `None` for non-`MAP` words. This is the choke
494    /// point every future in-place container mutation must go through.
495    pub fn as_map_mut(&mut self) -> Option<&mut BTreeMap<String, Value>> {
496        self.read_heap_mut::<MapValue>(MapValue::descriptor())
497            .map(|v| &mut v.0)
498    }
499
500    /// Borrow a pipe-name payload. Returns `None` for non-`PIPE` words.
501    pub fn as_pipe_name(&self) -> Option<&str> {
502        self.read_heap::<PipeValue>(PipeValue::descriptor())
503            .map(|v| v.0.as_str())
504    }
505
506    /// Read a duration payload. Returns `None` for non-`DURATION` words.
507    pub fn as_duration(&self) -> Option<Duration> {
508        self.read_heap::<DurationValue>(DurationValue::descriptor())
509            .map(|v| v.0)
510    }
511
512    /// Borrow a path payload. Returns `None` for non-`PATH` words.
513    #[allow(clippy::disallowed_types)]
514    pub fn as_path(&self) -> Option<&std::path::Path> {
515        self.read_heap::<PathValue>(PathValue::descriptor())
516            .map(|v| v.0.as_path())
517    }
518}
519
520impl Clone for Value {
521    fn clone(&self) -> Self {
522        let payload = unsafe { (self.vtable.clone)(self.payload) };
523        Self {
524            vtable: self.vtable,
525            payload,
526        }
527    }
528}
529
530impl Drop for Value {
531    fn drop(&mut self) {
532        unsafe { (self.vtable.drop)(self.payload) };
533    }
534}
535
536impl PartialEq for Value {
537    fn eq(&self, other: &Self) -> bool {
538        if !std::ptr::eq(self.vtable, other.vtable) {
539            return false;
540        }
541        unsafe { (self.vtable.eq)(self.payload, other.payload) }
542    }
543}
544
545impl fmt::Debug for Value {
546    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
547        write!(f, "{}(", self.vtable.name)?;
548        unsafe { (self.vtable.fmt)(self.payload, f) }?;
549        write!(f, ")")
550    }
551}
552
553impl fmt::Display for Value {
554    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
555        unsafe { (self.vtable.fmt)(self.payload, f) }
556    }
557}
558
559/// Export hook for a DSL payload type, implemented by `#[oxdock_type]` on
560/// the payload struct itself. The canonical descriptor singleton backs
561/// every word of the type; user code never names a generated symbol.
562pub trait OxDockType {
563    /// The canonical descriptor deriving from the struct's name plus doc
564    /// comments. The same reference every call: pointer-compare words
565    /// against it.
566    fn descriptor() -> &'static TypeDescriptor;
567}
568
569/// Vtable for one type: lifecycle plus operations. All hooks are plain
570/// function pointers (never closures) so descriptors stay `Copy` and the
571/// global table hands them out by value. Every hook documents the payload
572/// domain it expects; calling one with a foreign payload is unsound, and
573/// every call site is a single choke point reviewed with the layout.
574///
575/// `unshare` is the copy-on-write gate: it rewrites the payload to a
576/// uniquely owned buffer when necessary and returns a mutable pointer the
577/// caller exclusively owns. Mutation must always go through
578/// [`Value::read_heap_mut`]; deriving `&mut` from a payload any other way
579/// is unsound for shared heaps.
580#[derive(Clone, Copy)]
581pub struct TypeDescriptor {
582    pub name: &'static str,
583    pub summary: &'static str,
584    pub docs: &'static str,
585    pub clone: unsafe fn(ValuePayload) -> ValuePayload,
586    pub drop: unsafe fn(ValuePayload),
587    pub eq: unsafe fn(ValuePayload, ValuePayload) -> bool,
588    pub fmt: unsafe fn(ValuePayload, &mut fmt::Formatter<'_>) -> fmt::Result,
589    pub unshare: unsafe fn(&mut ValuePayload) -> *mut (),
590}
591
592// ---------------------------------------------------------------------------
593// Payload adapters: one inline set, one exclusive-heap set, and one shared-
594// heap set drive `clone`/`drop`/`eq`/`fmt`/`unshare` for every type through
595// monomorphic function pointers. These are `pub` solely so `#[oxdock_type]`-
596// generated descriptors can name them; hosts never call them directly.
597// ---------------------------------------------------------------------------
598
599/// Copy a `Copy` value's bytes into a payload. Panics when `T` exceeds 64
600/// bits: such types must use the heap path.
601pub fn store_inline<T>(value: T) -> ValuePayload
602where
603    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
604{
605    assert!(
606        std::mem::size_of::<T>() <= 8,
607        "inline payloads hold at most 64 bits"
608    );
609    let mut bits: u64 = 0;
610    unsafe {
611        std::ptr::copy_nonoverlapping(
612            &value as *const T as *const u8,
613            &mut bits as *mut u64 as *mut u8,
614            std::mem::size_of::<T>(),
615        );
616    }
617    // No `mem::forget`: `T: Copy` has no finalizer, so the source needs no
618    // suppression after its bytes are copied out.
619    ValuePayload { as_u64: bits }
620}
621
622/// Reconstruct a `Copy` value from an inline payload.
623///
624/// # Safety
625/// The payload must hold bytes stored by [`store_inline`] for `T`.
626pub unsafe fn load_inline<T>(payload: ValuePayload) -> T
627where
628    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
629{
630    // (Spelled as an explicit gate because `debug_assert!` expands to the
631    // banned `cfg!` macro.)
632    #[cfg(debug_assertions)]
633    if std::mem::size_of::<T>() > 8 {
634        panic!("inline payloads hold at most 64 bits");
635    }
636    let mut value = std::mem::MaybeUninit::<T>::uninit();
637    unsafe {
638        std::ptr::copy_nonoverlapping(
639            &payload.as_u64 as *const u64 as *const u8,
640            value.as_mut_ptr() as *mut u8,
641            std::mem::size_of::<T>(),
642        );
643        value.assume_init()
644    }
645}
646
647/// Inline `clone`: payloads are plain bytes.
648///
649/// # Safety
650/// The payload must hold inline bytes (never a live pointer).
651pub unsafe fn clone_copy(payload: ValuePayload) -> ValuePayload {
652    payload
653}
654
655/// Inline `drop`: nothing owns anything.
656///
657/// # Safety
658/// The payload must hold inline bytes (never a live pointer).
659pub unsafe fn drop_noop(_payload: ValuePayload) {}
660
661/// Inline `eq`: reconstruct both sides and compare.
662///
663/// # Safety
664/// Both payloads must hold bytes stored by [`store_inline`] for `T`.
665pub unsafe fn eq_inline<T>(a: ValuePayload, b: ValuePayload) -> bool
666where
667    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
668{
669    unsafe { load_inline::<T>(a) == load_inline::<T>(b) }
670}
671
672/// Inline `fmt`: reconstruct and render.
673///
674/// # Safety
675/// The payload must hold bytes stored by [`store_inline`] for `T`.
676pub unsafe fn fmt_inline<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
677where
678    T: Copy + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
679{
680    write!(f, "{}", unsafe { load_inline::<T>(payload) })
681}
682
683/// Heap `clone`: deep-copy the box.
684///
685/// # Safety
686/// The payload must own a `Box<T>` exactly once.
687pub unsafe fn clone_boxed<T>(payload: ValuePayload) -> ValuePayload
688where
689    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
690{
691    let source = unsafe { &*(payload.as_ptr as *const T) };
692    ValuePayload {
693        as_ptr: Box::into_raw(Box::new(source.clone())) as *mut (),
694    }
695}
696/// Heap `drop`: free the box.
697///
698/// # Safety
699/// The payload must own a `Box<T>` exactly once; it must never be used
700/// again afterwards.
701pub unsafe fn drop_boxed<T>(payload: ValuePayload)
702where
703    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
704{
705    drop(unsafe { Box::from_raw(payload.as_ptr as *mut T) });
706}
707
708/// Heap `eq`: compare the boxed values.
709///
710/// # Safety
711/// Both payloads must own a `Box<T>` exactly once.
712pub unsafe fn eq_boxed<T>(a: ValuePayload, b: ValuePayload) -> bool
713where
714    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
715{
716    let left = unsafe { &*(a.as_ptr as *const T) };
717    let right = unsafe { &*(b.as_ptr as *const T) };
718    left == right
719}
720
721/// Heap `fmt`: render the boxed value.
722///
723/// # Safety
724/// The payload must own a `Box<T>` exactly once.
725pub unsafe fn fmt_boxed<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
726where
727    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
728{
729    let value = unsafe { &*(payload.as_ptr as *const T) };
730    write!(f, "{value}")
731}
732
733/// Shared-heap `clone`: bump the `Arc` strong count, sharing the buffer.
734/// `O(1)` with no allocation.
735///
736/// # Safety
737/// The payload must co-own an `Arc<T>` buffer minted by
738/// [`Value::mint_heap_shared`] and cloned only through this hook, so one
739/// outstanding strong count exists per live word.
740pub unsafe fn clone_shared<T>(payload: ValuePayload) -> ValuePayload
741where
742    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
743{
744    unsafe { std::sync::Arc::increment_strong_count(payload.as_ptr as *const T) };
745    payload
746}
747
748/// Shared-heap `drop`: release one `Arc` strong count, freeing the buffer
749/// only when the final word drops.
750///
751/// # Safety
752/// The payload must co-own an `Arc<T>` buffer; it must never be used again
753/// afterwards.
754pub unsafe fn drop_shared<T>(payload: ValuePayload)
755where
756    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
757{
758    drop(unsafe { std::sync::Arc::from_raw(payload.as_ptr as *const T) });
759}
760
761/// Shared-heap `eq`: compare the shared values.
762///
763/// # Safety
764/// Both payloads must co-own an `Arc<T>` buffer.
765pub unsafe fn eq_shared<T>(a: ValuePayload, b: ValuePayload) -> bool
766where
767    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
768{
769    let left = unsafe { &*(a.as_ptr as *const T) };
770    let right = unsafe { &*(b.as_ptr as *const T) };
771    left == right
772}
773
774/// Shared-heap `fmt`: render the shared value.
775///
776/// # Safety
777/// The payload must co-own an `Arc<T>` buffer.
778pub unsafe fn fmt_shared<T>(payload: ValuePayload, f: &mut fmt::Formatter) -> fmt::Result
779where
780    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
781{
782    let value = unsafe { &*(payload.as_ptr as *const T) };
783    write!(f, "{value}")
784}
785
786/// Inline `unshare`: inline words hold bytes, not a heap buffer, so there
787/// is nothing to hand out mutably. Panics: reaching this hook means
788/// [`Value::read_heap_mut`] was called with an inline descriptor, a caller
789/// bug (mirrors [`store_inline`]'s size assert).
790///
791/// # Safety
792/// The payload must hold inline bytes (never a live pointer).
793pub unsafe fn unshare_inline(payload: &mut ValuePayload) -> *mut () {
794    let _ = payload;
795    panic!("inline words have no heap buffer to unshare");
796}
797
798/// Exclusive-heap `unshare`: the box is already uniquely owned, so the
799/// payload is returned unchanged with no allocation.
800///
801/// # Safety
802/// The payload must own a `Box<T>` exactly once. The returned pointer must
803/// only be written through while this word stays the sole owner.
804pub unsafe fn unshare_boxed<T>(payload: &mut ValuePayload) -> *mut ()
805where
806    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
807{
808    unsafe { payload.as_ptr }
809}
810
811/// Shared-heap `unshare`: detach on write. When the strong count is 1 the
812/// payload is returned unchanged (in-place, no allocation); otherwise the
813/// buffer is cloned, the word is rewritten to the private buffer, and the
814/// other clones keep the original. Either way the returned pointer addresses
815/// a buffer this word uniquely owns.
816///
817/// # Safety
818/// The payload must co-own an `Arc<T>` buffer minted by
819/// [`Value::mint_heap_shared`] with one outstanding strong count per live
820/// word. The returned pointer must only be written through while this word
821/// stays the sole owner of its (possibly fresh) buffer.
822pub unsafe fn unshare_shared<T>(payload: &mut ValuePayload) -> *mut ()
823where
824    T: Clone + PartialEq + fmt::Display + fmt::Debug + Send + Sync + 'static,
825{
826    let raw = unsafe { payload.as_ptr } as *const T;
827    let mut shared = unsafe { std::sync::Arc::from_raw(raw) };
828    let unique = std::sync::Arc::make_mut(&mut shared);
829    let out = unique as *mut T;
830    payload.as_ptr = std::sync::Arc::into_raw(shared) as *mut ();
831    out as *mut ()
832}