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