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