value-lang 0.2.0

Runtime value representation (tagged unions / NaN-boxing).
Documentation

Performance First

Every operation an interpreter runs per instruction is sub-nanosecond. Latest local Criterion means (cargo bench --bench bench, Linux x86_64 / WSL2, Rust stable, release build):

Operation Time
Construct nil 0.09 ns
Construct int / bool 0.20 ns
Construct float 0.31 ns
is_int (kind test) 0.20 ns
as_int (read payload) 0.29 ns
unpack (per value) 0.32 ns
== (two ints) 0.34 ns
Size of Value 8 bytes

Numbers vary by CPU and environment; run the suite on your target to establish a baseline. A Value is Copy and register-sized, so passing one costs the same as passing a u64.

Features

  • One 8-byte typenil, bool, i32, f64, and interned Symbol, all in a single Copy word.
  • NaN-boxed — real floats stored as themselves; every other kind hidden in spare quiet-NaN payloads.
  • Fully safe — no unsafe, no pointers, no raw transmutes. #![forbid(unsafe_code)].
  • no_std, no alloc — pure integer and float arithmetic; runs anywhere.
  • Ergonomic and compact — build with Value::int(…), test with is_*, read with as_*, and match on the Unpacked view when you need every kind at once.
  • serde ready — opt-in Serialize/Deserialize for Value.
  • Property-tested invariants — round-trip and kind-exclusivity checked across randomized inputs with proptest.

Installation

[dependencies]
value-lang = "0.2"

# With serde support:
value-lang = { version = "0.2", features = ["serde"] }

# no_std (drops the std forwarding to intern-lang):
value-lang = { version = "0.2", default-features = false }

MSRV is 1.85+ (Rust 2024 edition).

Quick Start

use value_lang::{Unpacked, Value};

// One eight-byte type for every runtime value.
let stack = [
    Value::int(2),
    Value::float(0.5),
    Value::bool(true),
    Value::nil(),
];

// Test a single kind and read it back.
assert!(stack[0].is_int());
assert_eq!(stack[0].as_int(), Some(2));
assert_eq!(stack[1].as_float(), Some(0.5));

// Or match on every kind at once.
fn describe(v: Value) -> &'static str {
    match v.unpack() {
        Unpacked::Nil => "nil",
        Unpacked::Bool(_) => "bool",
        Unpacked::Int(_) => "int",
        Unpacked::Float(_) => "float",
        Unpacked::Sym(_) => "sym",
    }
}
assert_eq!(describe(stack[3]), "nil");

// A Value is register-sized and Copy.
assert_eq!(core::mem::size_of::<Value>(), 8);

Interned names

Strings and identifiers are carried as Symbol handles. value-lang stores the handle; the interner owns the bytes.

use intern_lang::Interner;
use value_lang::Value;

let mut interner = Interner::new();
let name = interner.intern("total");

let v = Value::sym(name);
assert_eq!(v.as_sym(), Some(name));
assert_eq!(interner.resolve(v.as_sym().unwrap()), Some("total"));

How NaN-boxing works

A 64-bit IEEE-754 double is sign(1) | exponent(11) | mantissa(52). A value is boxed — not a real float — when the exponent is all ones and the top two mantissa bits are set. No finite double or infinity matches that pattern, and every genuine NaN is folded onto one canonical encoding on the way in, so the boxed space belongs to value-lang alone.

Within a boxed word, a fixed quiet-NaN header sits in the high bits, a small kind tag names the kind, and the low 32 bits hold the payload: an i32 bit-for-bit, or a Symbol's non-zero id. Decoding is one mask to check the header and a compare on the tag. There are no branches on the copy path and no heap indirection.

Because floats keep IEEE-754 semantics, equality does too: two Float values compare with f64 rules — so NaN != NaN and 0.0 == -0.0 — while every other kind compares by bit pattern. Value therefore does not implement Eq/Hash; see docs/API.md for the details and how to build a key.

API Overview

For the complete reference with examples, see docs/API.md.

  • Value — the eight-byte NaN-boxed value; constructors, is_* predicates, as_* accessors, bits, unpack.
  • Unpacked — the tagged-union view for exhaustive match.
  • Symbol — the interned string handle a Value can carry (re-exported from intern-lang).

Feature Flags

Feature Default Description
std Forwards std to intern-lang. The crate itself is no_std.
serde Serialize / Deserialize for Value (symbols as their raw id).

Testing

cargo test                 # unit + property + doctests
cargo test --all-features  # adds the serde round-trip tests
cargo bench --bench bench  # Criterion hot-path benchmarks

The property suite in tests/roundtrip.rs checks the core invariants — pack/unpack is the identity, every f64 bit pattern classifies as a float, and the kinds are mutually exclusive — across randomized inputs.

Cross-Platform Support

The representation is pure arithmetic with no platform-specific code, so it behaves identically everywhere Rust runs. CI covers Linux, macOS, and Windows on both stable and the 1.85 MSRV.

Contributing

See REPS.md for the engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.