pub enum Value {
I64(i64),
BigInt(Rc<BigInt>),
Symbol(Text),
Str(Text),
List(Vector<Value>),
Closure {
id: u64,
code: Rc<Lambda>,
applied: u8,
env: Env,
},
Map(HashTrieMap<Value, Value>),
Builtin {
op: Builtin,
args: Vec<Value>,
},
HostFn {
code: Rc<HostFn>,
args: Box<[Value]>,
},
Module(Rc<EnvNode>),
Iota {
home: Rc<EnvNode>,
index: u32,
},
Object(Rc<ObjectData>),
}Expand description
A runtime value.
Variants§
I64(i64)
A machine-word integer — the canonical representation for any integer
that fits in i64. Inline (one word, no heap), so the common arithmetic
path neither allocates nor sets the enum width. The invariant “an integer
is Value::BigInt iff it does not fit i64” is what makes equality and
hashing well-defined (no value has two representations); it is maintained by
routing every wide result through Value::from_bigint.
BigInt(Rc<BigInt>)
An arbitrary-precision integer, used only for magnitudes outside i64.
Behind an Rc so the ~4-word BigInt does not widen Value (one word) and
cloning a wide value is a refcount bump rather than a digit-vector copy — the
big ints are immutable, so sharing is sound. A value here is always
|n| > i64::MAX by the canonical invariant (see Value::I64).
Symbol(Text)
A symbol literal, stored without its dot (foo for .foo) as an owned
cheap-clone Text; the dot is re-added when displayed.
Str(Text)
An owned, immutable, cheap-clone UTF-8 string (Text). Equality and order
are Rust’s &str (byte order over UTF-8 is Unicode scalar-value order, so
lexicographic comparison is a plain str::cmp). Rendered quoted / re-escaped.
List(Vector<Value>)
A runtime list of any arity, backed by an immutable rpds::Vector
(cheap structural-sharing clone / append, O(log n) random access). The
empty list is unit — there is no separate Unit value.
Closure
A closure capturing its defining environment over shared code. id is a
per-runtime unique identity minted when the closure is built (an
&-abstraction evaluating, or a partial application producing a new
closure), so closures compare by identity (two separately-created &x x
are distinct, and f x twice yields two unequal partials) rather than by
structure — the shared code Rc is invisible to __eq. code is the
abstraction’s Lambda (parameter patterns + body); applied counts the
parameters already bound into env, so the remaining ones are
code.head[applied..]. A partial application is one Rc::clone(code) plus
a bumped applied and an extended env — no per-partial Lambda
allocation. Applying the closure matches the next arguments against those
remaining patterns (see apply_n).
Map(HashTrieMap<Value, Value>)
An immutable, persistent, unordered map from keys to values, backed by
rpds::HashTrieMap (cheap structural-sharing with; unspecified iteration
order, stable within a build). Any value is a key; keys are identified by
structural value equality (see impl PartialEq / impl Hash). See the
maps section of docs/elly-spec.md.
Builtin
A partially-applied builtin: a native __… operation plus the arguments
gathered so far. Invoked once args.len() reaches the op’s arity.
HostFn
A partially-applied host function: a callback the embedder supplied
(HostFn) plus the arguments gathered so far, shaped exactly like
Builtin and fired the same way — so a host callback
curries, over-applies and partially applies like everything else, with no
application path of its own.
This is the whole FFI boundary in the calling direction, and it is a
value rather than an Expr variant deliberately: a host function
carries its own code, so it stays meaningful wherever it is handed. The
alternative — an index into a table on the Ctx — would misdispatch as
soon as a value outlived the context that made it, which a host building a
fresh Ctx per call does by design (see Ctx::with_ids).
Identity is the Rc pointer plus the applied arguments, matching
Builtin’s “same operation, equal arguments”. The name
Value::HostObj is deliberately left unused, for opaque host handles.
The applied prefix is a Box<[Value]> rather than the Vec the builtin
arm carries, for width: an Rc plus a Vec is four payload words and
would take Value from four words to five, which the size goldens pin
(tests/sizes.rs) and which docs/done/2026-08-13_elly-modules-env.md
measured the cost of. It is not a slower shape — the builtin path clones
its Vec on every partial application, and Vec::clone allocates
capacity == len, so both are one allocation per curried step.
Module(Rc<EnvNode>)
A module instance: the module’s own Module
terminal — the environment its item bodies run in. The value is the
terminal rather than holding a ModuleData
beside one, so a module’s code and the environment it was instantiated
against cannot be recombined, and materializing an item reuses this node
instead of allocating a fresh one. One payload word, so Value stays four
words. Applied to a symbol it materializes that item (see
apply1/apply_n). Identity is the Rc pointer (like a closure’s
identity), not the item contents — two separate loads compare unequal; a
content-addressed brand is deferred (see
docs/done/2026-08-13_elly-modules-env.md).
Iota
An iota: an atomic identity a module declared with const, equal to
itself and to nothing else. It has no structure and no payload, so there
is nothing to read out of it and nothing outside the module can conjure
one — reaching it means reaching the module.
Identity is the pair (instance, position): home is the instance’s
Module terminal — the very node a Value::Module
holds — and index the iota’s position in
ModuleData::iota_names. Nothing is minted and no id is reserved; the
value is derived whenever the declaration is referenced, so two
references to one iota of one instance are equal while two instantiations
of the same module mint disjoint sets. See
docs/done/2026-08-13_elly-modules-iotas.md.
Object(Rc<ObjectData>)
An object: a payload plus the prototype module that gives it its type
(see ObjectData). Built only by the __new leaf
of the module that owns the type, so a value of a type is minted by that
type’s own code and nowhere else.
Applied to a symbol it dispatches: the name is materialized out of the
prototype and applied to the object itself, so x.m a is the module’s m
with the receiver already bound (see apply1/apply_n). Compared by
contents over the payload and by identity over the prototype, so objects
are usable as map keys.
Implementations§
Source§impl Value
impl Value
Sourcepub fn from_bigint(n: BigInt) -> Value
pub fn from_bigint(n: BigInt) -> Value
Build an integer value from a BigInt, demoting to the inline
Value::I64 form whenever it fits. This is the single choke point that
maintains the canonical invariant — an integer is Value::BigInt iff it
does not fit i64 — on which PartialEq, Hash, and ordering all rely, so
every wide arithmetic result must pass through here. Also the constructor
hosts (the Python bindings) use to admit an arbitrary BigInt.
Sourcepub fn from_bigint_ref(n: &BigInt) -> Value
pub fn from_bigint_ref(n: &BigInt) -> Value
from_bigint for a BigInt the caller holds by
reference — an Expr::Int literal, which the AST owns and eval must not
consume. The i64 demotion is decided before any clone, so a literal that
fits costs no allocation at all; only a genuinely wide one is copied. Cloning
first and demoting after allocated a digit vector per literal evaluation and
dropped it immediately, on a path every arithmetic loop runs.
Sourcepub fn host_fn(
name: impl Into<Text>,
arity: usize,
f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static,
) -> Value
pub fn host_fn( name: impl Into<Text>, arity: usize, f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static, ) -> Value
Sourcepub fn list(elems: impl IntoIterator<Item = Value>) -> Value
pub fn list(elems: impl IntoIterator<Item = Value>) -> Value
A list value from its elements. The backing rpds::Vector is an
implementation detail a host should not have to name: matching its version
and features against the core’s is a real hazard (workspace feature
unification is global), so building one goes through here.
Sourcepub fn map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value
pub fn map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value
A map value from its entries, later entries winning on an equal key. Here
for the reason list is.
Sourcepub fn list_items(&self) -> Option<impl ExactSizeIterator<Item = &Value>>
pub fn list_items(&self) -> Option<impl ExactSizeIterator<Item = &Value>>
The elements of a list value, or None for anything else — the reading
counterpart of list, so a host can take a list apart
without naming rpds either. The iterator knows its length, which is
usually the first thing a caller checks.
Sourcepub fn remaining_arity(&self) -> Option<usize>
pub fn remaining_arity(&self) -> Option<usize>
How many more arguments a function-shaped value wants before it runs:
a closure’s unbound parameters, or a builtin’s or host function’s
ungathered ones. None for everything else, including the values that
are applicable without being functions — a list and a module both take a
symbol, but that is projection, not a call with arguments outstanding.
A runner uses it to notice that it under-applied: __main = &(io, x) …
applied to one capability yields a partial, and the program quietly does
nothing unless someone says so (see docs/done/2026-08-14_elly-run.md).
Sourcepub fn module_data(&self) -> Option<&Rc<ModuleData>>
pub fn module_data(&self) -> Option<&Rc<ModuleData>>
The compiled module behind a Value::Module, or None for anything
else. A module value holds its Module terminal rather
than the ModuleData directly, so a host that only wants the item table
(the Python bindings’ Obj.keys / in) reaches it through here instead of
matching on the environment’s shape.
Sourcepub fn object_data(&self) -> Option<&Rc<ObjectData>>
pub fn object_data(&self) -> Option<&Rc<ObjectData>>
The ObjectData behind a Value::Object, or None for anything else
— the payload and the prototype, for a host that wants to look inside one
(the Python bindings’ Object.data / .proto).
Sourcepub fn to_display(&self) -> String
pub fn to_display(&self) -> String
A human-readable rendering used by the eval golden tests.
Trait Implementations§
Source§impl Hash for Value
A Hash consistent with the structural PartialEq above — required because a
Value is a HashTrieMap key. A per-variant discriminant is mixed in first,
so the symbol .0, the integer 0, and the string "0" cannot collide by
construction. A Map key hashes order-independently (each entry folded
through a fresh sub-hasher, the results summed) so two equal maps built in
different insertion orders — or with hash-colliding keys — still hash equal.
impl Hash for Value
A Hash consistent with the structural PartialEq above — required because a
Value is a HashTrieMap key. A per-variant discriminant is mixed in first,
so the symbol .0, the integer 0, and the string "0" cannot collide by
construction. A Map key hashes order-independently (each entry folded
through a fresh sub-hasher, the results summed) so two equal maps built in
different insertion orders — or with hash-colliding keys — still hash equal.
Source§impl PartialEq for Value
Structural value equality, eliminated by __eq (see the equality section of
docs/elly-spec.md). Data compares structurally (integers mathematically,
symbols/strings by text, lists elementwise, maps by content regardless of
build order); callables compare by identity — builtins by operation then
applied arguments, closures by their unique id. Values of different kinds are
never equal. There is no total order over all values: the < / > ordering
patterns compare only within Int / Str (see order_match).
impl PartialEq for Value
Structural value equality, eliminated by __eq (see the equality section of
docs/elly-spec.md). Data compares structurally (integers mathematically,
symbols/strings by text, lists elementwise, maps by content regardless of
build order); callables compare by identity — builtins by operation then
applied arguments, closures by their unique id. Values of different kinds are
never equal. There is no total order over all values: the < / > ordering
patterns compare only within Int / Str (see order_match).