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