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}