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}