Skip to main content

nomiscript/
ast.rs

1use core::fmt;
2
3use num_rational::Ratio;
4
5pub type Fraction = Ratio<i64>;
6
7/// Kind tag for one server-entity wasm struct. Each variant maps to a
8/// concrete `$<kind>` GC struct registered in
9/// `CompileContext::new_skeleton`, plus a set of typed field accessors
10/// in the native registry. Adding an entity adds one variant here +
11/// one row in the `nomi_entity!` macro invocation; the compiler and
12/// host-side allocators pick up the rest by expansion.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum EntityKind {
15    Account,
16    Commodity,
17    Transaction,
18    Split,
19    Tag,
20    Price,
21    SshKey,
22    /// Error condition raised by `(error 'code "msg")` and caught by
23    /// `(handler-case)`. Maps to the `$nomi_condition` struct registered
24    /// for exception support; the clause variable `e` binds at this kind so
25    /// `(error-code e)` / `(error-message e)` type-check.
26    Condition,
27}
28
29impl EntityKind {
30    /// Name of the wasm struct type the compiler registers in
31    /// `CompileContext::new_skeleton` for this entity kind. Keep in
32    /// lockstep with the `register_struct_type` calls there.
33    #[must_use]
34    pub fn type_name(self) -> &'static str {
35        match self {
36            Self::Account => "account",
37            Self::Commodity => "commodity_entity",
38            Self::Transaction => "transaction",
39            Self::Split => "split",
40            Self::Tag => "tag_entity",
41            Self::Price => "price",
42            Self::SshKey => "ssh_key",
43            Self::Condition => "nomi_condition",
44        }
45    }
46}
47
48impl fmt::Display for EntityKind {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{}", self.type_name())
51    }
52}
53
54/// Element type of a homogeneous `$pair` chain. Tracked at compile time so
55/// CAR/CDR emit the right downcast and CONS refuses heterogeneous mixing
56/// with a structured type error. Kept as a `Copy` enum so `WasmType` stays
57/// `Copy` — and so adding a new element type stays a one-line variant
58/// addition. Nesting (pairs-of-pairs) waits for a follow-up sub-slice
59/// once flat lists are stable across the consumers.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum PairElement {
62    I32,
63    /// Truth-value cell: shares `I32`'s i31-boxed car representation (CAR/CDR
64    /// emit the same i31 downcast), but distinct so a bool extracted from a
65    /// list serializes as `Nil` / `Bool`, not `Number` — the list-element
66    /// analogue of [`WasmType::Bool`] vs `I32`.
67    Bool,
68    Ratio,
69    Commodity,
70    StringRef,
71    Entity(EntityKind),
72    /// Heterogeneous escape hatch (ADR-0025). Cars stay as raw
73    /// `anyref`; CAR returns `WasmType::AnyRef` and the script must
74    /// downcast at the use site (or consume via type-test natives like
75    /// `ok?` / `err-code`). CONS widens to this variant when the two
76    /// arms disagree on element type, and host fns that produce
77    /// fundamentally heterogeneous lists (e.g. =catch-each= result
78    /// cells) construct directly into `PairRef(AnyRef)`.
79    AnyRef,
80}
81
82impl fmt::Display for PairElement {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::I32 => write!(f, "i32"),
86            Self::Bool => write!(f, "bool"),
87            Self::Ratio => write!(f, "ratio"),
88            Self::Commodity => write!(f, "commodity"),
89            Self::StringRef => write!(f, "string"),
90            Self::Entity(kind) => write!(f, "{kind}"),
91            Self::AnyRef => write!(f, "any"),
92        }
93    }
94}
95
96impl PairElement {
97    /// The matching `WasmType` for a pair's car when extracted.
98    #[must_use]
99    pub fn as_wasm_type(self) -> WasmType {
100        match self {
101            Self::I32 => WasmType::I32,
102            Self::Bool => WasmType::Bool,
103            Self::Ratio => WasmType::Ratio,
104            Self::Commodity => WasmType::Commodity,
105            Self::StringRef => WasmType::StringRef,
106            Self::Entity(kind) => WasmType::EntityRef(kind),
107            Self::AnyRef => WasmType::AnyRef,
108        }
109    }
110
111    /// Inverse of `as_wasm_type`: returns `None` if the given type can't
112    /// ride a `$pair`'s anyref car. `WasmType::AnyRef` round-trips to
113    /// `PairElement::AnyRef`; nested pairs and closures still need their
114    /// own follow-up lattice extension.
115    #[must_use]
116    pub fn from_wasm_type(ty: WasmType) -> Option<Self> {
117        match ty {
118            WasmType::I32 => Some(Self::I32),
119            // `Bool` keeps its own slot — it shares `I32`'s i31-boxed car
120            // representation (the CAR/CDR downcast is identical) but stays
121            // distinct so a bool extracted from a list serializes as Nil/Bool,
122            // not Number.
123            WasmType::Bool => Some(Self::Bool),
124            WasmType::Ratio => Some(Self::Ratio),
125            WasmType::Commodity => Some(Self::Commodity),
126            WasmType::StringRef => Some(Self::StringRef),
127            WasmType::EntityRef(kind) => Some(Self::Entity(kind)),
128            WasmType::AnyRef => Some(Self::AnyRef),
129            WasmType::PairRef(_) | WasmType::Closure(_) => None,
130        }
131    }
132
133    /// Widen `self` against `other` to the most-specific common
134    /// element. Identical types stay; everything else widens to
135    /// `AnyRef` so heterogeneous CONS no longer errors. Symmetric.
136    #[must_use]
137    pub fn widen(self, other: Self) -> Self {
138        if self == other { self } else { Self::AnyRef }
139    }
140}
141
142/// Identifier for a closure signature interned in the compile
143/// context's closure registry. Distinct closures sharing the same
144/// `(arg-types) -> ret-type` signature share an id (and therefore a
145/// `$closure_<id>` wasm GC type); their env-struct types vary
146/// independently per scope. The id is allocated in registration order
147/// — small, dense, `Copy` — so it can ride inside `WasmType` without
148/// growing the enum.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub struct ClosureSigId(pub u32);
151
152impl fmt::Display for ClosureSigId {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}", self.0)
155    }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub enum WasmType {
160    I32,
161    /// Boolean: a comparison / predicate / `and` / `or` result, or a runtime
162    /// `#t` / `#f`. Wasm representation is identical to `I32` (0 = false,
163    /// nonzero = true), so it composes with `if` / `br_if` and boxes via
164    /// `ref.i31` exactly like `I32`. It is a DISTINCT compile-time type only
165    /// so the debug serializer can surface it as `Nil` / `Bool` rather than
166    /// `Number`: an `I32` is a raw count (tag-count, split-count, …) that
167    /// serializes as a number, whereas a `Bool` is a truth value. Refused by
168    /// arithmetic like every non-numeric type.
169    Bool,
170    Ratio,
171    StringRef,
172    /// Commodity-bearing numeric: rational amount + originating commodity
173    /// uuid. Distinct from `Ratio` so the compiler can refuse mixing money
174    /// and pure-rational arithmetic at compile time. Wasm representation:
175    /// `(i64 numer, i64 denom, i64 commodity_hi, i64 commodity_lo)`.
176    Commodity,
177    /// Homogeneous list cell: `$pair` WasmGC struct with an `anyref` car
178    /// and a nullable `$pair` cdr. The `PairElement` records the car's
179    /// type at compile time so CAR/CDR can emit the correct downcast and
180    /// CONS refuses heterogeneous mixing. Retires the i32-only `$cons` —
181    /// every runtime list shape in the compiler routes through this type.
182    PairRef(PairElement),
183    /// Raw `anyref` car payload extracted from a `PairRef(AnyRef)` or
184    /// produced by a host fn that returns a heterogeneous value
185    /// (ADR-0025). Refused by arithmetic / numeric comparison / closure
186    /// call / typed pair construction; the script must downcast via
187    /// type-test natives (e.g. `ok?`, `err-code`) before consuming.
188    AnyRef,
189    /// Typed server-entity reference: a `(ref null $<kind>)` GC struct
190    /// whose field layout matches the `nomi_entity!` declaration for the
191    /// kind. Refused by arithmetic / list-CONS / numeric comparison; the
192    /// only legal operations are the entity-specific accessor natives
193    /// (`account-id`, `commodity-name`, etc.) registered alongside the
194    /// struct type.
195    EntityRef(EntityKind),
196    /// First-class closure value: `(ref null $closure_<sig>)` carrying a
197    /// typed funcref + nullable env ref. The `ClosureSigId` indexes the
198    /// per-context registry that owns the wasm types and per-call-site
199    /// env-struct layouts. Eligible for FUNCALL / APPLY via `call_ref`;
200    /// refused by arithmetic / numeric comparison / pair construction.
201    Closure(ClosureSigId),
202}
203
204impl fmt::Display for WasmType {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match self {
207            Self::I32 => write!(f, "i32"),
208            Self::Bool => write!(f, "bool"),
209            Self::Ratio => write!(f, "ratio"),
210            Self::StringRef => write!(f, "string"),
211            Self::Commodity => write!(f, "commodity"),
212            Self::PairRef(elem) => write!(f, "pair<{elem}>"),
213            Self::EntityRef(kind) => write!(f, "entity<{kind}>"),
214            Self::Closure(sig) => write!(f, "closure<{sig}>"),
215            Self::AnyRef => write!(f, "any"),
216        }
217    }
218}
219
220#[derive(Debug, Clone, PartialEq)]
221pub struct LambdaParams {
222    pub required: Vec<String>,
223    pub optional: Vec<(String, Option<Expr>)>,
224    pub rest: Option<String>,
225    pub key: Vec<(String, Option<Expr>)>,
226    pub aux: Vec<(String, Option<Expr>)>,
227}
228
229impl LambdaParams {
230    pub fn simple(params: Vec<String>) -> Self {
231        Self {
232            required: params,
233            optional: Vec::new(),
234            rest: None,
235            key: Vec::new(),
236            aux: Vec::new(),
237        }
238    }
239}
240
241#[derive(Debug, Clone, PartialEq)]
242pub enum Expr {
243    Nil,
244    Bool(bool),
245    Number(Fraction),
246    String(String),
247    Symbol(String),
248    Keyword(String),
249    Bytes(Vec<u8>),
250    Cons(Box<Expr>, Box<Expr>),
251    List(Vec<Expr>),
252    Quote(Box<Expr>),
253    Quasiquote(Box<Expr>),
254    Unquote(Box<Expr>),
255    UnquoteSplicing(Box<Expr>),
256    Lambda(LambdaParams, Box<Expr>),
257    RuntimeValue(crate::runtime::Value),
258    WasmRuntime(WasmType),
259    /// Value stored in WASM local variable (index, type)
260    WasmLocal(u32, WasmType),
261}
262
263impl Expr {
264    #[must_use]
265    pub fn cons(car: Expr, cdr: Expr) -> Self {
266        Expr::Cons(Box::new(car), Box::new(cdr))
267    }
268
269    /// Returns the `WasmType` if this expression is a runtime value (`WasmRuntime` or `WasmLocal`).
270    #[must_use]
271    pub fn wasm_type(&self) -> Option<WasmType> {
272        match self {
273            Self::WasmRuntime(ty) | Self::WasmLocal(_, ty) => Some(*ty),
274            _ => None,
275        }
276    }
277
278    /// True if this is a runtime value (`WasmRuntime` or `WasmLocal`).
279    #[must_use]
280    pub fn is_wasm_runtime(&self) -> bool {
281        matches!(self, Self::WasmRuntime(_) | Self::WasmLocal(_, _))
282    }
283}
284
285#[derive(Debug, Clone, PartialEq)]
286pub struct Annotation {
287    pub name: String,
288    pub value: Expr,
289}
290
291impl Expr {
292    #[must_use]
293    pub fn is_atom(&self) -> bool {
294        !matches!(self, Expr::List(_) | Expr::Cons(_, _))
295    }
296
297    #[must_use]
298    pub fn as_symbol(&self) -> Option<&str> {
299        match self {
300            Expr::Symbol(s) => Some(s),
301            _ => None,
302        }
303    }
304
305    #[must_use]
306    pub fn as_list(&self) -> Option<&[Expr]> {
307        match self {
308            Expr::List(l) => Some(l),
309            _ => None,
310        }
311    }
312
313    /// Splits a `Symbol` into `(namespace, name)`. A qualified symbol is
314    /// stored canonically as `NS:NAME` (ADR-0029); an unqualified one has no
315    /// `:` and yields `(None, name)`. Non-symbols yield `None`. The split is
316    /// on the single canonical `:` — `NS::NAME` was already folded to `NS:NAME`
317    /// by the reader, so at most one `:` is ever present here.
318    #[must_use]
319    pub fn symbol_parts(&self) -> Option<(Option<&str>, &str)> {
320        let name = self.as_symbol()?;
321        Some(match name.split_once(':') {
322            Some((ns, base)) => (Some(ns), base),
323            None => (None, name),
324        })
325    }
326}
327
328/// Builds the canonical symbol-table key for a (possibly namespaced) name.
329/// `NS:NAME` when a namespace is present, bare `NAME` otherwise — the single
330/// source of truth for how a qualified name maps to its flat table key.
331#[must_use]
332pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
333    match namespace {
334        Some(ns) => format!("{ns}:{name}"),
335        None => name.to_string(),
336    }
337}
338
339#[derive(Debug, Clone, Default)]
340pub struct Program {
341    pub exprs: Vec<Expr>,
342    pub annotations: Vec<Annotation>,
343}
344
345impl Program {
346    #[must_use]
347    pub fn new(exprs: Vec<Expr>) -> Self {
348        Self {
349            exprs,
350            annotations: Vec::new(),
351        }
352    }
353
354    #[must_use]
355    pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
356        Self { exprs, annotations }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_expr_is_atom() {
366        assert!(Expr::Nil.is_atom());
367        assert!(Expr::Bool(true).is_atom());
368        assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
369        assert!(Expr::String("hello".into()).is_atom());
370        assert!(Expr::Symbol("foo".into()).is_atom());
371        assert!(Expr::Keyword("bar".into()).is_atom());
372        assert!(!Expr::List(vec![]).is_atom());
373        assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
374        assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
375    }
376
377    #[test]
378    fn test_expr_as_symbol() {
379        assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
380        assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
381    }
382
383    #[test]
384    fn test_expr_as_list() {
385        let list = Expr::List(vec![Expr::Symbol("a".into())]);
386        assert!(list.as_list().is_some());
387        assert_eq!(list.as_list().unwrap().len(), 1);
388        assert!(Expr::Symbol("a".into()).as_list().is_none());
389    }
390}