Skip to main content

elly_core/
value.rs

1//! Runtime [`Value`] and payload types: [`ObjectData`] and [`HostFn`].
2//! Includes value equality, hashing, and human-readable rendering. Split from the
3//! evaluator, which drives these via `apply1`/`apply_n` and owns the
4//! environment types ([`EnvNode`], [`Raised`]); the realm is [`Ctx`].
5
6use alloc::boxed::Box;
7use alloc::rc::Rc;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10use core::hash::{Hash, Hasher};
11
12use num_bigint::BigInt;
13use rpds::{HashTrieMap, Vector};
14
15use crate::code::{json_str, push_sym, Text};
16use crate::code::{ChunkId, LambdaId};
17use crate::sym::Sym;
18
19use super::{Builtin, Ctx, Env, EnvNode, EnvRef, Raised};
20
21/// An object: a payload and the prototype that defines its type.
22/// A [`Value::Object`] is an `Rc` pointer to `ObjectData`.
23///
24/// `proto` is the [`Module`](EnvNode::Module) terminal that is the instance; methods
25/// dispatched on the object run in this environment. This is also the handle
26/// used by [`Value::Iota`]'s `home`.
27///
28/// `data` is a single-slot payload. Records with named fields are not yet implemented;
29/// maps are the current idiom. Only the module's own code can access this via
30/// [`__value`](crate::HomeLeaf::Value).
31#[derive(Debug)]
32pub struct ObjectData {
33    pub(crate) proto: EnvRef,
34    pub(crate) data: Value,
35}
36
37impl ObjectData {
38    /// The object's prototype, as the module value it is.
39    pub fn proto(&self) -> Value {
40        Value::Module(self.proto.clone())
41    }
42
43    /// The object's payload. Public to Rust because a debugger and an embedding
44    /// host sit outside the language's module scope; no *builtin* hands it out,
45    /// so Elly code cannot reach it except through its own module's `__value`.
46    pub fn data(&self) -> &Value {
47        &self.data
48    }
49}
50
51/// A runtime value.
52///
53/// On 64-bit targets `Value` is aligned to 16 bytes: it is already 32 bytes
54/// wide, so this costs it nothing, and its 16-byte halves never straddle a
55/// cache line. The `EnvNode` cons cell grows from 40 to 48 bytes (its `Rc`
56/// allocation from 56 to 64, which the system allocator rounds 56 up to
57/// anyway), for 3–7% on the environment-heavy benches. On wasm32 `Value` is 24
58/// bytes, and the alignment would pad it to 32 and cost 5–16%, so it is left off.
59#[derive(Debug, Clone)]
60#[cfg_attr(target_pointer_width = "64", repr(align(16)))]
61pub enum Value {
62    /// Machine-word integer: the canonical representation for any integer fitting
63    /// in `i64`. Stored inline to avoid allocation on common arithmetic paths.
64    /// The invariant "an integer is [`Value::BigInt`] iff it does not fit `i64`"
65    /// ensures well-defined equality and hashing.
66    I64(i64),
67    /// Arbitrary-precision integer for magnitudes outside `i64`. Stored behind
68    /// an `Rc` to keep `Value` width small and make cloning a refcount bump.
69    /// Always `|n| > i64::MAX` per the canonical invariant.
70    BigInt(Rc<BigInt>),
71    /// A symbol (its text stored without the leading dot), interned in the
72    /// realm as a [`Sym`]. Rendered with a leading dot.
73    Symbol(Sym),
74    /// An owned, immutable UTF-8 string ([`Text`]). Equality and order follow
75    /// Rust's `&str` (lexicographic byte order). Rendered quoted and re-escaped.
76    Str(Text),
77    /// The unit value `()`. A nullary variant that does not widen `Value`.
78    /// Distinct from the empty list.
79    Unit,
80    /// A runtime list backed by an immutable `rpds::Vector` (O(log n) access,
81    /// cheap structural-sharing clone/append). The empty list is a `List` of
82    /// arity zero, not `Unit`.
83    List(Vector<Value>),
84    /// A closure capturing its environment over the code of `lambda` in
85    /// `chunk`. `id` is a per-runtime unique identity minted at creation;
86    /// closures compare by identity rather than structure. `applied` tracks
87    /// bound parameters of the lambda's `arity`, so the remaining ones are the
88    /// head's from `applied` on. Partial application extends `env` and
89    /// increments `applied`; the code is not touched.
90    ///
91    /// `arity` copies the head's length, so that [`remaining_arity`] needs no
92    /// [`Ctx`] to read the code.
93    ///
94    /// [`remaining_arity`]: Value::remaining_arity
95    Closure {
96        id: u64,
97        chunk: ChunkId,
98        lambda: LambdaId,
99        applied: u8,
100        arity: u8,
101        env: Env,
102    },
103    /// An immutable, persistent, unordered map from keys to values, backed by
104    /// `rpds::HashTrieMap`. Keys are identified by structural value equality.
105    /// See `docs/elly-spec.md` § maps.
106    Map(HashTrieMap<Value, Value>),
107    /// A partially-applied builtin: a native `__…` operation and its
108    /// gathered arguments. Invoked when `args.len()` reaches the op's arity.
109    Builtin { op: Builtin, args: Vec<Value> },
110    /// A partially-applied host function: a callback provided by the embedder
111    /// ([`HostFn`]) and its gathered arguments. This is the primary FFI boundary
112    /// in the calling direction.
113    ///
114    /// It is a `Value` rather than a node of code so it retains meaning regardless of
115    /// the context it is handed. Identity is the `Rc` pointer plus applied
116    /// arguments.
117    ///
118    /// Applied arguments use `Box<[Value]>` instead of `Vec` to keep `Value`
119    /// width at four words, as pinned by `tests/sizes.rs`. Both are one
120    /// allocation per curried step.
121    HostFn {
122        code: Rc<HostFn>,
123        args: Box<[Value]>,
124    },
125    /// A module instance: the module's [`Module`](EnvNode::Module) terminal.
126    /// The value is the terminal itself, ensuring the module's code and its
127    /// instantiation environment are linked. Applying a symbol materializes
128    /// the item. Identity is the `Rc` pointer.
129    Module(EnvRef),
130    /// An iota: an atomic identity declared with `const`. Equal only to itself.
131    /// Identity is the pair (instance, position), where `home` is the instance's
132    /// [`Module`](EnvNode::Module) terminal and `index` is the position in
133    /// the module's iotas. Derived upon reference; two references to the
134    /// same iota of one instance are equal.
135    Iota { home: EnvRef, index: u32 },
136    /// An object: a payload plus the prototype module defining its type
137    /// (see [`ObjectData`]). Built by the [`__new`](crate::HomeLeaf::New) leaf
138    /// of the owning module. Applying a symbol dispatches the name by materializing
139    /// it from the prototype and applying it to the object. Compared by payload
140    /// contents and prototype identity.
141    Object(Rc<ObjectData>),
142}
143
144/// Structural value equality used by `__eq`. Data compares structurally
145/// (integers mathematically, symbols/strings by text, lists elementwise, maps by
146/// content); callables compare by identity (builtins by operation and arguments,
147/// closures by unique id). Different variants are never equal. Total order is not
148/// defined; `<` / `>` only compare `Int` / `Str`.
149impl PartialEq for Value {
150    fn eq(&self, other: &Self) -> bool {
151        match (self, other) {
152            (Value::I64(a), Value::I64(b)) => a == b,
153            (Value::BigInt(a), Value::BigInt(b)) => a == b,
154            // An `I64` and a `BigInt` never compare equal: by the canonical
155            // invariant they partition the integers (a `BigInt` never fits `i64`),
156            // so they cannot denote the same number.
157            (Value::Symbol(a), Value::Symbol(b)) => a == b,
158            (Value::Str(a), Value::Str(b)) => a == b,
159            // Unit is a singleton — one value, so any two are equal.
160            (Value::Unit, Value::Unit) => true,
161            (Value::List(a), Value::List(b)) => a == b,
162            // `HashTrieMap`'s own `PartialEq` is content-based (order-independent).
163            (Value::Map(a), Value::Map(b)) => a == b,
164            // Same operation and equal applied arguments.
165            (Value::Builtin { op: o1, args: a1 }, Value::Builtin { op: o2, args: a2 }) => {
166                o1 == o2 && a1 == a2
167            }
168            // The same reading for a host callback, with the allocation standing
169            // in for the operation: a `HostFn` has no structure to compare, so
170            // two of one name are two functions.
171            (
172                Value::HostFn {
173                    code: c1, args: a1, ..
174                },
175                Value::HostFn {
176                    code: c2, args: a2, ..
177                },
178            ) => Rc::ptr_eq(c1, c2) && a1 == a2,
179            (Value::Closure { id: i1, .. }, Value::Closure { id: i2, .. }) => i1 == i2,
180            // A module value is its instance's identity: the terminal's `Rc`
181            // pointer, not the item contents (mirrors the closure "compare by
182            // identity" precedent).
183            (Value::Module(a), Value::Module(b)) => EnvRef::ptr_eq(a, b),
184            // An iota is the instance it was declared by plus its position, so
185            // two iotas of one instance differ and one iota of two instantiations
186            // does too.
187            (
188                Value::Iota {
189                    home: a, index: i, ..
190                },
191                Value::Iota {
192                    home: b, index: j, ..
193                },
194            ) => i == j && EnvRef::ptr_eq(a, b),
195            // An object is its prototype by *identity* and its payload by
196            // *contents* — the nominal/structural mix objects are for. The payload
197            // half is what makes two accesses of one data item compare equal, and
198            // what lets an object be a map key.
199            (Value::Object(a), Value::Object(b)) => {
200                EnvRef::ptr_eq(&a.proto, &b.proto) && a.data == b.data
201            }
202            _ => false,
203        }
204    }
205}
206
207impl Eq for Value {}
208
209/// A `Hash` consistent with structural `PartialEq`. A per-variant discriminant
210/// prevents collisions between different types (e.g. symbol `.0` and integer `0`).
211/// `Map` keys hash order-independently by summing the hash of entries.
212impl Hash for Value {
213    fn hash<H: Hasher>(&self, state: &mut H) {
214        core::mem::discriminant(self).hash(state);
215        match self {
216            Value::I64(n) => n.hash(state),
217            Value::BigInt(n) => n.hash(state),
218            Value::Symbol(s) => s.hash(state),
219            Value::Str(s) => s.hash(state),
220            // Unit carries no payload; the discriminant above is its whole hash.
221            Value::Unit => {}
222            Value::List(vs) => {
223                for v in vs.iter() {
224                    v.hash(state);
225                }
226            }
227            Value::Map(m) => {
228                let mut acc: u64 = 0;
229                for (k, v) in m.iter() {
230                    let mut h = FnvHasher::new();
231                    k.hash(&mut h);
232                    v.hash(&mut h);
233                    acc = acc.wrapping_add(h.finish());
234                }
235                acc.hash(state);
236            }
237            Value::Builtin { op, args } => {
238                op.hash(state);
239                for a in args {
240                    a.hash(state);
241                }
242            }
243            Value::HostFn { code, args } => {
244                (Rc::as_ptr(code) as usize).hash(state);
245                for a in args {
246                    a.hash(state);
247                }
248            }
249            Value::Closure { id, .. } => id.hash(state),
250            // Hash by the same identity used for equality: the terminal's pointer.
251            Value::Module(home) => (EnvRef::as_ptr(home) as usize).hash(state),
252            Value::Iota { home, index } => {
253                (EnvRef::as_ptr(home) as usize).hash(state);
254                index.hash(state);
255            }
256            // Mirrors the equality above: the prototype's pointer, the payload's
257            // contents.
258            Value::Object(obj) => {
259                (EnvRef::as_ptr(&obj.proto) as usize).hash(state);
260                obj.data.hash(state);
261            }
262        }
263    }
264}
265
266/// A deterministic no_std FNV-1a `Hasher` used for order-independent map
267/// hashing (see `impl Hash for Value`) and by the symbol store's index. Not the
268/// hasher used by `HashTrieMap`.
269struct FnvHasher(u64);
270
271/// The FNV-1a hash of `bytes`.
272pub(crate) fn fnv1a(bytes: &[u8]) -> u64 {
273    let mut h = FnvHasher::new();
274    h.write(bytes);
275    h.finish()
276}
277
278impl FnvHasher {
279    fn new() -> Self {
280        FnvHasher(0xcbf2_9ce4_8422_2325)
281    }
282}
283
284impl Hasher for FnvHasher {
285    fn finish(&self) -> u64 {
286        self.0
287    }
288    fn write(&mut self, bytes: &[u8]) {
289        for &b in bytes {
290            self.0 ^= u64::from(b);
291            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
292        }
293    }
294}
295
296/// A host function: a native callback provided by the embedder, behind the `Rc`
297/// a [`Value::HostFn`] carries.
298///
299/// It includes a name, arity, and code. The evaluator gathers arguments according
300/// to `arity` before calling the function, making it strict and curried.
301///
302/// `Ctx` is passed to allow the callback to call Elly functions ([`apply_with`](crate::apply_with))
303/// using the same ID source, preventing identity collisions for minted closures.
304///
305/// Arity must be at least 1; zero-argument constants are supplied as values.
306pub struct HostFn {
307    pub(crate) name: Text,
308    pub(crate) arity: usize,
309    pub(crate) f: Box<HostCall>,
310}
311
312/// The contract a host callback signs: a whole call's arguments — exactly
313/// [`arity`](HostFn::arity) of them, since the evaluator gathers before it fires
314/// — plus the [`Ctx`], answering a value or a [`Raised`].
315pub type HostCall = dyn Fn(&[Value], &Ctx) -> Result<Value, Raised>;
316
317impl HostFn {
318    /// A host function taking `arity` arguments.
319    ///
320    /// Name is for diagnostics; identity is the allocation. Panics on `arity == 0`.
321    pub fn new(
322        name: impl Into<Text>,
323        arity: usize,
324        f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static,
325    ) -> HostFn {
326        assert!(arity > 0, "a host function takes at least one argument");
327        HostFn {
328            name: name.into(),
329            arity,
330            f: Box::new(f),
331        }
332    }
333
334    /// The name the function renders under.
335    pub fn name(&self) -> &Text {
336        &self.name
337    }
338
339    /// How many arguments it is called with.
340    pub fn arity(&self) -> usize {
341        self.arity
342    }
343}
344
345/// The callback is not `Debug`, so the derive cannot apply; the shape a
346/// `Value::HostFn` shows is its name and arity, which is what identifies it in a
347/// dump anyway.
348impl core::fmt::Debug for HostFn {
349    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
350        f.debug_struct("HostFn")
351            .field("name", &self.name)
352            .field("arity", &self.arity)
353            .finish_non_exhaustive()
354    }
355}
356
357impl Value {
358    /// Builds an integer value from a `BigInt`, demoting to [`Value::I64`] if it
359    /// fits. This maintains the canonical invariant that a `BigInt` is used only
360    /// when the value does not fit `i64`, ensuring consistent equality and hashing.
361    pub fn from_bigint(n: BigInt) -> Value {
362        match i64::try_from(&n) {
363            Ok(i) => Value::I64(i),
364            Err(_) => Value::BigInt(Rc::new(n)),
365        }
366    }
367
368    /// [`from_bigint`](Value::from_bigint) for a `BigInt` reference. Demotion
369    /// to `i64` is decided before cloning to avoid unnecessary allocations
370    /// for literals fitting in `i64`.
371    pub fn from_bigint_ref(n: &BigInt) -> Value {
372        match i64::try_from(n) {
373            Ok(i) => Value::I64(i),
374            Err(_) => Value::BigInt(Rc::new(n.clone())),
375        }
376    }
377
378    /// Creates a `Value::HostFn` from a callback. Unapplied, so the first
379    /// argument begins the currying process. Panics on `arity == 0`.
380    pub fn host_fn(
381        name: impl Into<Text>,
382        arity: usize,
383        f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static,
384    ) -> Value {
385        Value::HostFn {
386            code: Rc::new(HostFn::new(name, arity, f)),
387            args: Box::new([]),
388        }
389    }
390
391    /// Creates a list value from its elements. Hides the backing `rpds::Vector`
392    /// to avoid version/feature mismatches between the core and the host.
393    pub fn list(elems: impl IntoIterator<Item = Value>) -> Value {
394        Value::List(elems.into_iter().collect())
395    }
396
397    /// Creates a map value from its entries; later entries overwrite equal keys.
398    /// Hides `rpds` implementation details.
399    pub fn map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value {
400        let mut m = HashTrieMap::new();
401        for (k, v) in entries {
402            m.insert_mut(k, v);
403        }
404        Value::Map(m)
405    }
406
407    /// Returns the elements of a list value, or `None` otherwise. Hides
408    /// `rpds` implementation details.
409    pub fn list_items(&self) -> Option<impl ExactSizeIterator<Item = &Value>> {
410        match self {
411            Value::List(elems) => Some(elems.iter()),
412            _ => None,
413        }
414    }
415
416    /// Returns the number of arguments a function-shaped value needs before it
417    /// runs. `None` for non-functions, including applicable values like lists
418    /// or modules (which use projection, not calls).
419    ///
420    /// Runners use this to detect under-application (see `docs/done/2026-08-14_elly-run.md`).
421    pub fn remaining_arity(&self) -> Option<usize> {
422        match self {
423            Value::Closure { applied, arity, .. } => Some(usize::from(arity - applied)),
424            Value::Builtin { op, args } => Some(op.arity() - args.len()),
425            Value::HostFn { code, args } => Some(code.arity - args.len()),
426            _ => None,
427        }
428    }
429
430    /// The item names of a [`Value::Module`], sorted by `Sym`, or `None` for
431    /// any other value. Locals and iotas are not items.
432    pub fn item_names(&self, ctx: &Ctx) -> Option<Vec<Sym>> {
433        match self {
434            Value::Module(home) => match **home {
435                EnvNode::Module { chunk, module, .. } => {
436                    Some(ctx.chunk(chunk).item_names(module).collect())
437                }
438                _ => unreachable!("a module value always holds a module terminal"),
439            },
440            _ => None,
441        }
442    }
443
444    /// Returns the [`ObjectData`] of a [`Value::Object`], or `None`. Provides
445    /// access to the payload and prototype.
446    pub fn object_data(&self) -> Option<&Rc<ObjectData>> {
447        match self {
448            Value::Object(obj) => Some(obj),
449            _ => None,
450        }
451    }
452
453    /// Returns a human-readable rendering for golden tests.
454    pub fn to_display(&self, ctx: &Ctx) -> String {
455        let mut out = String::new();
456        self.write_display(&mut out, ctx);
457        out
458    }
459
460    fn write_display(&self, out: &mut String, ctx: &Ctx) {
461        match self {
462            Value::I64(n) => out.push_str(&n.to_string()),
463            Value::BigInt(n) => out.push_str(&n.to_string()),
464            Value::Symbol(s) => {
465                out.push('.');
466                push_sym(*s, out, ctx);
467            }
468            // A string renders as a quoted, re-escaped literal (`"foo"`),
469            // distinguishing it from symbols and names.
470            Value::Str(s) => json_str(s, out),
471            // Unit renders `()`, distinct from the empty list `[]`.
472            Value::Unit => out.push_str("()"),
473            // Lists render with brackets (`[]`, `[.a]`, `[.a, .b]`).
474            Value::List(vs) => {
475                out.push('[');
476                for (i, v) in vs.iter().enumerate() {
477                    if i > 0 {
478                        out.push_str(", ");
479                    }
480                    v.write_display(out, ctx);
481                }
482                out.push(']');
483            }
484            // Maps render sorted by rendered key text (ties broken by value)
485            // for stable dumps: `{}` or `{ .a: 1, .b: 2 }`.
486            Value::Map(m) => {
487                if m.is_empty() {
488                    out.push_str("{}");
489                } else {
490                    let mut entries: Vec<(String, String)> = m
491                        .iter()
492                        .map(|(k, v)| {
493                            let mut ks = String::new();
494                            k.write_display(&mut ks, ctx);
495                            let mut vs = String::new();
496                            v.write_display(&mut vs, ctx);
497                            (ks, vs)
498                        })
499                        .collect();
500                    entries.sort();
501                    out.push('{');
502                    for (i, (ks, vs)) in entries.iter().enumerate() {
503                        out.push_str(if i > 0 { ", " } else { " " });
504                        out.push_str(ks);
505                        out.push_str(": ");
506                        out.push_str(vs);
507                    }
508                    out.push_str(" }");
509                }
510            }
511            Value::Closure { .. } => out.push_str("<closure>"),
512            Value::Builtin { op, .. } => {
513                out.push_str("<builtin ");
514                out.push_str(op.name());
515                out.push('>');
516            }
517            // Host functions render by name and arity.
518            Value::HostFn { code, .. } => {
519                out.push_str("<hostfn ");
520                out.push_str(code.name.as_str());
521                out.push('>');
522            }
523            Value::Module(_) => out.push_str("<module>"),
524            // Iotas render as `<const .name>`. The module instance is not
525            // named to avoid dependence on host-defined resolver paths.
526            Value::Iota { home, index } => {
527                out.push_str("<const .");
528                if let EnvNode::Module { chunk, module, .. } = **home {
529                    let name = ctx.chunk(chunk).iota(module, *index);
530                    if let Some(name) = name {
531                        push_sym(name, out, ctx);
532                    }
533                }
534                out.push('>');
535            }
536            // Objects render as `<object payload>`. The prototype is not
537            // named for the same reason as modules. The payload is shown to support
538            // debugging by hosts.
539            Value::Object(obj) => {
540                out.push_str("<object ");
541                obj.data.write_display(out, ctx);
542                out.push('>');
543            }
544        }
545    }
546}