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}