Skip to main content

harn_vm/value/
core.rs

1use std::collections::HashMap;
2use std::sync::atomic::Ordering;
3use std::sync::Arc;
4use std::{future::Future, pin::Pin};
5
6use crate::harness::VmHarness;
7use crate::mcp::VmMcpClientHandle;
8use crate::BuiltinId;
9
10use super::{
11    VmAtomicHandle, VmChannelHandle, VmClosure, VmError, VmGenerator, VmRange,
12    VmResourceGuardHandle, VmRngHandle, VmSet, VmStream, VmSyncPermitHandle, VmVerdictReceipt,
13};
14
15/// An async builtin function for the VM.
16///
17/// Receives an explicit [`crate::vm::AsyncBuiltinCtx`] handle (threaded by the
18/// dispatch loop + the `#[harn_builtin]` macro) so handlers mint child VMs and
19/// forward output through the ctx they were given instead of relying on hidden
20/// task state.
21pub type VmAsyncBuiltinFn = Arc<
22    dyn Fn(
23            crate::vm::AsyncBuiltinCtx,
24            Vec<VmValue>,
25        ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + Send>>
26        + Send
27        + Sync,
28>;
29
30type Shared<T> = Arc<T>;
31
32/// Thin, reference-counted, immutable UTF-8 string used by every string-shaped
33/// [`VmValue`] variant (`String`, `BuiltinRef`, `TaskHandle`).
34///
35/// Unlike `Arc<str>` — whose fat pointer (data ptr + length) is 16 bytes and
36/// set the whole-enum size floor — [`arcstr::ArcStr`] is a single word: the
37/// length lives in the heap allocation alongside the refcount and bytes. That
38/// is what lets `VmValue` shrink to 16 bytes (paired with boxing the other
39/// oversized payloads). Cloning is a refcount bump, identical to `Arc<str>`;
40/// the unsafe pointer arithmetic is encapsulated and fuzzed inside the vetted
41/// `arcstr` crate, so the VM carries no hand-rolled unsafe for this.
42pub type HarnStr = arcstr::ArcStr;
43
44/// Backing store for [`VmValue::Dict`]: a persistent, ordered, structurally
45/// shared map.
46///
47/// Replacing the former `BTreeMap` with `imbl::OrdMap` turns the copy-on-write
48/// `Arc::make_mut` clone — performed on every dict mutation whenever the value
49/// is aliased (on the stack, in another local, captured by a closure) — from an
50/// O(n) deep copy of every key and entry into an O(log n) path copy. Ordering
51/// and the read API (`get` / `iter` / `keys` / `values` / `contains_key` /
52/// `range` / `len`) match `BTreeMap`, so dict reads are unchanged. The `Arc`
53/// wrapper is retained so reference identity (`Arc::ptr_eq`) — used by the `===`
54/// operator and `value_identity_key` — keeps its current semantics.
55pub type DictMap = imbl::OrdMap<HarnStr, VmValue>;
56
57/// Intern a dict key into a shared [`HarnStr`].
58///
59/// Agent workloads are dict-heavy and the same field names (`role`, `content`,
60/// `arguments`, …) recur across thousands of message/JSON dicts. Interning
61/// short keys lets every occurrence share one allocation (a refcount bump on
62/// reuse) instead of allocating a fresh string per key. The table is *bounded*
63/// — only keys up to [`MAX_INTERNED_KEY_LEN`] bytes are eligible, and once
64/// [`MAX_INTERNED_KEYS`] distinct keys are cached no new entries are added — so
65/// adversarial or high-cardinality keys (UUIDs, user input) fall back to a
66/// plain allocation and can never grow the table without bound.
67pub fn intern_key(key: &str) -> HarnStr {
68    const MAX_INTERNED_KEY_LEN: usize = 64;
69    const MAX_INTERNED_KEYS: usize = 8192;
70    static INTERNED_KEYS: std::sync::LazyLock<parking_lot::Mutex<HashMap<Box<str>, HarnStr>>> =
71        std::sync::LazyLock::new(|| parking_lot::Mutex::new(HashMap::new()));
72
73    if key.len() > MAX_INTERNED_KEY_LEN {
74        return HarnStr::from(key);
75    }
76    let mut table = INTERNED_KEYS.lock();
77    if let Some(existing) = table.get(key) {
78        return existing.clone();
79    }
80    let interned = HarnStr::from(key);
81    if table.len() < MAX_INTERNED_KEYS {
82        table.insert(Box::from(key), interned.clone());
83    }
84    interned
85}
86
87/// Conversion into an interned dict key.
88///
89/// Lets [`VmValue::dict`] accept the maps callers already build —
90/// `BTreeMap<String, _>` and the persistent [`DictMap`] (`OrdMap<HarnStr, _>`) —
91/// while routing freshly-owned string keys through [`intern_key`] and passing an
92/// already-shared [`HarnStr`] (e.g. from re-wrapping an existing dict) straight
93/// through without re-interning.
94pub trait IntoDictKey {
95    fn into_dict_key(self) -> HarnStr;
96}
97
98impl IntoDictKey for String {
99    fn into_dict_key(self) -> HarnStr {
100        intern_key(&self)
101    }
102}
103
104impl IntoDictKey for &str {
105    fn into_dict_key(self) -> HarnStr {
106        intern_key(self)
107    }
108}
109
110impl IntoDictKey for HarnStr {
111    fn into_dict_key(self) -> HarnStr {
112        self
113    }
114}
115
116/// Character count with a byte-length fast path for ASCII text.
117///
118/// Harn exposes string lengths as Unicode scalar counts. ASCII is one byte per
119/// scalar, so cached string `count` / `len` paths can avoid a full iterator
120/// scan without changing behavior for non-ASCII text.
121pub fn string_char_count(text: &str) -> usize {
122    if text.is_ascii() {
123        text.len()
124    } else {
125        text.chars().count()
126    }
127}
128
129/// Byte offset of char index `char_index`, clamped to the end of `text`.
130///
131/// Harn indexes strings by Unicode scalar, but every underlying operation is
132/// byte-addressed. Converting once, in a single forward pass, is what keeps
133/// char-indexed string methods linear: the natural spelling
134/// (`text.chars().nth(i)`, or collecting a `Vec<char>` to subscript it) is
135/// O(i) or O(n) *per call*, so a scan loop over a string becomes quadratic.
136pub fn char_to_byte_offset(text: &str, char_index: usize) -> usize {
137    if text.is_ascii() {
138        return char_index.min(text.len());
139    }
140    text.char_indices()
141        .nth(char_index)
142        .map(|(offset, _)| offset)
143        .unwrap_or(text.len())
144}
145
146/// Byte range for the half-open char range `[start, end)`, clamped to `text`.
147///
148/// One forward pass for both ends, so slicing costs the distance walked rather
149/// than the length of the whole string, and nothing is allocated on the way.
150pub fn char_range_to_byte_range(text: &str, start: usize, end: usize) -> (usize, usize) {
151    let end = end.max(start);
152    if text.is_ascii() {
153        let len = text.len();
154        return (start.min(len), end.min(len));
155    }
156    // A char index past the end of the string resolves to the end of the
157    // string, which is what leaves both ends clamped without a length probe.
158    let mut start_byte = text.len();
159    let mut end_byte = text.len();
160    for (char_index, (offset, _)) in text.char_indices().enumerate() {
161        if char_index == start {
162            start_byte = offset;
163        }
164        if char_index == end {
165            end_byte = offset;
166            break;
167        }
168    }
169    (start_byte, end_byte.max(start_byte))
170}
171
172/// Char index of the char that begins at `byte_offset`.
173///
174/// Byte-addressed searches (`str::find`) report byte offsets; Harn reports char
175/// indices. ASCII text needs no conversion at all, which is the case that
176/// matters for model output.
177pub fn byte_offset_to_char_index(text: &str, byte_offset: usize) -> usize {
178    if text.is_ascii() {
179        return byte_offset.min(text.len());
180    }
181    text[..byte_offset.min(text.len())].chars().count()
182}
183
184/// Indexed runtime layout for a Harn struct instance.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct StructLayout {
187    struct_name: String,
188    field_names: Vec<String>,
189    field_indexes: HashMap<String, usize>,
190}
191
192impl StructLayout {
193    pub fn new(struct_name: impl Into<String>, field_names: Vec<String>) -> Self {
194        let mut deduped = Vec::with_capacity(field_names.len());
195        let mut field_indexes = HashMap::with_capacity(field_names.len());
196        for field_name in field_names {
197            if field_indexes.contains_key(&field_name) {
198                continue;
199            }
200            let index = deduped.len();
201            field_indexes.insert(field_name.clone(), index);
202            deduped.push(field_name);
203        }
204
205        Self {
206            struct_name: struct_name.into(),
207            field_names: deduped,
208            field_indexes,
209        }
210    }
211
212    pub fn from_map(struct_name: impl Into<String>, fields: &crate::value::DictMap) -> Self {
213        Self::new(
214            struct_name,
215            fields.keys().map(|key| key.to_string()).collect(),
216        )
217    }
218
219    pub fn struct_name(&self) -> &str {
220        &self.struct_name
221    }
222
223    pub fn field_names(&self) -> &[String] {
224        &self.field_names
225    }
226
227    pub fn field_index(&self, field_name: &str) -> Option<usize> {
228        if self.field_names.len() <= 8 {
229            return self
230                .field_names
231                .iter()
232                .position(|candidate| candidate == field_name);
233        }
234        self.field_indexes.get(field_name).copied()
235    }
236
237    pub fn with_appended_field(&self, field_name: String) -> Self {
238        if self.field_indexes.contains_key(&field_name) {
239            return self.clone();
240        }
241        let mut field_names = self.field_names.clone();
242        field_names.push(field_name);
243        Self::new(self.struct_name.clone(), field_names)
244    }
245}
246
247/// Runtime payload for a Harn enum variant.
248#[derive(Debug, Clone)]
249pub struct VmEnumVariant {
250    pub enum_name: HarnStr,
251    pub variant: HarnStr,
252    pub fields: Shared<Vec<VmValue>>,
253}
254
255impl VmEnumVariant {
256    pub fn has_enum_name(&self, enum_name: &str) -> bool {
257        self.enum_name.as_str() == enum_name
258    }
259
260    pub fn is_variant(&self, enum_name: &str, variant: &str) -> bool {
261        self.has_enum_name(enum_name) && self.variant.as_str() == variant
262    }
263}
264
265/// Boxed payload for [`VmValue::BuiltinRefId`].
266///
267/// Pairs the compact [`BuiltinId`] used for direct dispatch with the builtin's
268/// registered name (kept for policy checks, diagnostics, and name-keyed
269/// fallback). Stored behind a `Shared` pointer in the value so the `{ id, name
270/// }` pair does not widen every `VmValue` to its 24-byte footprint.
271#[derive(Debug, Clone)]
272pub struct VmBuiltinRefId {
273    pub id: BuiltinId,
274    pub name: HarnStr,
275}
276
277/// Runtime layout + slots for a [`VmValue::StructInstance`].
278///
279/// Boxed behind a single `Shared` pointer so the `{ layout, fields }` pair —
280/// two pointers, 16 bytes inline — does not set the whole-enum size. Cloning a
281/// struct value is then a single refcount bump, and the variant fits in one
282/// word like every other compound payload.
283#[derive(Debug, Clone)]
284pub struct StructInstanceData {
285    pub layout: Shared<StructLayout>,
286    pub fields: Shared<Vec<Option<VmValue>>>,
287}
288
289/// VM runtime value.
290///
291/// Rare compound payloads use shared pointers so stack/local-slot traffic is
292/// bounded by the common scalar and pointer-sized value shapes. Every variant
293/// is held to a single machine word (8 bytes): the oversized payloads —
294/// `Range` (a 24-byte triple), `BuiltinRefId` (id + name), `Decimal` (16-byte
295/// base-10 mantissa), and `StructInstance` (two pointers) — are boxed behind a
296/// `Shared` pointer, and the string-shaped variants use the thin-pointer
297/// [`HarnStr`] instead of a 16-byte `Arc<str>` fat pointer. That keeps
298/// `VmValue` at 16 bytes (down from 24, and 32 before that) without inflating
299/// the common `Int` / `Float` / `List` / `Dict` / `String` shapes the
300/// interpreter moves on every push, pop, clone, and local-slot write. Unsafe
301/// layouts such as NaN boxing or tagged pointers remain deferred; the thin
302/// string's unsafe is encapsulated in the vetted `arcstr` crate.
303#[derive(Debug, Clone)]
304pub enum VmValue {
305    Int(i64),
306    Float(f64),
307    /// Exact base-10 decimal (96-bit mantissa, up to 28–29 significant digits)
308    /// for money and other values where binary float rounding is unacceptable.
309    /// Boxed behind a `Shared` pointer (`rust_decimal::Decimal` is 16 bytes, so
310    /// inlining it would set the whole-enum size); cloning is a refcount bump.
311    /// Constructed via the `decimal(value)` builtin; it is a distinct type from
312    /// `Int`/`Float` for equality/ordering/hashing (a clean island) but
313    /// promotes `Int` operands exactly in arithmetic. See `docs/src/decimal.md`.
314    Decimal(Shared<rust_decimal::Decimal>),
315    String(HarnStr),
316    Bytes(Shared<Vec<u8>>),
317    Bool(bool),
318    Nil,
319    List(Shared<Vec<VmValue>>),
320    Dict(Shared<DictMap>),
321    Closure(Shared<VmClosure>),
322    /// Reference to a registered builtin function, used when a builtin name is
323    /// referenced as a value (e.g. `snake_dict.rekeyed(snake_to_camel)`). The
324    /// contained string is the builtin's registered name.
325    BuiltinRef(HarnStr),
326    /// Compact builtin reference for callback positions. The boxed
327    /// [`VmBuiltinRefId`] carries the id plus the name for policy,
328    /// diagnostics, and fallback if the ID cannot be used. Boxed so the
329    /// `{ id, name }` pair does not widen every `VmValue`.
330    BuiltinRefId(Shared<VmBuiltinRefId>),
331    Duration(i64),
332    EnumVariant(Shared<VmEnumVariant>),
333    StructInstance(Shared<StructInstanceData>),
334    TaskHandle(HarnStr),
335    Channel(Shared<VmChannelHandle>),
336    Atomic(Shared<VmAtomicHandle>),
337    Rng(Shared<VmRngHandle>),
338    SyncPermit(Shared<VmSyncPermitHandle>),
339    ResourceGuard(Shared<VmResourceGuardHandle>),
340    McpClient(Shared<VmMcpClientHandle>),
341    /// A host-minted proof-of-execution receipt — the payload of a positive
342    /// `Verdict`. Constructed ONLY by the verdict issuance capability after the
343    /// host validated a real evidence artifact; no `.harn` code can build it.
344    VerdictReceipt(Shared<VmVerdictReceipt>),
345    Set(Shared<VmSet>),
346    Generator(Shared<VmGenerator>),
347    Stream(Shared<VmStream>),
348    /// Lazy numeric range. Boxed behind a `Shared` pointer so its 24-byte
349    /// `start/end/inclusive` payload does not set the whole-enum size; cloning
350    /// a range value is then a refcount bump.
351    Range(Shared<VmRange>),
352    /// Lazy iterator handle. Single-pass, fused. See `crate::vm::iter::VmIter`.
353    Iter(crate::vm::iter::VmIterHandle),
354    /// Two-element pair value. Produced by `pair(a, b)`, yielded by the
355    /// Dict iterator source, and (later) by `zip` / `enumerate` combinators.
356    /// Accessed via `.first` / `.second`, and destructurable in
357    /// `for (a, b) in ...` loops.
358    Pair(Shared<(VmValue, VmValue)>),
359    /// Capability handle threaded into `main(harness: Harness)`. The same
360    /// variant carries the root handle and each typed sub-handle (`stdio`,
361    /// `clock`, `fs`, `env`, `random`, `net`) so they share one value shape
362    /// but stay distinguishable via `VmHarness::kind`.
363    Harness(Shared<VmHarness>),
364}
365
366/// Process-wide interned `Arc<str>` for every single-byte ASCII character.
367///
368/// Materializing source text into per-character string values — the supported
369/// idiom for cursor-style scanners (`chars`, `char_at`, `s[i]`) — would
370/// otherwise heap-allocate once per character. Source files are overwhelmingly
371/// ASCII, so interning the 128 single-char strings lets those paths clone a
372/// cheap `Arc` (a refcount bump) instead of allocating, keeping a full-file
373/// scan linear with a low constant factor.
374static ASCII_CHAR_STRINGS: std::sync::LazyLock<[HarnStr; 128]> = std::sync::LazyLock::new(|| {
375    std::array::from_fn(|byte| {
376        let mut buffer = [0u8; 4];
377        HarnStr::from((byte as u8 as char).encode_utf8(&mut buffer))
378    })
379});
380
381impl VmValue {
382    /// Canonical `VmValue::String` constructor from anything string-like.
383    ///
384    /// Collapses the ubiquitous `VmValue::String(arcstr::ArcStr::from(..))`
385    /// spelling to a single call and performs exactly one allocation via
386    /// `Arc::<str>::from(&str)` regardless of whether the input is a `&str`,
387    /// `String`, `&String`, or `Cow<str>`. Prefer this over hand-writing the
388    /// `Arc::from` at call sites.
389    pub fn string(value: impl AsRef<str>) -> Self {
390        VmValue::String(HarnStr::from(value.as_ref()))
391    }
392
393    /// Canonical `VmValue::Decimal` constructor.
394    ///
395    /// Boxes the 16-byte [`rust_decimal::Decimal`] behind a `Shared` pointer so
396    /// the value stays one word wide; see [`VmValue::Decimal`].
397    pub fn decimal(value: rust_decimal::Decimal) -> Self {
398        VmValue::Decimal(Shared::new(value))
399    }
400
401    /// Builds a `VmValue::String` holding a single character, reusing the
402    /// interned ASCII table (see [`ASCII_CHAR_STRINGS`]) so the common ASCII
403    /// path does not allocate.
404    pub fn char_value(ch: char) -> Self {
405        if ch.is_ascii() {
406            return VmValue::String(ASCII_CHAR_STRINGS[ch as usize].clone());
407        }
408        let mut buffer = [0u8; 4];
409        VmValue::String(HarnStr::from(ch.encode_utf8(&mut buffer)))
410    }
411
412    /// Materializes a string into a `VmValue::List` of single-character string
413    /// values in one linear pass. Backs both the `chars` builtin and the
414    /// `.chars()` method, and is the cursor-scanner-friendly counterpart to the
415    /// O(n)-per-call `substring` / slice / `s[i]` operations on a `string`.
416    pub fn chars_list(text: &str) -> Self {
417        VmValue::List(Shared::new(text.chars().map(VmValue::char_value).collect()))
418    }
419
420    pub fn enum_variant(
421        enum_name: impl Into<HarnStr>,
422        variant: impl Into<HarnStr>,
423        fields: Vec<VmValue>,
424    ) -> Self {
425        VmValue::EnumVariant(Shared::new(VmEnumVariant {
426            enum_name: enum_name.into(),
427            variant: variant.into(),
428            fields: Shared::new(fields),
429        }))
430    }
431
432    pub fn task_handle(id: impl Into<HarnStr>) -> Self {
433        VmValue::TaskHandle(id.into())
434    }
435
436    /// Construct a boxed [`VmValue::Range`] from a [`VmRange`].
437    pub fn range(range: VmRange) -> Self {
438        VmValue::Range(Shared::new(range))
439    }
440
441    /// Construct a boxed [`VmValue::BuiltinRefId`] from its id and name.
442    pub fn builtin_ref_id(id: BuiltinId, name: impl Into<HarnStr>) -> Self {
443        VmValue::BuiltinRefId(Shared::new(VmBuiltinRefId {
444            id,
445            name: name.into(),
446        }))
447    }
448
449    /// Construct a [`VmValue::Dict`] from any iterator of `(key, value)`
450    /// entries. Accepts the `BTreeMap` that most builders still assemble (it is
451    /// `IntoIterator<Item = (String, VmValue)>`) and collects it into the
452    /// persistent [`DictMap`], so callers keep their familiar map-building code
453    /// while the stored value gains structural sharing.
454    pub fn dict<K: IntoDictKey>(entries: impl IntoIterator<Item = (K, VmValue)>) -> Self {
455        VmValue::Dict(Shared::new(
456            entries
457                .into_iter()
458                .map(|(k, v)| (k.into_dict_key(), v))
459                .collect::<DictMap>(),
460        ))
461    }
462
463    /// Construct a [`VmValue::Dict`] from an already-built [`DictMap`].
464    pub fn dict_map(map: DictMap) -> Self {
465        VmValue::Dict(Shared::new(map))
466    }
467
468    /// Construct a [`VmValue::Set`] from any iterator of values, deduplicating
469    /// by structural equality and preserving first-seen insertion order.
470    pub fn set(values: impl IntoIterator<Item = VmValue>) -> Self {
471        VmValue::Set(Shared::new(values.into_iter().collect::<VmSet>()))
472    }
473
474    /// Construct a [`VmValue::Set`] from an already-built [`VmSet`].
475    pub fn set_value(set: VmSet) -> Self {
476        VmValue::Set(Shared::new(set))
477    }
478
479    pub fn channel(handle: VmChannelHandle) -> Self {
480        VmValue::Channel(Shared::new(handle))
481    }
482
483    pub fn atomic(handle: VmAtomicHandle) -> Self {
484        VmValue::Atomic(Shared::new(handle))
485    }
486
487    pub fn rng(handle: VmRngHandle) -> Self {
488        VmValue::Rng(Shared::new(handle))
489    }
490
491    pub fn sync_permit(handle: VmSyncPermitHandle) -> Self {
492        VmValue::SyncPermit(Shared::new(handle))
493    }
494
495    pub fn resource_guard(handle: VmResourceGuardHandle) -> Self {
496        VmValue::ResourceGuard(Shared::new(handle))
497    }
498
499    pub fn mcp_client(handle: VmMcpClientHandle) -> Self {
500        VmValue::McpClient(Shared::new(handle))
501    }
502
503    /// Mint a verdict receipt value. Intentionally the ONLY constructor, and it
504    /// is called only from the verdict issuance capability (`harness.verdict`)
505    /// after host validation — never from a `.harn`-reachable builtin.
506    pub fn verdict_receipt(receipt: VmVerdictReceipt) -> Self {
507        VmValue::VerdictReceipt(Shared::new(receipt))
508    }
509
510    pub fn generator(generator: VmGenerator) -> Self {
511        VmValue::Generator(Shared::new(generator))
512    }
513
514    pub fn stream(stream: VmStream) -> Self {
515        VmValue::Stream(Shared::new(stream))
516    }
517
518    pub fn harness(handle: VmHarness) -> Self {
519        VmValue::Harness(Shared::new(handle))
520    }
521
522    pub fn struct_instance(
523        struct_name: impl Into<Shared<str>>,
524        fields: crate::value::DictMap,
525    ) -> Self {
526        Self::struct_instance_from_map(struct_name.into().to_string(), fields)
527    }
528
529    pub fn is_truthy(&self) -> bool {
530        match self {
531            VmValue::Bool(b) => *b,
532            VmValue::Nil => false,
533            VmValue::Int(n) => *n != 0,
534            VmValue::Float(n) => *n != 0.0,
535            VmValue::Decimal(d) => **d != rust_decimal::Decimal::ZERO,
536            VmValue::String(s) => !s.is_empty(),
537            VmValue::Bytes(bytes) => !bytes.is_empty(),
538            VmValue::List(l) => !l.is_empty(),
539            VmValue::Dict(d) => !d.is_empty(),
540            VmValue::Closure(_) => true,
541            VmValue::BuiltinRef(_) => true,
542            VmValue::BuiltinRefId(_) => true,
543            VmValue::Duration(ms) => *ms != 0,
544            VmValue::EnumVariant(_) => true,
545            VmValue::StructInstance(_) => true,
546            VmValue::TaskHandle(_) => true,
547            VmValue::Channel(_) => true,
548            VmValue::Atomic(_) => true,
549            VmValue::Rng(_) => true,
550            VmValue::SyncPermit(_) => true,
551            VmValue::ResourceGuard(_) => true,
552            VmValue::McpClient(_) => true,
553            VmValue::VerdictReceipt(_) => true,
554            VmValue::Set(s) => !s.is_empty(),
555            VmValue::Generator(_) => true,
556            VmValue::Stream(_) => true,
557            // Match Python semantics: range objects are always truthy,
558            // even the empty range (analogous to generators / iterators).
559            VmValue::Range(_) => true,
560            VmValue::Iter(_) => true,
561            VmValue::Pair(_) => true,
562            VmValue::Harness(_) => true,
563        }
564    }
565
566    /// Every tag [`VmValue::type_name`] can return, excluding harness-object
567    /// names (delegated to `HarnessValue::type_name`). Keep in lockstep with
568    /// the match below AND with `harn_builtin_meta::runtime_type_tags::ALL`
569    /// — a unit test asserts the latter, which is what keeps the
570    /// typechecker's `type_of` narrowing honest.
571    pub const ALL_TYPE_NAMES: &'static [&'static str] = &[
572        "string",
573        "bytes",
574        "int",
575        "float",
576        "decimal",
577        "bool",
578        "nil",
579        "list",
580        "dict",
581        "closure",
582        "builtin",
583        "duration",
584        "enum",
585        "struct",
586        "task_handle",
587        "channel",
588        "atomic",
589        "rng",
590        "sync_permit",
591        "resource_guard",
592        "mcp_client",
593        "verdict_receipt",
594        "set",
595        "generator",
596        "stream",
597        "range",
598        "iter",
599        "pair",
600    ];
601
602    pub fn type_name(&self) -> &'static str {
603        match self {
604            VmValue::String(_) => "string",
605            VmValue::Bytes(_) => "bytes",
606            VmValue::Int(_) => "int",
607            VmValue::Float(_) => "float",
608            VmValue::Decimal(_) => "decimal",
609            VmValue::Bool(_) => "bool",
610            VmValue::Nil => "nil",
611            VmValue::List(_) => "list",
612            VmValue::Dict(_) => "dict",
613            VmValue::Closure(_) => "closure",
614            VmValue::BuiltinRef(_) => "builtin",
615            VmValue::BuiltinRefId(_) => "builtin",
616            VmValue::Duration(_) => "duration",
617            VmValue::EnumVariant(_) => "enum",
618            VmValue::StructInstance(_) => "struct",
619            VmValue::TaskHandle(_) => "task_handle",
620            VmValue::Channel(_) => "channel",
621            VmValue::Atomic(_) => "atomic",
622            VmValue::Rng(_) => "rng",
623            VmValue::SyncPermit(_) => "sync_permit",
624            VmValue::ResourceGuard(_) => "resource_guard",
625            VmValue::McpClient(_) => "mcp_client",
626            VmValue::VerdictReceipt(_) => "verdict_receipt",
627            VmValue::Set(_) => "set",
628            VmValue::Generator(_) => "generator",
629            VmValue::Stream(_) => "stream",
630            VmValue::Range(_) => "range",
631            VmValue::Iter(_) => "iter",
632            VmValue::Pair(_) => "pair",
633            VmValue::Harness(h) => h.type_name(),
634        }
635    }
636
637    /// Borrows the string contents without allocating when the value is
638    /// already a string. Non-string values are rendered with `display()`,
639    /// matching the coercion callers apply at string boundaries. Hot string
640    /// builtins (regex, split, contains) use this to avoid cloning the
641    /// subject text on every call.
642    pub fn as_str_cow(&self) -> std::borrow::Cow<'_, str> {
643        match self {
644            VmValue::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
645            other => std::borrow::Cow::Owned(other.display()),
646        }
647    }
648
649    /// Borrows the boxed struct payload (layout + field slots) when this value
650    /// is a struct instance. The single accessor most match sites use instead
651    /// of destructuring the now-boxed variant.
652    pub fn struct_data(&self) -> Option<&StructInstanceData> {
653        match self {
654            VmValue::StructInstance(data) => Some(data),
655            _ => None,
656        }
657    }
658
659    pub fn struct_name(&self) -> Option<&str> {
660        match self {
661            VmValue::StructInstance(data) => Some(data.layout.struct_name()),
662            _ => None,
663        }
664    }
665
666    pub fn struct_field(&self, field_name: &str) -> Option<&VmValue> {
667        match self {
668            VmValue::StructInstance(data) => data
669                .layout
670                .field_index(field_name)
671                .and_then(|index| data.fields.get(index))
672                .and_then(Option::as_ref),
673            _ => None,
674        }
675    }
676
677    pub fn struct_fields_map(&self) -> Option<crate::value::DictMap> {
678        match self {
679            VmValue::StructInstance(data) => Some(struct_fields_to_map(&data.layout, &data.fields)),
680            _ => None,
681        }
682    }
683
684    pub fn struct_instance_from_map(
685        struct_name: impl Into<String>,
686        fields: crate::value::DictMap,
687    ) -> Self {
688        let layout = Shared::new(StructLayout::from_map(struct_name, &fields));
689        let slots = layout
690            .field_names()
691            .iter()
692            .map(|name| fields.get(name.as_str()).cloned())
693            .collect();
694        VmValue::StructInstance(Shared::new(StructInstanceData {
695            layout,
696            fields: Shared::new(slots),
697        }))
698    }
699
700    pub fn struct_instance_with_layout(
701        struct_name: impl Into<String>,
702        field_names: Vec<String>,
703        field_values: crate::value::DictMap,
704    ) -> Self {
705        let layout = Shared::new(StructLayout::new(struct_name, field_names));
706        let fields = layout
707            .field_names()
708            .iter()
709            .map(|name| field_values.get(name.as_str()).cloned())
710            .collect();
711        VmValue::StructInstance(Shared::new(StructInstanceData {
712            layout,
713            fields: Shared::new(fields),
714        }))
715    }
716
717    pub fn struct_instance_with_property(&self, field_name: &str, value: VmValue) -> Option<Self> {
718        let VmValue::StructInstance(data) = self else {
719            return None;
720        };
721        let (layout, fields) = (&data.layout, &data.fields);
722
723        let mut new_fields = fields.as_ref().clone();
724        let layout = match layout.field_index(field_name) {
725            Some(index) => {
726                if index >= new_fields.len() {
727                    new_fields.resize(index + 1, None);
728                }
729                new_fields[index] = Some(value);
730                Shared::clone(layout)
731            }
732            None => {
733                let new_layout = Shared::new(layout.with_appended_field(field_name.to_string()));
734                new_fields.push(Some(value));
735                new_layout
736            }
737        };
738
739        Some(VmValue::StructInstance(Shared::new(StructInstanceData {
740            layout,
741            fields: Shared::new(new_fields),
742        })))
743    }
744
745    pub fn display(&self) -> String {
746        let mut out = String::new();
747        self.write_display(&mut out);
748        out
749    }
750
751    /// Writes the display representation directly into `out`,
752    /// avoiding intermediate Vec<String> allocations for collections.
753    pub fn write_display(&self, out: &mut String) {
754        use std::fmt::Write;
755
756        match self {
757            VmValue::Int(n) => {
758                let _ = write!(out, "{n}");
759            }
760            VmValue::Float(n) => {
761                if *n == (*n as i64) as f64 && n.abs() < 1e15 {
762                    let _ = write!(out, "{n:.1}");
763                } else {
764                    let _ = write!(out, "{n}");
765                }
766            }
767            // Render the decimal at its stored scale (e.g. `1.50` stays `1.50`),
768            // which is what money formatting expects. Equality normalizes scale,
769            // so `1.5` and `1.50` are still equal even though they display
770            // differently.
771            VmValue::Decimal(d) => {
772                let _ = write!(out, "{d}");
773            }
774            VmValue::String(s) => out.push_str(s),
775            VmValue::Bytes(bytes) => {
776                const MAX_PREVIEW_BYTES: usize = 32;
777
778                out.push_str("b\"");
779                for byte in bytes.iter().take(MAX_PREVIEW_BYTES) {
780                    let _ = write!(out, "{byte:02x}");
781                }
782                if bytes.len() > MAX_PREVIEW_BYTES {
783                    let _ = write!(out, "...+{}", bytes.len() - MAX_PREVIEW_BYTES);
784                }
785                out.push('"');
786            }
787            VmValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
788            VmValue::Nil => out.push_str("nil"),
789            VmValue::List(items) => {
790                out.push('[');
791                crate::value::recursion::guard_recursion(|| {
792                    for (i, item) in items.iter().enumerate() {
793                        if i > 0 {
794                            out.push_str(", ");
795                        }
796                        item.write_display(out);
797                    }
798                });
799                out.push(']');
800            }
801            VmValue::Dict(map) => {
802                out.push('{');
803                crate::value::recursion::guard_recursion(|| {
804                    for (i, (k, v)) in map.iter().enumerate() {
805                        if i > 0 {
806                            out.push_str(", ");
807                        }
808                        out.push_str(k);
809                        out.push_str(": ");
810                        v.write_display(out);
811                    }
812                });
813                out.push('}');
814            }
815            VmValue::Closure(c) => {
816                let names: Vec<&str> = c.func.param_names().collect();
817                let _ = write!(out, "<fn({})>", names.join(", "));
818            }
819            VmValue::BuiltinRef(name) => {
820                let _ = write!(out, "<builtin {name}>");
821            }
822            VmValue::BuiltinRefId(r) => {
823                let _ = write!(out, "<builtin {}>", r.name);
824            }
825            VmValue::Duration(ms) => {
826                let sign = if *ms < 0 { "-" } else { "" };
827                let abs_ms = ms.unsigned_abs();
828                if abs_ms >= 604_800_000 && abs_ms % 604_800_000 == 0 {
829                    let _ = write!(out, "{}{}w", sign, abs_ms / 604_800_000);
830                } else if abs_ms >= 86_400_000 && abs_ms % 86_400_000 == 0 {
831                    let _ = write!(out, "{}{}d", sign, abs_ms / 86_400_000);
832                } else if abs_ms >= 3_600_000 && abs_ms % 3_600_000 == 0 {
833                    let _ = write!(out, "{}{}h", sign, abs_ms / 3_600_000);
834                } else if abs_ms >= 60_000 && abs_ms % 60_000 == 0 {
835                    let _ = write!(out, "{}{}m", sign, abs_ms / 60_000);
836                } else if abs_ms >= 1000 && abs_ms % 1000 == 0 {
837                    let _ = write!(out, "{}{}s", sign, abs_ms / 1000);
838                } else {
839                    let _ = write!(out, "{sign}{abs_ms}ms");
840                }
841            }
842            VmValue::EnumVariant(enum_variant) => {
843                if enum_variant.fields.is_empty() {
844                    let _ = write!(out, "{}.{}", enum_variant.enum_name, enum_variant.variant);
845                } else {
846                    let _ = write!(out, "{}.{}(", enum_variant.enum_name, enum_variant.variant);
847                    crate::value::recursion::guard_recursion(|| {
848                        for (i, v) in enum_variant.fields.iter().enumerate() {
849                            if i > 0 {
850                                out.push_str(", ");
851                            }
852                            v.write_display(out);
853                        }
854                    });
855                    out.push(')');
856                }
857            }
858            VmValue::StructInstance(data) => {
859                let (layout, fields) = (&data.layout, &data.fields);
860                let _ = write!(out, "{} {{", layout.struct_name());
861                crate::value::recursion::guard_recursion(|| {
862                    for (i, (k, v)) in struct_fields_to_map(layout, fields).iter().enumerate() {
863                        if i > 0 {
864                            out.push_str(", ");
865                        }
866                        out.push_str(k);
867                        out.push_str(": ");
868                        v.write_display(out);
869                    }
870                });
871                out.push('}');
872            }
873            VmValue::TaskHandle(id) => {
874                let _ = write!(out, "<task:{id}>");
875            }
876            VmValue::Channel(ch) => {
877                let _ = write!(out, "<channel:{}>", ch.name);
878            }
879            VmValue::Atomic(a) => {
880                let _ = write!(out, "<atomic:{}>", a.value.load(Ordering::SeqCst));
881            }
882            VmValue::Rng(_) => {
883                out.push_str("<rng>");
884            }
885            VmValue::SyncPermit(p) => {
886                let _ = write!(out, "<sync_permit:{}:{}>", p.kind(), p.key());
887            }
888            VmValue::ResourceGuard(guard) => {
889                let _ = write!(out, "<resource_guard:{}>", guard.label());
890            }
891            VmValue::McpClient(c) => {
892                let _ = write!(out, "<mcp_client:{}>", c.name);
893            }
894            // Authority-free: the display MUST NOT leak the receipt payload
895            // (hash/run identity), because display feeds the lenient JSON and
896            // structural-hash fallbacks. It is an opaque marker only.
897            VmValue::VerdictReceipt(_) => {
898                out.push_str("<verdict_receipt>");
899            }
900            VmValue::Set(items) => {
901                out.push_str("set(");
902                crate::value::recursion::guard_recursion(|| {
903                    for (i, item) in items.iter().enumerate() {
904                        if i > 0 {
905                            out.push_str(", ");
906                        }
907                        item.write_display(out);
908                    }
909                });
910                out.push(')');
911            }
912            VmValue::Generator(g) => {
913                if g.is_done() {
914                    out.push_str("<generator (done)>");
915                } else {
916                    out.push_str("<generator>");
917                }
918            }
919            VmValue::Stream(s) => {
920                if s.is_done() {
921                    out.push_str("<stream (done)>");
922                } else {
923                    out.push_str("<stream>");
924                }
925            }
926            // Print form mirrors source syntax: `1 to 5` / `0 to 3 exclusive`.
927            // `.to_list()` is the explicit path to materialize for display.
928            VmValue::Range(r) => {
929                let _ = write!(out, "{} to {}", r.start, r.end);
930                if !r.inclusive {
931                    out.push_str(" exclusive");
932                }
933            }
934            VmValue::Iter(h) => {
935                if matches!(&*h.lock(), crate::vm::iter::VmIter::Exhausted) {
936                    out.push_str("<iter (exhausted)>");
937                } else {
938                    out.push_str("<iter>");
939                }
940            }
941            VmValue::Harness(h) => {
942                let _ = write!(out, "<{}>", h.type_name());
943            }
944            VmValue::Pair(p) => {
945                out.push('(');
946                crate::value::recursion::guard_recursion(|| {
947                    p.0.write_display(out);
948                    out.push_str(", ");
949                    p.1.write_display(out);
950                });
951                out.push(')');
952            }
953        }
954    }
955
956    /// Get the value as a [`DictMap`] reference, if it's a Dict.
957    pub fn as_dict(&self) -> Option<&DictMap> {
958        if let VmValue::Dict(d) = self {
959            Some(d)
960        } else {
961            None
962        }
963    }
964
965    pub fn as_int(&self) -> Option<i64> {
966        if let VmValue::Int(n) = self {
967            Some(*n)
968        } else {
969            None
970        }
971    }
972
973    pub fn as_bytes(&self) -> Option<&[u8]> {
974        if let VmValue::Bytes(bytes) = self {
975            Some(bytes.as_slice())
976        } else {
977            None
978        }
979    }
980}
981
982pub fn struct_fields_to_map(
983    layout: &StructLayout,
984    fields: &[Option<VmValue>],
985) -> crate::value::DictMap {
986    layout
987        .field_names()
988        .iter()
989        .enumerate()
990        .filter_map(|(index, name)| {
991            fields
992                .get(index)
993                .and_then(Option::as_ref)
994                .map(|value| (intern_key(name), value.clone()))
995        })
996        .collect()
997}
998
999/// Sync builtin function for the VM.
1000pub type VmBuiltinFn =
1001    Arc<dyn Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync>;
1002
1003#[cfg(test)]
1004mod runtime_type_tag_tests {
1005    use super::VmValue;
1006
1007    /// The canonical tag registry in `harn-builtin-meta` is what the
1008    /// typechecker's `type_of` narrowing trusts; this assertion is the link
1009    /// that keeps it in lockstep with what the runtime actually produces.
1010    #[test]
1011    fn type_name_tags_match_canonical_registry() {
1012        let canonical = harn_builtin_meta::runtime_type_tags::ALL;
1013        for tag in VmValue::ALL_TYPE_NAMES {
1014            assert!(
1015                canonical.contains(tag),
1016                "VmValue::type_name tag `{tag}` missing from harn_builtin_meta::runtime_type_tags::ALL"
1017            );
1018        }
1019        for tag in canonical {
1020            assert!(
1021                VmValue::ALL_TYPE_NAMES.contains(tag),
1022                "canonical tag `{tag}` is not produced by VmValue::type_name; remove it or update ALL_TYPE_NAMES"
1023            );
1024        }
1025    }
1026}