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/// Backing store for [`VmValue::Dict`]: a persistent, ordered, structurally
33/// shared map.
34///
35/// Replacing the former `BTreeMap` with `imbl::OrdMap` turns the copy-on-write
36/// `Arc::make_mut` clone — performed on every dict mutation whenever the value
37/// is aliased (on the stack, in another local, captured by a closure) — from an
38/// O(n) deep copy of every key and entry into an O(log n) path copy. Ordering
39/// and the read API (`get` / `iter` / `keys` / `values` / `contains_key` /
40/// `range` / `len`) match `BTreeMap`, so dict reads are unchanged. The `Arc`
41/// wrapper is retained so reference identity (`Arc::ptr_eq`) — used by the `===`
42/// operator and `value_identity_key` — keeps its current semantics.
43pub type DictMap = imbl::OrdMap<String, VmValue>;
44
45/// Character count with a byte-length fast path for ASCII text.
46///
47/// Harn exposes string lengths as Unicode scalar counts. ASCII is one byte per
48/// scalar, so cached string `count` / `len` paths can avoid a full iterator
49/// scan without changing behavior for non-ASCII text.
50pub fn string_char_count(text: &str) -> usize {
51    if text.is_ascii() {
52        text.len()
53    } else {
54        text.chars().count()
55    }
56}
57
58/// Indexed runtime layout for a Harn struct instance.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct StructLayout {
61    struct_name: String,
62    field_names: Vec<String>,
63    field_indexes: HashMap<String, usize>,
64}
65
66impl StructLayout {
67    pub fn new(struct_name: impl Into<String>, field_names: Vec<String>) -> Self {
68        let mut deduped = Vec::with_capacity(field_names.len());
69        let mut field_indexes = HashMap::with_capacity(field_names.len());
70        for field_name in field_names {
71            if field_indexes.contains_key(&field_name) {
72                continue;
73            }
74            let index = deduped.len();
75            field_indexes.insert(field_name.clone(), index);
76            deduped.push(field_name);
77        }
78
79        Self {
80            struct_name: struct_name.into(),
81            field_names: deduped,
82            field_indexes,
83        }
84    }
85
86    pub fn from_map(struct_name: impl Into<String>, fields: &crate::value::DictMap) -> Self {
87        Self::new(struct_name, fields.keys().cloned().collect())
88    }
89
90    pub fn struct_name(&self) -> &str {
91        &self.struct_name
92    }
93
94    pub fn field_names(&self) -> &[String] {
95        &self.field_names
96    }
97
98    pub fn field_index(&self, field_name: &str) -> Option<usize> {
99        if self.field_names.len() <= 8 {
100            return self
101                .field_names
102                .iter()
103                .position(|candidate| candidate == field_name);
104        }
105        self.field_indexes.get(field_name).copied()
106    }
107
108    pub fn with_appended_field(&self, field_name: String) -> Self {
109        if self.field_indexes.contains_key(&field_name) {
110            return self.clone();
111        }
112        let mut field_names = self.field_names.clone();
113        field_names.push(field_name);
114        Self::new(self.struct_name.clone(), field_names)
115    }
116}
117
118/// Runtime payload for a Harn enum variant.
119#[derive(Debug, Clone)]
120pub struct VmEnumVariant {
121    pub enum_name: Shared<str>,
122    pub variant: Shared<str>,
123    pub fields: Shared<Vec<VmValue>>,
124}
125
126impl VmEnumVariant {
127    pub fn has_enum_name(&self, enum_name: &str) -> bool {
128        self.enum_name.as_ref() == enum_name
129    }
130
131    pub fn is_variant(&self, enum_name: &str, variant: &str) -> bool {
132        self.has_enum_name(enum_name) && self.variant.as_ref() == variant
133    }
134}
135
136/// Boxed payload for [`VmValue::BuiltinRefId`].
137///
138/// Pairs the compact [`BuiltinId`] used for direct dispatch with the builtin's
139/// registered name (kept for policy checks, diagnostics, and name-keyed
140/// fallback). Stored behind a `Shared` pointer in the value so the `{ id, name
141/// }` pair does not widen every `VmValue` to its 24-byte footprint.
142#[derive(Debug, Clone)]
143pub struct VmBuiltinRefId {
144    pub id: BuiltinId,
145    pub name: Shared<str>,
146}
147
148/// VM runtime value.
149///
150/// Rare compound payloads use shared pointers so stack/local-slot traffic is
151/// bounded by the common scalar and pointer-sized value shapes. The two
152/// oversized inline payloads — `Range` (a 24-byte `start/end/inclusive`
153/// triple) and `BuiltinRefId` (an id + `Arc<str>` name) — are boxed behind a
154/// `Shared` pointer so no variant exceeds a fat pointer. That keeps `VmValue`
155/// at 24 bytes (down from 32) without inflating the common `Int` / `Float` /
156/// `List` / `Dict` / `String` shapes the interpreter moves on every push,
157/// pop, clone, and local-slot write. Unsafe layouts such as NaN boxing or
158/// tagged pointers are deliberately deferred until Harn has a stronger
159/// object/heap story.
160#[derive(Debug, Clone)]
161pub enum VmValue {
162    Int(i64),
163    Float(f64),
164    /// Exact base-10 decimal (96-bit mantissa, up to 28–29 significant digits)
165    /// for money and other values where binary float rounding is unacceptable.
166    /// Inline (`rust_decimal::Decimal` is `Copy` and 16 bytes, the same width as
167    /// the existing widest variants). Constructed via the `decimal(value)`
168    /// builtin; it is a distinct type from `Int`/`Float` for
169    /// equality/ordering/hashing (a clean island) but promotes `Int` operands
170    /// exactly in arithmetic. See `docs/src/decimal.md`.
171    Decimal(rust_decimal::Decimal),
172    String(Shared<str>),
173    Bytes(Shared<Vec<u8>>),
174    Bool(bool),
175    Nil,
176    List(Shared<Vec<VmValue>>),
177    Dict(Shared<DictMap>),
178    Closure(Shared<VmClosure>),
179    /// Reference to a registered builtin function, used when a builtin name is
180    /// referenced as a value (e.g. `snake_dict.rekey(snake_to_camel)`). The
181    /// contained string is the builtin's registered name.
182    BuiltinRef(Shared<str>),
183    /// Compact builtin reference for callback positions. The boxed
184    /// [`VmBuiltinRefId`] carries the id plus the name for policy,
185    /// diagnostics, and fallback if the ID cannot be used. Boxed so the
186    /// `{ id, name }` pair does not widen every `VmValue`.
187    BuiltinRefId(Shared<VmBuiltinRefId>),
188    Duration(i64),
189    EnumVariant(Shared<VmEnumVariant>),
190    StructInstance {
191        layout: Shared<StructLayout>,
192        fields: Shared<Vec<Option<VmValue>>>,
193    },
194    TaskHandle(Shared<str>),
195    Channel(Shared<VmChannelHandle>),
196    Atomic(Shared<VmAtomicHandle>),
197    Rng(Shared<VmRngHandle>),
198    SyncPermit(Shared<VmSyncPermitHandle>),
199    McpClient(Shared<VmMcpClientHandle>),
200    Set(Shared<VmSet>),
201    Generator(Shared<VmGenerator>),
202    Stream(Shared<VmStream>),
203    /// Lazy numeric range. Boxed behind a `Shared` pointer so its 24-byte
204    /// `start/end/inclusive` payload does not set the whole-enum size; cloning
205    /// a range value is then a refcount bump.
206    Range(Shared<VmRange>),
207    /// Lazy iterator handle. Single-pass, fused. See `crate::vm::iter::VmIter`.
208    Iter(crate::vm::iter::VmIterHandle),
209    /// Two-element pair value. Produced by `pair(a, b)`, yielded by the
210    /// Dict iterator source, and (later) by `zip` / `enumerate` combinators.
211    /// Accessed via `.first` / `.second`, and destructurable in
212    /// `for (a, b) in ...` loops.
213    Pair(Shared<(VmValue, VmValue)>),
214    /// Capability handle threaded into `main(harness: Harness)`. The same
215    /// variant carries the root handle and each typed sub-handle (`stdio`,
216    /// `clock`, `fs`, `env`, `random`, `net`) so they share one value shape
217    /// but stay distinguishable via `VmHarness::kind`.
218    Harness(Shared<VmHarness>),
219}
220
221/// Process-wide interned `Arc<str>` for every single-byte ASCII character.
222///
223/// Materializing source text into per-character string values — the supported
224/// idiom for cursor-style scanners (`chars`, `char_at`, `s[i]`) — would
225/// otherwise heap-allocate once per character. Source files are overwhelmingly
226/// ASCII, so interning the 128 single-char strings lets those paths clone a
227/// cheap `Arc` (a refcount bump) instead of allocating, keeping a full-file
228/// scan linear with a low constant factor.
229static ASCII_CHAR_STRINGS: std::sync::LazyLock<[Arc<str>; 128]> = std::sync::LazyLock::new(|| {
230    std::array::from_fn(|byte| {
231        let mut buffer = [0u8; 4];
232        Arc::from((byte as u8 as char).encode_utf8(&mut buffer))
233    })
234});
235
236impl VmValue {
237    /// Canonical `VmValue::String` constructor from anything string-like.
238    ///
239    /// Collapses the ubiquitous `VmValue::String(std::sync::Arc::from(..))`
240    /// spelling to a single call and performs exactly one allocation via
241    /// `Arc::<str>::from(&str)` regardless of whether the input is a `&str`,
242    /// `String`, `&String`, or `Cow<str>`. Prefer this over hand-writing the
243    /// `Arc::from` at call sites.
244    pub fn string(value: impl AsRef<str>) -> Self {
245        VmValue::String(Arc::from(value.as_ref()))
246    }
247
248    /// Builds a `VmValue::String` holding a single character, reusing the
249    /// interned ASCII table (see [`ASCII_CHAR_STRINGS`]) so the common ASCII
250    /// path does not allocate.
251    pub fn char_value(ch: char) -> Self {
252        if ch.is_ascii() {
253            return VmValue::String(Arc::clone(&ASCII_CHAR_STRINGS[ch as usize]));
254        }
255        let mut buffer = [0u8; 4];
256        VmValue::String(Arc::from(ch.encode_utf8(&mut buffer)))
257    }
258
259    /// Materializes a string into a `VmValue::List` of single-character string
260    /// values in one linear pass. Backs both the `chars` builtin and the
261    /// `.chars()` method, and is the cursor-scanner-friendly counterpart to the
262    /// O(n)-per-call `substring` / slice / `s[i]` operations on a `string`.
263    pub fn chars_list(text: &str) -> Self {
264        VmValue::List(Shared::new(text.chars().map(VmValue::char_value).collect()))
265    }
266
267    pub fn enum_variant(
268        enum_name: impl Into<Shared<str>>,
269        variant: impl Into<Shared<str>>,
270        fields: Vec<VmValue>,
271    ) -> Self {
272        VmValue::EnumVariant(Shared::new(VmEnumVariant {
273            enum_name: enum_name.into(),
274            variant: variant.into(),
275            fields: Shared::new(fields),
276        }))
277    }
278
279    pub fn task_handle(id: impl Into<Shared<str>>) -> Self {
280        VmValue::TaskHandle(id.into())
281    }
282
283    /// Construct a boxed [`VmValue::Range`] from a [`VmRange`].
284    pub fn range(range: VmRange) -> Self {
285        VmValue::Range(Shared::new(range))
286    }
287
288    /// Construct a boxed [`VmValue::BuiltinRefId`] from its id and name.
289    pub fn builtin_ref_id(id: BuiltinId, name: impl Into<Shared<str>>) -> Self {
290        VmValue::BuiltinRefId(Shared::new(VmBuiltinRefId {
291            id,
292            name: name.into(),
293        }))
294    }
295
296    /// Construct a [`VmValue::Dict`] from any iterator of `(key, value)`
297    /// entries. Accepts the `BTreeMap` that most builders still assemble (it is
298    /// `IntoIterator<Item = (String, VmValue)>`) and collects it into the
299    /// persistent [`DictMap`], so callers keep their familiar map-building code
300    /// while the stored value gains structural sharing.
301    pub fn dict(entries: impl IntoIterator<Item = (String, VmValue)>) -> Self {
302        VmValue::Dict(Shared::new(entries.into_iter().collect::<DictMap>()))
303    }
304
305    /// Construct a [`VmValue::Dict`] from an already-built [`DictMap`].
306    pub fn dict_map(map: DictMap) -> Self {
307        VmValue::Dict(Shared::new(map))
308    }
309
310    /// Construct a [`VmValue::Set`] from any iterator of values, deduplicating
311    /// by structural equality and preserving first-seen insertion order.
312    pub fn set(values: impl IntoIterator<Item = VmValue>) -> Self {
313        VmValue::Set(Shared::new(values.into_iter().collect::<VmSet>()))
314    }
315
316    /// Construct a [`VmValue::Set`] from an already-built [`VmSet`].
317    pub fn set_value(set: VmSet) -> Self {
318        VmValue::Set(Shared::new(set))
319    }
320
321    pub fn channel(handle: VmChannelHandle) -> Self {
322        VmValue::Channel(Shared::new(handle))
323    }
324
325    pub fn atomic(handle: VmAtomicHandle) -> Self {
326        VmValue::Atomic(Shared::new(handle))
327    }
328
329    pub fn rng(handle: VmRngHandle) -> Self {
330        VmValue::Rng(Shared::new(handle))
331    }
332
333    pub fn sync_permit(handle: VmSyncPermitHandle) -> Self {
334        VmValue::SyncPermit(Shared::new(handle))
335    }
336
337    pub fn mcp_client(handle: VmMcpClientHandle) -> Self {
338        VmValue::McpClient(Shared::new(handle))
339    }
340
341    pub fn generator(generator: VmGenerator) -> Self {
342        VmValue::Generator(Shared::new(generator))
343    }
344
345    pub fn stream(stream: VmStream) -> Self {
346        VmValue::Stream(Shared::new(stream))
347    }
348
349    pub fn harness(handle: VmHarness) -> Self {
350        VmValue::Harness(Shared::new(handle))
351    }
352
353    pub fn struct_instance(
354        struct_name: impl Into<Shared<str>>,
355        fields: crate::value::DictMap,
356    ) -> Self {
357        Self::struct_instance_from_map(struct_name.into().to_string(), fields)
358    }
359
360    pub fn is_truthy(&self) -> bool {
361        match self {
362            VmValue::Bool(b) => *b,
363            VmValue::Nil => false,
364            VmValue::Int(n) => *n != 0,
365            VmValue::Float(n) => *n != 0.0,
366            VmValue::Decimal(d) => *d != rust_decimal::Decimal::ZERO,
367            VmValue::String(s) => !s.is_empty(),
368            VmValue::Bytes(bytes) => !bytes.is_empty(),
369            VmValue::List(l) => !l.is_empty(),
370            VmValue::Dict(d) => !d.is_empty(),
371            VmValue::Closure(_) => true,
372            VmValue::BuiltinRef(_) => true,
373            VmValue::BuiltinRefId(_) => true,
374            VmValue::Duration(ms) => *ms != 0,
375            VmValue::EnumVariant(_) => true,
376            VmValue::StructInstance { .. } => true,
377            VmValue::TaskHandle(_) => true,
378            VmValue::Channel(_) => true,
379            VmValue::Atomic(_) => true,
380            VmValue::Rng(_) => true,
381            VmValue::SyncPermit(_) => true,
382            VmValue::McpClient(_) => true,
383            VmValue::Set(s) => !s.is_empty(),
384            VmValue::Generator(_) => true,
385            VmValue::Stream(_) => true,
386            // Match Python semantics: range objects are always truthy,
387            // even the empty range (analogous to generators / iterators).
388            VmValue::Range(_) => true,
389            VmValue::Iter(_) => true,
390            VmValue::Pair(_) => true,
391            VmValue::Harness(_) => true,
392        }
393    }
394
395    pub fn type_name(&self) -> &'static str {
396        match self {
397            VmValue::String(_) => "string",
398            VmValue::Bytes(_) => "bytes",
399            VmValue::Int(_) => "int",
400            VmValue::Float(_) => "float",
401            VmValue::Decimal(_) => "decimal",
402            VmValue::Bool(_) => "bool",
403            VmValue::Nil => "nil",
404            VmValue::List(_) => "list",
405            VmValue::Dict(_) => "dict",
406            VmValue::Closure(_) => "closure",
407            VmValue::BuiltinRef(_) => "builtin",
408            VmValue::BuiltinRefId(_) => "builtin",
409            VmValue::Duration(_) => "duration",
410            VmValue::EnumVariant(_) => "enum",
411            VmValue::StructInstance { .. } => "struct",
412            VmValue::TaskHandle(_) => "task_handle",
413            VmValue::Channel(_) => "channel",
414            VmValue::Atomic(_) => "atomic",
415            VmValue::Rng(_) => "rng",
416            VmValue::SyncPermit(_) => "sync_permit",
417            VmValue::McpClient(_) => "mcp_client",
418            VmValue::Set(_) => "set",
419            VmValue::Generator(_) => "generator",
420            VmValue::Stream(_) => "stream",
421            VmValue::Range(_) => "range",
422            VmValue::Iter(_) => "iter",
423            VmValue::Pair(_) => "pair",
424            VmValue::Harness(h) => h.type_name(),
425        }
426    }
427
428    /// Borrows the string contents without allocating when the value is
429    /// already a string. Non-string values are rendered with `display()`,
430    /// matching the coercion callers apply at string boundaries. Hot string
431    /// builtins (regex, split, contains) use this to avoid cloning the
432    /// subject text on every call.
433    pub fn as_str_cow(&self) -> std::borrow::Cow<'_, str> {
434        match self {
435            VmValue::String(s) => std::borrow::Cow::Borrowed(s),
436            other => std::borrow::Cow::Owned(other.display()),
437        }
438    }
439
440    pub fn struct_name(&self) -> Option<&str> {
441        match self {
442            VmValue::StructInstance { layout, .. } => Some(layout.struct_name()),
443            _ => None,
444        }
445    }
446
447    pub fn struct_field(&self, field_name: &str) -> Option<&VmValue> {
448        match self {
449            VmValue::StructInstance { layout, fields } => layout
450                .field_index(field_name)
451                .and_then(|index| fields.get(index))
452                .and_then(Option::as_ref),
453            _ => None,
454        }
455    }
456
457    pub fn struct_fields_map(&self) -> Option<crate::value::DictMap> {
458        match self {
459            VmValue::StructInstance { layout, fields } => {
460                Some(struct_fields_to_map(layout, fields))
461            }
462            _ => None,
463        }
464    }
465
466    pub fn struct_instance_from_map(
467        struct_name: impl Into<String>,
468        fields: crate::value::DictMap,
469    ) -> Self {
470        let layout = Shared::new(StructLayout::from_map(struct_name, &fields));
471        let slots = layout
472            .field_names()
473            .iter()
474            .map(|name| fields.get(name).cloned())
475            .collect();
476        VmValue::StructInstance {
477            layout,
478            fields: Shared::new(slots),
479        }
480    }
481
482    pub fn struct_instance_with_layout(
483        struct_name: impl Into<String>,
484        field_names: Vec<String>,
485        field_values: crate::value::DictMap,
486    ) -> Self {
487        let layout = Shared::new(StructLayout::new(struct_name, field_names));
488        let fields = layout
489            .field_names()
490            .iter()
491            .map(|name| field_values.get(name).cloned())
492            .collect();
493        VmValue::StructInstance {
494            layout,
495            fields: Shared::new(fields),
496        }
497    }
498
499    pub fn struct_instance_with_property(&self, field_name: &str, value: VmValue) -> Option<Self> {
500        let VmValue::StructInstance { layout, fields } = self else {
501            return None;
502        };
503
504        let mut new_fields = fields.as_ref().clone();
505        let layout = match layout.field_index(field_name) {
506            Some(index) => {
507                if index >= new_fields.len() {
508                    new_fields.resize(index + 1, None);
509                }
510                new_fields[index] = Some(value);
511                Shared::clone(layout)
512            }
513            None => {
514                let new_layout = Shared::new(layout.with_appended_field(field_name.to_string()));
515                new_fields.push(Some(value));
516                new_layout
517            }
518        };
519
520        Some(VmValue::StructInstance {
521            layout,
522            fields: Shared::new(new_fields),
523        })
524    }
525
526    pub fn display(&self) -> String {
527        let mut out = String::new();
528        self.write_display(&mut out);
529        out
530    }
531
532    /// Writes the display representation directly into `out`,
533    /// avoiding intermediate Vec<String> allocations for collections.
534    pub fn write_display(&self, out: &mut String) {
535        use std::fmt::Write;
536
537        match self {
538            VmValue::Int(n) => {
539                let _ = write!(out, "{n}");
540            }
541            VmValue::Float(n) => {
542                if *n == (*n as i64) as f64 && n.abs() < 1e15 {
543                    let _ = write!(out, "{n:.1}");
544                } else {
545                    let _ = write!(out, "{n}");
546                }
547            }
548            // Render the decimal at its stored scale (e.g. `1.50` stays `1.50`),
549            // which is what money formatting expects. Equality normalizes scale,
550            // so `1.5` and `1.50` are still equal even though they display
551            // differently.
552            VmValue::Decimal(d) => {
553                let _ = write!(out, "{d}");
554            }
555            VmValue::String(s) => out.push_str(s),
556            VmValue::Bytes(bytes) => {
557                const MAX_PREVIEW_BYTES: usize = 32;
558
559                out.push_str("b\"");
560                for byte in bytes.iter().take(MAX_PREVIEW_BYTES) {
561                    let _ = write!(out, "{byte:02x}");
562                }
563                if bytes.len() > MAX_PREVIEW_BYTES {
564                    let _ = write!(out, "...+{}", bytes.len() - MAX_PREVIEW_BYTES);
565                }
566                out.push('"');
567            }
568            VmValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
569            VmValue::Nil => out.push_str("nil"),
570            VmValue::List(items) => {
571                out.push('[');
572                crate::value::recursion::guard_recursion(|| {
573                    for (i, item) in items.iter().enumerate() {
574                        if i > 0 {
575                            out.push_str(", ");
576                        }
577                        item.write_display(out);
578                    }
579                });
580                out.push(']');
581            }
582            VmValue::Dict(map) => {
583                out.push('{');
584                crate::value::recursion::guard_recursion(|| {
585                    for (i, (k, v)) in map.iter().enumerate() {
586                        if i > 0 {
587                            out.push_str(", ");
588                        }
589                        out.push_str(k);
590                        out.push_str(": ");
591                        v.write_display(out);
592                    }
593                });
594                out.push('}');
595            }
596            VmValue::Closure(c) => {
597                let names: Vec<&str> = c.func.param_names().collect();
598                let _ = write!(out, "<fn({})>", names.join(", "));
599            }
600            VmValue::BuiltinRef(name) => {
601                let _ = write!(out, "<builtin {name}>");
602            }
603            VmValue::BuiltinRefId(r) => {
604                let _ = write!(out, "<builtin {}>", r.name);
605            }
606            VmValue::Duration(ms) => {
607                let sign = if *ms < 0 { "-" } else { "" };
608                let abs_ms = ms.unsigned_abs();
609                if abs_ms >= 604_800_000 && abs_ms % 604_800_000 == 0 {
610                    let _ = write!(out, "{}{}w", sign, abs_ms / 604_800_000);
611                } else if abs_ms >= 86_400_000 && abs_ms % 86_400_000 == 0 {
612                    let _ = write!(out, "{}{}d", sign, abs_ms / 86_400_000);
613                } else if abs_ms >= 3_600_000 && abs_ms % 3_600_000 == 0 {
614                    let _ = write!(out, "{}{}h", sign, abs_ms / 3_600_000);
615                } else if abs_ms >= 60_000 && abs_ms % 60_000 == 0 {
616                    let _ = write!(out, "{}{}m", sign, abs_ms / 60_000);
617                } else if abs_ms >= 1000 && abs_ms % 1000 == 0 {
618                    let _ = write!(out, "{}{}s", sign, abs_ms / 1000);
619                } else {
620                    let _ = write!(out, "{sign}{abs_ms}ms");
621                }
622            }
623            VmValue::EnumVariant(enum_variant) => {
624                if enum_variant.fields.is_empty() {
625                    let _ = write!(out, "{}.{}", enum_variant.enum_name, enum_variant.variant);
626                } else {
627                    let _ = write!(out, "{}.{}(", enum_variant.enum_name, enum_variant.variant);
628                    crate::value::recursion::guard_recursion(|| {
629                        for (i, v) in enum_variant.fields.iter().enumerate() {
630                            if i > 0 {
631                                out.push_str(", ");
632                            }
633                            v.write_display(out);
634                        }
635                    });
636                    out.push(')');
637                }
638            }
639            VmValue::StructInstance { layout, fields } => {
640                let _ = write!(out, "{} {{", layout.struct_name());
641                crate::value::recursion::guard_recursion(|| {
642                    for (i, (k, v)) in struct_fields_to_map(layout, fields).iter().enumerate() {
643                        if i > 0 {
644                            out.push_str(", ");
645                        }
646                        out.push_str(k);
647                        out.push_str(": ");
648                        v.write_display(out);
649                    }
650                });
651                out.push('}');
652            }
653            VmValue::TaskHandle(id) => {
654                let _ = write!(out, "<task:{id}>");
655            }
656            VmValue::Channel(ch) => {
657                let _ = write!(out, "<channel:{}>", ch.name);
658            }
659            VmValue::Atomic(a) => {
660                let _ = write!(out, "<atomic:{}>", a.value.load(Ordering::SeqCst));
661            }
662            VmValue::Rng(_) => {
663                out.push_str("<rng>");
664            }
665            VmValue::SyncPermit(p) => {
666                let _ = write!(out, "<sync_permit:{}:{}>", p.kind(), p.key());
667            }
668            VmValue::McpClient(c) => {
669                let _ = write!(out, "<mcp_client:{}>", c.name);
670            }
671            VmValue::Set(items) => {
672                out.push_str("set(");
673                crate::value::recursion::guard_recursion(|| {
674                    for (i, item) in items.iter().enumerate() {
675                        if i > 0 {
676                            out.push_str(", ");
677                        }
678                        item.write_display(out);
679                    }
680                });
681                out.push(')');
682            }
683            VmValue::Generator(g) => {
684                if g.is_done() {
685                    out.push_str("<generator (done)>");
686                } else {
687                    out.push_str("<generator>");
688                }
689            }
690            VmValue::Stream(s) => {
691                if s.is_done() {
692                    out.push_str("<stream (done)>");
693                } else {
694                    out.push_str("<stream>");
695                }
696            }
697            // Print form mirrors source syntax: `1 to 5` / `0 to 3 exclusive`.
698            // `.to_list()` is the explicit path to materialize for display.
699            VmValue::Range(r) => {
700                let _ = write!(out, "{} to {}", r.start, r.end);
701                if !r.inclusive {
702                    out.push_str(" exclusive");
703                }
704            }
705            VmValue::Iter(h) => {
706                if matches!(&*h.lock(), crate::vm::iter::VmIter::Exhausted) {
707                    out.push_str("<iter (exhausted)>");
708                } else {
709                    out.push_str("<iter>");
710                }
711            }
712            VmValue::Harness(h) => {
713                let _ = write!(out, "<{}>", h.type_name());
714            }
715            VmValue::Pair(p) => {
716                out.push('(');
717                crate::value::recursion::guard_recursion(|| {
718                    p.0.write_display(out);
719                    out.push_str(", ");
720                    p.1.write_display(out);
721                });
722                out.push(')');
723            }
724        }
725    }
726
727    /// Get the value as a [`DictMap`] reference, if it's a Dict.
728    pub fn as_dict(&self) -> Option<&DictMap> {
729        if let VmValue::Dict(d) = self {
730            Some(d)
731        } else {
732            None
733        }
734    }
735
736    pub fn as_int(&self) -> Option<i64> {
737        if let VmValue::Int(n) = self {
738            Some(*n)
739        } else {
740            None
741        }
742    }
743
744    pub fn as_bytes(&self) -> Option<&[u8]> {
745        if let VmValue::Bytes(bytes) = self {
746            Some(bytes.as_slice())
747        } else {
748            None
749        }
750    }
751}
752
753pub fn struct_fields_to_map(
754    layout: &StructLayout,
755    fields: &[Option<VmValue>],
756) -> crate::value::DictMap {
757    layout
758        .field_names()
759        .iter()
760        .enumerate()
761        .filter_map(|(index, name)| {
762            fields
763                .get(index)
764                .and_then(Option::as_ref)
765                .map(|value| (name.clone(), value.clone()))
766        })
767        .collect()
768}
769
770/// Sync builtin function for the VM.
771pub type VmBuiltinFn =
772    Arc<dyn Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync>;