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, VmRngHandle, VmSet,
12    VmStream, VmSyncPermitHandle,
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/// Indexed runtime layout for a Harn struct instance.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct StructLayout {
132    struct_name: String,
133    field_names: Vec<String>,
134    field_indexes: HashMap<String, usize>,
135}
136
137impl StructLayout {
138    pub fn new(struct_name: impl Into<String>, field_names: Vec<String>) -> Self {
139        let mut deduped = Vec::with_capacity(field_names.len());
140        let mut field_indexes = HashMap::with_capacity(field_names.len());
141        for field_name in field_names {
142            if field_indexes.contains_key(&field_name) {
143                continue;
144            }
145            let index = deduped.len();
146            field_indexes.insert(field_name.clone(), index);
147            deduped.push(field_name);
148        }
149
150        Self {
151            struct_name: struct_name.into(),
152            field_names: deduped,
153            field_indexes,
154        }
155    }
156
157    pub fn from_map(struct_name: impl Into<String>, fields: &crate::value::DictMap) -> Self {
158        Self::new(
159            struct_name,
160            fields.keys().map(|key| key.to_string()).collect(),
161        )
162    }
163
164    pub fn struct_name(&self) -> &str {
165        &self.struct_name
166    }
167
168    pub fn field_names(&self) -> &[String] {
169        &self.field_names
170    }
171
172    pub fn field_index(&self, field_name: &str) -> Option<usize> {
173        if self.field_names.len() <= 8 {
174            return self
175                .field_names
176                .iter()
177                .position(|candidate| candidate == field_name);
178        }
179        self.field_indexes.get(field_name).copied()
180    }
181
182    pub fn with_appended_field(&self, field_name: String) -> Self {
183        if self.field_indexes.contains_key(&field_name) {
184            return self.clone();
185        }
186        let mut field_names = self.field_names.clone();
187        field_names.push(field_name);
188        Self::new(self.struct_name.clone(), field_names)
189    }
190}
191
192/// Runtime payload for a Harn enum variant.
193#[derive(Debug, Clone)]
194pub struct VmEnumVariant {
195    pub enum_name: HarnStr,
196    pub variant: HarnStr,
197    pub fields: Shared<Vec<VmValue>>,
198}
199
200impl VmEnumVariant {
201    pub fn has_enum_name(&self, enum_name: &str) -> bool {
202        self.enum_name.as_str() == enum_name
203    }
204
205    pub fn is_variant(&self, enum_name: &str, variant: &str) -> bool {
206        self.has_enum_name(enum_name) && self.variant.as_str() == variant
207    }
208}
209
210/// Boxed payload for [`VmValue::BuiltinRefId`].
211///
212/// Pairs the compact [`BuiltinId`] used for direct dispatch with the builtin's
213/// registered name (kept for policy checks, diagnostics, and name-keyed
214/// fallback). Stored behind a `Shared` pointer in the value so the `{ id, name
215/// }` pair does not widen every `VmValue` to its 24-byte footprint.
216#[derive(Debug, Clone)]
217pub struct VmBuiltinRefId {
218    pub id: BuiltinId,
219    pub name: HarnStr,
220}
221
222/// Runtime layout + slots for a [`VmValue::StructInstance`].
223///
224/// Boxed behind a single `Shared` pointer so the `{ layout, fields }` pair —
225/// two pointers, 16 bytes inline — does not set the whole-enum size. Cloning a
226/// struct value is then a single refcount bump, and the variant fits in one
227/// word like every other compound payload.
228#[derive(Debug, Clone)]
229pub struct StructInstanceData {
230    pub layout: Shared<StructLayout>,
231    pub fields: Shared<Vec<Option<VmValue>>>,
232}
233
234/// VM runtime value.
235///
236/// Rare compound payloads use shared pointers so stack/local-slot traffic is
237/// bounded by the common scalar and pointer-sized value shapes. Every variant
238/// is held to a single machine word (8 bytes): the oversized payloads —
239/// `Range` (a 24-byte triple), `BuiltinRefId` (id + name), `Decimal` (16-byte
240/// base-10 mantissa), and `StructInstance` (two pointers) — are boxed behind a
241/// `Shared` pointer, and the string-shaped variants use the thin-pointer
242/// [`HarnStr`] instead of a 16-byte `Arc<str>` fat pointer. That keeps
243/// `VmValue` at 16 bytes (down from 24, and 32 before that) without inflating
244/// the common `Int` / `Float` / `List` / `Dict` / `String` shapes the
245/// interpreter moves on every push, pop, clone, and local-slot write. Unsafe
246/// layouts such as NaN boxing or tagged pointers remain deferred; the thin
247/// string's unsafe is encapsulated in the vetted `arcstr` crate.
248#[derive(Debug, Clone)]
249pub enum VmValue {
250    Int(i64),
251    Float(f64),
252    /// Exact base-10 decimal (96-bit mantissa, up to 28–29 significant digits)
253    /// for money and other values where binary float rounding is unacceptable.
254    /// Boxed behind a `Shared` pointer (`rust_decimal::Decimal` is 16 bytes, so
255    /// inlining it would set the whole-enum size); cloning is a refcount bump.
256    /// Constructed via the `decimal(value)` builtin; it is a distinct type from
257    /// `Int`/`Float` for equality/ordering/hashing (a clean island) but
258    /// promotes `Int` operands exactly in arithmetic. See `docs/src/decimal.md`.
259    Decimal(Shared<rust_decimal::Decimal>),
260    String(HarnStr),
261    Bytes(Shared<Vec<u8>>),
262    Bool(bool),
263    Nil,
264    List(Shared<Vec<VmValue>>),
265    Dict(Shared<DictMap>),
266    Closure(Shared<VmClosure>),
267    /// Reference to a registered builtin function, used when a builtin name is
268    /// referenced as a value (e.g. `snake_dict.rekey(snake_to_camel)`). The
269    /// contained string is the builtin's registered name.
270    BuiltinRef(HarnStr),
271    /// Compact builtin reference for callback positions. The boxed
272    /// [`VmBuiltinRefId`] carries the id plus the name for policy,
273    /// diagnostics, and fallback if the ID cannot be used. Boxed so the
274    /// `{ id, name }` pair does not widen every `VmValue`.
275    BuiltinRefId(Shared<VmBuiltinRefId>),
276    Duration(i64),
277    EnumVariant(Shared<VmEnumVariant>),
278    StructInstance(Shared<StructInstanceData>),
279    TaskHandle(HarnStr),
280    Channel(Shared<VmChannelHandle>),
281    Atomic(Shared<VmAtomicHandle>),
282    Rng(Shared<VmRngHandle>),
283    SyncPermit(Shared<VmSyncPermitHandle>),
284    McpClient(Shared<VmMcpClientHandle>),
285    Set(Shared<VmSet>),
286    Generator(Shared<VmGenerator>),
287    Stream(Shared<VmStream>),
288    /// Lazy numeric range. Boxed behind a `Shared` pointer so its 24-byte
289    /// `start/end/inclusive` payload does not set the whole-enum size; cloning
290    /// a range value is then a refcount bump.
291    Range(Shared<VmRange>),
292    /// Lazy iterator handle. Single-pass, fused. See `crate::vm::iter::VmIter`.
293    Iter(crate::vm::iter::VmIterHandle),
294    /// Two-element pair value. Produced by `pair(a, b)`, yielded by the
295    /// Dict iterator source, and (later) by `zip` / `enumerate` combinators.
296    /// Accessed via `.first` / `.second`, and destructurable in
297    /// `for (a, b) in ...` loops.
298    Pair(Shared<(VmValue, VmValue)>),
299    /// Capability handle threaded into `main(harness: Harness)`. The same
300    /// variant carries the root handle and each typed sub-handle (`stdio`,
301    /// `clock`, `fs`, `env`, `random`, `net`) so they share one value shape
302    /// but stay distinguishable via `VmHarness::kind`.
303    Harness(Shared<VmHarness>),
304}
305
306/// Process-wide interned `Arc<str>` for every single-byte ASCII character.
307///
308/// Materializing source text into per-character string values — the supported
309/// idiom for cursor-style scanners (`chars`, `char_at`, `s[i]`) — would
310/// otherwise heap-allocate once per character. Source files are overwhelmingly
311/// ASCII, so interning the 128 single-char strings lets those paths clone a
312/// cheap `Arc` (a refcount bump) instead of allocating, keeping a full-file
313/// scan linear with a low constant factor.
314static ASCII_CHAR_STRINGS: std::sync::LazyLock<[HarnStr; 128]> = std::sync::LazyLock::new(|| {
315    std::array::from_fn(|byte| {
316        let mut buffer = [0u8; 4];
317        HarnStr::from((byte as u8 as char).encode_utf8(&mut buffer))
318    })
319});
320
321impl VmValue {
322    /// Canonical `VmValue::String` constructor from anything string-like.
323    ///
324    /// Collapses the ubiquitous `VmValue::String(arcstr::ArcStr::from(..))`
325    /// spelling to a single call and performs exactly one allocation via
326    /// `Arc::<str>::from(&str)` regardless of whether the input is a `&str`,
327    /// `String`, `&String`, or `Cow<str>`. Prefer this over hand-writing the
328    /// `Arc::from` at call sites.
329    pub fn string(value: impl AsRef<str>) -> Self {
330        VmValue::String(HarnStr::from(value.as_ref()))
331    }
332
333    /// Canonical `VmValue::Decimal` constructor.
334    ///
335    /// Boxes the 16-byte [`rust_decimal::Decimal`] behind a `Shared` pointer so
336    /// the value stays one word wide; see [`VmValue::Decimal`].
337    pub fn decimal(value: rust_decimal::Decimal) -> Self {
338        VmValue::Decimal(Shared::new(value))
339    }
340
341    /// Builds a `VmValue::String` holding a single character, reusing the
342    /// interned ASCII table (see [`ASCII_CHAR_STRINGS`]) so the common ASCII
343    /// path does not allocate.
344    pub fn char_value(ch: char) -> Self {
345        if ch.is_ascii() {
346            return VmValue::String(ASCII_CHAR_STRINGS[ch as usize].clone());
347        }
348        let mut buffer = [0u8; 4];
349        VmValue::String(HarnStr::from(ch.encode_utf8(&mut buffer)))
350    }
351
352    /// Materializes a string into a `VmValue::List` of single-character string
353    /// values in one linear pass. Backs both the `chars` builtin and the
354    /// `.chars()` method, and is the cursor-scanner-friendly counterpart to the
355    /// O(n)-per-call `substring` / slice / `s[i]` operations on a `string`.
356    pub fn chars_list(text: &str) -> Self {
357        VmValue::List(Shared::new(text.chars().map(VmValue::char_value).collect()))
358    }
359
360    pub fn enum_variant(
361        enum_name: impl Into<HarnStr>,
362        variant: impl Into<HarnStr>,
363        fields: Vec<VmValue>,
364    ) -> Self {
365        VmValue::EnumVariant(Shared::new(VmEnumVariant {
366            enum_name: enum_name.into(),
367            variant: variant.into(),
368            fields: Shared::new(fields),
369        }))
370    }
371
372    pub fn task_handle(id: impl Into<HarnStr>) -> Self {
373        VmValue::TaskHandle(id.into())
374    }
375
376    /// Construct a boxed [`VmValue::Range`] from a [`VmRange`].
377    pub fn range(range: VmRange) -> Self {
378        VmValue::Range(Shared::new(range))
379    }
380
381    /// Construct a boxed [`VmValue::BuiltinRefId`] from its id and name.
382    pub fn builtin_ref_id(id: BuiltinId, name: impl Into<HarnStr>) -> Self {
383        VmValue::BuiltinRefId(Shared::new(VmBuiltinRefId {
384            id,
385            name: name.into(),
386        }))
387    }
388
389    /// Construct a [`VmValue::Dict`] from any iterator of `(key, value)`
390    /// entries. Accepts the `BTreeMap` that most builders still assemble (it is
391    /// `IntoIterator<Item = (String, VmValue)>`) and collects it into the
392    /// persistent [`DictMap`], so callers keep their familiar map-building code
393    /// while the stored value gains structural sharing.
394    pub fn dict<K: IntoDictKey>(entries: impl IntoIterator<Item = (K, VmValue)>) -> Self {
395        VmValue::Dict(Shared::new(
396            entries
397                .into_iter()
398                .map(|(k, v)| (k.into_dict_key(), v))
399                .collect::<DictMap>(),
400        ))
401    }
402
403    /// Construct a [`VmValue::Dict`] from an already-built [`DictMap`].
404    pub fn dict_map(map: DictMap) -> Self {
405        VmValue::Dict(Shared::new(map))
406    }
407
408    /// Construct a [`VmValue::Set`] from any iterator of values, deduplicating
409    /// by structural equality and preserving first-seen insertion order.
410    pub fn set(values: impl IntoIterator<Item = VmValue>) -> Self {
411        VmValue::Set(Shared::new(values.into_iter().collect::<VmSet>()))
412    }
413
414    /// Construct a [`VmValue::Set`] from an already-built [`VmSet`].
415    pub fn set_value(set: VmSet) -> Self {
416        VmValue::Set(Shared::new(set))
417    }
418
419    pub fn channel(handle: VmChannelHandle) -> Self {
420        VmValue::Channel(Shared::new(handle))
421    }
422
423    pub fn atomic(handle: VmAtomicHandle) -> Self {
424        VmValue::Atomic(Shared::new(handle))
425    }
426
427    pub fn rng(handle: VmRngHandle) -> Self {
428        VmValue::Rng(Shared::new(handle))
429    }
430
431    pub fn sync_permit(handle: VmSyncPermitHandle) -> Self {
432        VmValue::SyncPermit(Shared::new(handle))
433    }
434
435    pub fn mcp_client(handle: VmMcpClientHandle) -> Self {
436        VmValue::McpClient(Shared::new(handle))
437    }
438
439    pub fn generator(generator: VmGenerator) -> Self {
440        VmValue::Generator(Shared::new(generator))
441    }
442
443    pub fn stream(stream: VmStream) -> Self {
444        VmValue::Stream(Shared::new(stream))
445    }
446
447    pub fn harness(handle: VmHarness) -> Self {
448        VmValue::Harness(Shared::new(handle))
449    }
450
451    pub fn struct_instance(
452        struct_name: impl Into<Shared<str>>,
453        fields: crate::value::DictMap,
454    ) -> Self {
455        Self::struct_instance_from_map(struct_name.into().to_string(), fields)
456    }
457
458    pub fn is_truthy(&self) -> bool {
459        match self {
460            VmValue::Bool(b) => *b,
461            VmValue::Nil => false,
462            VmValue::Int(n) => *n != 0,
463            VmValue::Float(n) => *n != 0.0,
464            VmValue::Decimal(d) => **d != rust_decimal::Decimal::ZERO,
465            VmValue::String(s) => !s.is_empty(),
466            VmValue::Bytes(bytes) => !bytes.is_empty(),
467            VmValue::List(l) => !l.is_empty(),
468            VmValue::Dict(d) => !d.is_empty(),
469            VmValue::Closure(_) => true,
470            VmValue::BuiltinRef(_) => true,
471            VmValue::BuiltinRefId(_) => true,
472            VmValue::Duration(ms) => *ms != 0,
473            VmValue::EnumVariant(_) => true,
474            VmValue::StructInstance(_) => true,
475            VmValue::TaskHandle(_) => true,
476            VmValue::Channel(_) => true,
477            VmValue::Atomic(_) => true,
478            VmValue::Rng(_) => true,
479            VmValue::SyncPermit(_) => true,
480            VmValue::McpClient(_) => true,
481            VmValue::Set(s) => !s.is_empty(),
482            VmValue::Generator(_) => true,
483            VmValue::Stream(_) => true,
484            // Match Python semantics: range objects are always truthy,
485            // even the empty range (analogous to generators / iterators).
486            VmValue::Range(_) => true,
487            VmValue::Iter(_) => true,
488            VmValue::Pair(_) => true,
489            VmValue::Harness(_) => true,
490        }
491    }
492
493    /// Every tag [`VmValue::type_name`] can return, excluding harness-object
494    /// names (delegated to `HarnessValue::type_name`). Keep in lockstep with
495    /// the match below AND with `harn_builtin_meta::runtime_type_tags::ALL`
496    /// — a unit test asserts the latter, which is what keeps the
497    /// typechecker's `type_of` narrowing honest.
498    pub const ALL_TYPE_NAMES: &'static [&'static str] = &[
499        "string",
500        "bytes",
501        "int",
502        "float",
503        "decimal",
504        "bool",
505        "nil",
506        "list",
507        "dict",
508        "closure",
509        "builtin",
510        "duration",
511        "enum",
512        "struct",
513        "task_handle",
514        "channel",
515        "atomic",
516        "rng",
517        "sync_permit",
518        "mcp_client",
519        "set",
520        "generator",
521        "stream",
522        "range",
523        "iter",
524        "pair",
525    ];
526
527    pub fn type_name(&self) -> &'static str {
528        match self {
529            VmValue::String(_) => "string",
530            VmValue::Bytes(_) => "bytes",
531            VmValue::Int(_) => "int",
532            VmValue::Float(_) => "float",
533            VmValue::Decimal(_) => "decimal",
534            VmValue::Bool(_) => "bool",
535            VmValue::Nil => "nil",
536            VmValue::List(_) => "list",
537            VmValue::Dict(_) => "dict",
538            VmValue::Closure(_) => "closure",
539            VmValue::BuiltinRef(_) => "builtin",
540            VmValue::BuiltinRefId(_) => "builtin",
541            VmValue::Duration(_) => "duration",
542            VmValue::EnumVariant(_) => "enum",
543            VmValue::StructInstance(_) => "struct",
544            VmValue::TaskHandle(_) => "task_handle",
545            VmValue::Channel(_) => "channel",
546            VmValue::Atomic(_) => "atomic",
547            VmValue::Rng(_) => "rng",
548            VmValue::SyncPermit(_) => "sync_permit",
549            VmValue::McpClient(_) => "mcp_client",
550            VmValue::Set(_) => "set",
551            VmValue::Generator(_) => "generator",
552            VmValue::Stream(_) => "stream",
553            VmValue::Range(_) => "range",
554            VmValue::Iter(_) => "iter",
555            VmValue::Pair(_) => "pair",
556            VmValue::Harness(h) => h.type_name(),
557        }
558    }
559
560    /// Borrows the string contents without allocating when the value is
561    /// already a string. Non-string values are rendered with `display()`,
562    /// matching the coercion callers apply at string boundaries. Hot string
563    /// builtins (regex, split, contains) use this to avoid cloning the
564    /// subject text on every call.
565    pub fn as_str_cow(&self) -> std::borrow::Cow<'_, str> {
566        match self {
567            VmValue::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
568            other => std::borrow::Cow::Owned(other.display()),
569        }
570    }
571
572    /// Borrows the boxed struct payload (layout + field slots) when this value
573    /// is a struct instance. The single accessor most match sites use instead
574    /// of destructuring the now-boxed variant.
575    pub fn struct_data(&self) -> Option<&StructInstanceData> {
576        match self {
577            VmValue::StructInstance(data) => Some(data),
578            _ => None,
579        }
580    }
581
582    pub fn struct_name(&self) -> Option<&str> {
583        match self {
584            VmValue::StructInstance(data) => Some(data.layout.struct_name()),
585            _ => None,
586        }
587    }
588
589    pub fn struct_field(&self, field_name: &str) -> Option<&VmValue> {
590        match self {
591            VmValue::StructInstance(data) => data
592                .layout
593                .field_index(field_name)
594                .and_then(|index| data.fields.get(index))
595                .and_then(Option::as_ref),
596            _ => None,
597        }
598    }
599
600    pub fn struct_fields_map(&self) -> Option<crate::value::DictMap> {
601        match self {
602            VmValue::StructInstance(data) => Some(struct_fields_to_map(&data.layout, &data.fields)),
603            _ => None,
604        }
605    }
606
607    pub fn struct_instance_from_map(
608        struct_name: impl Into<String>,
609        fields: crate::value::DictMap,
610    ) -> Self {
611        let layout = Shared::new(StructLayout::from_map(struct_name, &fields));
612        let slots = layout
613            .field_names()
614            .iter()
615            .map(|name| fields.get(name.as_str()).cloned())
616            .collect();
617        VmValue::StructInstance(Shared::new(StructInstanceData {
618            layout,
619            fields: Shared::new(slots),
620        }))
621    }
622
623    pub fn struct_instance_with_layout(
624        struct_name: impl Into<String>,
625        field_names: Vec<String>,
626        field_values: crate::value::DictMap,
627    ) -> Self {
628        let layout = Shared::new(StructLayout::new(struct_name, field_names));
629        let fields = layout
630            .field_names()
631            .iter()
632            .map(|name| field_values.get(name.as_str()).cloned())
633            .collect();
634        VmValue::StructInstance(Shared::new(StructInstanceData {
635            layout,
636            fields: Shared::new(fields),
637        }))
638    }
639
640    pub fn struct_instance_with_property(&self, field_name: &str, value: VmValue) -> Option<Self> {
641        let VmValue::StructInstance(data) = self else {
642            return None;
643        };
644        let (layout, fields) = (&data.layout, &data.fields);
645
646        let mut new_fields = fields.as_ref().clone();
647        let layout = match layout.field_index(field_name) {
648            Some(index) => {
649                if index >= new_fields.len() {
650                    new_fields.resize(index + 1, None);
651                }
652                new_fields[index] = Some(value);
653                Shared::clone(layout)
654            }
655            None => {
656                let new_layout = Shared::new(layout.with_appended_field(field_name.to_string()));
657                new_fields.push(Some(value));
658                new_layout
659            }
660        };
661
662        Some(VmValue::StructInstance(Shared::new(StructInstanceData {
663            layout,
664            fields: Shared::new(new_fields),
665        })))
666    }
667
668    pub fn display(&self) -> String {
669        let mut out = String::new();
670        self.write_display(&mut out);
671        out
672    }
673
674    /// Writes the display representation directly into `out`,
675    /// avoiding intermediate Vec<String> allocations for collections.
676    pub fn write_display(&self, out: &mut String) {
677        use std::fmt::Write;
678
679        match self {
680            VmValue::Int(n) => {
681                let _ = write!(out, "{n}");
682            }
683            VmValue::Float(n) => {
684                if *n == (*n as i64) as f64 && n.abs() < 1e15 {
685                    let _ = write!(out, "{n:.1}");
686                } else {
687                    let _ = write!(out, "{n}");
688                }
689            }
690            // Render the decimal at its stored scale (e.g. `1.50` stays `1.50`),
691            // which is what money formatting expects. Equality normalizes scale,
692            // so `1.5` and `1.50` are still equal even though they display
693            // differently.
694            VmValue::Decimal(d) => {
695                let _ = write!(out, "{d}");
696            }
697            VmValue::String(s) => out.push_str(s),
698            VmValue::Bytes(bytes) => {
699                const MAX_PREVIEW_BYTES: usize = 32;
700
701                out.push_str("b\"");
702                for byte in bytes.iter().take(MAX_PREVIEW_BYTES) {
703                    let _ = write!(out, "{byte:02x}");
704                }
705                if bytes.len() > MAX_PREVIEW_BYTES {
706                    let _ = write!(out, "...+{}", bytes.len() - MAX_PREVIEW_BYTES);
707                }
708                out.push('"');
709            }
710            VmValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
711            VmValue::Nil => out.push_str("nil"),
712            VmValue::List(items) => {
713                out.push('[');
714                crate::value::recursion::guard_recursion(|| {
715                    for (i, item) in items.iter().enumerate() {
716                        if i > 0 {
717                            out.push_str(", ");
718                        }
719                        item.write_display(out);
720                    }
721                });
722                out.push(']');
723            }
724            VmValue::Dict(map) => {
725                out.push('{');
726                crate::value::recursion::guard_recursion(|| {
727                    for (i, (k, v)) in map.iter().enumerate() {
728                        if i > 0 {
729                            out.push_str(", ");
730                        }
731                        out.push_str(k);
732                        out.push_str(": ");
733                        v.write_display(out);
734                    }
735                });
736                out.push('}');
737            }
738            VmValue::Closure(c) => {
739                let names: Vec<&str> = c.func.param_names().collect();
740                let _ = write!(out, "<fn({})>", names.join(", "));
741            }
742            VmValue::BuiltinRef(name) => {
743                let _ = write!(out, "<builtin {name}>");
744            }
745            VmValue::BuiltinRefId(r) => {
746                let _ = write!(out, "<builtin {}>", r.name);
747            }
748            VmValue::Duration(ms) => {
749                let sign = if *ms < 0 { "-" } else { "" };
750                let abs_ms = ms.unsigned_abs();
751                if abs_ms >= 604_800_000 && abs_ms % 604_800_000 == 0 {
752                    let _ = write!(out, "{}{}w", sign, abs_ms / 604_800_000);
753                } else if abs_ms >= 86_400_000 && abs_ms % 86_400_000 == 0 {
754                    let _ = write!(out, "{}{}d", sign, abs_ms / 86_400_000);
755                } else if abs_ms >= 3_600_000 && abs_ms % 3_600_000 == 0 {
756                    let _ = write!(out, "{}{}h", sign, abs_ms / 3_600_000);
757                } else if abs_ms >= 60_000 && abs_ms % 60_000 == 0 {
758                    let _ = write!(out, "{}{}m", sign, abs_ms / 60_000);
759                } else if abs_ms >= 1000 && abs_ms % 1000 == 0 {
760                    let _ = write!(out, "{}{}s", sign, abs_ms / 1000);
761                } else {
762                    let _ = write!(out, "{sign}{abs_ms}ms");
763                }
764            }
765            VmValue::EnumVariant(enum_variant) => {
766                if enum_variant.fields.is_empty() {
767                    let _ = write!(out, "{}.{}", enum_variant.enum_name, enum_variant.variant);
768                } else {
769                    let _ = write!(out, "{}.{}(", enum_variant.enum_name, enum_variant.variant);
770                    crate::value::recursion::guard_recursion(|| {
771                        for (i, v) in enum_variant.fields.iter().enumerate() {
772                            if i > 0 {
773                                out.push_str(", ");
774                            }
775                            v.write_display(out);
776                        }
777                    });
778                    out.push(')');
779                }
780            }
781            VmValue::StructInstance(data) => {
782                let (layout, fields) = (&data.layout, &data.fields);
783                let _ = write!(out, "{} {{", layout.struct_name());
784                crate::value::recursion::guard_recursion(|| {
785                    for (i, (k, v)) in struct_fields_to_map(layout, fields).iter().enumerate() {
786                        if i > 0 {
787                            out.push_str(", ");
788                        }
789                        out.push_str(k);
790                        out.push_str(": ");
791                        v.write_display(out);
792                    }
793                });
794                out.push('}');
795            }
796            VmValue::TaskHandle(id) => {
797                let _ = write!(out, "<task:{id}>");
798            }
799            VmValue::Channel(ch) => {
800                let _ = write!(out, "<channel:{}>", ch.name);
801            }
802            VmValue::Atomic(a) => {
803                let _ = write!(out, "<atomic:{}>", a.value.load(Ordering::SeqCst));
804            }
805            VmValue::Rng(_) => {
806                out.push_str("<rng>");
807            }
808            VmValue::SyncPermit(p) => {
809                let _ = write!(out, "<sync_permit:{}:{}>", p.kind(), p.key());
810            }
811            VmValue::McpClient(c) => {
812                let _ = write!(out, "<mcp_client:{}>", c.name);
813            }
814            VmValue::Set(items) => {
815                out.push_str("set(");
816                crate::value::recursion::guard_recursion(|| {
817                    for (i, item) in items.iter().enumerate() {
818                        if i > 0 {
819                            out.push_str(", ");
820                        }
821                        item.write_display(out);
822                    }
823                });
824                out.push(')');
825            }
826            VmValue::Generator(g) => {
827                if g.is_done() {
828                    out.push_str("<generator (done)>");
829                } else {
830                    out.push_str("<generator>");
831                }
832            }
833            VmValue::Stream(s) => {
834                if s.is_done() {
835                    out.push_str("<stream (done)>");
836                } else {
837                    out.push_str("<stream>");
838                }
839            }
840            // Print form mirrors source syntax: `1 to 5` / `0 to 3 exclusive`.
841            // `.to_list()` is the explicit path to materialize for display.
842            VmValue::Range(r) => {
843                let _ = write!(out, "{} to {}", r.start, r.end);
844                if !r.inclusive {
845                    out.push_str(" exclusive");
846                }
847            }
848            VmValue::Iter(h) => {
849                if matches!(&*h.lock(), crate::vm::iter::VmIter::Exhausted) {
850                    out.push_str("<iter (exhausted)>");
851                } else {
852                    out.push_str("<iter>");
853                }
854            }
855            VmValue::Harness(h) => {
856                let _ = write!(out, "<{}>", h.type_name());
857            }
858            VmValue::Pair(p) => {
859                out.push('(');
860                crate::value::recursion::guard_recursion(|| {
861                    p.0.write_display(out);
862                    out.push_str(", ");
863                    p.1.write_display(out);
864                });
865                out.push(')');
866            }
867        }
868    }
869
870    /// Get the value as a [`DictMap`] reference, if it's a Dict.
871    pub fn as_dict(&self) -> Option<&DictMap> {
872        if let VmValue::Dict(d) = self {
873            Some(d)
874        } else {
875            None
876        }
877    }
878
879    pub fn as_int(&self) -> Option<i64> {
880        if let VmValue::Int(n) = self {
881            Some(*n)
882        } else {
883            None
884        }
885    }
886
887    pub fn as_bytes(&self) -> Option<&[u8]> {
888        if let VmValue::Bytes(bytes) = self {
889            Some(bytes.as_slice())
890        } else {
891            None
892        }
893    }
894}
895
896pub fn struct_fields_to_map(
897    layout: &StructLayout,
898    fields: &[Option<VmValue>],
899) -> crate::value::DictMap {
900    layout
901        .field_names()
902        .iter()
903        .enumerate()
904        .filter_map(|(index, name)| {
905            fields
906                .get(index)
907                .and_then(Option::as_ref)
908                .map(|value| (intern_key(name), value.clone()))
909        })
910        .collect()
911}
912
913/// Sync builtin function for the VM.
914pub type VmBuiltinFn =
915    Arc<dyn Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync>;
916
917#[cfg(test)]
918mod runtime_type_tag_tests {
919    use super::VmValue;
920
921    /// The canonical tag registry in `harn-builtin-meta` is what the
922    /// typechecker's `type_of` narrowing trusts; this assertion is the link
923    /// that keeps it in lockstep with what the runtime actually produces.
924    #[test]
925    fn type_name_tags_match_canonical_registry() {
926        let canonical = harn_builtin_meta::runtime_type_tags::ALL;
927        for tag in VmValue::ALL_TYPE_NAMES {
928            assert!(
929                canonical.contains(tag),
930                "VmValue::type_name tag `{tag}` missing from harn_builtin_meta::runtime_type_tags::ALL"
931            );
932        }
933        for tag in canonical {
934            assert!(
935                VmValue::ALL_TYPE_NAMES.contains(tag),
936                "canonical tag `{tag}` is not produced by VmValue::type_name; remove it or update ALL_TYPE_NAMES"
937            );
938        }
939    }
940}