Skip to main content

sema_core/
value.rs

1use std::any::Any;
2use std::cell::{Cell, RefCell};
3use std::collections::BTreeMap;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::rc::Rc;
7
8use hashbrown::HashMap as SpurMap;
9use lasso::{Key, Rodeo, Spur};
10use num_bigint::BigInt;
11use num_rational::BigRational;
12use num_traits::ToPrimitive;
13
14use crate::error::SemaError;
15use crate::number::Complex as SemaComplex;
16use crate::number::SemaNumber;
17use crate::runtime::{NativeCallContext, NativeOutcome, NativeResult};
18use crate::EvalContext;
19
20// Compile-time check: NaN-boxing requires 64-bit pointers that fit in 48-bit VA space.
21// 32-bit platforms cannot use this representation (pointers don't fit the encoding).
22// wasm32 is exempted because its 32-bit pointers always fit in 45 bits.
23#[cfg(not(any(target_pointer_width = "64", target_arch = "wasm32")))]
24compile_error!("sema-core NaN-boxed Value requires a 64-bit platform (or wasm32)");
25
26// ── String interning ──────────────────────────────────────────────
27
28thread_local! {
29    static INTERNER: RefCell<Rodeo> = RefCell::new(Rodeo::default());
30}
31
32/// Intern a string, returning a Spur key.
33pub fn intern(s: &str) -> Spur {
34    INTERNER.with(|r| r.borrow_mut().get_or_intern(s))
35}
36
37/// Resolve a Spur key back to a String.
38pub fn resolve(spur: Spur) -> String {
39    INTERNER.with(|r| r.borrow().resolve(&spur).to_string())
40}
41
42// A `Spur` must fit in the 32-bit NaN-box payload below for the packing to round-trip.
43const _: () = assert!(std::mem::size_of::<Spur>() == 4);
44
45/// Pack an interned [`Spur`] into the 32 raw bits stored in a NaN-boxed `Value`
46/// symbol/keyword payload (inverse of [`bits_to_spur`]).
47///
48/// The bits are the Spur's underlying `NonZeroU32` value, so two `Value`s holding
49/// the same symbol compare equal by raw bits. This and [`bits_to_spur`] are the
50/// single place that encodes the Spur↔bits mapping (via lasso's stable [`Key`]
51/// API) — both `sema-core` and `sema-vm` go through them instead of `transmute`.
52#[inline(always)]
53pub fn spur_to_bits(spur: Spur) -> u32 {
54    // `Key::into_usize` is offset-by-one (it returns `inner.get() - 1`); the bits
55    // we store are the raw `NonZeroU32` value, i.e. `into_usize() + 1`.
56    spur.into_usize() as u32 + 1
57}
58
59/// Reconstruct a [`Spur`] from the 32 raw bits stored in a NaN-boxed `Value`
60/// symbol/keyword payload (inverse of [`spur_to_bits`]).
61///
62/// `bits` is always a value produced by [`spur_to_bits`] from a real interned
63/// key, so it is non-zero and the conversion cannot fail; a zero/invalid `bits`
64/// would indicate memory corruption and panics.
65#[inline(always)]
66pub fn bits_to_spur(bits: u32) -> Spur {
67    Spur::try_from_usize((bits - 1) as usize)
68        .expect("NaN-boxed symbol/keyword payload is not a valid interned key")
69}
70
71/// Resolve a Spur and call f with the &str, avoiding allocation.
72pub fn with_resolved<F, R>(spur: Spur, f: F) -> R
73where
74    F: FnOnce(&str) -> R,
75{
76    INTERNER.with(|r| {
77        let interner = r.borrow();
78        f(interner.resolve(&spur))
79    })
80}
81
82/// Return interner statistics: (count, estimated_memory_bytes).
83pub fn interner_stats() -> (usize, usize) {
84    INTERNER.with(|r| {
85        let interner = r.borrow();
86        let count = interner.len();
87        let bytes = count * 16; // approximate: Spur (4 bytes) + average string data
88        (count, bytes)
89    })
90}
91
92// ── Gensym counter ────────────────────────────────────────────────
93
94thread_local! {
95    static GENSYM_COUNTER: Cell<u64> = const { Cell::new(0) };
96}
97
98/// Generate a unique symbol name: `<prefix>__<counter>`.
99/// Used by both manual `(gensym)` and auto-gensym `foo#` in quasiquote.
100/// Single shared counter prevents collisions between the two mechanisms.
101pub fn next_gensym(prefix: &str) -> String {
102    GENSYM_COUNTER.with(|c| {
103        let val = c.get();
104        c.set(val.wrapping_add(1));
105        format!("{prefix}__{val}")
106    })
107}
108
109/// Compare two Spurs by their resolved string content (lexicographic).
110pub fn compare_spurs(a: Spur, b: Spur) -> std::cmp::Ordering {
111    if a == b {
112        return std::cmp::Ordering::Equal;
113    }
114    INTERNER.with(|r| {
115        let interner = r.borrow();
116        interner.resolve(&a).cmp(interner.resolve(&b))
117    })
118}
119
120// ── Supporting types (unchanged public API) ───────────────────────
121
122/// A native function callable from Sema.
123pub type NativeFnInner = dyn Fn(&EvalContext, &[Value]) -> Result<Value, SemaError>;
124type RuntimeNativeFnInner = dyn for<'a> Fn(&mut NativeCallContext<'a>, &[Value]) -> NativeResult;
125
126/// How a native's runtime ABI can leave the synchronous happy path.
127///
128/// Consumed by the VM's non-suspending HOF fast path: a callback whose call
129/// graph reaches only `Inert` natives (and `CallbackDriven` natives whose
130/// callback arguments are themselves provably inert) can run its element loop
131/// synchronously, skipping the cooperative drive round-trip per call.
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub enum NativeSuspensionClass {
134    /// Settles synchronously — returns a value or raises. Every native without
135    /// a runtime ABI is inherently inert (its only outcome is `Return`).
136    Inert,
137    /// Emits `NativeOutcome::Call` for user callbacks taken at these argument
138    /// positions and suspends only when one of those callbacks suspends (the
139    /// cooperative HOFs: `map`/`filter`/`foldl`/…).
140    CallbackDriven(&'static [usize]),
141    /// May park the task or issue runtime requests (async/channel/IO offload).
142    /// The conservative default for every native with a runtime ABI.
143    MaySuspend,
144}
145
146pub struct NativeFn {
147    pub name: String,
148    /// Legacy callback ABI.
149    ///
150    /// Invariant I2: this boxed callback must not strongly capture a `Value`,
151    /// `Env`, or anything that transitively owns either. Traceable state belongs
152    /// in a registered `payload`; host infrastructure may capture `Weak` handles.
153    pub func: Box<NativeFnInner>,
154    pub payload: Option<Rc<dyn Any>>,
155    /// Fixed parameter names, in declaration order, for VM-compiled closures
156    /// wrapped as native functions. `None` for genuine native functions and
157    /// closures whose metadata is unavailable. Rc-shared with the compiled
158    /// `Function` so wrapper creation stays allocation-free.
159    pub param_names: Option<Rc<[Spur]>>,
160    /// True when this `NativeFn` is actually the fallback wrapper for a VM
161    /// closure (a user-defined `lambda`/`fn`), not a genuine builtin. The VM
162    /// represents closures as `NativeFn`s carrying a `VmClosurePayload`; this
163    /// flag lets `type`/`type_name` report `:lambda` instead of `:native-fn`
164    /// without sema-core/sema-stdlib needing to know the VM's payload type.
165    pub is_closure: bool,
166    /// Runtime-aware callback ABI.
167    ///
168    /// Invariant I2: this boxed callback must not strongly capture a `Value`,
169    /// `Env`, or anything that transitively owns either. Traceable state belongs
170    /// in a registered `payload`; host infrastructure may capture `Weak` handles.
171    runtime_func: Option<Box<RuntimeNativeFnInner>>,
172    /// True when this native has ONLY a runtime ABI — its legacy `func` is the
173    /// "requires runtime invocation" hard-error stub (async/spawn, channel/*,
174    /// async/resolved, …). A dual-ABI native (`simple_with_runtime` /
175    /// `with_ctx_runtime`, e.g. `async/sleep`, `__llm-chat-blocking`) has a real
176    /// `func` and is NOT runtime-only. Synchronous compatibility entry points
177    /// use this to reject only shapes whose value ABI is an error stub; runtime
178    /// callback drivers route all callables through `NativeOutcome::Call`.
179    runtime_only: bool,
180    /// Argument positions whose values the native retains beyond this call.
181    /// The VM snapshots closures reachable from only these arguments while the
182    /// owning frame is known, avoiding an all-native graph walk.
183    escaping_args: &'static [usize],
184    /// Explicit suspension classification, when a registration overrides the
185    /// ABI-derived default (see [`NativeFn::suspension_class`]).
186    suspension: Option<NativeSuspensionClass>,
187}
188
189impl NativeFn {
190    /// Constructs a context-free legacy native callback.
191    ///
192    /// Invariant I2 applies to `f`: do not strongly capture a `Value`, `Env`, or
193    /// a transitive owner. Put traceable state in a registered payload; host
194    /// infrastructure may capture `Weak` handles.
195    pub fn simple(
196        name: impl Into<String>,
197        f: impl Fn(&[Value]) -> Result<Value, SemaError> + 'static,
198    ) -> Self {
199        Self {
200            name: name.into(),
201            func: Box::new(move |_ctx, args| f(args)),
202            payload: None,
203            param_names: None,
204            is_closure: false,
205            runtime_func: None,
206            runtime_only: false,
207            escaping_args: &[],
208            suspension: None,
209        }
210    }
211
212    /// Constructs a legacy native callback receiving the evaluator context.
213    ///
214    /// Invariant I2 applies to `f`: do not strongly capture a `Value`, `Env`, or
215    /// a transitive owner. Put traceable state in a registered payload; host
216    /// infrastructure may capture `Weak` handles.
217    pub fn with_ctx(
218        name: impl Into<String>,
219        f: impl Fn(&EvalContext, &[Value]) -> Result<Value, SemaError> + 'static,
220    ) -> Self {
221        Self {
222            name: name.into(),
223            func: Box::new(f),
224            payload: None,
225            param_names: None,
226            is_closure: false,
227            runtime_func: None,
228            runtime_only: false,
229            escaping_args: &[],
230            suspension: None,
231        }
232    }
233
234    /// Constructs a legacy native callback with collector-traceable payload.
235    ///
236    /// Invariant I2 applies to `f`: do not strongly capture a `Value`, `Env`, or
237    /// a transitive owner. Traceable state belongs in `payload`, whose type must
238    /// have a registered tracer; host infrastructure may capture `Weak` handles.
239    pub fn with_payload(
240        name: impl Into<String>,
241        payload: Rc<dyn Any>,
242        f: impl Fn(&EvalContext, &[Value]) -> Result<Value, SemaError> + 'static,
243    ) -> Self {
244        Self {
245            name: name.into(),
246            func: Box::new(f),
247            payload: Some(payload),
248            param_names: None,
249            is_closure: false,
250            runtime_func: None,
251            runtime_only: false,
252            escaping_args: &[],
253            suspension: None,
254        }
255    }
256
257    /// Constructs a context-free runtime-aware native callback.
258    ///
259    /// Invariant I2 applies to `f`: do not strongly capture a `Value`, `Env`, or
260    /// a transitive owner. Put traceable state in a registered payload; host
261    /// infrastructure may capture `Weak` handles.
262    pub fn simple_result(
263        name: impl Into<String>,
264        f: impl Fn(&[Value]) -> NativeResult + 'static,
265    ) -> Self {
266        let name = name.into();
267        let error_name = name.clone();
268        Self {
269            name,
270            func: Box::new(move |_, _| {
271                Err(SemaError::eval(format!(
272                    "internal error: runtime native function '{error_name}' requires runtime invocation"
273                )))
274            }),
275            runtime_func: Some(Box::new(move |_, args| f(args))),
276            payload: None,
277            param_names: None,
278            is_closure: false,
279            runtime_only: true,
280            escaping_args: &[],
281            suspension: None,
282        }
283    }
284
285    /// Constructs a runtime-aware callback receiving only its native call
286    /// context and arguments. The evaluator context is not passed to `f`.
287    ///
288    /// Invariant I2 applies to `f`: do not strongly capture a `Value`, `Env`, or
289    /// a transitive owner. Put traceable state in a registered payload; host
290    /// infrastructure may capture `Weak` handles.
291    pub fn with_context_result(
292        name: impl Into<String>,
293        f: impl for<'a> Fn(&mut NativeCallContext<'a>, &[Value]) -> NativeResult + 'static,
294    ) -> Self {
295        let name = name.into();
296        let error_name = name.clone();
297        Self {
298            name,
299            func: Box::new(move |_, _| {
300                Err(SemaError::eval(format!(
301                    "internal error: runtime native function '{error_name}' requires runtime invocation"
302                )))
303            }),
304            runtime_func: Some(Box::new(f)),
305            payload: None,
306            param_names: None,
307            is_closure: false,
308            runtime_only: true,
309            escaping_args: &[],
310            suspension: None,
311        }
312    }
313
314    /// Constructs a runtime-aware callback with typed, collector-traceable state.
315    ///
316    /// The payload type must have a tracer registered with
317    /// [`crate::register_payload_tracer`] when it can reach a [`Value`] or
318    /// [`crate::Env`]. The payload field owns the sole strong callback-state
319    /// edge; the runtime callback captures only a `Weak<T>` and the function
320    /// pointer, then temporarily upgrades the weak handle for each invocation.
321    pub fn with_payload_result<T: Any + 'static>(
322        name: impl Into<String>,
323        payload: Rc<T>,
324        f: for<'a> fn(&T, &mut NativeCallContext<'a>, &[Value]) -> NativeResult,
325    ) -> Self {
326        let name = name.into();
327        let error_name = name.clone();
328        let weak_payload = Rc::downgrade(&payload);
329        let payload: Rc<dyn Any> = payload;
330        Self {
331            name,
332            func: Box::new(move |_, _| {
333                Err(SemaError::eval(format!(
334                    "internal error: runtime native function '{error_name}' requires runtime invocation"
335                )))
336            }),
337            payload: Some(payload),
338            param_names: None,
339            is_closure: false,
340            runtime_func: Some(Box::new(move |context, args| {
341                let payload = weak_payload.upgrade().ok_or_else(|| {
342                    SemaError::eval("internal error: runtime native payload is unavailable")
343                })?;
344                f(&payload, context, args)
345            })),
346            runtime_only: true,
347            escaping_args: &[],
348            suspension: None,
349        }
350    }
351
352    /// Constructs a dual-ABI native with typed, collector-traceable state.
353    ///
354    /// The payload type must have a tracer registered with
355    /// [`crate::register_payload_tracer`] when it can reach a [`Value`] or
356    /// [`crate::Env`]. The payload field owns the sole strong callback-state
357    /// edge; both callbacks capture only a `Weak<T>` and their function
358    /// pointers, then temporarily upgrade the weak handle for each invocation.
359    pub fn with_payload_ctx_runtime<T: Any + 'static>(
360        name: impl Into<String>,
361        payload: Rc<T>,
362        func: fn(&T, &EvalContext, &[Value]) -> Result<Value, SemaError>,
363        runtime: for<'a> fn(&T, &mut NativeCallContext<'a>, &[Value]) -> NativeResult,
364    ) -> Self {
365        let legacy_payload = Rc::downgrade(&payload);
366        let runtime_payload = Rc::downgrade(&payload);
367        let payload: Rc<dyn Any> = payload;
368        Self {
369            name: name.into(),
370            func: Box::new(move |context, args| {
371                let payload = legacy_payload.upgrade().ok_or_else(|| {
372                    SemaError::eval("internal error: native payload is unavailable")
373                })?;
374                func(&payload, context, args)
375            }),
376            payload: Some(payload),
377            param_names: None,
378            is_closure: false,
379            runtime_func: Some(Box::new(move |context, args| {
380                let payload = runtime_payload.upgrade().ok_or_else(|| {
381                    SemaError::eval("internal error: runtime native payload is unavailable")
382                })?;
383                runtime(&payload, context, args)
384            })),
385            runtime_only: false,
386            escaping_args: &[],
387            suspension: None,
388        }
389    }
390
391    /// Constructs a native carrying both the synchronous value ABI and the
392    /// runtime ABI. The runtime callback runs with an installed
393    /// [`TaskContext`](crate::runtime::TaskContext); `func` handles callers that
394    /// invoke the native outside a runtime quantum.
395    ///
396    /// Invariant I2 applies to both callbacks: do not strongly capture a
397    /// `Value`, `Env`, or a transitive owner. Put traceable state in a registered
398    /// payload; host infrastructure may capture `Weak` handles.
399    pub fn simple_with_runtime(
400        name: impl Into<String>,
401        func: impl Fn(&[Value]) -> Result<Value, SemaError> + 'static,
402        runtime: impl for<'a> Fn(&mut NativeCallContext<'a>, &[Value]) -> NativeResult + 'static,
403    ) -> Self {
404        Self {
405            name: name.into(),
406            func: Box::new(move |_ctx, args| func(args)),
407            runtime_func: Some(Box::new(runtime)),
408            payload: None,
409            param_names: None,
410            is_closure: false,
411            runtime_only: false,
412            escaping_args: &[],
413            suspension: None,
414        }
415    }
416
417    /// Like [`simple_with_runtime`](Self::simple_with_runtime), but the legacy
418    /// callback receives the evaluator context (parity with
419    /// [`with_ctx`](Self::with_ctx)). The runtime callback drives the native
420    /// under the unified cooperative runtime; `func` handles synchronous callers
421    /// outside a runtime quantum and therefore only produces plain values.
422    ///
423    /// Invariant I2 applies to both callbacks: do not strongly capture a
424    /// `Value`, `Env`, or a transitive owner. Put traceable state in a registered
425    /// payload; host infrastructure may capture `Weak` handles.
426    pub fn with_ctx_runtime(
427        name: impl Into<String>,
428        func: impl Fn(&EvalContext, &[Value]) -> Result<Value, SemaError> + 'static,
429        runtime: impl for<'a> Fn(&mut NativeCallContext<'a>, &[Value]) -> NativeResult + 'static,
430    ) -> Self {
431        Self {
432            name: name.into(),
433            func: Box::new(func),
434            runtime_func: Some(Box::new(runtime)),
435            payload: None,
436            param_names: None,
437            is_closure: false,
438            runtime_only: false,
439            escaping_args: &[],
440            suspension: None,
441        }
442    }
443
444    /// Mark the argument positions whose values this native stores into a
445    /// longer-lived object or runtime wait. The metadata is static and carries
446    /// no traceable state, preserving invariant I2.
447    pub fn with_escaping_args(mut self, indices: &'static [usize]) -> Self {
448        self.escaping_args = indices;
449        self
450    }
451
452    /// Argument positions that must be snapshotted before this native runs.
453    pub fn escaping_args(&self) -> &'static [usize] {
454        self.escaping_args
455    }
456
457    /// Mark this native as suspending only through the user callbacks it takes
458    /// at `positions` (a cooperative HOF). The metadata is static and carries
459    /// no traceable state, preserving invariant I2.
460    pub fn with_callback_suspension(mut self, positions: &'static [usize]) -> Self {
461        self.suspension = Some(NativeSuspensionClass::CallbackDriven(positions));
462        self
463    }
464
465    /// This native's suspension classification. Without an explicit override,
466    /// derived from the ABI: no runtime ABI means the only possible outcome is
467    /// `Return` (inert); any runtime ABI is conservatively `MaySuspend`.
468    pub fn suspension_class(&self) -> NativeSuspensionClass {
469        self.suspension.unwrap_or(if self.runtime_func.is_none() {
470            NativeSuspensionClass::Inert
471        } else {
472            NativeSuspensionClass::MaySuspend
473        })
474    }
475
476    #[doc(hidden)]
477    /// Invokes the runtime ABI. A legacy callback fallback observes the same
478    /// evaluator context carried by the runtime invocation.
479    pub fn invoke_runtime(
480        &self,
481        runtime_context: &mut NativeCallContext<'_>,
482        args: &[Value],
483    ) -> NativeResult {
484        match &self.runtime_func {
485            Some(f) => f(runtime_context, args),
486            None => (self.func)(runtime_context.eval_context, args).map(NativeOutcome::Return),
487        }
488    }
489
490    /// True when this native can ONLY run through the runtime ABI — its legacy
491    /// value `func` is the "requires runtime invocation" hard-error stub
492    /// (`async/spawn`, `channel/*`, `async/resolved`, …). Callback-driving
493    /// builtins consult this when they must diagnose an attempted synchronous
494    /// invocation. Runtime callback drivers structurally invoke every callable.
495    pub fn is_runtime_only(&self) -> bool {
496        self.runtime_only
497    }
498}
499
500impl fmt::Debug for NativeFn {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        write!(f, "<native-fn {}>", self.name)
503    }
504}
505
506/// A user-defined lambda.
507#[derive(Debug, Clone)]
508pub struct Lambda {
509    pub params: Vec<Spur>,
510    pub rest_param: Option<Spur>,
511    pub body: Vec<Value>,
512    pub env: Env,
513    pub name: Option<Spur>,
514}
515
516/// A macro definition.
517///
518/// A procedural `defmacro` uses `params`/`rest_param`/`body` and leaves
519/// `syntax_rules` as `None`. An R7RS `(define-syntax name (syntax-rules ...))`
520/// leaves the procedural fields empty and carries its transformer in
521/// `syntax_rules`. Both share `TAG_MACRO` so env lookup, display, and GC
522/// tracing treat them uniformly.
523#[derive(Debug, Clone)]
524pub struct Macro {
525    pub params: Vec<Spur>,
526    pub rest_param: Option<Spur>,
527    pub body: Vec<Value>,
528    pub name: Spur,
529    /// `Some` for a `syntax-rules` transformer; `None` for procedural macros.
530    pub syntax_rules: Option<Rc<SyntaxRules>>,
531}
532
533/// An R7RS `syntax-rules` transformer: a list of `(pattern template)` rewrite
534/// rules, a set of literal identifiers matched by name, and the ellipsis symbol
535/// (`...` by default, or a custom one). Patterns and templates are stored as raw
536/// quoted `Value` data (list/symbol structure), so they are traced by the GC.
537#[derive(Debug, Clone)]
538pub struct SyntaxRules {
539    pub literals: Vec<Spur>,
540    pub ellipsis: Spur,
541    /// Each entry is `(pattern, template)`.
542    pub rules: Vec<(Value, Value)>,
543}
544
545/// A lazy promise: delay/force with memoization.
546pub struct Thunk {
547    pub body: Value,
548    pub forced: RefCell<Option<Value>>,
549}
550
551impl fmt::Debug for Thunk {
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        if self.forced.borrow().is_some() {
554            write!(f, "<promise (forced)>")
555        } else {
556            write!(f, "<promise>")
557        }
558    }
559}
560
561impl Clone for Thunk {
562    fn clone(&self) -> Self {
563        Thunk {
564            body: self.body.clone(),
565            forced: RefCell::new(self.forced.borrow().clone()),
566        }
567    }
568}
569
570/// State of an async promise/future.
571///
572/// `Cancelled` is a peer of `Rejected`, not a sub-kind of it: a promise that
573/// the user explicitly cancels via `async/cancel` is *not* a normal rejection
574/// (which a user might catch and recover from). Keeping the two distinct lets
575/// `async/cancelled?` be precise without string-matching, and lets
576/// `async/rejected?` honestly report `#f` for cancelled promises.
577#[derive(Debug, Clone, PartialEq, Eq)]
578pub enum PromiseState {
579    Pending,
580    Resolved(Value),
581    Rejected(String),
582    Cancelled,
583}
584
585/// An async promise: a thin handle to a promise in the unified runtime's
586/// `PromiseRegistry`, which is the single source of truth for its state and its
587/// settled value. The handle carries only the checked `PromiseId` (Copy,
588/// runtime-scoped), so it holds no GC edges — the resolved value lives in the
589/// registry (retained there via `Rc`), not on this handle.
590#[derive(Clone, Copy)]
591pub struct AsyncPromise {
592    pub id: crate::runtime::PromiseId,
593}
594
595impl fmt::Debug for AsyncPromise {
596    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
597        write!(f, "<async-promise>")
598    }
599}
600
601/// A bounded async channel for communication between coroutines.
602/// A bounded async channel: a thin handle to a channel in the unified runtime's
603/// `ChannelRegistry`, which owns the buffer, capacity, closed flag, and the
604/// parked sender/receiver queues. The handle carries only the checked
605/// `ChannelId` (Copy, runtime-scoped), so it holds no GC edges — the buffered
606/// values live in the registry, not on this handle.
607#[derive(Clone, Copy)]
608pub struct Channel {
609    pub id: crate::runtime::ChannelId,
610}
611
612impl fmt::Debug for Channel {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        f.write_str("<channel>")
615    }
616}
617
618/// A record: tagged product type created by define-record-type.
619#[derive(Debug, Clone)]
620pub struct Record {
621    pub type_tag: Spur,
622    pub field_names: Vec<Spur>,
623    pub fields: Vec<Value>,
624}
625
626/// A mutable array: an in-place mutable vector of Values (Janet-style
627/// `array`). Sharing is by reference — pushing through one handle is visible
628/// through every other handle to the same array.
629pub struct MutableArray {
630    pub items: RefCell<Vec<Value>>,
631}
632
633impl fmt::Debug for MutableArray {
634    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635        // Length only: a mutable array can contain itself, so formatting the
636        // elements could recurse forever.
637        match self.items.try_borrow() {
638            Ok(items) => write!(f, "<mutable-array {}>", items.len()),
639            Err(_) => write!(f, "<mutable-array (borrowed)>"),
640        }
641    }
642}
643
644impl Clone for MutableArray {
645    fn clone(&self) -> Self {
646        MutableArray {
647            items: RefCell::new(self.items.borrow().clone()),
648        }
649    }
650}
651
652/// A mutable cell: a single in-place mutable Value slot (Janet-style boxed
653/// value). Like [`MutableArray`], sharing is by reference.
654pub struct MutableCell {
655    pub value: RefCell<Value>,
656}
657
658impl fmt::Debug for MutableCell {
659    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660        // No contents: a cell can contain itself (directly or through an
661        // array), so formatting the inner value could recurse forever.
662        write!(f, "<mutable-cell>")
663    }
664}
665
666impl Clone for MutableCell {
667    fn clone(&self) -> Self {
668        MutableCell {
669            value: RefCell::new(self.value.borrow().clone()),
670        }
671    }
672}
673
674/// A message role in a conversation.
675#[derive(Debug, Clone, PartialEq, Eq)]
676pub enum Role {
677    System,
678    User,
679    Assistant,
680    Tool,
681}
682
683impl fmt::Display for Role {
684    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685        match self {
686            Role::System => write!(f, "system"),
687            Role::User => write!(f, "user"),
688            Role::Assistant => write!(f, "assistant"),
689            Role::Tool => write!(f, "tool"),
690        }
691    }
692}
693
694/// A base64-encoded image attachment.
695#[derive(Debug, Clone)]
696pub struct ImageAttachment {
697    pub data: String,
698    pub media_type: String,
699}
700
701/// A single message in a conversation.
702#[derive(Debug, Clone)]
703pub struct Message {
704    pub role: Role,
705    pub content: String,
706    /// Optional image attachments (base64-encoded).
707    pub images: Vec<ImageAttachment>,
708}
709
710/// A prompt: a structured list of messages.
711#[derive(Debug, Clone)]
712pub struct Prompt {
713    pub messages: Vec<Message>,
714}
715
716/// A conversation: immutable history + provider config.
717#[derive(Debug, Clone)]
718pub struct Conversation {
719    pub messages: Vec<Message>,
720    pub model: String,
721    pub metadata: BTreeMap<String, String>,
722}
723
724/// A tool definition for LLM function calling.
725#[derive(Debug, Clone)]
726pub struct ToolDefinition {
727    pub name: String,
728    pub description: String,
729    pub parameters: Value,
730    pub policy_subjects: Vec<ToolPolicySubject>,
731    pub handler: Value,
732}
733
734/// Static, inspectable description of the security-relevant subject a tool acts on.
735///
736/// Argument names refer to the tool's JSON schema. The policy runtime resolves
737/// them before invoking the handler and never infers authority from the tool name.
738#[derive(Debug, Clone, PartialEq, Eq)]
739pub enum ToolPolicySubject {
740    File {
741        access: FileAccess,
742        path_arg: String,
743    },
744    NetworkRequest {
745        method: Option<String>,
746        url_arg: String,
747    },
748    Command {
749        command_arg: String,
750    },
751    ExternalAction {
752        action: String,
753        target_arg: Option<String>,
754    },
755}
756
757/// File-system authority represented by a tool policy subject.
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
759pub enum FileAccess {
760    Read,
761    Write,
762    Delete,
763}
764
765/// An agent: system prompt + tools + config for autonomous loops.
766#[derive(Debug, Clone)]
767pub struct Agent {
768    pub name: String,
769    pub system: String,
770    pub tools: Vec<Value>,
771    pub max_turns: usize,
772    pub model: String,
773}
774
775/// A multimethod: dispatch-function + method table.
776/// Interior-mutable so `defmethod` can add methods after creation.
777pub struct MultiMethod {
778    pub name: Spur,
779    pub dispatch_fn: Value,
780    pub methods: RefCell<BTreeMap<Value, Value>>,
781    pub default: RefCell<Option<Value>>,
782}
783
784impl fmt::Debug for MultiMethod {
785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786        write!(f, "<multimethod {}>", resolve(self.name))
787    }
788}
789
790/// Invoke a multimethod's dispatch function synchronously and select its
791/// handler. This is the host-only value-ABI path used outside an active runtime
792/// quantum. Runtime callers structurally invoke the dispatch function and then
793/// use [`select_multimethod_handler`] with its returned dispatch value.
794pub fn resolve_multimethod_handler(
795    ctx: &EvalContext,
796    mm: &MultiMethod,
797    args: &[Value],
798) -> Result<Value, SemaError> {
799    let dispatch_val = crate::call_callback(ctx, &mm.dispatch_fn, args)?;
800    select_multimethod_handler(mm, &dispatch_val)
801}
802
803/// Select the handler for an already-computed multimethod dispatch value.
804/// Performs no evaluator callback and is therefore safe inside runtime
805/// continuations.
806pub fn select_multimethod_handler(
807    mm: &MultiMethod,
808    dispatch_val: &Value,
809) -> Result<Value, SemaError> {
810    let methods = mm.methods.borrow();
811    if let Some(handler) = methods.get(dispatch_val) {
812        Ok(handler.clone())
813    } else {
814        drop(methods);
815        let default = mm.default.borrow().clone();
816        default.ok_or_else(|| {
817            SemaError::eval(format!(
818                "no method in multimethod '{}' for dispatch value: {}",
819                resolve(mm.name),
820                dispatch_val
821            ))
822            .with_hint("add a (defmethod name :default handler) to handle unmatched values")
823        })
824    }
825}
826
827/// Trait for stream implementations (files, buffers, serial ports, etc.).
828/// All methods take `&self` — interior mutability is handled by the implementation.
829pub trait SemaStream: fmt::Debug {
830    fn read(&self, buf: &mut [u8]) -> Result<usize, SemaError>;
831    fn write(&self, data: &[u8]) -> Result<usize, SemaError>;
832    fn available(&self) -> Result<bool, SemaError> {
833        Ok(false)
834    }
835    fn flush(&self) -> Result<(), SemaError> {
836        Ok(())
837    }
838    fn close(&self) -> Result<(), SemaError> {
839        Ok(())
840    }
841    fn is_readable(&self) -> bool {
842        true
843    }
844    fn is_writable(&self) -> bool {
845        true
846    }
847    fn stream_type(&self) -> &'static str;
848    fn as_any(&self) -> &dyn std::any::Any;
849}
850
851/// Sized wrapper around `dyn SemaStream` for NaN-boxing (thin pointer via Rc<StreamBox>).
852/// Tracks closed state centrally so all impls get close-guarding for free.
853pub struct StreamBox {
854    inner: RefCell<Box<dyn SemaStream>>,
855    closed: Cell<bool>,
856}
857
858impl StreamBox {
859    pub fn new(s: impl SemaStream + 'static) -> Self {
860        StreamBox {
861            inner: RefCell::new(Box::new(s)),
862            closed: Cell::new(false),
863        }
864    }
865
866    pub fn read(&self, buf: &mut [u8]) -> Result<usize, SemaError> {
867        if self.closed.get() {
868            return Err(SemaError::eval("stream/read: stream is closed"));
869        }
870        self.inner.borrow().read(buf)
871    }
872
873    pub fn write(&self, data: &[u8]) -> Result<usize, SemaError> {
874        if self.closed.get() {
875            return Err(SemaError::eval("stream/write: stream is closed"));
876        }
877        self.inner.borrow().write(data)
878    }
879
880    pub fn flush(&self) -> Result<(), SemaError> {
881        if self.closed.get() {
882            return Err(SemaError::eval("stream/flush: stream is closed"));
883        }
884        self.inner.borrow().flush()
885    }
886
887    pub fn close(&self) -> Result<(), SemaError> {
888        if self.closed.get() {
889            return Ok(()); // double-close is a no-op
890        }
891        self.inner.borrow().close()?;
892        self.closed.set(true);
893        Ok(())
894    }
895
896    pub fn is_closed(&self) -> bool {
897        self.closed.get()
898    }
899
900    pub fn is_readable(&self) -> bool {
901        !self.closed.get() && self.inner.borrow().is_readable()
902    }
903
904    pub fn is_writable(&self) -> bool {
905        !self.closed.get() && self.inner.borrow().is_writable()
906    }
907
908    pub fn available(&self) -> Result<bool, SemaError> {
909        if self.closed.get() {
910            return Ok(false);
911        }
912        self.inner.borrow().available()
913    }
914
915    pub fn stream_type(&self) -> &'static str {
916        self.inner.borrow().stream_type()
917    }
918
919    pub fn borrow_inner(&self) -> std::cell::Ref<'_, Box<dyn SemaStream>> {
920        self.inner.borrow()
921    }
922}
923
924impl fmt::Debug for StreamBox {
925    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
926        write!(f, "<stream:{}>", self.stream_type())
927    }
928}
929
930impl Clone for MultiMethod {
931    fn clone(&self) -> Self {
932        MultiMethod {
933            name: self.name,
934            dispatch_fn: self.dispatch_fn.clone(),
935            methods: RefCell::new(self.methods.borrow().clone()),
936            default: RefCell::new(self.default.borrow().clone()),
937        }
938    }
939}
940
941// ── NaN-boxing constants ──────────────────────────────────────────
942
943// IEEE 754 double layout:
944//   bit 63:     sign
945//   bits 62-52: exponent (11 bits)
946//   bits 51-0:  mantissa (52 bits), bit 51 = quiet NaN bit
947//
948// Boxed (non-float) values use: sign=1, exp=all 1s, quiet=1
949//   Then bits 50-45 = TAG (6 bits), bits 44-0 = PAYLOAD (45 bits)
950
951/// Mask for checking if a value is boxed (sign + exponent + quiet bit)
952const BOX_MASK: u64 = 0xFFF8_0000_0000_0000;
953
954/// The 45-bit payload mask
955const PAYLOAD_MASK: u64 = (1u64 << 45) - 1; // 0x1FFF_FFFF_FFFF
956
957/// Sign-extension bit for 45-bit signed integers
958const INT_SIGN_BIT: u64 = 1u64 << 44;
959
960/// 6-bit mask for extracting the tag from a boxed value (bits 50-45).
961const TAG_MASK_6BIT: u64 = 0x3F;
962
963/// Canonical quiet NaN (sign=0) — used for NaN float values to avoid collision with boxed
964const CANONICAL_NAN: u64 = 0x7FF8_0000_0000_0000;
965
966// Tags (6 bits, encoded in bits 50-45)
967const TAG_NIL: u64 = 0;
968const TAG_FALSE: u64 = 1;
969const TAG_TRUE: u64 = 2;
970const TAG_INT_SMALL: u64 = 3;
971const TAG_CHAR: u64 = 4;
972const TAG_SYMBOL: u64 = 5;
973const TAG_KEYWORD: u64 = 6;
974const TAG_INT_BIG: u64 = 7;
975const TAG_STRING: u64 = 8;
976const TAG_LIST: u64 = 9;
977const TAG_VECTOR: u64 = 10;
978const TAG_MAP: u64 = 11;
979const TAG_HASHMAP: u64 = 12;
980const TAG_LAMBDA: u64 = 13;
981const TAG_MACRO: u64 = 14;
982pub const TAG_NATIVE_FN: u64 = 15;
983const TAG_PROMPT: u64 = 16;
984const TAG_MESSAGE: u64 = 17;
985const TAG_CONVERSATION: u64 = 18;
986const TAG_TOOL_DEF: u64 = 19;
987const TAG_AGENT: u64 = 20;
988const TAG_THUNK: u64 = 21;
989const TAG_RECORD: u64 = 22;
990const TAG_BYTEVECTOR: u64 = 23;
991const TAG_MULTIMETHOD: u64 = 24;
992const TAG_STREAM: u64 = 25;
993const TAG_F64_ARRAY: u64 = 26;
994const TAG_I64_ARRAY: u64 = 27;
995const TAG_ASYNC_PROMISE: u64 = 28;
996const TAG_CHANNEL: u64 = 29;
997const TAG_BIGINT: u64 = 30;
998const TAG_RATIONAL: u64 = 31;
999const TAG_COMPLEX: u64 = 32;
1000const TAG_MUTABLE_ARRAY: u64 = 33;
1001const TAG_MUTABLE_CELL: u64 = 34;
1002
1003/// Small-int range: [-2^44, 2^44 - 1] = [-17_592_186_044_416, +17_592_186_044_415]
1004const SMALL_INT_MIN: i64 = -(1i64 << 44);
1005const SMALL_INT_MAX: i64 = (1i64 << 44) - 1;
1006
1007// ── Public NaN-boxing constants for VM use ────────────────────────
1008
1009/// Tag + box combined mask: upper 19 bits (sign + exponent + quiet + 6-bit tag).
1010pub const NAN_TAG_MASK: u64 = BOX_MASK | (TAG_MASK_6BIT << 45); // 0xFFFF_E000_0000_0000
1011
1012/// The expected upper bits for a small int value: BOX_MASK | (TAG_INT_SMALL << 45).
1013pub const NAN_INT_SMALL_PATTERN: u64 = BOX_MASK | (TAG_INT_SMALL << 45);
1014
1015/// Public payload mask (45 bits).
1016pub const NAN_PAYLOAD_MASK: u64 = PAYLOAD_MASK;
1017
1018/// Sign bit within the 45-bit payload (bit 44) — for sign-extending small ints.
1019pub const NAN_INT_SIGN_BIT: u64 = INT_SIGN_BIT;
1020
1021/// Number of payload bits in NaN-boxed values (45).
1022pub const NAN_PAYLOAD_BITS: u32 = 45;
1023
1024// ── Helpers for encoding/decoding ─────────────────────────────────
1025
1026#[inline(always)]
1027fn make_boxed(tag: u64, payload: u64) -> u64 {
1028    BOX_MASK | (tag << 45) | (payload & PAYLOAD_MASK)
1029}
1030
1031#[inline(always)]
1032fn is_boxed(bits: u64) -> bool {
1033    (bits & BOX_MASK) == BOX_MASK
1034}
1035
1036#[inline(always)]
1037fn get_tag(bits: u64) -> u64 {
1038    (bits >> 45) & TAG_MASK_6BIT
1039}
1040
1041#[inline(always)]
1042fn get_payload(bits: u64) -> u64 {
1043    bits & PAYLOAD_MASK
1044}
1045
1046#[inline(always)]
1047fn ptr_to_payload(ptr: *const u8) -> u64 {
1048    let raw = ptr as u64;
1049    debug_assert!(raw & 0x7 == 0, "pointer not 8-byte aligned: 0x{:x}", raw);
1050    debug_assert!(
1051        raw >> 48 == 0,
1052        "pointer exceeds 48-bit VA space: 0x{:x}",
1053        raw
1054    );
1055    raw >> 3
1056}
1057
1058#[inline(always)]
1059fn payload_to_ptr(payload: u64) -> *const u8 {
1060    (payload << 3) as *const u8
1061}
1062
1063// ── ValueView: pattern-matching enum ──────────────────────────────
1064
1065/// A view of a NaN-boxed Value for pattern matching.
1066/// Returned by `Value::view()`. Heap types hold Rc (refcount bumped).
1067pub enum ValueView {
1068    Nil,
1069    Bool(bool),
1070    Int(i64),
1071    BigInt(Rc<BigInt>),
1072    Rational(Rc<BigRational>),
1073    Complex(Rc<SemaComplex>),
1074    Float(f64),
1075    String(Rc<String>),
1076    Symbol(Spur),
1077    Keyword(Spur),
1078    Char(char),
1079    List(Rc<Vec<Value>>),
1080    Vector(Rc<Vec<Value>>),
1081    Map(Rc<BTreeMap<Value, Value>>),
1082    HashMap(Rc<hashbrown::HashMap<Value, Value>>),
1083    Lambda(Rc<Lambda>),
1084    Macro(Rc<Macro>),
1085    NativeFn(Rc<NativeFn>),
1086    Prompt(Rc<Prompt>),
1087    Message(Rc<Message>),
1088    Conversation(Rc<Conversation>),
1089    ToolDef(Rc<ToolDefinition>),
1090    Agent(Rc<Agent>),
1091    Thunk(Rc<Thunk>),
1092    Record(Rc<Record>),
1093    Bytevector(Rc<Vec<u8>>),
1094    MultiMethod(Rc<MultiMethod>),
1095    Stream(Rc<StreamBox>),
1096    F64Array(Rc<Vec<f64>>),
1097    I64Array(Rc<Vec<i64>>),
1098    AsyncPromise(Rc<AsyncPromise>),
1099    Channel(Rc<Channel>),
1100    MutableArray(Rc<MutableArray>),
1101    MutableCell(Rc<MutableCell>),
1102}
1103
1104/// A borrowing view of a `Value` — like `ValueView` but returns references
1105/// instead of cloning `Rc`s, avoiding refcount mutations on every comparison,
1106/// hash, and ordering operation.
1107pub enum ValueViewRef<'a> {
1108    Nil,
1109    Bool(bool),
1110    Int(i64),
1111    BigInt(&'a BigInt),
1112    Rational(&'a BigRational),
1113    Complex(&'a SemaComplex),
1114    Float(f64),
1115    String(&'a str),
1116    Symbol(Spur),
1117    Keyword(Spur),
1118    Char(char),
1119    List(&'a [Value]),
1120    Vector(&'a [Value]),
1121    Map(&'a BTreeMap<Value, Value>),
1122    HashMap(&'a hashbrown::HashMap<Value, Value>),
1123    Lambda(&'a Lambda),
1124    Macro(&'a Macro),
1125    NativeFn(&'a NativeFn),
1126    Prompt(&'a Prompt),
1127    Message(&'a Message),
1128    Conversation(&'a Conversation),
1129    ToolDef(&'a ToolDefinition),
1130    Agent(&'a Agent),
1131    Thunk(&'a Thunk),
1132    Record(&'a Record),
1133    Bytevector(&'a [u8]),
1134    MultiMethod(&'a MultiMethod),
1135    Stream(&'a StreamBox),
1136    F64Array(&'a [f64]),
1137    I64Array(&'a [i64]),
1138    AsyncPromise(&'a AsyncPromise),
1139    Channel(&'a Channel),
1140    MutableArray(&'a MutableArray),
1141    MutableCell(&'a MutableCell),
1142}
1143
1144// ── The NaN-boxed Value type ──────────────────────────────────────
1145
1146/// The core Value type for all Sema data.
1147/// NaN-boxed: stored as 8 bytes. Floats stored directly,
1148/// everything else encoded in quiet-NaN payload space.
1149#[repr(transparent)]
1150pub struct Value(u64);
1151
1152// ── Constructors ──────────────────────────────────────────────────
1153
1154impl Value {
1155    // -- Immediate constructors --
1156
1157    pub const NIL: Value = Value(make_boxed_const(TAG_NIL, 0));
1158    pub const TRUE: Value = Value(make_boxed_const(TAG_TRUE, 0));
1159    pub const FALSE: Value = Value(make_boxed_const(TAG_FALSE, 0));
1160
1161    #[inline(always)]
1162    pub fn nil() -> Value {
1163        Value::NIL
1164    }
1165
1166    #[inline(always)]
1167    pub fn bool(b: bool) -> Value {
1168        if b {
1169            Value::TRUE
1170        } else {
1171            Value::FALSE
1172        }
1173    }
1174
1175    #[inline(always)]
1176    pub fn int(n: i64) -> Value {
1177        if (SMALL_INT_MIN..=SMALL_INT_MAX).contains(&n) {
1178            // Encode as small int (45-bit two's complement)
1179            let payload = (n as u64) & PAYLOAD_MASK;
1180            Value(make_boxed(TAG_INT_SMALL, payload))
1181        } else {
1182            // Out of range: heap-allocate (through the boxing funnel so the
1183            // uniform-header guards in `from_rc_ptr` apply here too)
1184            Value::from_rc_ptr(TAG_INT_BIG, Rc::new(n))
1185        }
1186    }
1187
1188    #[inline(always)]
1189    pub fn float(f: f64) -> Value {
1190        let bits = f.to_bits();
1191        if f.is_nan() {
1192            // Canonicalize NaN to avoid collision with boxed patterns
1193            Value(CANONICAL_NAN)
1194        } else {
1195            // Check: a non-NaN float could still have the BOX_MASK pattern
1196            // This happens for negative infinity and some subnormals — but
1197            // negative infinity is 0xFFF0_0000_0000_0000 which does NOT match
1198            // BOX_MASK (0xFFF8...) because bit 51 (quiet) is 0.
1199            // In IEEE 754, the only values with all exponent bits set AND quiet bit set
1200            // are quiet NaNs, which we've already canonicalized above.
1201            debug_assert!(
1202                !is_boxed(bits),
1203                "non-NaN float collides with boxed pattern: {:?} = 0x{:016x}",
1204                f,
1205                bits
1206            );
1207            Value(bits)
1208        }
1209    }
1210
1211    #[inline(always)]
1212    pub fn char(c: char) -> Value {
1213        Value(make_boxed(TAG_CHAR, c as u64))
1214    }
1215
1216    #[inline(always)]
1217    pub fn symbol_from_spur(spur: Spur) -> Value {
1218        Value(make_boxed(TAG_SYMBOL, spur_to_bits(spur) as u64))
1219    }
1220
1221    pub fn symbol(s: &str) -> Value {
1222        Value::symbol_from_spur(intern(s))
1223    }
1224
1225    #[inline(always)]
1226    pub fn keyword_from_spur(spur: Spur) -> Value {
1227        Value(make_boxed(TAG_KEYWORD, spur_to_bits(spur) as u64))
1228    }
1229
1230    pub fn keyword(s: &str) -> Value {
1231        Value::keyword_from_spur(intern(s))
1232    }
1233
1234    // -- Heap constructors --
1235
1236    fn from_rc_ptr<T>(tag: u64, rc: Rc<T>) -> Value {
1237        // Compile-time guard for the uniform clone/drop fast path: a payload
1238        // whose alignment exceeded the RcBox header would sit at a different
1239        // offset, and the tag-free header read would corrupt the refcount.
1240        // Enforced per-instantiation, so every future heap payload type is
1241        // covered automatically (see RC_HEADER).
1242        const { assert!(std::mem::align_of::<T>() <= RC_HEADER) };
1243        #[cfg(debug_assertions)]
1244        let count = Rc::strong_count(&rc);
1245        let ptr = Rc::into_raw(rc) as *const u8;
1246        // Validate the header offset on every boxing in debug builds: the
1247        // strong count read through the uniform offset must agree with `Rc`.
1248        #[cfg(debug_assertions)]
1249        unsafe {
1250            debug_assert_eq!(
1251                rc_strong_cell(ptr).get(),
1252                count,
1253                "RcBox header offset mismatch for {}",
1254                std::any::type_name::<T>()
1255            );
1256        }
1257        Value(make_boxed(tag, ptr_to_payload(ptr)))
1258    }
1259
1260    /// Construct an integer of any magnitude, normalizing to the tightest
1261    /// representation: values in i64 range become a fixnum/int-big, larger
1262    /// values are heap-boxed under `TAG_BIGINT`.
1263    pub fn from_bigint(n: BigInt) -> Value {
1264        match n.to_i64() {
1265            Some(i) => Value::int(i),
1266            None => Value::from_rc_ptr(TAG_BIGINT, Rc::new(n)),
1267        }
1268    }
1269
1270    /// Construct an exact rational, normalizing integer-valued rationals
1271    /// (e.g. `6/3`) down to the tightest integer representation.
1272    pub fn rational(r: BigRational) -> Value {
1273        if r.is_integer() {
1274            Value::from_bigint(r.to_integer())
1275        } else {
1276            Value::from_rc_ptr(TAG_RATIONAL, Rc::new(r))
1277        }
1278    }
1279
1280    /// Construct a complex number from its real/imaginary tower components,
1281    /// normalizing an exact-zero imaginary part down to the real part alone.
1282    pub fn complex(re: SemaNumber, im: SemaNumber) -> Value {
1283        Value::from_number(SemaNumber::Complex(Box::new(SemaComplex { re, im })))
1284    }
1285
1286    pub fn string(s: &str) -> Value {
1287        Value::from_rc_ptr(TAG_STRING, Rc::new(s.to_string()))
1288    }
1289
1290    /// Construct a string value from an already-owned `String` without the
1291    /// extra copy `Value::string(&s)` would make. Prefer this whenever the
1292    /// caller has just built the `String` and won't use it again.
1293    pub fn string_owned(s: String) -> Value {
1294        Value::from_rc_ptr(TAG_STRING, Rc::new(s))
1295    }
1296
1297    pub fn string_from_rc(rc: Rc<String>) -> Value {
1298        Value::from_rc_ptr(TAG_STRING, rc)
1299    }
1300
1301    pub fn list(v: Vec<Value>) -> Value {
1302        Value::from_rc_ptr(TAG_LIST, Rc::new(v))
1303    }
1304
1305    pub fn list_from_rc(rc: Rc<Vec<Value>>) -> Value {
1306        Value::from_rc_ptr(TAG_LIST, rc)
1307    }
1308
1309    pub fn vector(v: Vec<Value>) -> Value {
1310        Value::from_rc_ptr(TAG_VECTOR, Rc::new(v))
1311    }
1312
1313    pub fn vector_from_rc(rc: Rc<Vec<Value>>) -> Value {
1314        Value::from_rc_ptr(TAG_VECTOR, rc)
1315    }
1316
1317    pub fn map(m: BTreeMap<Value, Value>) -> Value {
1318        Value::from_rc_ptr(TAG_MAP, Rc::new(m))
1319    }
1320
1321    pub fn map_from_rc(rc: Rc<BTreeMap<Value, Value>>) -> Value {
1322        Value::from_rc_ptr(TAG_MAP, rc)
1323    }
1324
1325    pub fn hashmap(entries: Vec<(Value, Value)>) -> Value {
1326        let map: hashbrown::HashMap<Value, Value> = entries.into_iter().collect();
1327        Value::from_rc_ptr(TAG_HASHMAP, Rc::new(map))
1328    }
1329
1330    pub fn hashmap_from_rc(rc: Rc<hashbrown::HashMap<Value, Value>>) -> Value {
1331        Value::from_rc_ptr(TAG_HASHMAP, rc)
1332    }
1333
1334    pub fn lambda(l: Lambda) -> Value {
1335        Value::from_rc_ptr(TAG_LAMBDA, Rc::new(l))
1336    }
1337
1338    pub fn lambda_from_rc(rc: Rc<Lambda>) -> Value {
1339        Value::from_rc_ptr(TAG_LAMBDA, rc)
1340    }
1341
1342    pub fn macro_val(m: Macro) -> Value {
1343        Value::from_rc_ptr(TAG_MACRO, Rc::new(m))
1344    }
1345
1346    pub fn macro_from_rc(rc: Rc<Macro>) -> Value {
1347        Value::from_rc_ptr(TAG_MACRO, rc)
1348    }
1349
1350    pub fn native_fn(f: NativeFn) -> Value {
1351        Value::from_rc_ptr(TAG_NATIVE_FN, Rc::new(f))
1352    }
1353
1354    pub fn native_fn_from_rc(rc: Rc<NativeFn>) -> Value {
1355        Value::from_rc_ptr(TAG_NATIVE_FN, rc)
1356    }
1357
1358    pub fn prompt(p: Prompt) -> Value {
1359        Value::from_rc_ptr(TAG_PROMPT, Rc::new(p))
1360    }
1361
1362    pub fn prompt_from_rc(rc: Rc<Prompt>) -> Value {
1363        Value::from_rc_ptr(TAG_PROMPT, rc)
1364    }
1365
1366    pub fn message(m: Message) -> Value {
1367        Value::from_rc_ptr(TAG_MESSAGE, Rc::new(m))
1368    }
1369
1370    pub fn message_from_rc(rc: Rc<Message>) -> Value {
1371        Value::from_rc_ptr(TAG_MESSAGE, rc)
1372    }
1373
1374    pub fn conversation(c: Conversation) -> Value {
1375        Value::from_rc_ptr(TAG_CONVERSATION, Rc::new(c))
1376    }
1377
1378    pub fn conversation_from_rc(rc: Rc<Conversation>) -> Value {
1379        Value::from_rc_ptr(TAG_CONVERSATION, rc)
1380    }
1381
1382    pub fn tool_def(t: ToolDefinition) -> Value {
1383        Value::from_rc_ptr(TAG_TOOL_DEF, Rc::new(t))
1384    }
1385
1386    pub fn tool_def_from_rc(rc: Rc<ToolDefinition>) -> Value {
1387        Value::from_rc_ptr(TAG_TOOL_DEF, rc)
1388    }
1389
1390    pub fn agent(a: Agent) -> Value {
1391        Value::from_rc_ptr(TAG_AGENT, Rc::new(a))
1392    }
1393
1394    pub fn agent_from_rc(rc: Rc<Agent>) -> Value {
1395        Value::from_rc_ptr(TAG_AGENT, rc)
1396    }
1397
1398    pub fn thunk(t: Thunk) -> Value {
1399        let rc = Rc::new(t);
1400        // Cold data-cycle constructor (CORE-2, plan §5.2): a thunk can carry a
1401        // closure-free cycle through its `forced` cell, so every fresh thunk
1402        // is a collector candidate. `from_rc` wrappers are exempt — they wrap
1403        // allocations registered at their own creation site.
1404        crate::cycle::register_candidate(crate::cycle::GcNode::Thunk(Rc::downgrade(&rc)));
1405        Value::from_rc_ptr(TAG_THUNK, rc)
1406    }
1407
1408    pub fn thunk_from_rc(rc: Rc<Thunk>) -> Value {
1409        Value::from_rc_ptr(TAG_THUNK, rc)
1410    }
1411
1412    pub fn record(r: Record) -> Value {
1413        Value::from_rc_ptr(TAG_RECORD, Rc::new(r))
1414    }
1415
1416    pub fn record_from_rc(rc: Rc<Record>) -> Value {
1417        Value::from_rc_ptr(TAG_RECORD, rc)
1418    }
1419
1420    pub fn bytevector(bytes: Vec<u8>) -> Value {
1421        Value::from_rc_ptr(TAG_BYTEVECTOR, Rc::new(bytes))
1422    }
1423
1424    pub fn bytevector_from_rc(rc: Rc<Vec<u8>>) -> Value {
1425        Value::from_rc_ptr(TAG_BYTEVECTOR, rc)
1426    }
1427
1428    pub fn f64_array(data: Vec<f64>) -> Value {
1429        Value::from_rc_ptr(TAG_F64_ARRAY, Rc::new(data))
1430    }
1431
1432    pub fn f64_array_from_rc(rc: Rc<Vec<f64>>) -> Value {
1433        Value::from_rc_ptr(TAG_F64_ARRAY, rc)
1434    }
1435
1436    pub fn i64_array(data: Vec<i64>) -> Value {
1437        Value::from_rc_ptr(TAG_I64_ARRAY, Rc::new(data))
1438    }
1439
1440    pub fn i64_array_from_rc(rc: Rc<Vec<i64>>) -> Value {
1441        Value::from_rc_ptr(TAG_I64_ARRAY, rc)
1442    }
1443
1444    pub fn multimethod(m: MultiMethod) -> Value {
1445        let rc = Rc::new(m);
1446        // Cold data-cycle constructor (CORE-2): the method table / default
1447        // cells can close a closure-free cycle (e.g. a method value that is
1448        // the multimethod itself).
1449        crate::cycle::register_candidate(crate::cycle::GcNode::MultiMethod(Rc::downgrade(&rc)));
1450        Value::from_rc_ptr(TAG_MULTIMETHOD, rc)
1451    }
1452
1453    pub fn multimethod_from_rc(rc: Rc<MultiMethod>) -> Value {
1454        Value::from_rc_ptr(TAG_MULTIMETHOD, rc)
1455    }
1456
1457    pub fn stream(s: impl SemaStream + 'static) -> Value {
1458        Value::from_rc_ptr(TAG_STREAM, Rc::new(StreamBox::new(s)))
1459    }
1460
1461    pub fn stream_from_rc(rc: Rc<StreamBox>) -> Value {
1462        Value::from_rc_ptr(TAG_STREAM, rc)
1463    }
1464
1465    pub fn async_promise(promise: AsyncPromise) -> Value {
1466        // Cold data-cycle constructor (CORE-2): the handle carries only a
1467        // `PromiseId`, but the promise's SETTLED value lives in the runtime's
1468        // `PromiseRegistry` and can reach back to this handle (a promise that
1469        // resolves to a structure holding the promise). Register the handle as a
1470        // candidate — carrying the id so a dead-handle prune can also evict the
1471        // registry record — so the collector traces that interior (via the
1472        // runtime interior hooks) and severs the cycle. No hook = no interior
1473        // edge (leaf), which is safe.
1474        let rc = Rc::new(promise);
1475        crate::cycle::register_candidate(crate::cycle::GcNode::Promise {
1476            weak: Rc::downgrade(&rc),
1477            id: rc.id,
1478        });
1479        Value::from_rc_ptr(TAG_ASYNC_PROMISE, rc)
1480    }
1481    /// Build a promise `Value` from a runtime `PromiseId`. The registry owns the
1482    /// promise's state; this is just the language-facing handle to it.
1483    pub fn async_promise_id(id: crate::runtime::PromiseId) -> Value {
1484        Value::async_promise(AsyncPromise { id })
1485    }
1486    /// Rebuild a promise handle `Value` from an existing `Rc` WITHOUT re-registering
1487    /// a GC candidate — used by the collector to upgrade a candidate `Weak` into a
1488    /// snapshot handle for the duration of a pass.
1489    pub fn async_promise_from_rc(rc: Rc<AsyncPromise>) -> Value {
1490        Value::from_rc_ptr(TAG_ASYNC_PROMISE, rc)
1491    }
1492    pub fn channel(ch: Channel) -> Value {
1493        // Cold data-cycle constructor (CORE-2): the handle carries only a
1494        // `ChannelId`, but the channel's BUFFER lives in the runtime's
1495        // `ChannelRegistry` and can hold values that reach back to this handle
1496        // (a channel that buffers itself, or a closure captured into its
1497        // buffer). Register the handle as a candidate — carrying the id so a
1498        // dead-handle prune can also evict the registry record — so the
1499        // collector traces the buffer (via the runtime interior hooks) and
1500        // severs the cycle. No hook = no interior edge (leaf), which is safe.
1501        let rc = Rc::new(ch);
1502        crate::cycle::register_candidate(crate::cycle::GcNode::Channel {
1503            weak: Rc::downgrade(&rc),
1504            id: rc.id,
1505        });
1506        Value::from_rc_ptr(TAG_CHANNEL, rc)
1507    }
1508    /// Build a channel `Value` from a runtime `ChannelId`. The registry owns the
1509    /// channel's buffer/state; this is just the language-facing handle to it.
1510    pub fn channel_id(id: crate::runtime::ChannelId) -> Value {
1511        Value::channel(Channel { id })
1512    }
1513    /// Rebuild a channel handle `Value` from an existing `Rc` WITHOUT re-registering
1514    /// a GC candidate — used by the collector to upgrade a candidate `Weak` into a
1515    /// snapshot handle for the duration of a pass.
1516    pub fn channel_from_rc(rc: Rc<Channel>) -> Value {
1517        Value::from_rc_ptr(TAG_CHANNEL, rc)
1518    }
1519    pub fn mutable_array(items: Vec<Value>) -> Value {
1520        let rc = Rc::new(MutableArray {
1521            items: RefCell::new(items),
1522        });
1523        // Cold data-cycle constructor (CORE-2): the array can hold values
1524        // that reach back to the array itself (e.g. an array pushed into
1525        // itself) with no closure on the cycle.
1526        crate::cycle::register_candidate(crate::cycle::GcNode::MutableArray(Rc::downgrade(&rc)));
1527        Value::from_rc_ptr(TAG_MUTABLE_ARRAY, rc)
1528    }
1529    pub fn mutable_array_from_rc(rc: Rc<MutableArray>) -> Value {
1530        Value::from_rc_ptr(TAG_MUTABLE_ARRAY, rc)
1531    }
1532    pub fn mutable_cell(value: Value) -> Value {
1533        let rc = Rc::new(MutableCell {
1534            value: RefCell::new(value),
1535        });
1536        // Cold data-cycle constructor (CORE-2): the cell's slot can close a
1537        // closure-free cycle (e.g. a cell set to a list containing the cell).
1538        crate::cycle::register_candidate(crate::cycle::GcNode::MutableCell(Rc::downgrade(&rc)));
1539        Value::from_rc_ptr(TAG_MUTABLE_CELL, rc)
1540    }
1541    pub fn mutable_cell_from_rc(rc: Rc<MutableCell>) -> Value {
1542        Value::from_rc_ptr(TAG_MUTABLE_CELL, rc)
1543    }
1544}
1545
1546// Const-compatible boxed encoding (no function calls)
1547const fn make_boxed_const(tag: u64, payload: u64) -> u64 {
1548    BOX_MASK | (tag << 45) | (payload & PAYLOAD_MASK)
1549}
1550
1551// ── Accessors ─────────────────────────────────────────────────────
1552
1553impl Value {
1554    /// Get the raw bits (for debugging/testing).
1555    #[inline(always)]
1556    pub fn raw_bits(&self) -> u64 {
1557        self.0
1558    }
1559
1560    /// Construct a Value from raw NaN-boxed bits.
1561    ///
1562    /// # Safety
1563    ///
1564    /// Caller must ensure `bits` represents a valid NaN-boxed value.
1565    /// For immediate types (nil, bool, int, symbol, keyword, char), this is always safe.
1566    /// For heap-pointer types, the encoded pointer must be valid and have its Rc ownership
1567    /// accounted for (i.e., the caller must ensure the refcount is correct).
1568    #[inline(always)]
1569    pub unsafe fn from_raw_bits(bits: u64) -> Value {
1570        Value(bits)
1571    }
1572
1573    /// Get the NaN-boxing tag of a boxed value (0-63).
1574    /// Returns `None` for non-boxed values (floats).
1575    #[inline(always)]
1576    pub fn raw_tag(&self) -> Option<u64> {
1577        if is_boxed(self.0) {
1578            Some(get_tag(self.0))
1579        } else {
1580            None
1581        }
1582    }
1583
1584    /// Borrow the underlying NativeFn without bumping the Rc refcount.
1585    /// SAFETY: The returned reference is valid as long as this Value is alive.
1586    #[inline(always)]
1587    pub fn as_native_fn_ref(&self) -> Option<&NativeFn> {
1588        if is_boxed(self.0) && get_tag(self.0) == TAG_NATIVE_FN {
1589            Some(unsafe { self.borrow_ref::<NativeFn>() })
1590        } else {
1591            None
1592        }
1593    }
1594
1595    /// Check if this is a float (non-boxed).
1596    #[inline(always)]
1597    pub fn is_float(&self) -> bool {
1598        !is_boxed(self.0)
1599    }
1600
1601    /// Recover an Rc<T> pointer from the payload WITHOUT consuming ownership.
1602    /// This increments the refcount (returns a new Rc).
1603    #[inline(always)]
1604    unsafe fn get_rc<T>(&self) -> Rc<T> {
1605        let payload = get_payload(self.0);
1606        let ptr = payload_to_ptr(payload) as *const T;
1607        Rc::increment_strong_count(ptr);
1608        Rc::from_raw(ptr)
1609    }
1610
1611    /// Borrow the underlying T from a heap-tagged Value.
1612    /// SAFETY: caller must ensure the tag matches and T is correct.
1613    #[inline(always)]
1614    unsafe fn borrow_ref<T>(&self) -> &T {
1615        let payload = get_payload(self.0);
1616        let ptr = payload_to_ptr(payload) as *const T;
1617        &*ptr
1618    }
1619
1620    /// Pattern-match friendly view of this value.
1621    /// For heap types, this bumps the Rc refcount.
1622    pub fn view(&self) -> ValueView {
1623        if !is_boxed(self.0) {
1624            return ValueView::Float(f64::from_bits(self.0));
1625        }
1626        let tag = get_tag(self.0);
1627        match tag {
1628            TAG_NIL => ValueView::Nil,
1629            TAG_FALSE => ValueView::Bool(false),
1630            TAG_TRUE => ValueView::Bool(true),
1631            TAG_INT_SMALL => {
1632                let payload = get_payload(self.0);
1633                let val = if payload & INT_SIGN_BIT != 0 {
1634                    (payload | !PAYLOAD_MASK) as i64
1635                } else {
1636                    payload as i64
1637                };
1638                ValueView::Int(val)
1639            }
1640            TAG_CHAR => {
1641                let payload = get_payload(self.0);
1642                ValueView::Char(unsafe { char::from_u32_unchecked(payload as u32) })
1643            }
1644            TAG_SYMBOL => {
1645                let payload = get_payload(self.0);
1646                ValueView::Symbol(bits_to_spur(payload as u32))
1647            }
1648            TAG_KEYWORD => {
1649                let payload = get_payload(self.0);
1650                ValueView::Keyword(bits_to_spur(payload as u32))
1651            }
1652            TAG_INT_BIG => {
1653                let val = unsafe { *self.borrow_ref::<i64>() };
1654                ValueView::Int(val)
1655            }
1656            TAG_BIGINT => ValueView::BigInt(unsafe { self.get_rc::<BigInt>() }),
1657            TAG_RATIONAL => ValueView::Rational(unsafe { self.get_rc::<BigRational>() }),
1658            TAG_COMPLEX => ValueView::Complex(unsafe { self.get_rc::<SemaComplex>() }),
1659            // SAFETY: every TAG_X arm below calls `get_rc::<T>()` where T matches the
1660            // type stored by the corresponding Value::<x>() constructor. The Clone and
1661            // Drop impls elsewhere in this file mirror this dispatch table — when adding
1662            // a new tag here, update both. The tag check above each branch is what makes
1663            // the transmute inside get_rc sound.
1664            TAG_STRING => ValueView::String(unsafe { self.get_rc::<String>() }),
1665            TAG_LIST => ValueView::List(unsafe { self.get_rc::<Vec<Value>>() }),
1666            TAG_VECTOR => ValueView::Vector(unsafe { self.get_rc::<Vec<Value>>() }),
1667            TAG_MAP => ValueView::Map(unsafe { self.get_rc::<BTreeMap<Value, Value>>() }),
1668            TAG_HASHMAP => {
1669                ValueView::HashMap(unsafe { self.get_rc::<hashbrown::HashMap<Value, Value>>() })
1670            }
1671            TAG_LAMBDA => ValueView::Lambda(unsafe { self.get_rc::<Lambda>() }),
1672            TAG_MACRO => ValueView::Macro(unsafe { self.get_rc::<Macro>() }),
1673            TAG_NATIVE_FN => ValueView::NativeFn(unsafe { self.get_rc::<NativeFn>() }),
1674            TAG_PROMPT => ValueView::Prompt(unsafe { self.get_rc::<Prompt>() }),
1675            TAG_MESSAGE => ValueView::Message(unsafe { self.get_rc::<Message>() }),
1676            TAG_CONVERSATION => ValueView::Conversation(unsafe { self.get_rc::<Conversation>() }),
1677            TAG_TOOL_DEF => ValueView::ToolDef(unsafe { self.get_rc::<ToolDefinition>() }),
1678            TAG_AGENT => ValueView::Agent(unsafe { self.get_rc::<Agent>() }),
1679            TAG_THUNK => ValueView::Thunk(unsafe { self.get_rc::<Thunk>() }),
1680            TAG_RECORD => ValueView::Record(unsafe { self.get_rc::<Record>() }),
1681            TAG_BYTEVECTOR => ValueView::Bytevector(unsafe { self.get_rc::<Vec<u8>>() }),
1682            TAG_MULTIMETHOD => ValueView::MultiMethod(unsafe { self.get_rc::<MultiMethod>() }),
1683            TAG_STREAM => ValueView::Stream(unsafe { self.get_rc::<StreamBox>() }),
1684            TAG_F64_ARRAY => ValueView::F64Array(unsafe { self.get_rc::<Vec<f64>>() }),
1685            TAG_I64_ARRAY => ValueView::I64Array(unsafe { self.get_rc::<Vec<i64>>() }),
1686            TAG_ASYNC_PROMISE => ValueView::AsyncPromise(unsafe { self.get_rc::<AsyncPromise>() }),
1687            TAG_CHANNEL => ValueView::Channel(unsafe { self.get_rc::<Channel>() }),
1688            TAG_MUTABLE_ARRAY => ValueView::MutableArray(unsafe { self.get_rc::<MutableArray>() }),
1689            TAG_MUTABLE_CELL => ValueView::MutableCell(unsafe { self.get_rc::<MutableCell>() }),
1690            _ => unreachable!("invalid NaN-boxed tag: {}", tag),
1691        }
1692    }
1693
1694    /// Borrowing view — like `view()` but returns references instead of
1695    /// bumping Rc refcounts.  Use this in hot paths like `PartialEq`,
1696    /// `Hash`, `Ord`, and `Display`.
1697    #[inline(always)]
1698    pub fn view_ref(&self) -> ValueViewRef<'_> {
1699        if !is_boxed(self.0) {
1700            return ValueViewRef::Float(f64::from_bits(self.0));
1701        }
1702        let tag = get_tag(self.0);
1703        match tag {
1704            TAG_NIL => ValueViewRef::Nil,
1705            TAG_FALSE => ValueViewRef::Bool(false),
1706            TAG_TRUE => ValueViewRef::Bool(true),
1707            TAG_INT_SMALL => {
1708                let payload = get_payload(self.0);
1709                let val = if payload & INT_SIGN_BIT != 0 {
1710                    (payload | !PAYLOAD_MASK) as i64
1711                } else {
1712                    payload as i64
1713                };
1714                ValueViewRef::Int(val)
1715            }
1716            TAG_CHAR => {
1717                let payload = get_payload(self.0);
1718                ValueViewRef::Char(unsafe { char::from_u32_unchecked(payload as u32) })
1719            }
1720            TAG_SYMBOL => {
1721                let payload = get_payload(self.0);
1722                ValueViewRef::Symbol(bits_to_spur(payload as u32))
1723            }
1724            TAG_KEYWORD => {
1725                let payload = get_payload(self.0);
1726                ValueViewRef::Keyword(bits_to_spur(payload as u32))
1727            }
1728            TAG_INT_BIG => {
1729                let val = unsafe { *self.borrow_ref::<i64>() };
1730                ValueViewRef::Int(val)
1731            }
1732            TAG_BIGINT => ValueViewRef::BigInt(unsafe { self.borrow_ref::<BigInt>() }),
1733            TAG_RATIONAL => ValueViewRef::Rational(unsafe { self.borrow_ref::<BigRational>() }),
1734            TAG_COMPLEX => ValueViewRef::Complex(unsafe { self.borrow_ref::<SemaComplex>() }),
1735            // SAFETY: same tag/type correspondence as view() — see the
1736            // comment in view().  borrow_ref returns &T without touching
1737            // the refcount.
1738            TAG_STRING => ValueViewRef::String(unsafe { self.borrow_ref::<String>() }),
1739            TAG_LIST => ValueViewRef::List(unsafe { self.borrow_ref::<Vec<Value>>() }),
1740            TAG_VECTOR => ValueViewRef::Vector(unsafe { self.borrow_ref::<Vec<Value>>() }),
1741            TAG_MAP => ValueViewRef::Map(unsafe { self.borrow_ref::<BTreeMap<Value, Value>>() }),
1742            TAG_HASHMAP => ValueViewRef::HashMap(unsafe {
1743                self.borrow_ref::<hashbrown::HashMap<Value, Value>>()
1744            }),
1745            TAG_LAMBDA => ValueViewRef::Lambda(unsafe { self.borrow_ref::<Lambda>() }),
1746            TAG_MACRO => ValueViewRef::Macro(unsafe { self.borrow_ref::<Macro>() }),
1747            TAG_NATIVE_FN => ValueViewRef::NativeFn(unsafe { self.borrow_ref::<NativeFn>() }),
1748            TAG_PROMPT => ValueViewRef::Prompt(unsafe { self.borrow_ref::<Prompt>() }),
1749            TAG_MESSAGE => ValueViewRef::Message(unsafe { self.borrow_ref::<Message>() }),
1750            TAG_CONVERSATION => {
1751                ValueViewRef::Conversation(unsafe { self.borrow_ref::<Conversation>() })
1752            }
1753            TAG_TOOL_DEF => ValueViewRef::ToolDef(unsafe { self.borrow_ref::<ToolDefinition>() }),
1754            TAG_AGENT => ValueViewRef::Agent(unsafe { self.borrow_ref::<Agent>() }),
1755            TAG_THUNK => ValueViewRef::Thunk(unsafe { self.borrow_ref::<Thunk>() }),
1756            TAG_RECORD => ValueViewRef::Record(unsafe { self.borrow_ref::<Record>() }),
1757            TAG_BYTEVECTOR => ValueViewRef::Bytevector(unsafe { self.borrow_ref::<Vec<u8>>() }),
1758            TAG_MULTIMETHOD => {
1759                ValueViewRef::MultiMethod(unsafe { self.borrow_ref::<MultiMethod>() })
1760            }
1761            TAG_STREAM => ValueViewRef::Stream(unsafe { self.borrow_ref::<StreamBox>() }),
1762            TAG_F64_ARRAY => ValueViewRef::F64Array(unsafe { self.borrow_ref::<Vec<f64>>() }),
1763            TAG_I64_ARRAY => ValueViewRef::I64Array(unsafe { self.borrow_ref::<Vec<i64>>() }),
1764            TAG_ASYNC_PROMISE => {
1765                ValueViewRef::AsyncPromise(unsafe { self.borrow_ref::<AsyncPromise>() })
1766            }
1767            TAG_CHANNEL => ValueViewRef::Channel(unsafe { self.borrow_ref::<Channel>() }),
1768            TAG_MUTABLE_ARRAY => {
1769                ValueViewRef::MutableArray(unsafe { self.borrow_ref::<MutableArray>() })
1770            }
1771            TAG_MUTABLE_CELL => {
1772                ValueViewRef::MutableCell(unsafe { self.borrow_ref::<MutableCell>() })
1773            }
1774            _ => unreachable!("invalid NaN-boxed tag: {}", tag),
1775        }
1776    }
1777
1778    /// Data pointer of the heap allocation behind this value — the cycle
1779    /// collector's node identity. `None` for floats and immediates.
1780    pub(crate) fn heap_ptr(&self) -> Option<*const u8> {
1781        if !is_boxed(self.0) {
1782            return None;
1783        }
1784        if is_immediate_tag(get_tag(self.0)) {
1785            return None;
1786        }
1787        Some(payload_to_ptr(get_payload(self.0)))
1788    }
1789
1790    /// `Rc::strong_count` of the heap allocation behind this value, read
1791    /// without perturbing the count (the collector's trial-deletion seed).
1792    /// `None` for floats and immediates.
1793    pub(crate) fn heap_strong_count(&self) -> Option<usize> {
1794        let ptr = self.heap_ptr()?;
1795        // SAFETY: `ptr` is a live heap payload pointer for this value (guaranteed
1796        // by `heap_ptr`'s tag/boxed checks), and every heap tag's `RcBox` header
1797        // sits at the same `RC_HEADER` offset (the invariant `Clone`/`Drop` rely
1798        // on, pinned by `tests::rc_header_matches_std_layout`).
1799        Some(unsafe { rc_strong_cell(ptr).get() })
1800    }
1801
1802    // -- Typed accessors (ergonomic, avoid full view match) --
1803
1804    #[inline(always)]
1805    pub fn type_name(&self) -> &'static str {
1806        if !is_boxed(self.0) {
1807            return "float";
1808        }
1809        match get_tag(self.0) {
1810            TAG_NIL => "nil",
1811            TAG_FALSE | TAG_TRUE => "bool",
1812            TAG_INT_SMALL | TAG_INT_BIG | TAG_BIGINT => "int",
1813            TAG_RATIONAL => "rational",
1814            TAG_COMPLEX => "complex",
1815            TAG_CHAR => "char",
1816            TAG_SYMBOL => "symbol",
1817            TAG_KEYWORD => "keyword",
1818            TAG_STRING => "string",
1819            TAG_LIST => "list",
1820            TAG_VECTOR => "vector",
1821            TAG_MAP => "map",
1822            TAG_HASHMAP => "hashmap",
1823            TAG_LAMBDA => "lambda",
1824            TAG_MACRO => "macro",
1825            TAG_NATIVE_FN => "native-fn",
1826            TAG_PROMPT => "prompt",
1827            TAG_MESSAGE => "message",
1828            TAG_CONVERSATION => "conversation",
1829            TAG_TOOL_DEF => "tool",
1830            TAG_AGENT => "agent",
1831            TAG_THUNK => "promise",
1832            TAG_RECORD => "record",
1833            TAG_BYTEVECTOR => "bytevector",
1834            TAG_MULTIMETHOD => "multimethod",
1835            TAG_STREAM => "stream",
1836            TAG_F64_ARRAY => "f64-array",
1837            TAG_I64_ARRAY => "i64-array",
1838            TAG_ASYNC_PROMISE => "async-promise",
1839            TAG_CHANNEL => "channel",
1840            TAG_MUTABLE_ARRAY => "mutable-array",
1841            TAG_MUTABLE_CELL => "mutable-cell",
1842            _ => "unknown",
1843        }
1844    }
1845
1846    #[inline(always)]
1847    pub fn is_nil(&self) -> bool {
1848        self.0 == Value::NIL.0
1849    }
1850
1851    #[inline(always)]
1852    pub fn is_truthy(&self) -> bool {
1853        self.0 != Value::NIL.0 && self.0 != Value::FALSE.0
1854    }
1855
1856    #[inline(always)]
1857    pub fn is_falsy(&self) -> bool {
1858        !self.is_truthy()
1859    }
1860
1861    #[inline(always)]
1862    pub fn is_bool(&self) -> bool {
1863        self.0 == Value::TRUE.0 || self.0 == Value::FALSE.0
1864    }
1865
1866    #[inline(always)]
1867    pub fn is_int(&self) -> bool {
1868        is_boxed(self.0) && matches!(get_tag(self.0), TAG_INT_SMALL | TAG_INT_BIG)
1869    }
1870
1871    #[inline(always)]
1872    pub fn is_bigint(&self) -> bool {
1873        is_boxed(self.0) && get_tag(self.0) == TAG_BIGINT
1874    }
1875
1876    #[inline(always)]
1877    pub fn is_rational(&self) -> bool {
1878        is_boxed(self.0) && get_tag(self.0) == TAG_RATIONAL
1879    }
1880
1881    #[inline(always)]
1882    pub fn is_complex(&self) -> bool {
1883        is_boxed(self.0) && get_tag(self.0) == TAG_COMPLEX
1884    }
1885
1886    #[inline(always)]
1887    pub fn is_symbol(&self) -> bool {
1888        is_boxed(self.0) && get_tag(self.0) == TAG_SYMBOL
1889    }
1890
1891    #[inline(always)]
1892    pub fn is_keyword(&self) -> bool {
1893        is_boxed(self.0) && get_tag(self.0) == TAG_KEYWORD
1894    }
1895
1896    #[inline(always)]
1897    pub fn is_string(&self) -> bool {
1898        is_boxed(self.0) && get_tag(self.0) == TAG_STRING
1899    }
1900
1901    #[inline(always)]
1902    pub fn is_list(&self) -> bool {
1903        is_boxed(self.0) && get_tag(self.0) == TAG_LIST
1904    }
1905
1906    #[inline(always)]
1907    pub fn is_pair(&self) -> bool {
1908        if let Some(items) = self.as_list() {
1909            !items.is_empty()
1910        } else {
1911            false
1912        }
1913    }
1914
1915    #[inline(always)]
1916    pub fn is_vector(&self) -> bool {
1917        is_boxed(self.0) && get_tag(self.0) == TAG_VECTOR
1918    }
1919
1920    #[inline(always)]
1921    pub fn is_map(&self) -> bool {
1922        is_boxed(self.0) && matches!(get_tag(self.0), TAG_MAP | TAG_HASHMAP)
1923    }
1924
1925    #[inline(always)]
1926    pub fn is_lambda(&self) -> bool {
1927        is_boxed(self.0) && get_tag(self.0) == TAG_LAMBDA
1928    }
1929
1930    #[inline(always)]
1931    pub fn is_native_fn(&self) -> bool {
1932        is_boxed(self.0) && get_tag(self.0) == TAG_NATIVE_FN
1933    }
1934
1935    #[inline(always)]
1936    pub fn is_thunk(&self) -> bool {
1937        is_boxed(self.0) && get_tag(self.0) == TAG_THUNK
1938    }
1939
1940    #[inline(always)]
1941    pub fn is_async_promise(&self) -> bool {
1942        is_boxed(self.0) && get_tag(self.0) == TAG_ASYNC_PROMISE
1943    }
1944    #[inline(always)]
1945    pub fn is_channel(&self) -> bool {
1946        is_boxed(self.0) && get_tag(self.0) == TAG_CHANNEL
1947    }
1948
1949    #[inline(always)]
1950    pub fn is_record(&self) -> bool {
1951        is_boxed(self.0) && get_tag(self.0) == TAG_RECORD
1952    }
1953
1954    #[inline(always)]
1955    pub fn as_int(&self) -> Option<i64> {
1956        if !is_boxed(self.0) {
1957            return None;
1958        }
1959        match get_tag(self.0) {
1960            TAG_INT_SMALL => {
1961                let payload = get_payload(self.0);
1962                let val = if payload & INT_SIGN_BIT != 0 {
1963                    (payload | !PAYLOAD_MASK) as i64
1964                } else {
1965                    payload as i64
1966                };
1967                Some(val)
1968            }
1969            TAG_INT_BIG => Some(unsafe { *self.borrow_ref::<i64>() }),
1970            _ => None,
1971        }
1972    }
1973
1974    /// Lift any integer Value (fixnum, int-big, or bignum) to `BigInt`.
1975    /// `None` for non-integers.
1976    pub fn as_bigint(&self) -> Option<BigInt> {
1977        match self.view_ref() {
1978            ValueViewRef::Int(n) => Some(BigInt::from(n)),
1979            ValueViewRef::BigInt(n) => Some(n.clone()),
1980            _ => None,
1981        }
1982    }
1983
1984    /// Lift any exact Value (fixnum, bignum, or rational) to `BigRational`.
1985    /// `None` for non-exact-numeric Values (including floats).
1986    pub fn as_rational(&self) -> Option<BigRational> {
1987        match self.view_ref() {
1988            ValueViewRef::Int(n) => Some(BigRational::from(BigInt::from(n))),
1989            ValueViewRef::BigInt(n) => Some(BigRational::from(n.clone())),
1990            ValueViewRef::Rational(r) => Some(r.clone()),
1991            _ => None,
1992        }
1993    }
1994
1995    /// Lift any numeric Value into the tower type for arithmetic. `None` for
1996    /// non-numbers.
1997    pub fn as_number(&self) -> Option<SemaNumber> {
1998        match self.view_ref() {
1999            ValueViewRef::Int(n) => Some(SemaNumber::from_i64(n)),
2000            ValueViewRef::BigInt(n) => Some(SemaNumber::Integer(n.clone())),
2001            ValueViewRef::Rational(r) => Some(SemaNumber::Rational(r.clone())),
2002            ValueViewRef::Complex(c) => Some(SemaNumber::Complex(Box::new(c.clone()))),
2003            ValueViewRef::Float(f) => Some(SemaNumber::Real(f)),
2004            _ => None,
2005        }
2006    }
2007
2008    /// Lower a tower number to the tightest Value.
2009    pub fn from_number(n: SemaNumber) -> Value {
2010        match n.normalize() {
2011            SemaNumber::Integer(big) => Value::from_bigint(big),
2012            SemaNumber::Rational(r) => Value::rational(r),
2013            SemaNumber::Real(f) => Value::float(f),
2014            SemaNumber::Complex(c) => Value::from_rc_ptr(TAG_COMPLEX, Rc::new(*c)),
2015        }
2016    }
2017
2018    /// Lift a complex Value to the tower's `Complex` component pair. `None`
2019    /// for non-complex Values.
2020    pub fn as_complex(&self) -> Option<SemaComplex> {
2021        if let ValueViewRef::Complex(c) = self.view_ref() {
2022            Some(c.clone())
2023        } else {
2024            None
2025        }
2026    }
2027
2028    /// Convert a user-supplied integer to a `usize` index/count, rejecting
2029    /// non-integers and negative values. Centralizes the negativity guard that
2030    /// `list/take`, `list/drop`, `string/repeat` (and the Pattern-A audit sites)
2031    /// all need — a bare `as usize` would wrap a negative `i64` to a huge value
2032    /// and trigger an OOM allocation or out-of-bounds panic.
2033    pub fn as_index(&self, name: &str) -> Result<usize, SemaError> {
2034        let n = self.as_int().ok_or_else(|| {
2035            SemaError::type_error("int", self.type_name())
2036                .with_hint(format!("{name}: argument must be an integer"))
2037        })?;
2038        if n < 0 {
2039            return Err(SemaError::eval(format!(
2040                "{name}: expected a non-negative integer, got {n}"
2041            ))
2042            .with_hint("pass 0 or a positive integer"));
2043        }
2044        Ok(n as usize)
2045    }
2046
2047    #[inline(always)]
2048    pub fn as_float(&self) -> Option<f64> {
2049        if !is_boxed(self.0) {
2050            return Some(f64::from_bits(self.0));
2051        }
2052        match get_tag(self.0) {
2053            TAG_INT_SMALL => {
2054                let payload = get_payload(self.0);
2055                let val = if payload & INT_SIGN_BIT != 0 {
2056                    (payload | !PAYLOAD_MASK) as i64
2057                } else {
2058                    payload as i64
2059                };
2060                Some(val as f64)
2061            }
2062            TAG_INT_BIG => Some(unsafe { *self.borrow_ref::<i64>() } as f64),
2063            _ => None,
2064        }
2065    }
2066
2067    #[inline(always)]
2068    pub fn as_bool(&self) -> Option<bool> {
2069        if self.0 == Value::TRUE.0 {
2070            Some(true)
2071        } else if self.0 == Value::FALSE.0 {
2072            Some(false)
2073        } else {
2074            None
2075        }
2076    }
2077
2078    #[inline(always)]
2079    pub fn as_str(&self) -> Option<&str> {
2080        if is_boxed(self.0) && get_tag(self.0) == TAG_STRING {
2081            Some(unsafe { self.borrow_ref::<String>() })
2082        } else {
2083            None
2084        }
2085    }
2086
2087    pub fn as_string_rc(&self) -> Option<Rc<String>> {
2088        if is_boxed(self.0) && get_tag(self.0) == TAG_STRING {
2089            Some(unsafe { self.get_rc::<String>() })
2090        } else {
2091            None
2092        }
2093    }
2094
2095    pub fn as_symbol(&self) -> Option<String> {
2096        self.as_symbol_spur().map(resolve)
2097    }
2098
2099    pub fn as_symbol_spur(&self) -> Option<Spur> {
2100        if is_boxed(self.0) && get_tag(self.0) == TAG_SYMBOL {
2101            let payload = get_payload(self.0);
2102            Some(bits_to_spur(payload as u32))
2103        } else {
2104            None
2105        }
2106    }
2107
2108    pub fn as_keyword(&self) -> Option<String> {
2109        self.as_keyword_spur().map(resolve)
2110    }
2111
2112    pub fn as_keyword_spur(&self) -> Option<Spur> {
2113        if is_boxed(self.0) && get_tag(self.0) == TAG_KEYWORD {
2114            let payload = get_payload(self.0);
2115            Some(bits_to_spur(payload as u32))
2116        } else {
2117            None
2118        }
2119    }
2120
2121    pub fn as_char(&self) -> Option<char> {
2122        if is_boxed(self.0) && get_tag(self.0) == TAG_CHAR {
2123            let payload = get_payload(self.0);
2124            char::from_u32(payload as u32)
2125        } else {
2126            None
2127        }
2128    }
2129
2130    pub fn as_list(&self) -> Option<&[Value]> {
2131        if is_boxed(self.0) && get_tag(self.0) == TAG_LIST {
2132            Some(unsafe { self.borrow_ref::<Vec<Value>>() })
2133        } else {
2134            None
2135        }
2136    }
2137
2138    pub fn as_list_rc(&self) -> Option<Rc<Vec<Value>>> {
2139        if is_boxed(self.0) && get_tag(self.0) == TAG_LIST {
2140            Some(unsafe { self.get_rc::<Vec<Value>>() })
2141        } else {
2142            None
2143        }
2144    }
2145
2146    /// Returns the contents as a slice if this is a list OR a vector.
2147    pub fn as_seq(&self) -> Option<&[Value]> {
2148        self.as_list().or_else(|| self.as_vector())
2149    }
2150
2151    pub fn as_vector(&self) -> Option<&[Value]> {
2152        if is_boxed(self.0) && get_tag(self.0) == TAG_VECTOR {
2153            Some(unsafe { self.borrow_ref::<Vec<Value>>() })
2154        } else {
2155            None
2156        }
2157    }
2158
2159    pub fn as_vector_rc(&self) -> Option<Rc<Vec<Value>>> {
2160        if is_boxed(self.0) && get_tag(self.0) == TAG_VECTOR {
2161            Some(unsafe { self.get_rc::<Vec<Value>>() })
2162        } else {
2163            None
2164        }
2165    }
2166
2167    pub fn as_map_rc(&self) -> Option<Rc<BTreeMap<Value, Value>>> {
2168        if is_boxed(self.0) && get_tag(self.0) == TAG_MAP {
2169            Some(unsafe { self.get_rc::<BTreeMap<Value, Value>>() })
2170        } else {
2171            None
2172        }
2173    }
2174
2175    pub fn as_hashmap_rc(&self) -> Option<Rc<hashbrown::HashMap<Value, Value>>> {
2176        if is_boxed(self.0) && get_tag(self.0) == TAG_HASHMAP {
2177            Some(unsafe { self.get_rc::<hashbrown::HashMap<Value, Value>>() })
2178        } else {
2179            None
2180        }
2181    }
2182
2183    /// Borrow the underlying HashMap without bumping the Rc refcount.
2184    #[inline(always)]
2185    pub fn as_hashmap_ref(&self) -> Option<&hashbrown::HashMap<Value, Value>> {
2186        if is_boxed(self.0) && get_tag(self.0) == TAG_HASHMAP {
2187            Some(unsafe { self.borrow_ref::<hashbrown::HashMap<Value, Value>>() })
2188        } else {
2189            None
2190        }
2191    }
2192
2193    /// Borrow the underlying BTreeMap without bumping the Rc refcount.
2194    #[inline(always)]
2195    pub fn as_map_ref(&self) -> Option<&BTreeMap<Value, Value>> {
2196        if is_boxed(self.0) && get_tag(self.0) == TAG_MAP {
2197            Some(unsafe { self.borrow_ref::<BTreeMap<Value, Value>>() })
2198        } else {
2199            None
2200        }
2201    }
2202
2203    /// If this is a hashmap with refcount==1, mutate it in place.
2204    /// Returns `None` if not a hashmap or if shared (refcount > 1).
2205    /// SAFETY: relies on no other references to the inner data existing.
2206    #[inline(always)]
2207    pub fn with_hashmap_mut_if_unique<R>(
2208        &self,
2209        f: impl FnOnce(&mut hashbrown::HashMap<Value, Value>) -> R,
2210    ) -> Option<R> {
2211        if !is_boxed(self.0) || get_tag(self.0) != TAG_HASHMAP {
2212            return None;
2213        }
2214        let payload = get_payload(self.0);
2215        let ptr = payload_to_ptr(payload) as *const hashbrown::HashMap<Value, Value>;
2216        let rc = std::mem::ManuallyDrop::new(unsafe { Rc::from_raw(ptr) });
2217        if Rc::strong_count(&rc) != 1 {
2218            return None;
2219        }
2220        // strong_count==1: we are the sole owner, safe to mutate
2221        let ptr_mut = ptr as *mut hashbrown::HashMap<Value, Value>;
2222        Some(f(unsafe { &mut *ptr_mut }))
2223    }
2224
2225    /// If this is a map (BTreeMap) with refcount==1, mutate it in place.
2226    /// Returns `None` if not a map or if shared (refcount > 1).
2227    #[inline(always)]
2228    pub fn with_map_mut_if_unique<R>(
2229        &self,
2230        f: impl FnOnce(&mut BTreeMap<Value, Value>) -> R,
2231    ) -> Option<R> {
2232        if !is_boxed(self.0) || get_tag(self.0) != TAG_MAP {
2233            return None;
2234        }
2235        let payload = get_payload(self.0);
2236        let ptr = payload_to_ptr(payload) as *const BTreeMap<Value, Value>;
2237        let rc = std::mem::ManuallyDrop::new(unsafe { Rc::from_raw(ptr) });
2238        if Rc::strong_count(&rc) != 1 {
2239            return None;
2240        }
2241        let ptr_mut = ptr as *mut BTreeMap<Value, Value>;
2242        Some(f(unsafe { &mut *ptr_mut }))
2243    }
2244
2245    /// Consume this Value and extract the inner Rc without a refcount bump.
2246    /// Returns `Err(self)` if not a hashmap.
2247    pub fn into_hashmap_rc(self) -> Result<Rc<hashbrown::HashMap<Value, Value>>, Value> {
2248        if is_boxed(self.0) && get_tag(self.0) == TAG_HASHMAP {
2249            let payload = get_payload(self.0);
2250            let ptr = payload_to_ptr(payload) as *const hashbrown::HashMap<Value, Value>;
2251            // Prevent Drop from decrementing the refcount — we're taking ownership
2252            std::mem::forget(self);
2253            Ok(unsafe { Rc::from_raw(ptr) })
2254        } else {
2255            Err(self)
2256        }
2257    }
2258
2259    /// Consume this Value and extract the inner Rc without a refcount bump.
2260    /// Returns `Err(self)` if not a map.
2261    pub fn into_map_rc(self) -> Result<Rc<BTreeMap<Value, Value>>, Value> {
2262        if is_boxed(self.0) && get_tag(self.0) == TAG_MAP {
2263            let payload = get_payload(self.0);
2264            let ptr = payload_to_ptr(payload) as *const BTreeMap<Value, Value>;
2265            std::mem::forget(self);
2266            Ok(unsafe { Rc::from_raw(ptr) })
2267        } else {
2268            Err(self)
2269        }
2270    }
2271
2272    pub fn as_lambda_rc(&self) -> Option<Rc<Lambda>> {
2273        if is_boxed(self.0) && get_tag(self.0) == TAG_LAMBDA {
2274            Some(unsafe { self.get_rc::<Lambda>() })
2275        } else {
2276            None
2277        }
2278    }
2279
2280    pub fn as_macro_rc(&self) -> Option<Rc<Macro>> {
2281        if is_boxed(self.0) && get_tag(self.0) == TAG_MACRO {
2282            Some(unsafe { self.get_rc::<Macro>() })
2283        } else {
2284            None
2285        }
2286    }
2287
2288    pub fn as_native_fn_rc(&self) -> Option<Rc<NativeFn>> {
2289        if is_boxed(self.0) && get_tag(self.0) == TAG_NATIVE_FN {
2290            Some(unsafe { self.get_rc::<NativeFn>() })
2291        } else {
2292            None
2293        }
2294    }
2295
2296    pub fn as_thunk_rc(&self) -> Option<Rc<Thunk>> {
2297        if is_boxed(self.0) && get_tag(self.0) == TAG_THUNK {
2298            Some(unsafe { self.get_rc::<Thunk>() })
2299        } else {
2300            None
2301        }
2302    }
2303
2304    pub fn as_record(&self) -> Option<&Record> {
2305        if is_boxed(self.0) && get_tag(self.0) == TAG_RECORD {
2306            Some(unsafe { self.borrow_ref::<Record>() })
2307        } else {
2308            None
2309        }
2310    }
2311
2312    pub fn as_record_rc(&self) -> Option<Rc<Record>> {
2313        if is_boxed(self.0) && get_tag(self.0) == TAG_RECORD {
2314            Some(unsafe { self.get_rc::<Record>() })
2315        } else {
2316            None
2317        }
2318    }
2319
2320    pub fn as_bytevector(&self) -> Option<&[u8]> {
2321        if is_boxed(self.0) && get_tag(self.0) == TAG_BYTEVECTOR {
2322            Some(unsafe { self.borrow_ref::<Vec<u8>>() })
2323        } else {
2324            None
2325        }
2326    }
2327
2328    pub fn as_bytevector_rc(&self) -> Option<Rc<Vec<u8>>> {
2329        if is_boxed(self.0) && get_tag(self.0) == TAG_BYTEVECTOR {
2330            Some(unsafe { self.get_rc::<Vec<u8>>() })
2331        } else {
2332            None
2333        }
2334    }
2335
2336    pub fn as_f64_array(&self) -> Option<&[f64]> {
2337        if is_boxed(self.0) && get_tag(self.0) == TAG_F64_ARRAY {
2338            Some(unsafe { self.borrow_ref::<Vec<f64>>() })
2339        } else {
2340            None
2341        }
2342    }
2343
2344    pub fn as_f64_array_rc(&self) -> Option<Rc<Vec<f64>>> {
2345        if is_boxed(self.0) && get_tag(self.0) == TAG_F64_ARRAY {
2346            Some(unsafe { self.get_rc::<Vec<f64>>() })
2347        } else {
2348            None
2349        }
2350    }
2351
2352    pub fn as_i64_array(&self) -> Option<&[i64]> {
2353        if is_boxed(self.0) && get_tag(self.0) == TAG_I64_ARRAY {
2354            Some(unsafe { self.borrow_ref::<Vec<i64>>() })
2355        } else {
2356            None
2357        }
2358    }
2359
2360    pub fn as_i64_array_rc(&self) -> Option<Rc<Vec<i64>>> {
2361        if is_boxed(self.0) && get_tag(self.0) == TAG_I64_ARRAY {
2362            Some(unsafe { self.get_rc::<Vec<i64>>() })
2363        } else {
2364            None
2365        }
2366    }
2367
2368    pub fn as_stream(&self) -> Option<&StreamBox> {
2369        if is_boxed(self.0) && get_tag(self.0) == TAG_STREAM {
2370            Some(unsafe { self.borrow_ref::<StreamBox>() })
2371        } else {
2372            None
2373        }
2374    }
2375
2376    pub fn as_stream_rc(&self) -> Option<Rc<StreamBox>> {
2377        if is_boxed(self.0) && get_tag(self.0) == TAG_STREAM {
2378            Some(unsafe { self.get_rc::<StreamBox>() })
2379        } else {
2380            None
2381        }
2382    }
2383
2384    pub fn as_prompt_rc(&self) -> Option<Rc<Prompt>> {
2385        if is_boxed(self.0) && get_tag(self.0) == TAG_PROMPT {
2386            Some(unsafe { self.get_rc::<Prompt>() })
2387        } else {
2388            None
2389        }
2390    }
2391
2392    pub fn as_message_rc(&self) -> Option<Rc<Message>> {
2393        if is_boxed(self.0) && get_tag(self.0) == TAG_MESSAGE {
2394            Some(unsafe { self.get_rc::<Message>() })
2395        } else {
2396            None
2397        }
2398    }
2399
2400    pub fn as_conversation_rc(&self) -> Option<Rc<Conversation>> {
2401        if is_boxed(self.0) && get_tag(self.0) == TAG_CONVERSATION {
2402            Some(unsafe { self.get_rc::<Conversation>() })
2403        } else {
2404            None
2405        }
2406    }
2407
2408    pub fn as_tool_def_rc(&self) -> Option<Rc<ToolDefinition>> {
2409        if is_boxed(self.0) && get_tag(self.0) == TAG_TOOL_DEF {
2410            Some(unsafe { self.get_rc::<ToolDefinition>() })
2411        } else {
2412            None
2413        }
2414    }
2415
2416    pub fn as_agent_rc(&self) -> Option<Rc<Agent>> {
2417        if is_boxed(self.0) && get_tag(self.0) == TAG_AGENT {
2418            Some(unsafe { self.get_rc::<Agent>() })
2419        } else {
2420            None
2421        }
2422    }
2423
2424    pub fn as_multimethod_rc(&self) -> Option<Rc<MultiMethod>> {
2425        if is_boxed(self.0) && get_tag(self.0) == TAG_MULTIMETHOD {
2426            Some(unsafe { self.get_rc::<MultiMethod>() })
2427        } else {
2428            None
2429        }
2430    }
2431
2432    pub fn as_mutable_array(&self) -> Option<&MutableArray> {
2433        if is_boxed(self.0) && get_tag(self.0) == TAG_MUTABLE_ARRAY {
2434            Some(unsafe { self.borrow_ref::<MutableArray>() })
2435        } else {
2436            None
2437        }
2438    }
2439
2440    pub fn as_mutable_array_rc(&self) -> Option<Rc<MutableArray>> {
2441        if is_boxed(self.0) && get_tag(self.0) == TAG_MUTABLE_ARRAY {
2442            Some(unsafe { self.get_rc::<MutableArray>() })
2443        } else {
2444            None
2445        }
2446    }
2447
2448    pub fn as_mutable_cell(&self) -> Option<&MutableCell> {
2449        if is_boxed(self.0) && get_tag(self.0) == TAG_MUTABLE_CELL {
2450            Some(unsafe { self.borrow_ref::<MutableCell>() })
2451        } else {
2452            None
2453        }
2454    }
2455
2456    pub fn as_mutable_cell_rc(&self) -> Option<Rc<MutableCell>> {
2457        if is_boxed(self.0) && get_tag(self.0) == TAG_MUTABLE_CELL {
2458            Some(unsafe { self.get_rc::<MutableCell>() })
2459        } else {
2460            None
2461        }
2462    }
2463
2464    /// True for interior-mutable containers (mutable arrays and cells), whose
2465    /// contents can change after a map insertion. Map constructors reject
2466    /// these as keys — a mutated key would silently corrupt lookup order.
2467    #[inline(always)]
2468    pub fn is_mutable_container(&self) -> bool {
2469        is_boxed(self.0) && matches!(get_tag(self.0), TAG_MUTABLE_ARRAY | TAG_MUTABLE_CELL)
2470    }
2471
2472    /// True if this value is, or transitively contains, an interior-mutable
2473    /// container. Map keys must be deeply immutable: a vector wrapping a
2474    /// mutable array still mutates underneath the map, corrupting lookup
2475    /// order just as a bare mutable key would (Ord recurses into container
2476    /// elements). Iterative worklist — no visited set or depth cap needed
2477    /// because the walk never descends into a mutable container (it returns
2478    /// true on sight) and cycles are only constructible through one.
2479    pub fn contains_mutable_container(&self) -> bool {
2480        fn scan(v: &Value, pending: &mut Vec<Value>) -> bool {
2481            if v.is_mutable_container() {
2482                return true;
2483            }
2484            match v.view_ref() {
2485                ValueViewRef::List(items) | ValueViewRef::Vector(items) => {
2486                    pending.extend(items.iter().cloned());
2487                }
2488                ValueViewRef::Map(m) => {
2489                    for (k, val) in m.iter() {
2490                        pending.push(k.clone());
2491                        pending.push(val.clone());
2492                    }
2493                }
2494                ValueViewRef::HashMap(m) => {
2495                    for (k, val) in m.iter() {
2496                        pending.push(k.clone());
2497                        pending.push(val.clone());
2498                    }
2499                }
2500                ValueViewRef::Record(r) => pending.extend(r.fields.iter().cloned()),
2501                _ => {}
2502            }
2503            false
2504        }
2505        let mut pending = Vec::new();
2506        if scan(self, &mut pending) {
2507            return true;
2508        }
2509        while let Some(v) = pending.pop() {
2510            if scan(&v, &mut pending) {
2511                return true;
2512            }
2513        }
2514        false
2515    }
2516}
2517
2518// ── Clone ─────────────────────────────────────────────────────────
2519
2520/// Byte offset from an `Rc` payload pointer back to its box's strong count.
2521///
2522/// `std`'s `RcBox` is `#[repr(C)] { strong: Cell<usize>, weak: Cell<usize>, value: T }`,
2523/// so for any payload whose alignment fits the two-count header, the value
2524/// lands exactly one header past the box — on every target the strong count
2525/// lives at `ptr - RC_HEADER`, the same offset for all heap tags alike. That
2526/// uniformity is what lets `Clone`/`Drop` bump the refcount without
2527/// dispatching on the tag.
2528///
2529/// Two guards keep this sound: the alignment bound is a compile-time assert
2530/// per payload type in `from_rc_ptr` (the single boxing funnel), and the
2531/// header offset itself is pinned against a real `Rc` both by
2532/// `tests::rc_header_matches_std_layout` and by a debug assertion on every
2533/// boxing — a std `RcBox` layout change fails loudly instead of corrupting
2534/// memory.
2535const RC_HEADER: usize = 2 * std::mem::size_of::<usize>();
2536
2537/// The strong-count cell of the `RcBox` that owns `ptr`'s payload.
2538///
2539/// # Safety
2540/// `ptr` must have come from `Rc::into_raw` for one of the heap payload types
2541/// (all satisfy the `RC_HEADER` alignment bound above), and that `Rc`
2542/// allocation must still be live.
2543#[inline(always)]
2544unsafe fn rc_strong_cell<'a>(ptr: *const u8) -> &'a Cell<usize> {
2545    &*(ptr.sub(RC_HEADER) as *const Cell<usize>)
2546}
2547
2548/// Recursion budget for `free_heap_value`'s direct (non-worklist) path: at
2549/// depths at or below this bound, freeing a nested collection recurses
2550/// straight through Rust's native call stack instead of allocating and
2551/// draining a worklist `Vec`. The overwhelming majority of real Sema
2552/// structures (a handful of list/map levels) never approach this depth, so
2553/// the direct path is what almost every last-ref drop actually takes —
2554/// no `Vec::new`/push/pop indirection, just ordinary recursive calls the
2555/// compiler can inline and branch-predict well. 64 native frames (a few
2556/// hundred bytes each at most) is far below any realistic stack-overflow
2557/// threshold while comfortably exceeding realistic nesting depth; beyond it,
2558/// `free_heap_value` falls back to the worklist spill (see
2559/// `drop_last_heap_ref`'s original SIGABRT-prevention rationale, preserved
2560/// below), so teardown of one contiguous run of nested `TAG_LIST`/`TAG_VECTOR`/
2561/// `TAG_MAP`/`TAG_HASHMAP` (the types this module unrolls onto the worklist)
2562/// costs only O(1) native frames regardless of how deep that run goes.
2563///
2564/// That bound does NOT extend across a non-unrolled heap type (`Record`,
2565/// `Thunk`, `MutableArray`, `Lambda`, ...) reached mid-teardown: those drop
2566/// via ordinary Rust drop glue (`drop_leaf_heap_ref`), which recurses into
2567/// each `Value` field through `Value::drop` — a fresh call with no memory of
2568/// the depth this module was tracking, i.e. a fresh depth-0 budget. A
2569/// structure that interleaves such types with runs of nested collections
2570/// each ≤64 deep therefore costs O(reset-count × 64) native frames in the
2571/// worst case, not O(1) — accepted as a ~64x narrowing of the overflow
2572/// margin versus always spilling, since that interleaved shape is contrived
2573/// and the common case (long pure collection chains) is unaffected.
2574const DROP_DIRECT_RECURSION_BUDGET: u32 = 64;
2575
2576/// Typed free for a heap value whose last strong reference is going away:
2577/// reconstruct the `Rc` (strong count 1) and drop it, running the payload's
2578/// destructor and releasing the box exactly as a plain `Rc` drop would
2579/// (including the weak-count bookkeeping). Out of line — `Value::drop`'s hot
2580/// path is the uniform decrement; this per-tag dispatch is only paid on the
2581/// final release.
2582///
2583/// # Safety
2584/// `ptr` came from `Rc::into_raw` for the payload type `tag` denotes, and the
2585/// strong count is exactly 1 (this call consumes that last reference).
2586///
2587/// Out of line but deliberately NOT `#[cold]`: workloads that free on every
2588/// iteration (a throw/catch loop discarding condition maps, a churn loop)
2589/// make this path hot, and a cold attribute would override the PGO profile's
2590/// better judgment about its layout.
2591#[inline(never)]
2592unsafe fn drop_last_heap_ref(tag: u64, ptr: *const u8) {
2593    free_heap_value(tag, ptr, 0);
2594}
2595
2596/// Free one heap value's last strong reference at nesting `depth` (0 at the
2597/// drop root, +1 per nested immutable-collection level a recursive free
2598/// descends into). Depths within [`DROP_DIRECT_RECURSION_BUDGET`] free their
2599/// children via direct recursive calls; past the budget, this falls back to
2600/// the iterative worklist spill that the original (always-iterative)
2601/// implementation used unconditionally.
2602///
2603/// # Safety
2604/// Same contract as [`drop_last_heap_ref`]: `ptr` came from `Rc::into_raw`
2605/// for the payload type `tag` denotes, with strong count exactly 1.
2606unsafe fn free_heap_value(tag: u64, ptr: *const u8, depth: u32) {
2607    if depth > DROP_DIRECT_RECURSION_BUDGET {
2608        // Deeply nested immutable collections (a 5000-deep nested list, a
2609        // tree of maps) would otherwise free recursively: dropping the outer
2610        // `Vec<Value>` drops each child `Value`, whose last-ref free drops
2611        // *its* `Vec<Value>`, one native frame per level. On the
2612        // unified-runtime drive path that recursion starts from a deep
2613        // native baseline and overflows the OS stack (an uncatchable
2614        // SIGABRT) well before any Sema guard can fire. Flatten it: move
2615        // each collection's children onto an explicit heap worklist and free
2616        // them iteratively, so teardown depth from here is O(1) native
2617        // frames per contiguous run of the unrolled collection types
2618        // (`TAG_LIST`/`TAG_VECTOR`/`TAG_MAP`/`TAG_HASHMAP`), regardless of how
2619        // deep that run goes. Only those cycle-free immutable collections are
2620        // unrolled here; every other payload drops through
2621        // `drop_leaf_heap_ref` via ordinary Rust drop glue, which re-enters
2622        // `Value::drop` (and so `free_heap_value`) at a fresh depth 0 for
2623        // each `Value` field it holds — a structure that interleaves such
2624        // types with ≤64-deep collection runs costs O(reset-count × 64)
2625        // frames rather than O(1) (see `DROP_DIRECT_RECURSION_BUDGET` for the
2626        // accepted margin trade-off this implies).
2627        let mut worklist: Vec<Value> = Vec::new();
2628        free_heap_payload(tag, ptr, &mut worklist);
2629        drain_drop_worklist(worklist);
2630        return;
2631    }
2632    if !take_owned_children(tag, ptr, |child| drop_child_value(child, depth + 1)) {
2633        drop_leaf_heap_ref(tag, ptr);
2634    }
2635}
2636
2637/// For the three child-bearing immutable collection tags
2638/// (`TAG_LIST`/`TAG_VECTOR` → `Vec<Value>`, `TAG_MAP` → `BTreeMap`,
2639/// `TAG_HASHMAP` → `hashbrown::HashMap`), reconstruct the container's `Rc`
2640/// (strong count 1) and drop it — freeing the container's own allocation
2641/// exactly once via `Rc::into_inner` — then feed every owned child `Value` to
2642/// `sink` (for maps, key then value, preserving iteration order), and return
2643/// `true`. For any other tag, return `false` WITHOUT touching `ptr`, leaving
2644/// the caller to free that leaf allocation itself.
2645///
2646/// This is the single source of truth for which tags own child `Value`s and
2647/// how they are extracted; the direct-recursion (`free_heap_value`) and
2648/// worklist-spill (`free_heap_payload`) paths differ only in the `sink` they
2649/// pass (recurse vs push), so they share this enumeration.
2650///
2651/// # Safety
2652/// `ptr` came from `Rc::into_raw` for the payload type `tag` denotes, and the
2653/// strong count is exactly 1 — a `true` return consumes that last reference.
2654unsafe fn take_owned_children(tag: u64, ptr: *const u8, mut sink: impl FnMut(Value)) -> bool {
2655    match tag {
2656        TAG_LIST | TAG_VECTOR => {
2657            let items = Rc::into_inner(Rc::from_raw(ptr as *const Vec<Value>))
2658                .expect("caller guarantees the last strong reference");
2659            for value in items {
2660                sink(value);
2661            }
2662        }
2663        TAG_MAP => {
2664            let map = Rc::into_inner(Rc::from_raw(ptr as *const BTreeMap<Value, Value>))
2665                .expect("caller guarantees the last strong reference");
2666            for (k, v) in map {
2667                sink(k);
2668                sink(v);
2669            }
2670        }
2671        TAG_HASHMAP => {
2672            let map = Rc::into_inner(Rc::from_raw(ptr as *const hashbrown::HashMap<Value, Value>))
2673                .expect("caller guarantees the last strong reference");
2674            for (k, v) in map {
2675                sink(k);
2676                sink(v);
2677            }
2678        }
2679        _ => return false,
2680    }
2681    true
2682}
2683
2684/// Drop one child `Value` reached while freeing a collection's contents at
2685/// `depth`: decrement its refcount, recursing into `free_heap_value` (direct
2686/// or worklist-spilled, per `depth`) only when this was the last reference.
2687/// `mem::forget` keeps `Value`'s own `Drop` from re-entering the recursive
2688/// path this replaces.
2689#[inline]
2690unsafe fn drop_child_value(value: Value, depth: u32) {
2691    drop_value_ref(value, |tag, ptr| free_heap_value(tag, ptr, depth));
2692}
2693
2694/// True for the seven immediate (non-heap) tags — values that carry no
2695/// refcounted allocation and so need no clone/drop bookkeeping. `TAG_NIL..=
2696/// TAG_KEYWORD` are the contiguous range `0..=6` and `TAG_INT_BIG` (the first
2697/// heap tag) is `7`, so this single comparison is exactly the set
2698/// `{TAG_NIL, TAG_FALSE, TAG_TRUE, TAG_INT_SMALL, TAG_CHAR, TAG_SYMBOL,
2699/// TAG_KEYWORD}`.
2700#[inline]
2701fn is_immediate_tag(tag: u64) -> bool {
2702    tag < TAG_INT_BIG
2703}
2704
2705/// Shared refcount-decrement tail for a `Value` being released off a teardown
2706/// path (a collection child or a worklist entry): skip immediates, otherwise
2707/// decrement the strong count and, on the last reference, run `on_last(tag,
2708/// ptr)` — the caller-supplied free (direct-recursive `free_heap_value` for the
2709/// child path, worklist-spilling `free_heap_payload` for the drain path).
2710/// `mem::forget` keeps `Value`'s own `Drop` from re-entering the recursive path
2711/// this replaces.
2712///
2713/// # Safety
2714/// A boxed non-immediate `value` carries a live `Rc::into_raw` pointer (the
2715/// `rc_strong_cell` contract); on a strong count of 1 `on_last` receives the
2716/// last reference and must consume it.
2717#[inline]
2718unsafe fn drop_value_ref(value: Value, on_last: impl FnOnce(u64, *const u8)) {
2719    if is_boxed(value.0) {
2720        let tag = get_tag(value.0);
2721        if !is_immediate_tag(tag) {
2722            let ptr = payload_to_ptr(get_payload(value.0));
2723            let strong = rc_strong_cell(ptr);
2724            match strong.get() {
2725                1 => on_last(tag, ptr),
2726                n => strong.set(n - 1),
2727            }
2728        }
2729    }
2730    std::mem::forget(value);
2731}
2732
2733/// Drain a worklist of children spilled by the depth-budget fallback,
2734/// freeing each iteratively. Deliberately does NOT call back into
2735/// `free_heap_value`'s direct path — once a structure's teardown has
2736/// spilled, it stays on the O(1)-native-frames worklist path for the rest of
2737/// its depth, which is what makes the budget cutoff safe against arbitrarily
2738/// deep structures.
2739unsafe fn drain_drop_worklist(mut worklist: Vec<Value>) {
2740    while let Some(value) = worklist.pop() {
2741        drop_value_ref(value, |tag, ptr| free_heap_payload(tag, ptr, &mut worklist));
2742    }
2743}
2744
2745/// Free ONE heap allocation whose last strong reference is going away. For the
2746/// immutable collection types, move the owned child `Value`s onto `worklist`
2747/// (so they are freed iteratively by [`drop_last_heap_ref`]) and free only the
2748/// container's own allocation here; every other payload is dropped normally.
2749unsafe fn free_heap_payload(tag: u64, ptr: *const u8, worklist: &mut Vec<Value>) {
2750    if !take_owned_children(tag, ptr, |child| worklist.push(child)) {
2751        drop_leaf_heap_ref(tag, ptr);
2752    }
2753}
2754
2755/// Drop a single heap payload's last reference via Rust's normal drop glue.
2756/// The immutable collection tags are intercepted before this by
2757/// [`free_heap_payload`]; they remain here only as a total fallback.
2758unsafe fn drop_leaf_heap_ref(tag: u64, ptr: *const u8) {
2759    match tag {
2760        TAG_INT_BIG => drop(Rc::from_raw(ptr as *const i64)),
2761        TAG_BIGINT => drop(Rc::from_raw(ptr as *const BigInt)),
2762        TAG_RATIONAL => drop(Rc::from_raw(ptr as *const BigRational)),
2763        TAG_COMPLEX => drop(Rc::from_raw(ptr as *const SemaComplex)),
2764        TAG_STRING => drop(Rc::from_raw(ptr as *const String)),
2765        TAG_LIST | TAG_VECTOR => drop(Rc::from_raw(ptr as *const Vec<Value>)),
2766        TAG_MAP => drop(Rc::from_raw(ptr as *const BTreeMap<Value, Value>)),
2767        TAG_HASHMAP => drop(Rc::from_raw(ptr as *const hashbrown::HashMap<Value, Value>)),
2768        TAG_LAMBDA => drop(Rc::from_raw(ptr as *const Lambda)),
2769        TAG_MACRO => drop(Rc::from_raw(ptr as *const Macro)),
2770        TAG_NATIVE_FN => drop(Rc::from_raw(ptr as *const NativeFn)),
2771        TAG_PROMPT => drop(Rc::from_raw(ptr as *const Prompt)),
2772        TAG_MESSAGE => drop(Rc::from_raw(ptr as *const Message)),
2773        TAG_CONVERSATION => drop(Rc::from_raw(ptr as *const Conversation)),
2774        TAG_TOOL_DEF => drop(Rc::from_raw(ptr as *const ToolDefinition)),
2775        TAG_AGENT => drop(Rc::from_raw(ptr as *const Agent)),
2776        TAG_THUNK => drop(Rc::from_raw(ptr as *const Thunk)),
2777        TAG_RECORD => drop(Rc::from_raw(ptr as *const Record)),
2778        TAG_BYTEVECTOR => drop(Rc::from_raw(ptr as *const Vec<u8>)),
2779        TAG_MULTIMETHOD => drop(Rc::from_raw(ptr as *const MultiMethod)),
2780        TAG_STREAM => drop(Rc::from_raw(ptr as *const StreamBox)),
2781        TAG_F64_ARRAY => drop(Rc::from_raw(ptr as *const Vec<f64>)),
2782        TAG_I64_ARRAY => drop(Rc::from_raw(ptr as *const Vec<i64>)),
2783        TAG_ASYNC_PROMISE => drop(Rc::from_raw(ptr as *const AsyncPromise)),
2784        TAG_CHANNEL => drop(Rc::from_raw(ptr as *const Channel)),
2785        TAG_MUTABLE_ARRAY => drop(Rc::from_raw(ptr as *const MutableArray)),
2786        TAG_MUTABLE_CELL => drop(Rc::from_raw(ptr as *const MutableCell)),
2787        _ => {} // unreachable, but don't panic in drop
2788    }
2789}
2790
2791impl Clone for Value {
2792    #[inline(always)]
2793    fn clone(&self) -> Self {
2794        if !is_boxed(self.0) {
2795            // Float: trivial copy
2796            return Value(self.0);
2797        }
2798        let tag = get_tag(self.0);
2799        // Immediates: trivial copy
2800        if is_immediate_tag(tag) {
2801            return Value(self.0);
2802        }
2803        // Heap pointers: one uniform strong-count bump — the RcBox header
2804        // sits at the same offset for every heap payload (see RC_HEADER),
2805        // so no per-tag dispatch is needed.
2806        debug_assert!(
2807            (TAG_INT_BIG..=TAG_MUTABLE_CELL).contains(&tag),
2808            "invalid heap tag in clone: {tag}"
2809        );
2810        let ptr = payload_to_ptr(get_payload(self.0));
2811        // SAFETY: every boxed non-immediate tag carries a live
2812        // `Rc::into_raw` pointer (the `rc_strong_cell` contract).
2813        unsafe {
2814            let strong = rc_strong_cell(ptr);
2815            let n = strong.get().wrapping_add(1);
2816            if n == 0 {
2817                // Refcount overflow — abort, as `Rc::clone` would.
2818                std::process::abort();
2819            }
2820            strong.set(n);
2821        }
2822        Value(self.0)
2823    }
2824}
2825
2826// ── Drop ──────────────────────────────────────────────────────────
2827
2828impl Drop for Value {
2829    #[inline(always)]
2830    fn drop(&mut self) {
2831        if !is_boxed(self.0) {
2832            return; // Float
2833        }
2834        let tag = get_tag(self.0);
2835        // Immediates: nothing to free
2836        if is_immediate_tag(tag) {
2837            return;
2838        }
2839        // Heap pointers: uniform decrement; the per-tag typed free runs
2840        // only when the last reference goes away (cold, out of line).
2841        debug_assert!(
2842            (TAG_INT_BIG..=TAG_MUTABLE_CELL).contains(&tag),
2843            "invalid heap tag in drop: {tag}"
2844        );
2845        let ptr = payload_to_ptr(get_payload(self.0));
2846        // SAFETY: same RcBox-header contract as `Clone`; at count 1
2847        // this value owns the final reference, which the typed free
2848        // consumes.
2849        unsafe {
2850            let strong = rc_strong_cell(ptr);
2851            match strong.get() {
2852                1 => drop_last_heap_ref(tag, ptr),
2853                n => strong.set(n - 1),
2854            }
2855        }
2856    }
2857}
2858
2859// ── PartialEq / Eq ────────────────────────────────────────────────
2860
2861thread_local! {
2862    /// Pairs of mutable-container allocations currently being compared
2863    /// structurally. Mutable arrays/cells are the only heap types with both
2864    /// content-based comparison and interior mutability, so they are the only
2865    /// place `PartialEq`/`Ord` can meet cyclic data: without this guard,
2866    /// comparing two distinct self-referential arrays would recurse forever.
2867    /// Shared between `PartialEq` and `Ord` — an equality assumption in
2868    /// flight is exactly the coinductive hypothesis a nested `cmp` of the
2869    /// same pair should answer `Equal` to (and vice versa).
2870    static CMP_IN_FLIGHT: RefCell<Vec<(usize, usize)>> = const { RefCell::new(Vec::new()) };
2871}
2872
2873/// Run `body` with the `(a, b)` allocation pair marked in-flight. A pair
2874/// already in flight yields `on_cycle` without descending (coinductive
2875/// comparison — the R7RS `equal?` answer for cyclic structures: `true` for
2876/// equality, `Ordering::Equal` for ordering).
2877fn with_cycle_guard<T>(a: usize, b: usize, on_cycle: T, body: impl FnOnce() -> T) -> T {
2878    let already_in_flight = CMP_IN_FLIGHT.with(|s| {
2879        let mut s = s.borrow_mut();
2880        if s.contains(&(a, b)) {
2881            true
2882        } else {
2883            s.push((a, b));
2884            false
2885        }
2886    });
2887    if already_in_flight {
2888        return on_cycle;
2889    }
2890    // Pop on every exit path (including a panicking comparator) so a stale
2891    // pair can never make a later, unrelated comparison lie.
2892    struct PopGuard;
2893    impl Drop for PopGuard {
2894        fn drop(&mut self) {
2895            CMP_IN_FLIGHT.with(|s| {
2896                s.borrow_mut().pop();
2897            });
2898        }
2899    }
2900    let _guard = PopGuard;
2901    body()
2902}
2903
2904impl PartialEq for Value {
2905    fn eq(&self, other: &Self) -> bool {
2906        // Fast path: identical bits
2907        if self.0 == other.0 {
2908            // For floats, NaN != NaN per IEEE, but our canonical NaN is unique,
2909            // so identical bits means equal for all types.
2910            // Exception: need to handle -0.0 == +0.0
2911            if !is_boxed(self.0) {
2912                let f = f64::from_bits(self.0);
2913                // NaN check: if both are canonical NaN (same bits), we say not equal
2914                if f.is_nan() {
2915                    return false;
2916                }
2917                return true;
2918            }
2919            return true;
2920        }
2921        // Different bits: could still be equal for heap types or -0.0/+0.0
2922        match (self.view_ref(), other.view_ref()) {
2923            (ValueViewRef::Nil, ValueViewRef::Nil) => true,
2924            (ValueViewRef::Bool(a), ValueViewRef::Bool(b)) => a == b,
2925            (ValueViewRef::Int(a), ValueViewRef::Int(b)) => a == b,
2926            (ValueViewRef::BigInt(a), ValueViewRef::BigInt(b)) => a == b,
2927            (ValueViewRef::Rational(a), ValueViewRef::Rational(b)) => a == b,
2928            (ValueViewRef::Complex(a), ValueViewRef::Complex(b)) => a.re == b.re && a.im == b.im,
2929            (ValueViewRef::Float(a), ValueViewRef::Float(b)) => a == b,
2930            (ValueViewRef::String(a), ValueViewRef::String(b)) => a == b,
2931            (ValueViewRef::Symbol(a), ValueViewRef::Symbol(b)) => a == b,
2932            (ValueViewRef::Keyword(a), ValueViewRef::Keyword(b)) => a == b,
2933            (ValueViewRef::Char(a), ValueViewRef::Char(b)) => a == b,
2934            (ValueViewRef::List(a), ValueViewRef::List(b)) => a == b,
2935            (ValueViewRef::Vector(a), ValueViewRef::Vector(b)) => a == b,
2936            (ValueViewRef::Map(a), ValueViewRef::Map(b)) => a == b,
2937            (ValueViewRef::HashMap(a), ValueViewRef::HashMap(b)) => a == b,
2938            (ValueViewRef::Record(a), ValueViewRef::Record(b)) => {
2939                a.type_tag == b.type_tag && a.fields == b.fields
2940            }
2941            (ValueViewRef::Bytevector(a), ValueViewRef::Bytevector(b)) => a == b,
2942            (ValueViewRef::F64Array(a), ValueViewRef::F64Array(b)) => {
2943                a.len() == b.len()
2944                    && a.iter()
2945                        .zip(b.iter())
2946                        .all(|(x, y)| x.to_bits() == y.to_bits())
2947            }
2948            (ValueViewRef::I64Array(a), ValueViewRef::I64Array(b)) => a == b,
2949            (ValueViewRef::Stream(a), ValueViewRef::Stream(b)) => std::ptr::eq(a, b),
2950            (ValueViewRef::AsyncPromise(a), ValueViewRef::AsyncPromise(b)) => std::ptr::eq(a, b),
2951            (ValueViewRef::Channel(a), ValueViewRef::Channel(b)) => std::ptr::eq(a, b),
2952            // Content equality (identity is the bits fast path above). The
2953            // guard keeps self-referential arrays/cells from recursing
2954            // forever; an unavailable borrow (contents mid-mutation) is not
2955            // observably equal, so it compares false.
2956            (ValueViewRef::MutableArray(a), ValueViewRef::MutableArray(b)) => with_cycle_guard(
2957                a as *const MutableArray as usize,
2958                b as *const MutableArray as usize,
2959                true,
2960                || match (a.items.try_borrow(), b.items.try_borrow()) {
2961                    (Ok(x), Ok(y)) => *x == *y,
2962                    _ => false,
2963                },
2964            ),
2965            (ValueViewRef::MutableCell(a), ValueViewRef::MutableCell(b)) => with_cycle_guard(
2966                a as *const MutableCell as usize,
2967                b as *const MutableCell as usize,
2968                true,
2969                || match (a.value.try_borrow(), b.value.try_borrow()) {
2970                    (Ok(x), Ok(y)) => *x == *y,
2971                    _ => false,
2972                },
2973            ),
2974            _ => false,
2975        }
2976    }
2977}
2978
2979impl Eq for Value {}
2980
2981// ── Hash ──────────────────────────────────────────────────────────
2982
2983impl Hash for Value {
2984    fn hash<H: Hasher>(&self, state: &mut H) {
2985        match self.view_ref() {
2986            ValueViewRef::Nil => 0u8.hash(state),
2987            ValueViewRef::Bool(b) => {
2988                1u8.hash(state);
2989                b.hash(state);
2990            }
2991            ValueViewRef::Int(n) => {
2992                2u8.hash(state);
2993                n.hash(state);
2994            }
2995            ValueViewRef::BigInt(n) => {
2996                30u8.hash(state);
2997                n.hash(state);
2998            }
2999            ValueViewRef::Rational(r) => {
3000                31u8.hash(state);
3001                r.hash(state);
3002            }
3003            ValueViewRef::Complex(c) => {
3004                32u8.hash(state);
3005                c.re.hash(state);
3006                c.im.hash(state);
3007            }
3008            ValueViewRef::Float(f) => {
3009                3u8.hash(state);
3010                let bits = if f == 0.0 { 0u64 } else { f.to_bits() };
3011                bits.hash(state);
3012            }
3013            ValueViewRef::String(s) => {
3014                4u8.hash(state);
3015                s.hash(state);
3016            }
3017            ValueViewRef::Symbol(s) => {
3018                5u8.hash(state);
3019                s.hash(state);
3020            }
3021            ValueViewRef::Keyword(s) => {
3022                6u8.hash(state);
3023                s.hash(state);
3024            }
3025            ValueViewRef::Char(c) => {
3026                7u8.hash(state);
3027                c.hash(state);
3028            }
3029            ValueViewRef::List(l) => {
3030                8u8.hash(state);
3031                l.hash(state);
3032            }
3033            ValueViewRef::Vector(v) => {
3034                9u8.hash(state);
3035                v.hash(state);
3036            }
3037            ValueViewRef::Record(r) => {
3038                10u8.hash(state);
3039                r.type_tag.hash(state);
3040                r.fields.hash(state);
3041            }
3042            ValueViewRef::Bytevector(bv) => {
3043                11u8.hash(state);
3044                bv.hash(state);
3045            }
3046            ValueViewRef::F64Array(arr) => {
3047                26u8.hash(state);
3048                for v in arr.iter() {
3049                    v.to_bits().hash(state);
3050                }
3051            }
3052            ValueViewRef::I64Array(arr) => {
3053                27u8.hash(state);
3054                arr.hash(state);
3055            }
3056            ValueViewRef::Stream(s) => {
3057                25u8.hash(state);
3058                (s as *const _ as usize).hash(state);
3059            }
3060            ValueViewRef::AsyncPromise(p) => {
3061                28u8.hash(state);
3062                (p as *const _ as usize).hash(state);
3063            }
3064            ValueViewRef::Channel(c) => {
3065                29u8.hash(state);
3066                (c as *const _ as usize).hash(state);
3067            }
3068            // Discriminant only: equality is content-based but the contents
3069            // can mutate, so hashing them would let a mutated key silently
3070            // land in the wrong bucket. A constant hash keeps the Hash/Eq
3071            // contract under mutation; map constructors reject these as keys
3072            // anyway (`is_mutable_container`).
3073            ValueViewRef::MutableArray(_) => 33u8.hash(state),
3074            ValueViewRef::MutableCell(_) => 34u8.hash(state),
3075            _ => {}
3076        }
3077    }
3078}
3079
3080// ── Ord ───────────────────────────────────────────────────────────
3081
3082impl PartialOrd for Value {
3083    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3084        Some(self.cmp(other))
3085    }
3086}
3087
3088impl Ord for Value {
3089    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3090        use std::cmp::Ordering;
3091        fn type_order(v: &Value) -> u8 {
3092            match v.view_ref() {
3093                ValueViewRef::Nil => 0,
3094                ValueViewRef::Bool(_) => 1,
3095                ValueViewRef::Int(_) | ValueViewRef::BigInt(_) => 2,
3096                ValueViewRef::Float(_) => 3,
3097                ValueViewRef::Char(_) => 4,
3098                ValueViewRef::String(_) => 5,
3099                ValueViewRef::Symbol(_) => 6,
3100                ValueViewRef::Keyword(_) => 7,
3101                ValueViewRef::List(_) => 8,
3102                ValueViewRef::Vector(_) => 9,
3103                ValueViewRef::Map(_) => 10,
3104                ValueViewRef::HashMap(_) => 11,
3105                ValueViewRef::Record(_) => 12,
3106                ValueViewRef::Bytevector(_) => 13,
3107                ValueViewRef::F64Array(_) => 14,
3108                ValueViewRef::I64Array(_) => 15,
3109                ValueViewRef::Stream(_) => 16,
3110                ValueViewRef::Rational(_) => 18,
3111                ValueViewRef::Complex(_) => 19,
3112                // Distinct ranks: an array and a cell must never compare
3113                // Equal (Eq/Ord consistency — see the cmp arms below).
3114                ValueViewRef::MutableArray(_) => 20,
3115                ValueViewRef::MutableCell(_) => 21,
3116                _ => 17,
3117            }
3118        }
3119        match (self.view_ref(), other.view_ref()) {
3120            (ValueViewRef::Nil, ValueViewRef::Nil) => Ordering::Equal,
3121            (ValueViewRef::Bool(a), ValueViewRef::Bool(b)) => a.cmp(&b),
3122            (ValueViewRef::Int(a), ValueViewRef::Int(b)) => a.cmp(&b),
3123            (ValueViewRef::BigInt(a), ValueViewRef::BigInt(b)) => a.cmp(b),
3124            (ValueViewRef::Int(a), ValueViewRef::BigInt(b)) => BigInt::from(a).cmp(b),
3125            (ValueViewRef::BigInt(a), ValueViewRef::Int(b)) => a.cmp(&BigInt::from(b)),
3126            (ValueViewRef::Rational(a), ValueViewRef::Rational(b)) => a.cmp(b),
3127            (ValueViewRef::Complex(a), ValueViewRef::Complex(b)) => {
3128                a.re.cmp(&b.re).then_with(|| a.im.cmp(&b.im))
3129            }
3130            (ValueViewRef::Float(a), ValueViewRef::Float(b)) => {
3131                // Normalize signed zeros so -0.0 and +0.0 are the same map key:
3132                // Hash already collapses them and `=` treats them equal, but
3133                // total_cmp otherwise orders -0.0 < +0.0, silently splitting a
3134                // BTreeMap key. (NaN handling is unaffected.)
3135                let norm = |f: f64| if f == 0.0 { 0.0 } else { f };
3136                norm(a).total_cmp(&norm(b))
3137            }
3138            (ValueViewRef::String(a), ValueViewRef::String(b)) => a.cmp(b),
3139            (ValueViewRef::Symbol(a), ValueViewRef::Symbol(b)) => compare_spurs(a, b),
3140            (ValueViewRef::Keyword(a), ValueViewRef::Keyword(b)) => compare_spurs(a, b),
3141            (ValueViewRef::Char(a), ValueViewRef::Char(b)) => a.cmp(&b),
3142            (ValueViewRef::List(a), ValueViewRef::List(b)) => a.cmp(b),
3143            (ValueViewRef::Vector(a), ValueViewRef::Vector(b)) => a.cmp(b),
3144            (ValueViewRef::Record(a), ValueViewRef::Record(b)) => {
3145                compare_spurs(a.type_tag, b.type_tag).then_with(|| a.fields.cmp(&b.fields))
3146            }
3147            (ValueViewRef::Bytevector(a), ValueViewRef::Bytevector(b)) => a.cmp(b),
3148            (ValueViewRef::I64Array(a), ValueViewRef::I64Array(b)) => a.cmp(b),
3149            (ValueViewRef::F64Array(a), ValueViewRef::F64Array(b)) => a
3150                .iter()
3151                .zip(b.iter())
3152                .map(|(x, y)| x.total_cmp(y))
3153                .find(|o| *o != std::cmp::Ordering::Equal)
3154                .unwrap_or_else(|| a.len().cmp(&b.len())),
3155            // Deep content comparison, parallel to the immutable Vector/List
3156            // arms and consistent with `PartialEq` (Eq/Ord agreement is what
3157            // keeps distinct values from aliasing as BTreeMap/BTreeSet keys).
3158            // An in-flight pair compares Equal — the coinductive convention
3159            // `PartialEq` uses — so a self-referential array cannot hang cmp.
3160            // Contents mid-mutation (borrow unavailable) order by allocation
3161            // address: a stable within-process tiebreak that, like the
3162            // PartialEq `false` answer, never calls two distinct live
3163            // mutations equal.
3164            (ValueViewRef::MutableArray(a), ValueViewRef::MutableArray(b)) => {
3165                let pa = a as *const MutableArray as usize;
3166                let pb = b as *const MutableArray as usize;
3167                with_cycle_guard(pa, pb, Ordering::Equal, || {
3168                    match (a.items.try_borrow(), b.items.try_borrow()) {
3169                        (Ok(x), Ok(y)) => (*x).cmp(&*y),
3170                        _ => pa.cmp(&pb),
3171                    }
3172                })
3173            }
3174            (ValueViewRef::MutableCell(a), ValueViewRef::MutableCell(b)) => {
3175                let pa = a as *const MutableCell as usize;
3176                let pb = b as *const MutableCell as usize;
3177                with_cycle_guard(pa, pb, Ordering::Equal, || {
3178                    match (a.value.try_borrow(), b.value.try_borrow()) {
3179                        (Ok(x), Ok(y)) => (*x).cmp(&y),
3180                        _ => pa.cmp(&pb),
3181                    }
3182                })
3183            }
3184            _ => type_order(self).cmp(&type_order(other)),
3185        }
3186    }
3187}
3188
3189// ── Display ───────────────────────────────────────────────────────
3190
3191fn truncate(s: &str, max: usize) -> String {
3192    let mut iter = s.chars();
3193    let prefix: String = iter.by_ref().take(max).collect();
3194    if iter.next().is_none() {
3195        prefix
3196    } else {
3197        format!("{prefix}...")
3198    }
3199}
3200
3201impl fmt::Display for Value {
3202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3203        // Grow the stack on demand so printing a deeply nested value can't
3204        // overflow the OS thread stack and abort the process.
3205        crate::stack::maybe_grow(|| match self.view_ref() {
3206            ValueViewRef::Nil => write!(f, "nil"),
3207            ValueViewRef::Bool(true) => write!(f, "#t"),
3208            ValueViewRef::Bool(false) => write!(f, "#f"),
3209            ValueViewRef::Int(n) => write!(f, "{n}"),
3210            ValueViewRef::BigInt(n) => write!(f, "{n}"),
3211            ValueViewRef::Rational(r) => write!(f, "{}/{}", r.numer(), r.denom()),
3212            ValueViewRef::Complex(c) => {
3213                write!(f, "{}", SemaNumber::Complex(Box::new((*c).clone())))
3214            }
3215            ValueViewRef::Float(n) => {
3216                if n.fract() == 0.0 {
3217                    write!(f, "{n:.1}")
3218                } else {
3219                    write!(f, "{n}")
3220                }
3221            }
3222            ValueViewRef::String(s) => {
3223                write!(f, "\"")?;
3224                for c in s.chars() {
3225                    match c {
3226                        '"' => write!(f, "\\\"")?,
3227                        '\\' => write!(f, "\\\\")?,
3228                        '\n' => write!(f, "\\n")?,
3229                        '\t' => write!(f, "\\t")?,
3230                        '\r' => write!(f, "\\r")?,
3231                        c => write!(f, "{c}")?,
3232                    }
3233                }
3234                write!(f, "\"")
3235            }
3236            ValueViewRef::Symbol(s) => with_resolved(s, |name| write!(f, "{name}")),
3237            ValueViewRef::Keyword(s) => with_resolved(s, |name| write!(f, ":{name}")),
3238            ValueViewRef::Char(c) => match c {
3239                ' ' => write!(f, "#\\space"),
3240                '\n' => write!(f, "#\\newline"),
3241                '\t' => write!(f, "#\\tab"),
3242                '\r' => write!(f, "#\\return"),
3243                '\0' => write!(f, "#\\nul"),
3244                _ => write!(f, "#\\{c}"),
3245            },
3246            ValueViewRef::List(items) => {
3247                write!(f, "(")?;
3248                for (i, item) in items.iter().enumerate() {
3249                    if i > 0 {
3250                        write!(f, " ")?;
3251                    }
3252                    write!(f, "{item}")?;
3253                }
3254                write!(f, ")")
3255            }
3256            ValueViewRef::Vector(items) => {
3257                write!(f, "[")?;
3258                for (i, item) in items.iter().enumerate() {
3259                    if i > 0 {
3260                        write!(f, " ")?;
3261                    }
3262                    write!(f, "{item}")?;
3263                }
3264                write!(f, "]")
3265            }
3266            ValueViewRef::Map(map) => {
3267                write!(f, "{{")?;
3268                for (i, (k, v)) in map.iter().enumerate() {
3269                    if i > 0 {
3270                        write!(f, " ")?;
3271                    }
3272                    write!(f, "{k} {v}")?;
3273                }
3274                write!(f, "}}")
3275            }
3276            ValueViewRef::HashMap(map) => {
3277                let mut entries: Vec<_> = map.iter().collect();
3278                entries.sort_by_key(|(k1, _)| *k1);
3279                write!(f, "{{")?;
3280                for (i, (k, v)) in entries.iter().enumerate() {
3281                    if i > 0 {
3282                        write!(f, " ")?;
3283                    }
3284                    write!(f, "{k} {v}")?;
3285                }
3286                write!(f, "}}")
3287            }
3288            ValueViewRef::Lambda(l) => {
3289                if let Some(name) = &l.name {
3290                    with_resolved(*name, |n| write!(f, "<lambda {n}>"))
3291                } else {
3292                    write!(f, "<lambda>")
3293                }
3294            }
3295            ValueViewRef::Macro(m) => with_resolved(m.name, |n| write!(f, "<macro {n}>")),
3296            ValueViewRef::NativeFn(n) => write!(f, "<native-fn {}>", n.name),
3297            ValueViewRef::Prompt(p) => write!(f, "<prompt {} messages>", p.messages.len()),
3298            ValueViewRef::Message(m) => {
3299                write!(f, "<message {} \"{}\">", m.role, truncate(&m.content, 40))
3300            }
3301            ValueViewRef::Conversation(c) => {
3302                write!(f, "<conversation {} messages>", c.messages.len())
3303            }
3304            ValueViewRef::ToolDef(t) => write!(f, "<tool {}>", t.name),
3305            ValueViewRef::Agent(a) => write!(f, "<agent {}>", a.name),
3306            ValueViewRef::Thunk(t) => {
3307                if t.forced.borrow().is_some() {
3308                    write!(f, "<promise (forced)>")
3309                } else {
3310                    write!(f, "<promise>")
3311                }
3312            }
3313            ValueViewRef::Record(r) => {
3314                with_resolved(r.type_tag, |tag| write!(f, "#<record {tag}"))?;
3315                for field in &r.fields {
3316                    write!(f, " {field}")?;
3317                }
3318                write!(f, ">")
3319            }
3320            ValueViewRef::Bytevector(bv) => {
3321                write!(f, "#u8(")?;
3322                for (i, byte) in bv.iter().enumerate() {
3323                    if i > 0 {
3324                        write!(f, " ")?;
3325                    }
3326                    write!(f, "{byte}")?;
3327                }
3328                write!(f, ")")
3329            }
3330            ValueViewRef::F64Array(arr) => {
3331                write!(f, "#f64(")?;
3332                for (i, v) in arr.iter().enumerate() {
3333                    if i > 0 {
3334                        write!(f, " ")?;
3335                    }
3336                    write!(f, "{v}")?;
3337                }
3338                write!(f, ")")
3339            }
3340            ValueViewRef::I64Array(arr) => {
3341                write!(f, "#i64(")?;
3342                for (i, v) in arr.iter().enumerate() {
3343                    if i > 0 {
3344                        write!(f, " ")?;
3345                    }
3346                    write!(f, "{v}")?;
3347                }
3348                write!(f, ")")
3349            }
3350            ValueViewRef::MultiMethod(m) => {
3351                with_resolved(m.name, |n| write!(f, "<multimethod {n}>"))
3352            }
3353            ValueViewRef::Stream(s) => write!(f, "<stream:{}>", s.stream_type()),
3354            ValueViewRef::AsyncPromise(_) => write!(f, "<async-promise>"),
3355            ValueViewRef::Channel(_) => write!(f, "<channel>"),
3356            // Length/opaque only: a mutable array or cell can contain itself,
3357            // so printing contents could recurse forever. Freeze with
3358            // `mutable-array/->vector` (or `mutable-cell/get`) to inspect.
3359            ValueViewRef::MutableArray(a) => match a.items.try_borrow() {
3360                Ok(items) => write!(f, "<mutable-array {}>", items.len()),
3361                Err(_) => write!(f, "<mutable-array (borrowed)>"),
3362            },
3363            ValueViewRef::MutableCell(_) => write!(f, "<mutable-cell>"),
3364        })
3365    }
3366}
3367
3368// ── Pretty-print ──────────────────────────────────────────────────
3369
3370/// Pretty-print a value with line breaks and indentation when the compact
3371/// representation exceeds `max_width` columns.  Small values that fit in
3372/// one line are returned in the normal compact format.
3373pub fn pretty_print(value: &Value, max_width: usize) -> String {
3374    let mut buf = String::new();
3375    pp_value(value, 0, max_width, &mut buf);
3376    buf
3377}
3378
3379/// Render `value` into `buf` at the given `indent` level.  If the compact
3380/// form fits in `max_width - indent` columns we use it; otherwise we break
3381/// the container across multiple lines.
3382fn pp_value(value: &Value, indent: usize, max_width: usize, buf: &mut String) {
3383    let compact = format!("{value}");
3384    let remaining = max_width.saturating_sub(indent);
3385    if compact.len() <= remaining {
3386        buf.push_str(&compact);
3387        return;
3388    }
3389
3390    // Grow the stack on demand so pretty-printing a deeply nested value can't
3391    // overflow the OS thread stack and abort the process.
3392    crate::stack::maybe_grow(|| match value.view_ref() {
3393        ValueViewRef::List(items) => {
3394            pp_seq(items.iter(), '(', ')', indent, max_width, buf);
3395        }
3396        ValueViewRef::Vector(items) => {
3397            pp_seq(items.iter(), '[', ']', indent, max_width, buf);
3398        }
3399        ValueViewRef::Map(map) => {
3400            pp_map(
3401                map.iter().map(|(k, v)| (k.clone(), v.clone())),
3402                indent,
3403                max_width,
3404                buf,
3405            );
3406        }
3407        ValueViewRef::HashMap(map) => {
3408            let mut entries: Vec<_> = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
3409            entries.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
3410            pp_map(entries.into_iter(), indent, max_width, buf);
3411        }
3412        _ => buf.push_str(&compact),
3413    })
3414}
3415
3416/// Pretty-print a list or vector.
3417fn pp_seq<'a>(
3418    items: impl Iterator<Item = &'a Value>,
3419    open: char,
3420    close: char,
3421    indent: usize,
3422    max_width: usize,
3423    buf: &mut String,
3424) {
3425    buf.push(open);
3426    let child_indent = indent + 1;
3427    let pad = " ".repeat(child_indent);
3428    for (i, item) in items.enumerate() {
3429        if i > 0 {
3430            buf.push('\n');
3431            buf.push_str(&pad);
3432        }
3433        pp_value(item, child_indent, max_width, buf);
3434    }
3435    buf.push(close);
3436}
3437
3438/// Pretty-print a map (BTreeMap or HashMap).
3439fn pp_map(
3440    entries: impl Iterator<Item = (Value, Value)>,
3441    indent: usize,
3442    max_width: usize,
3443    buf: &mut String,
3444) {
3445    buf.push('{');
3446    let child_indent = indent + 1;
3447    let pad = " ".repeat(child_indent);
3448    for (i, (k, v)) in entries.enumerate() {
3449        if i > 0 {
3450            buf.push('\n');
3451            buf.push_str(&pad);
3452        }
3453        // Key is always compact
3454        let key_str = format!("{k}");
3455        buf.push_str(&key_str);
3456
3457        // Check if the value fits inline after the key
3458        let inline_indent = child_indent + key_str.len() + 1;
3459        let compact_val = format!("{v}");
3460        let remaining = max_width.saturating_sub(inline_indent);
3461
3462        if compact_val.len() <= remaining {
3463            // Fits inline
3464            buf.push(' ');
3465            buf.push_str(&compact_val);
3466        } else if is_compound(&v) {
3467            // Complex value: break to next line indented 2 from key
3468            let nested_indent = child_indent + 2;
3469            let nested_pad = " ".repeat(nested_indent);
3470            buf.push('\n');
3471            buf.push_str(&nested_pad);
3472            pp_value(&v, nested_indent, max_width, buf);
3473        } else {
3474            // Simple value that's just long: keep inline
3475            buf.push(' ');
3476            buf.push_str(&compact_val);
3477        }
3478    }
3479    buf.push('}');
3480}
3481
3482/// Check whether a value is a compound container (list, vector, map, hashmap).
3483fn is_compound(value: &Value) -> bool {
3484    matches!(
3485        value.view(),
3486        ValueView::List(_) | ValueView::Vector(_) | ValueView::Map(_) | ValueView::HashMap(_)
3487    )
3488}
3489
3490// ── Debug ─────────────────────────────────────────────────────────
3491
3492impl fmt::Debug for Value {
3493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3494        match self.view() {
3495            ValueView::Nil => write!(f, "Nil"),
3496            ValueView::Bool(b) => write!(f, "Bool({b})"),
3497            ValueView::Int(n) => write!(f, "Int({n})"),
3498            ValueView::BigInt(n) => write!(f, "Int({n})"),
3499            ValueView::Rational(r) => write!(f, "Rational({}/{})", r.numer(), r.denom()),
3500            ValueView::Complex(c) => write!(
3501                f,
3502                "Complex({})",
3503                SemaNumber::Complex(Box::new((*c).clone()))
3504            ),
3505            ValueView::Float(n) => write!(f, "Float({n})"),
3506            ValueView::String(s) => write!(f, "String({:?})", &**s),
3507            ValueView::Symbol(s) => write!(f, "Symbol({})", resolve(s)),
3508            ValueView::Keyword(s) => write!(f, "Keyword({})", resolve(s)),
3509            ValueView::Char(c) => write!(f, "Char({c:?})"),
3510            ValueView::List(items) => write!(f, "List({items:?})"),
3511            ValueView::Vector(items) => write!(f, "Vector({items:?})"),
3512            ValueView::Map(map) => write!(f, "Map({map:?})"),
3513            ValueView::HashMap(map) => write!(f, "HashMap({map:?})"),
3514            ValueView::Lambda(l) => write!(f, "{l:?}"),
3515            ValueView::Macro(m) => write!(f, "{m:?}"),
3516            ValueView::NativeFn(n) => write!(f, "{n:?}"),
3517            ValueView::Prompt(p) => write!(f, "{p:?}"),
3518            ValueView::Message(m) => write!(f, "{m:?}"),
3519            ValueView::Conversation(c) => write!(f, "{c:?}"),
3520            ValueView::ToolDef(t) => write!(f, "{t:?}"),
3521            ValueView::Agent(a) => write!(f, "{a:?}"),
3522            ValueView::Thunk(t) => write!(f, "{t:?}"),
3523            ValueView::Record(r) => write!(f, "{r:?}"),
3524            ValueView::Bytevector(bv) => write!(f, "Bytevector({bv:?})"),
3525            ValueView::F64Array(arr) => write!(f, "F64Array({arr:?})"),
3526            ValueView::I64Array(arr) => write!(f, "I64Array({arr:?})"),
3527            ValueView::MultiMethod(m) => write!(f, "{m:?}"),
3528            ValueView::Stream(s) => write!(f, "Stream({:?})", s.stream_type()),
3529            ValueView::AsyncPromise(p) => write!(f, "{p:?}"),
3530            ValueView::Channel(c) => write!(f, "{c:?}"),
3531            ValueView::MutableArray(a) => write!(f, "{a:?}"),
3532            ValueView::MutableCell(c) => write!(f, "{c:?}"),
3533        }
3534    }
3535}
3536
3537// ── Env ───────────────────────────────────────────────────────────
3538
3539/// A Sema environment: a chain of scopes with bindings.
3540#[derive(Debug, Clone)]
3541pub struct Env {
3542    pub bindings: Rc<RefCell<SpurMap<Spur, Value>>>,
3543    pub parent: Option<Rc<Env>>,
3544    /// Monotonic mutation counter the VM's inline global cache is keyed on.
3545    /// Held behind an `Rc` so it travels *with* `bindings`: every `Env` handle
3546    /// cloned from this one shares this same cell. A mutation through any handle
3547    /// (a `set!` on a `load`ed unit's per-form-VM clone, a different frame's
3548    /// home-globals handle) is therefore observed by a cache entry keyed on any
3549    /// other handle to the same bindings — a fresh cell per clone would let a
3550    /// recursive reader keep serving a value its own handle's `set!` never bumped
3551    /// (issue #82). A distinct bindings map (`new`/`with_parent`) still gets its
3552    /// own cell, since it is a genuinely independent scope.
3553    pub version: Rc<Cell<u64>>,
3554}
3555
3556impl Env {
3557    pub fn new() -> Self {
3558        Env {
3559            bindings: Rc::new(RefCell::new(SpurMap::new())),
3560            parent: None,
3561            version: Rc::new(Cell::new(0)),
3562        }
3563    }
3564
3565    pub fn with_parent(parent: Rc<Env>) -> Self {
3566        Env {
3567            bindings: Rc::new(RefCell::new(SpurMap::new())),
3568            parent: Some(parent),
3569            version: Rc::new(Cell::new(0)),
3570        }
3571    }
3572
3573    /// Bump the version counter shared by every handle to this scope's bindings.
3574    /// The VM's inline global cache is keyed on it, so bumping after any mutation
3575    /// makes every VM observing this scope (through any clone) re-read instead of
3576    /// serving a stale cached value.
3577    pub fn bump_version(&self) {
3578        self.version.set(self.version.get().wrapping_add(1));
3579    }
3580
3581    pub fn get(&self, name: Spur) -> Option<Value> {
3582        if let Some(val) = self.bindings.borrow().get(&name) {
3583            Some(val.clone())
3584        } else if let Some(parent) = &self.parent {
3585            parent.get(name)
3586        } else {
3587            None
3588        }
3589    }
3590
3591    pub fn get_str(&self, name: &str) -> Option<Value> {
3592        self.get(intern(name))
3593    }
3594
3595    pub fn set(&self, name: Spur, val: Value) {
3596        self.bindings.borrow_mut().insert(name, val);
3597        self.bump_version();
3598    }
3599
3600    pub fn set_str(&self, name: &str, val: Value) {
3601        self.set(intern(name), val);
3602    }
3603
3604    /// Update a binding that already exists in the current scope.
3605    pub fn update(&self, name: Spur, val: Value) {
3606        let mut bindings = self.bindings.borrow_mut();
3607        if let Some(entry) = bindings.get_mut(&name) {
3608            *entry = val;
3609        } else {
3610            bindings.insert(name, val);
3611        }
3612        drop(bindings);
3613        self.bump_version();
3614    }
3615
3616    /// Remove and return a binding from the current scope only.
3617    pub fn take(&self, name: Spur) -> Option<Value> {
3618        let result = self.bindings.borrow_mut().remove(&name);
3619        if result.is_some() {
3620            self.bump_version();
3621        }
3622        result
3623    }
3624
3625    /// Remove and return a binding from any scope in the parent chain.
3626    pub fn take_anywhere(&self, name: Spur) -> Option<Value> {
3627        if let Some(val) = self.bindings.borrow_mut().remove(&name) {
3628            self.bump_version();
3629            Some(val)
3630        } else if let Some(parent) = &self.parent {
3631            parent.take_anywhere(name)
3632        } else {
3633            None
3634        }
3635    }
3636
3637    /// Set a variable in the scope where it's defined (for set!).
3638    pub fn set_existing(&self, name: Spur, val: Value) -> bool {
3639        let mut bindings = self.bindings.borrow_mut();
3640        if let Some(entry) = bindings.get_mut(&name) {
3641            *entry = val;
3642            drop(bindings);
3643            self.bump_version();
3644            true
3645        } else {
3646            drop(bindings);
3647            if let Some(parent) = &self.parent {
3648                parent.set_existing(name, val)
3649            } else {
3650                false
3651            }
3652        }
3653    }
3654
3655    /// Collect all bound variable names across all scopes (for suggestions).
3656    pub fn all_names(&self) -> Vec<Spur> {
3657        let mut names: Vec<Spur> = self.bindings.borrow().keys().copied().collect();
3658        if let Some(parent) = &self.parent {
3659            names.extend(parent.all_names());
3660        }
3661        names.sort_unstable();
3662        names.dedup();
3663        names
3664    }
3665
3666    /// Iterate over bindings in the current scope only (not parent scopes).
3667    pub fn iter_bindings(&self, mut f: impl FnMut(Spur, &Value)) {
3668        let bindings = self.bindings.borrow();
3669        for (&spur, value) in bindings.iter() {
3670            f(spur, value);
3671        }
3672    }
3673
3674    /// Get a binding from the current scope only (not parent scopes).
3675    pub fn get_local(&self, name: Spur) -> Option<Value> {
3676        self.bindings.borrow().get(&name).cloned()
3677    }
3678
3679    /// Replace all bindings in the current scope with the given iterator.
3680    /// Used for bulk restore (e.g., undo/rollback).
3681    pub fn replace_bindings(&self, new_bindings: impl IntoIterator<Item = (Spur, Value)>) {
3682        let mut bindings = self.bindings.borrow_mut();
3683        bindings.clear();
3684        for (spur, value) in new_bindings {
3685            bindings.insert(spur, value);
3686        }
3687        drop(bindings);
3688        self.bump_version();
3689    }
3690}
3691
3692impl Default for Env {
3693    fn default() -> Self {
3694        Self::new()
3695    }
3696}
3697
3698// ── Tests ─────────────────────────────────────────────────────────
3699
3700#[cfg(test)]
3701#[allow(clippy::approx_constant)]
3702mod tests {
3703    use super::*;
3704
3705    #[test]
3706    fn test_size_of_value() {
3707        assert_eq!(std::mem::size_of::<Value>(), 8);
3708    }
3709
3710    #[test]
3711    fn rc_header_matches_std_layout() {
3712        // The uniform clone/drop fast path reads the strong count at
3713        // `payload_ptr - RC_HEADER`; pin that offset against a real `Rc` so a
3714        // std `RcBox` layout change fails loudly instead of corrupting memory.
3715        let rc = Rc::new(String::from("layout probe"));
3716        let extra = Rc::clone(&rc);
3717        let raw = Rc::into_raw(rc);
3718        unsafe {
3719            assert_eq!(rc_strong_cell(raw as *const u8).get(), 2);
3720            drop(Rc::from_raw(raw));
3721        }
3722        drop(extra);
3723    }
3724
3725    #[test]
3726    fn drop_mixed_shallow_and_deep_nesting_does_not_double_free_or_leak() {
3727        // A shape that crosses `DROP_DIRECT_RECURSION_BUDGET` (64) well
3728        // before reaching a splice: a 5000-deep nested-list spine (far past
3729        // the budget, forcing the worklist-spill fallback) with a map
3730        // spliced into its middle that itself holds a 100-deep nested list.
3731        // By the depth the map is reached, the outer spine has already
3732        // spilled to the worklist, so the map and its inner list are freed
3733        // by `free_heap_payload` draining the worklist — pushing children
3734        // directly rather than recursing — never re-entering the direct
3735        // recursive path (`free_heap_value`'s depth-tracked branch). Two leaf
3736        // strings are wrapped in `Rc` so their weak counts pin "freed exactly
3737        // once" — a double-free would abort or corrupt the allocator before
3738        // this assertion runs, and a leak would leave the strong count above
3739        // zero.
3740        fn deep_list(depth: usize, leaf: Value) -> Value {
3741            let mut v = leaf;
3742            for _ in 0..depth {
3743                v = Value::list(vec![v]);
3744            }
3745            v
3746        }
3747
3748        let shallow_leaf = Rc::new(String::from("shallow leaf"));
3749        let deep_leaf = Rc::new(String::from("deep leaf"));
3750        let shallow_weak = Rc::downgrade(&shallow_leaf);
3751        let deep_weak = Rc::downgrade(&deep_leaf);
3752
3753        let shallow_value = Value::string_from_rc(shallow_leaf);
3754        let deep_value = Value::string_from_rc(deep_leaf);
3755
3756        // 100-deep list (past the budget on its own) nested inside a map.
3757        let inner_deep_list = deep_list(100, deep_value);
3758        let mut inner_map = BTreeMap::new();
3759        inner_map.insert(Value::keyword("payload"), inner_deep_list);
3760        let map_value = Value::map(inner_map);
3761
3762        // Splice the map into the middle of a 5000-deep spine (2500 levels
3763        // on either side, each well past the budget).
3764        let below = deep_list(2500, map_value);
3765        let spine_bottom = Value::list(vec![shallow_value, below]);
3766        let full = deep_list(2500, spine_bottom);
3767
3768        drop(full);
3769
3770        assert_eq!(
3771            shallow_weak.strong_count(),
3772            0,
3773            "shallow leaf freed exactly once"
3774        );
3775        assert_eq!(deep_weak.strong_count(), 0, "deep leaf freed exactly once");
3776    }
3777
3778    #[test]
3779    fn test_spur_bits_round_trip() {
3780        // spur_to_bits / bits_to_spur must be exact inverses for freshly interned
3781        // keys, and a NaN-boxed symbol/keyword must round-trip to the same Spur.
3782        for s in ["x", "map", "string->symbol", "a-very-long-symbol-name", "λ"] {
3783            let spur = intern(s);
3784            assert_eq!(
3785                bits_to_spur(spur_to_bits(spur)),
3786                spur,
3787                "raw round-trip for {s:?}"
3788            );
3789
3790            let sym = Value::symbol_from_spur(spur);
3791            assert_eq!(sym.as_symbol_spur(), Some(spur), "symbol Value for {s:?}");
3792            assert_eq!(resolve(spur), s);
3793
3794            let kw = Value::keyword_from_spur(spur);
3795            assert_eq!(kw.as_keyword_spur(), Some(spur), "keyword Value for {s:?}");
3796        }
3797    }
3798
3799    #[test]
3800    fn as_index_rejects_negative() {
3801        let e = Value::int(-1).as_index("test").unwrap_err();
3802        assert!(
3803            matches!(e.inner(), SemaError::Eval(_)),
3804            "expected Eval error, got {e:?}"
3805        );
3806        assert!(e.to_string().contains("test"));
3807    }
3808
3809    #[test]
3810    fn as_index_accepts_non_negative() {
3811        assert_eq!(Value::int(0).as_index("test").unwrap(), 0);
3812        assert_eq!(Value::int(5).as_index("test").unwrap(), 5);
3813    }
3814
3815    #[test]
3816    fn as_index_rejects_non_int() {
3817        assert!(Value::string("x").as_index("test").is_err());
3818    }
3819
3820    #[test]
3821    fn test_nil() {
3822        let v = Value::nil();
3823        assert!(v.is_nil());
3824        assert!(!v.is_truthy());
3825        assert_eq!(v.type_name(), "nil");
3826        assert_eq!(format!("{v}"), "nil");
3827    }
3828
3829    #[test]
3830    fn test_bool() {
3831        let t = Value::bool(true);
3832        let f = Value::bool(false);
3833        assert!(t.is_truthy());
3834        assert!(!f.is_truthy());
3835        assert_eq!(t.as_bool(), Some(true));
3836        assert_eq!(f.as_bool(), Some(false));
3837        assert_eq!(format!("{t}"), "#t");
3838        assert_eq!(format!("{f}"), "#f");
3839    }
3840
3841    #[test]
3842    fn test_small_int() {
3843        let v = Value::int(42);
3844        assert_eq!(v.as_int(), Some(42));
3845        assert_eq!(v.type_name(), "int");
3846        assert_eq!(format!("{v}"), "42");
3847
3848        let neg = Value::int(-100);
3849        assert_eq!(neg.as_int(), Some(-100));
3850        assert_eq!(format!("{neg}"), "-100");
3851
3852        let zero = Value::int(0);
3853        assert_eq!(zero.as_int(), Some(0));
3854    }
3855
3856    #[test]
3857    fn test_small_int_boundaries() {
3858        let max = Value::int(SMALL_INT_MAX);
3859        assert_eq!(max.as_int(), Some(SMALL_INT_MAX));
3860
3861        let min = Value::int(SMALL_INT_MIN);
3862        assert_eq!(min.as_int(), Some(SMALL_INT_MIN));
3863    }
3864
3865    #[test]
3866    fn test_big_int() {
3867        let big = Value::int(i64::MAX);
3868        assert_eq!(big.as_int(), Some(i64::MAX));
3869        assert_eq!(big.type_name(), "int");
3870
3871        let big_neg = Value::int(i64::MIN);
3872        assert_eq!(big_neg.as_int(), Some(i64::MIN));
3873
3874        // Just outside small range
3875        let just_over = Value::int(SMALL_INT_MAX + 1);
3876        assert_eq!(just_over.as_int(), Some(SMALL_INT_MAX + 1));
3877    }
3878
3879    #[test]
3880    fn test_float() {
3881        let v = Value::float(3.14);
3882        assert_eq!(v.as_float(), Some(3.14));
3883        assert_eq!(v.type_name(), "float");
3884
3885        let neg = Value::float(-0.5);
3886        assert_eq!(neg.as_float(), Some(-0.5));
3887
3888        let inf = Value::float(f64::INFINITY);
3889        assert_eq!(inf.as_float(), Some(f64::INFINITY));
3890
3891        let neg_inf = Value::float(f64::NEG_INFINITY);
3892        assert_eq!(neg_inf.as_float(), Some(f64::NEG_INFINITY));
3893    }
3894
3895    #[test]
3896    fn test_float_nan() {
3897        let nan = Value::float(f64::NAN);
3898        let f = nan.as_float().unwrap();
3899        assert!(f.is_nan());
3900    }
3901
3902    #[test]
3903    fn test_string() {
3904        let v = Value::string("hello");
3905        assert_eq!(v.as_str(), Some("hello"));
3906        assert_eq!(v.type_name(), "string");
3907        assert_eq!(format!("{v}"), "\"hello\"");
3908    }
3909
3910    #[test]
3911    fn test_symbol() {
3912        let v = Value::symbol("foo");
3913        assert!(v.as_symbol_spur().is_some());
3914        assert_eq!(v.as_symbol(), Some("foo".to_string()));
3915        assert_eq!(v.type_name(), "symbol");
3916        assert_eq!(format!("{v}"), "foo");
3917    }
3918
3919    #[test]
3920    fn test_keyword() {
3921        let v = Value::keyword("bar");
3922        assert!(v.as_keyword_spur().is_some());
3923        assert_eq!(v.as_keyword(), Some("bar".to_string()));
3924        assert_eq!(v.type_name(), "keyword");
3925        assert_eq!(format!("{v}"), ":bar");
3926    }
3927
3928    #[test]
3929    fn test_char() {
3930        let v = Value::char('λ');
3931        assert_eq!(v.as_char(), Some('λ'));
3932        assert_eq!(v.type_name(), "char");
3933    }
3934
3935    #[test]
3936    fn test_list() {
3937        let v = Value::list(vec![Value::int(1), Value::int(2), Value::int(3)]);
3938        assert_eq!(v.as_list().unwrap().len(), 3);
3939        assert_eq!(v.type_name(), "list");
3940        assert_eq!(format!("{v}"), "(1 2 3)");
3941    }
3942
3943    #[test]
3944    fn test_clone_immediate() {
3945        let v = Value::int(42);
3946        let v2 = v.clone();
3947        assert_eq!(v.as_int(), v2.as_int());
3948    }
3949
3950    #[test]
3951    fn test_clone_heap() {
3952        let v = Value::string("hello");
3953        let v2 = v.clone();
3954        assert_eq!(v.as_str(), v2.as_str());
3955        // Both should work after clone
3956        assert_eq!(format!("{v}"), format!("{v2}"));
3957    }
3958
3959    #[test]
3960    fn test_equality() {
3961        assert_eq!(Value::int(42), Value::int(42));
3962        assert_ne!(Value::int(42), Value::int(43));
3963        assert_eq!(Value::nil(), Value::nil());
3964        assert_eq!(Value::bool(true), Value::bool(true));
3965        assert_ne!(Value::bool(true), Value::bool(false));
3966        assert_eq!(Value::string("a"), Value::string("a"));
3967        assert_ne!(Value::string("a"), Value::string("b"));
3968        assert_eq!(Value::symbol("x"), Value::symbol("x"));
3969    }
3970
3971    #[test]
3972    fn record_field_names_do_not_affect_language_semantics() {
3973        use std::collections::hash_map::DefaultHasher;
3974        use std::hash::{Hash, Hasher};
3975
3976        let a = Value::record(Record {
3977            type_tag: intern("point"),
3978            field_names: vec![intern("x"), intern("y")],
3979            fields: vec![Value::int(1), Value::int(2)],
3980        });
3981        let b = Value::record(Record {
3982            type_tag: intern("point"),
3983            field_names: vec![intern("left"), intern("top")],
3984            fields: vec![Value::int(1), Value::int(2)],
3985        });
3986
3987        assert_eq!(a, b);
3988        assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
3989        assert_eq!(format!("{a}"), "#<record point 1 2>");
3990        assert_eq!(format!("{a}"), format!("{b}"));
3991
3992        let mut a_hasher = DefaultHasher::new();
3993        a.hash(&mut a_hasher);
3994        let mut b_hasher = DefaultHasher::new();
3995        b.hash(&mut b_hasher);
3996        assert_eq!(a_hasher.finish(), b_hasher.finish());
3997    }
3998
3999    #[test]
4000    fn test_big_int_equality() {
4001        assert_eq!(Value::int(i64::MAX), Value::int(i64::MAX));
4002        assert_ne!(Value::int(i64::MAX), Value::int(i64::MIN));
4003    }
4004
4005    #[test]
4006    fn test_view_pattern_matching() {
4007        let v = Value::int(42);
4008        match v.view() {
4009            ValueView::Int(n) => assert_eq!(n, 42),
4010            _ => panic!("expected int"),
4011        }
4012
4013        let v = Value::string("hello");
4014        match v.view() {
4015            ValueView::String(s) => assert_eq!(&**s, "hello"),
4016            _ => panic!("expected string"),
4017        }
4018    }
4019
4020    #[test]
4021    fn test_env() {
4022        let env = Env::new();
4023        env.set_str("x", Value::int(42));
4024        assert_eq!(env.get_str("x"), Some(Value::int(42)));
4025    }
4026
4027    #[test]
4028    fn test_native_fn_simple() {
4029        let f = NativeFn::simple("add1", |args| Ok(args[0].clone()));
4030        let ctx = EvalContext::new();
4031        assert!((f.func)(&ctx, &[Value::int(42)]).is_ok());
4032    }
4033
4034    #[test]
4035    fn test_native_fn_with_ctx() {
4036        let f = NativeFn::with_ctx("get-depth", |ctx, _args| {
4037            Ok(Value::int(ctx.eval_depth.get() as i64))
4038        });
4039        let ctx = EvalContext::new();
4040        assert_eq!((f.func)(&ctx, &[]).unwrap(), Value::int(0));
4041    }
4042
4043    #[test]
4044    fn test_drop_doesnt_leak() {
4045        // Create and drop many heap values to check for leaks
4046        for _ in 0..10000 {
4047            let _ = Value::string("test");
4048            let _ = Value::list(vec![Value::int(1), Value::int(2)]);
4049            let _ = Value::int(i64::MAX); // big int
4050        }
4051    }
4052
4053    #[test]
4054    fn test_is_truthy() {
4055        assert!(!Value::nil().is_truthy());
4056        assert!(!Value::bool(false).is_truthy());
4057        assert!(Value::bool(true).is_truthy());
4058        assert!(Value::int(0).is_truthy());
4059        assert!(Value::int(1).is_truthy());
4060        assert!(Value::string("").is_truthy());
4061        assert!(Value::list(vec![]).is_truthy());
4062    }
4063
4064    #[test]
4065    fn test_as_float_from_int() {
4066        assert_eq!(Value::int(42).as_float(), Some(42.0));
4067        assert_eq!(Value::float(3.14).as_float(), Some(3.14));
4068    }
4069
4070    #[test]
4071    fn test_next_gensym_unique() {
4072        let a = next_gensym("x");
4073        let b = next_gensym("x");
4074        let c = next_gensym("y");
4075        assert_ne!(a, b);
4076        assert_ne!(a, c);
4077        assert_ne!(b, c);
4078        assert!(a.starts_with("x__"));
4079        assert!(b.starts_with("x__"));
4080        assert!(c.starts_with("y__"));
4081    }
4082
4083    #[test]
4084    fn test_next_gensym_counter_does_not_panic_near_max() {
4085        // Set counter near u64::MAX and verify no panic on wrapping
4086        GENSYM_COUNTER.with(|c| c.set(u64::MAX - 1));
4087        let a = next_gensym("z");
4088        assert!(a.contains(&(u64::MAX - 1).to_string()));
4089        // This would panic with `val + 1` instead of wrapping_add
4090        let b = next_gensym("z");
4091        assert!(b.contains(&u64::MAX.to_string()));
4092        // Wraps to 0
4093        let c = next_gensym("z");
4094        assert!(c.contains("__0"));
4095    }
4096
4097    // ── StreamBox tests ──────────────────────────────────────────────
4098
4099    #[derive(Debug)]
4100    struct TestStream {
4101        data: RefCell<Vec<u8>>,
4102        readable: bool,
4103        writable: bool,
4104    }
4105
4106    impl TestStream {
4107        fn new(readable: bool, writable: bool) -> Self {
4108            TestStream {
4109                data: RefCell::new(Vec::new()),
4110                readable,
4111                writable,
4112            }
4113        }
4114    }
4115
4116    impl SemaStream for TestStream {
4117        fn read(&self, buf: &mut [u8]) -> Result<usize, SemaError> {
4118            let mut data = self.data.borrow_mut();
4119            let n = buf.len().min(data.len());
4120            buf[..n].copy_from_slice(&data[..n]);
4121            data.drain(..n);
4122            Ok(n)
4123        }
4124
4125        fn write(&self, data: &[u8]) -> Result<usize, SemaError> {
4126            self.data.borrow_mut().extend_from_slice(data);
4127            Ok(data.len())
4128        }
4129
4130        fn flush(&self) -> Result<(), SemaError> {
4131            Ok(())
4132        }
4133
4134        fn close(&self) -> Result<(), SemaError> {
4135            Ok(())
4136        }
4137
4138        fn available(&self) -> Result<bool, SemaError> {
4139            Ok(!self.data.borrow().is_empty())
4140        }
4141
4142        fn is_readable(&self) -> bool {
4143            self.readable
4144        }
4145
4146        fn is_writable(&self) -> bool {
4147            self.writable
4148        }
4149
4150        fn stream_type(&self) -> &'static str {
4151            "test"
4152        }
4153
4154        fn as_any(&self) -> &dyn std::any::Any {
4155            self
4156        }
4157    }
4158
4159    #[test]
4160    fn streambox_read_writes_data() {
4161        let sb = StreamBox::new(TestStream::new(true, true));
4162        sb.write(b"hello").unwrap();
4163        let mut buf = [0u8; 5];
4164        let n = sb.read(&mut buf).unwrap();
4165        assert_eq!(n, 5);
4166        assert_eq!(&buf, b"hello");
4167    }
4168
4169    #[test]
4170    fn streambox_close_prevents_read() {
4171        let sb = StreamBox::new(TestStream::new(true, true));
4172        sb.close().unwrap();
4173        let mut buf = [0u8; 5];
4174        let err = sb.read(&mut buf).unwrap_err();
4175        assert!(err.to_string().contains("closed"));
4176    }
4177
4178    #[test]
4179    fn streambox_close_prevents_write() {
4180        let sb = StreamBox::new(TestStream::new(true, true));
4181        sb.close().unwrap();
4182        let err = sb.write(b"data").unwrap_err();
4183        assert!(err.to_string().contains("closed"));
4184    }
4185
4186    #[test]
4187    fn streambox_close_prevents_flush() {
4188        let sb = StreamBox::new(TestStream::new(true, true));
4189        sb.close().unwrap();
4190        let err = sb.flush().unwrap_err();
4191        assert!(err.to_string().contains("closed"));
4192    }
4193
4194    #[test]
4195    fn streambox_double_close_is_noop() {
4196        let sb = StreamBox::new(TestStream::new(true, true));
4197        sb.close().unwrap();
4198        sb.close().unwrap(); // second close should be Ok
4199    }
4200
4201    #[test]
4202    fn streambox_is_closed() {
4203        let sb = StreamBox::new(TestStream::new(true, true));
4204        assert!(!sb.is_closed());
4205        sb.close().unwrap();
4206        assert!(sb.is_closed());
4207    }
4208
4209    #[test]
4210    fn streambox_is_readable() {
4211        let sb = StreamBox::new(TestStream::new(true, false));
4212        assert!(sb.is_readable());
4213        sb.close().unwrap();
4214        assert!(!sb.is_readable());
4215    }
4216
4217    #[test]
4218    fn streambox_is_writable() {
4219        let sb = StreamBox::new(TestStream::new(false, true));
4220        assert!(sb.is_writable());
4221        sb.close().unwrap();
4222        assert!(!sb.is_writable());
4223    }
4224
4225    #[test]
4226    fn streambox_available_when_closed() {
4227        let sb = StreamBox::new(TestStream::new(true, true));
4228        sb.close().unwrap();
4229        assert!(!sb.available().unwrap());
4230    }
4231
4232    #[test]
4233    fn streambox_stream_type() {
4234        let sb = StreamBox::new(TestStream::new(true, true));
4235        assert_eq!(sb.stream_type(), "test");
4236    }
4237
4238    #[test]
4239    fn bigint_roundtrip_and_normalize() {
4240        use num_bigint::BigInt;
4241        use std::str::FromStr;
4242        // A value beyond i64 stays a bignum and prints exactly.
4243        let big = BigInt::from_str("170141183460469231731687303715884105728").unwrap();
4244        let v = Value::from_bigint(big.clone());
4245        assert!(v.is_bigint());
4246        assert_eq!(v.to_string(), "170141183460469231731687303715884105728");
4247        assert_eq!(v.type_name(), "int");
4248        assert_eq!(v.as_int(), None); // does not fit i64
4249        assert_eq!(v.as_bigint(), Some(big));
4250        // A bignum that fits i64 normalizes back to a fixnum.
4251        let small = Value::from_bigint(BigInt::from(42));
4252        assert!(!small.is_bigint());
4253        assert_eq!(small.as_int(), Some(42));
4254        // Clone/Drop refcount safety.
4255        let v2 = v.clone();
4256        assert_eq!(v, v2);
4257    }
4258
4259    #[test]
4260    fn rational_roundtrip_and_normalize() {
4261        use num_bigint::BigInt;
4262        use num_rational::BigRational;
4263        use num_traits::One;
4264        let third = Value::rational(BigRational::new(BigInt::one(), BigInt::from(3)));
4265        assert!(third.is_rational());
4266        assert_eq!(third.to_string(), "1/3");
4267        assert_eq!(third.type_name(), "rational");
4268        // 6/3 normalizes to the integer 2
4269        let two = Value::rational(BigRational::new(BigInt::from(6), BigInt::from(3)));
4270        assert!(!two.is_rational());
4271        assert_eq!(two.as_int(), Some(2));
4272        assert_eq!(third.clone(), third);
4273    }
4274
4275    #[test]
4276    fn complex_roundtrip_and_normalize() {
4277        use crate::number::SemaNumber;
4278        let n = |v: i64| SemaNumber::from_i64(v);
4279        let c = Value::complex(n(3), n(4));
4280        assert!(c.is_complex());
4281        assert_eq!(c.to_string(), "3+4i");
4282        assert_eq!(c.type_name(), "complex");
4283        let comp = c.as_complex().unwrap();
4284        assert_eq!(comp.re, n(3));
4285        assert_eq!(comp.im, n(4));
4286        // Structural equality/clone/drop refcount safety.
4287        let c2 = c.clone();
4288        assert_eq!(c, c2);
4289        // Exact-zero imaginary part normalizes down to the real part alone.
4290        let real = Value::complex(n(5), n(0));
4291        assert!(!real.is_complex());
4292        assert_eq!(real.as_int(), Some(5));
4293    }
4294}