Lines
86.23 %
Functions
45.92 %
Branches
100 %
use core::fmt;
use num_rational::Ratio;
pub type Fraction = Ratio<i64>;
/// Kind tag for one server-entity wasm struct. Each variant maps to a
/// concrete `$<kind>` GC struct registered in
/// `CompileContext::new_skeleton`, plus a set of typed field accessors
/// in the native registry. Adding an entity adds one variant here +
/// one row in the `nomi_entity!` macro invocation; the compiler and
/// host-side allocators pick up the rest by expansion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntityKind {
Account,
Commodity,
Transaction,
Split,
Tag,
Price,
SshKey,
/// Hierarchical report node — recursive entity carrying the
/// per-account/per-period balance rows that the report family
/// of natives produces. `$report_node.children` is a
/// `pair<report_node>` chain, so `(dolist (n (node-children r))
/// ...)` walks the tree without leaving the typed lattice.
ReportNode,
/// Error condition raised by `(error 'code "msg")` and caught by
/// `(handler-case)`. Maps to the `$nomi_condition` struct registered
/// for exception support; the clause variable `e` binds at this kind so
/// `(error-code e)` / `(error-message e)` type-check.
Condition,
}
impl EntityKind {
/// Name of the wasm struct type the compiler registers in
/// `CompileContext::new_skeleton` for this entity kind. Keep in
/// lockstep with the `register_struct_type` calls there.
#[must_use]
pub fn type_name(self) -> &'static str {
match self {
Self::Account => "account",
Self::Commodity => "commodity_entity",
Self::Transaction => "transaction",
Self::Split => "split",
Self::Tag => "tag_entity",
Self::Price => "price",
Self::SshKey => "ssh_key",
Self::ReportNode => "report_node",
Self::Condition => "nomi_condition",
impl fmt::Display for EntityKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.type_name())
/// Element type of a homogeneous `$pair` chain. Tracked at compile time so
/// CAR/CDR emit the right downcast and CONS refuses heterogeneous mixing
/// with a structured type error. Kept as a `Copy` enum so `WasmType` stays
/// `Copy` — and so adding a new element type stays a one-line variant
/// addition. Nesting (pairs-of-pairs) waits for a follow-up sub-slice
/// once flat lists are stable across the consumers.
pub enum PairElement {
I32,
/// Truth-value cell: shares `I32`'s i31-boxed car representation (CAR/CDR
/// emit the same i31 downcast), but distinct so a bool extracted from a
/// list serializes as `Nil` / `Bool`, not `Number` — the list-element
/// analogue of [`WasmType::Bool`] vs `I32`.
Bool,
Ratio,
StringRef,
Entity(EntityKind),
/// Heterogeneous escape hatch (ADR-0025). Cars stay as raw
/// `anyref`; CAR returns `WasmType::AnyRef` and the script must
/// downcast at the use site (or consume via type-test natives like
/// `ok?` / `err-code`). CONS widens to this variant when the two
/// arms disagree on element type, and host fns that produce
/// fundamentally heterogeneous lists (e.g. =catch-each= result
/// cells) construct directly into `PairRef(AnyRef)`.
AnyRef,
impl fmt::Display for PairElement {
Self::I32 => write!(f, "i32"),
Self::Bool => write!(f, "bool"),
Self::Ratio => write!(f, "ratio"),
Self::Commodity => write!(f, "commodity"),
Self::StringRef => write!(f, "string"),
Self::Entity(kind) => write!(f, "{kind}"),
Self::AnyRef => write!(f, "any"),
impl PairElement {
/// The matching `WasmType` for a pair's car when extracted.
pub fn as_wasm_type(self) -> WasmType {
Self::I32 => WasmType::I32,
Self::Bool => WasmType::Bool,
Self::Ratio => WasmType::Ratio,
Self::Commodity => WasmType::Commodity,
Self::StringRef => WasmType::StringRef,
Self::Entity(kind) => WasmType::EntityRef(kind),
Self::AnyRef => WasmType::AnyRef,
/// Inverse of `as_wasm_type`: returns `None` if the given type can't
/// ride a `$pair`'s anyref car. `WasmType::AnyRef` round-trips to
/// `PairElement::AnyRef`; nested pairs and closures still need their
/// own follow-up lattice extension.
pub fn from_wasm_type(ty: WasmType) -> Option<Self> {
match ty {
WasmType::I32 => Some(Self::I32),
// `Bool` keeps its own slot — it shares `I32`'s i31-boxed car
// representation (the CAR/CDR downcast is identical) but stays
// distinct so a bool extracted from a list serializes as Nil/Bool,
// not Number.
WasmType::Bool => Some(Self::Bool),
WasmType::Ratio => Some(Self::Ratio),
WasmType::Commodity => Some(Self::Commodity),
WasmType::StringRef => Some(Self::StringRef),
WasmType::EntityRef(kind) => Some(Self::Entity(kind)),
WasmType::AnyRef => Some(Self::AnyRef),
WasmType::PairRef(_) | WasmType::Closure(_) => None,
/// Widen `self` against `other` to the most-specific common
/// element. Identical types stay; everything else widens to
/// `AnyRef` so heterogeneous CONS no longer errors. Symmetric.
pub fn widen(self, other: Self) -> Self {
if self == other { self } else { Self::AnyRef }
/// Identifier for a closure signature interned in the compile
/// context's closure registry. Distinct closures sharing the same
/// `(arg-types) -> ret-type` signature share an id (and therefore a
/// `$closure_<id>` wasm GC type); their env-struct types vary
/// independently per scope. The id is allocated in registration order
/// — small, dense, `Copy` — so it can ride inside `WasmType` without
/// growing the enum.
pub struct ClosureSigId(pub u32);
impl fmt::Display for ClosureSigId {
write!(f, "{}", self.0)
pub enum WasmType {
/// Boolean: a comparison / predicate / `and` / `or` result, or a runtime
/// `#t` / `#f`. Wasm representation is identical to `I32` (0 = false,
/// nonzero = true), so it composes with `if` / `br_if` and boxes via
/// `ref.i31` exactly like `I32`. It is a DISTINCT compile-time type only
/// so the debug serializer can surface it as `Nil` / `Bool` rather than
/// `Number`: an `I32` is a raw count (tag-count, split-count, …) that
/// serializes as a number, whereas a `Bool` is a truth value. Refused by
/// arithmetic like every non-numeric type.
/// Commodity-bearing numeric: rational amount + originating commodity
/// uuid. Distinct from `Ratio` so the compiler can refuse mixing money
/// and pure-rational arithmetic at compile time. Wasm representation:
/// `(i64 numer, i64 denom, i64 commodity_hi, i64 commodity_lo)`.
/// Homogeneous list cell: `$pair` WasmGC struct with an `anyref` car
/// and a nullable `$pair` cdr. The `PairElement` records the car's
/// type at compile time so CAR/CDR can emit the correct downcast and
/// CONS refuses heterogeneous mixing. Retires the i32-only `$cons` —
/// every runtime list shape in the compiler routes through this type.
PairRef(PairElement),
/// Raw `anyref` car payload extracted from a `PairRef(AnyRef)` or
/// produced by a host fn that returns a heterogeneous value
/// (ADR-0025). Refused by arithmetic / numeric comparison / closure
/// call / typed pair construction; the script must downcast via
/// type-test natives (e.g. `ok?`, `err-code`) before consuming.
/// Typed server-entity reference: a `(ref null $<kind>)` GC struct
/// whose field layout matches the `nomi_entity!` declaration for the
/// kind. Refused by arithmetic / list-CONS / numeric comparison; the
/// only legal operations are the entity-specific accessor natives
/// (`account-id`, `commodity-name`, etc.) registered alongside the
/// struct type.
EntityRef(EntityKind),
/// First-class closure value: `(ref null $closure_<sig>)` carrying a
/// typed funcref + nullable env ref. The `ClosureSigId` indexes the
/// per-context registry that owns the wasm types and per-call-site
/// env-struct layouts. Eligible for FUNCALL / APPLY via `call_ref`;
/// refused by arithmetic / numeric comparison / pair construction.
Closure(ClosureSigId),
impl fmt::Display for WasmType {
Self::PairRef(elem) => write!(f, "pair<{elem}>"),
Self::EntityRef(kind) => write!(f, "entity<{kind}>"),
Self::Closure(sig) => write!(f, "closure<{sig}>"),
#[derive(Debug, Clone, PartialEq)]
pub struct LambdaParams {
pub required: Vec<String>,
pub optional: Vec<(String, Option<Expr>)>,
pub rest: Option<String>,
pub key: Vec<(String, Option<Expr>)>,
pub aux: Vec<(String, Option<Expr>)>,
impl LambdaParams {
pub fn simple(params: Vec<String>) -> Self {
Self {
required: params,
optional: Vec::new(),
rest: None,
key: Vec::new(),
aux: Vec::new(),
pub enum Expr {
Nil,
Bool(bool),
Number(Fraction),
String(String),
Symbol(String),
Keyword(String),
Bytes(Vec<u8>),
Cons(Box<Expr>, Box<Expr>),
List(Vec<Expr>),
Quote(Box<Expr>),
Quasiquote(Box<Expr>),
Unquote(Box<Expr>),
UnquoteSplicing(Box<Expr>),
Lambda(LambdaParams, Box<Expr>),
RuntimeValue(crate::runtime::Value),
WasmRuntime(WasmType),
/// Value stored in WASM local variable (index, type)
WasmLocal(u32, WasmType),
impl Expr {
pub fn cons(car: Expr, cdr: Expr) -> Self {
Expr::Cons(Box::new(car), Box::new(cdr))
/// Returns the `WasmType` if this expression is a runtime value (`WasmRuntime` or `WasmLocal`).
pub fn wasm_type(&self) -> Option<WasmType> {
Self::WasmRuntime(ty) | Self::WasmLocal(_, ty) => Some(*ty),
_ => None,
/// True if this is a runtime value (`WasmRuntime` or `WasmLocal`).
pub fn is_wasm_runtime(&self) -> bool {
matches!(self, Self::WasmRuntime(_) | Self::WasmLocal(_, _))
pub struct Annotation {
pub name: String,
pub value: Expr,
pub fn is_atom(&self) -> bool {
!matches!(self, Expr::List(_) | Expr::Cons(_, _))
pub fn as_symbol(&self) -> Option<&str> {
Expr::Symbol(s) => Some(s),
pub fn as_list(&self) -> Option<&[Expr]> {
Expr::List(l) => Some(l),
/// Splits a `Symbol` into `(namespace, name)`. A qualified symbol is
/// stored canonically as `NS:NAME` (ADR-0029); an unqualified one has no
/// `:` and yields `(None, name)`. Non-symbols yield `None`. The split is
/// on the single canonical `:` — `NS::NAME` was already folded to `NS:NAME`
/// by the reader, so at most one `:` is ever present here.
pub fn symbol_parts(&self) -> Option<(Option<&str>, &str)> {
let name = self.as_symbol()?;
Some(match name.split_once(':') {
Some((ns, base)) => (Some(ns), base),
None => (None, name),
})
/// Builds the canonical symbol-table key for a (possibly namespaced) name.
/// `NS:NAME` when a namespace is present, bare `NAME` otherwise — the single
/// source of truth for how a qualified name maps to its flat table key.
pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
match namespace {
Some(ns) => format!("{ns}:{name}"),
None => name.to_string(),
#[derive(Debug, Clone, Default)]
pub struct Program {
pub exprs: Vec<Expr>,
pub annotations: Vec<Annotation>,
impl Program {
pub fn new(exprs: Vec<Expr>) -> Self {
exprs,
annotations: Vec::new(),
pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
Self { exprs, annotations }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expr_is_atom() {
assert!(Expr::Nil.is_atom());
assert!(Expr::Bool(true).is_atom());
assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
assert!(Expr::String("hello".into()).is_atom());
assert!(Expr::Symbol("foo".into()).is_atom());
assert!(Expr::Keyword("bar".into()).is_atom());
assert!(!Expr::List(vec![]).is_atom());
assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
fn test_expr_as_symbol() {
assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
fn test_expr_as_list() {
let list = Expr::List(vec![Expr::Symbol("a".into())]);
assert!(list.as_list().is_some());
assert_eq!(list.as_list().unwrap().len(), 1);
assert!(Expr::Symbol("a".into()).as_list().is_none());