1
use core::fmt;
2

            
3
use num_rational::Ratio;
4

            
5
pub 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)]
14
pub enum EntityKind {
15
    Account,
16
    Commodity,
17
    Transaction,
18
    Split,
19
    Tag,
20
    Price,
21
    SshKey,
22
    /// Hierarchical report node — recursive entity carrying the
23
    /// per-account/per-period balance rows that the report family
24
    /// of natives produces. `$report_node.children` is a
25
    /// `pair<report_node>` chain, so `(dolist (n (node-children r))
26
    /// ...)` walks the tree without leaving the typed lattice.
27
    ReportNode,
28
    /// Error condition raised by `(error 'code "msg")` and caught by
29
    /// `(handler-case)`. Maps to the `$nomi_condition` struct registered
30
    /// for exception support; the clause variable `e` binds at this kind so
31
    /// `(error-code e)` / `(error-message e)` type-check.
32
    Condition,
33
}
34

            
35
impl EntityKind {
36
    /// Name of the wasm struct type the compiler registers in
37
    /// `CompileContext::new_skeleton` for this entity kind. Keep in
38
    /// lockstep with the `register_struct_type` calls there.
39
    #[must_use]
40
78
    pub fn type_name(self) -> &'static str {
41
78
        match self {
42
3
            Self::Account => "account",
43
2
            Self::Commodity => "commodity_entity",
44
1
            Self::Transaction => "transaction",
45
1
            Self::Split => "split",
46
1
            Self::Tag => "tag_entity",
47
1
            Self::Price => "price",
48
1
            Self::SshKey => "ssh_key",
49
            Self::ReportNode => "report_node",
50
68
            Self::Condition => "nomi_condition",
51
        }
52
78
    }
53
}
54

            
55
impl fmt::Display for EntityKind {
56
71
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57
71
        write!(f, "{}", self.type_name())
58
71
    }
59
}
60

            
61
/// Element type of a homogeneous `$pair` chain. Tracked at compile time so
62
/// CAR/CDR emit the right downcast and CONS refuses heterogeneous mixing
63
/// with a structured type error. Kept as a `Copy` enum so `WasmType` stays
64
/// `Copy` — and so adding a new element type stays a one-line variant
65
/// addition. Nesting (pairs-of-pairs) waits for a follow-up sub-slice
66
/// once flat lists are stable across the consumers.
67
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68
pub enum PairElement {
69
    I32,
70
    /// Truth-value cell: shares `I32`'s i31-boxed car representation (CAR/CDR
71
    /// emit the same i31 downcast), but distinct so a bool extracted from a
72
    /// list serializes as `Nil` / `Bool`, not `Number` — the list-element
73
    /// analogue of [`WasmType::Bool`] vs `I32`.
74
    Bool,
75
    Ratio,
76
    Commodity,
77
    StringRef,
78
    Entity(EntityKind),
79
    /// Heterogeneous escape hatch (ADR-0025). Cars stay as raw
80
    /// `anyref`; CAR returns `WasmType::AnyRef` and the script must
81
    /// downcast at the use site (or consume via type-test natives like
82
    /// `ok?` / `err-code`). CONS widens to this variant when the two
83
    /// arms disagree on element type, and host fns that produce
84
    /// fundamentally heterogeneous lists (e.g. =catch-each= result
85
    /// cells) construct directly into `PairRef(AnyRef)`.
86
    AnyRef,
87
}
88

            
89
impl fmt::Display for PairElement {
90
408
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91
408
        match self {
92
136
            Self::I32 => write!(f, "i32"),
93
            Self::Bool => write!(f, "bool"),
94
204
            Self::Ratio => write!(f, "ratio"),
95
            Self::Commodity => write!(f, "commodity"),
96
68
            Self::StringRef => write!(f, "string"),
97
            Self::Entity(kind) => write!(f, "{kind}"),
98
            Self::AnyRef => write!(f, "any"),
99
        }
100
408
    }
101
}
102

            
103
impl PairElement {
104
    /// The matching `WasmType` for a pair's car when extracted.
105
    #[must_use]
106
23243
    pub fn as_wasm_type(self) -> WasmType {
107
23243
        match self {
108
9912
            Self::I32 => WasmType::I32,
109
1564
            Self::Bool => WasmType::Bool,
110
7956
            Self::Ratio => WasmType::Ratio,
111
            Self::Commodity => WasmType::Commodity,
112
612
            Self::StringRef => WasmType::StringRef,
113
1634
            Self::Entity(kind) => WasmType::EntityRef(kind),
114
1565
            Self::AnyRef => WasmType::AnyRef,
115
        }
116
23243
    }
117

            
118
    /// Inverse of `as_wasm_type`: returns `None` if the given type can't
119
    /// ride a `$pair`'s anyref car. `WasmType::AnyRef` round-trips to
120
    /// `PairElement::AnyRef`; nested pairs and closures still need their
121
    /// own follow-up lattice extension.
122
    #[must_use]
123
45246
    pub fn from_wasm_type(ty: WasmType) -> Option<Self> {
124
45246
        match ty {
125
18992
            WasmType::I32 => Some(Self::I32),
126
            // `Bool` keeps its own slot — it shares `I32`'s i31-boxed car
127
            // representation (the CAR/CDR downcast is identical) but stays
128
            // distinct so a bool extracted from a list serializes as Nil/Bool,
129
            // not Number.
130
3944
            WasmType::Bool => Some(Self::Bool),
131
18907
            WasmType::Ratio => Some(Self::Ratio),
132
274
            WasmType::Commodity => Some(Self::Commodity),
133
3060
            WasmType::StringRef => Some(Self::StringRef),
134
            WasmType::EntityRef(kind) => Some(Self::Entity(kind)),
135
69
            WasmType::AnyRef => Some(Self::AnyRef),
136
            WasmType::PairRef(_) | WasmType::Closure(_) => None,
137
        }
138
45246
    }
139

            
140
    /// Widen `self` against `other` to the most-specific common
141
    /// element. Identical types stay; everything else widens to
142
    /// `AnyRef` so heterogeneous CONS no longer errors. Symmetric.
143
    #[must_use]
144
21409
    pub fn widen(self, other: Self) -> Self {
145
21409
        if self == other { self } else { Self::AnyRef }
146
21409
    }
147
}
148

            
149
/// Identifier for a closure signature interned in the compile
150
/// context's closure registry. Distinct closures sharing the same
151
/// `(arg-types) -> ret-type` signature share an id (and therefore a
152
/// `$closure_<id>` wasm GC type); their env-struct types vary
153
/// independently per scope. The id is allocated in registration order
154
/// — small, dense, `Copy` — so it can ride inside `WasmType` without
155
/// growing the enum.
156
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157
pub struct ClosureSigId(pub u32);
158

            
159
impl fmt::Display for ClosureSigId {
160
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161
        write!(f, "{}", self.0)
162
    }
163
}
164

            
165
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166
pub enum WasmType {
167
    I32,
168
    /// Boolean: a comparison / predicate / `and` / `or` result, or a runtime
169
    /// `#t` / `#f`. Wasm representation is identical to `I32` (0 = false,
170
    /// nonzero = true), so it composes with `if` / `br_if` and boxes via
171
    /// `ref.i31` exactly like `I32`. It is a DISTINCT compile-time type only
172
    /// so the debug serializer can surface it as `Nil` / `Bool` rather than
173
    /// `Number`: an `I32` is a raw count (tag-count, split-count, …) that
174
    /// serializes as a number, whereas a `Bool` is a truth value. Refused by
175
    /// arithmetic like every non-numeric type.
176
    Bool,
177
    Ratio,
178
    StringRef,
179
    /// Commodity-bearing numeric: rational amount + originating commodity
180
    /// uuid. Distinct from `Ratio` so the compiler can refuse mixing money
181
    /// and pure-rational arithmetic at compile time. Wasm representation:
182
    /// `(i64 numer, i64 denom, i64 commodity_hi, i64 commodity_lo)`.
183
    Commodity,
184
    /// Homogeneous list cell: `$pair` WasmGC struct with an `anyref` car
185
    /// and a nullable `$pair` cdr. The `PairElement` records the car's
186
    /// type at compile time so CAR/CDR can emit the correct downcast and
187
    /// CONS refuses heterogeneous mixing. Retires the i32-only `$cons` —
188
    /// every runtime list shape in the compiler routes through this type.
189
    PairRef(PairElement),
190
    /// Raw `anyref` car payload extracted from a `PairRef(AnyRef)` or
191
    /// produced by a host fn that returns a heterogeneous value
192
    /// (ADR-0025). Refused by arithmetic / numeric comparison / closure
193
    /// call / typed pair construction; the script must downcast via
194
    /// type-test natives (e.g. `ok?`, `err-code`) before consuming.
195
    AnyRef,
196
    /// Typed server-entity reference: a `(ref null $<kind>)` GC struct
197
    /// whose field layout matches the `nomi_entity!` declaration for the
198
    /// kind. Refused by arithmetic / list-CONS / numeric comparison; the
199
    /// only legal operations are the entity-specific accessor natives
200
    /// (`account-id`, `commodity-name`, etc.) registered alongside the
201
    /// struct type.
202
    EntityRef(EntityKind),
203
    /// First-class closure value: `(ref null $closure_<sig>)` carrying a
204
    /// typed funcref + nullable env ref. The `ClosureSigId` indexes the
205
    /// per-context registry that owns the wasm types and per-call-site
206
    /// env-struct layouts. Eligible for FUNCALL / APPLY via `call_ref`;
207
    /// refused by arithmetic / numeric comparison / pair construction.
208
    Closure(ClosureSigId),
209
}
210

            
211
impl fmt::Display for WasmType {
212
2724
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213
2724
        match self {
214
884
            Self::I32 => write!(f, "i32"),
215
136
            Self::Bool => write!(f, "bool"),
216
612
            Self::Ratio => write!(f, "ratio"),
217
341
            Self::StringRef => write!(f, "string"),
218
            Self::Commodity => write!(f, "commodity"),
219
340
            Self::PairRef(elem) => write!(f, "pair<{elem}>"),
220
71
            Self::EntityRef(kind) => write!(f, "entity<{kind}>"),
221
            Self::Closure(sig) => write!(f, "closure<{sig}>"),
222
340
            Self::AnyRef => write!(f, "any"),
223
        }
224
2724
    }
225
}
226

            
227
#[derive(Debug, Clone, PartialEq)]
228
pub struct LambdaParams {
229
    pub required: Vec<String>,
230
    pub optional: Vec<(String, Option<Expr>)>,
231
    pub rest: Option<String>,
232
    pub key: Vec<(String, Option<Expr>)>,
233
    pub aux: Vec<(String, Option<Expr>)>,
234
}
235

            
236
impl LambdaParams {
237
5736017
    pub fn simple(params: Vec<String>) -> Self {
238
5736017
        Self {
239
5736017
            required: params,
240
5736017
            optional: Vec::new(),
241
5736017
            rest: None,
242
5736017
            key: Vec::new(),
243
5736017
            aux: Vec::new(),
244
5736017
        }
245
5736017
    }
246
}
247

            
248
#[derive(Debug, Clone, PartialEq)]
249
pub enum Expr {
250
    Nil,
251
    Bool(bool),
252
    Number(Fraction),
253
    String(String),
254
    Symbol(String),
255
    Keyword(String),
256
    Bytes(Vec<u8>),
257
    Cons(Box<Expr>, Box<Expr>),
258
    List(Vec<Expr>),
259
    Quote(Box<Expr>),
260
    Quasiquote(Box<Expr>),
261
    Unquote(Box<Expr>),
262
    UnquoteSplicing(Box<Expr>),
263
    Lambda(LambdaParams, Box<Expr>),
264
    RuntimeValue(crate::runtime::Value),
265
    WasmRuntime(WasmType),
266
    /// Value stored in WASM local variable (index, type)
267
    WasmLocal(u32, WasmType),
268
}
269

            
270
impl Expr {
271
    #[must_use]
272
148
    pub fn cons(car: Expr, cdr: Expr) -> Self {
273
148
        Expr::Cons(Box::new(car), Box::new(cdr))
274
148
    }
275

            
276
    /// Returns the `WasmType` if this expression is a runtime value (`WasmRuntime` or `WasmLocal`).
277
    #[must_use]
278
330524
    pub fn wasm_type(&self) -> Option<WasmType> {
279
330524
        match self {
280
272979
            Self::WasmRuntime(ty) | Self::WasmLocal(_, ty) => Some(*ty),
281
57545
            _ => None,
282
        }
283
330524
    }
284

            
285
    /// True if this is a runtime value (`WasmRuntime` or `WasmLocal`).
286
    #[must_use]
287
135879
    pub fn is_wasm_runtime(&self) -> bool {
288
135879
        matches!(self, Self::WasmRuntime(_) | Self::WasmLocal(_, _))
289
135879
    }
290
}
291

            
292
#[derive(Debug, Clone, PartialEq)]
293
pub struct Annotation {
294
    pub name: String,
295
    pub value: Expr,
296
}
297

            
298
impl Expr {
299
    #[must_use]
300
9
    pub fn is_atom(&self) -> bool {
301
9
        !matches!(self, Expr::List(_) | Expr::Cons(_, _))
302
9
    }
303

            
304
    #[must_use]
305
3663113
    pub fn as_symbol(&self) -> Option<&str> {
306
3663113
        match self {
307
3662364
            Expr::Symbol(s) => Some(s),
308
749
            _ => None,
309
        }
310
3663113
    }
311

            
312
    #[must_use]
313
358755
    pub fn as_list(&self) -> Option<&[Expr]> {
314
358755
        match self {
315
358686
            Expr::List(l) => Some(l),
316
69
            _ => None,
317
        }
318
358755
    }
319

            
320
    /// Splits a `Symbol` into `(namespace, name)`. A qualified symbol is
321
    /// stored canonically as `NS:NAME` (ADR-0029); an unqualified one has no
322
    /// `:` and yields `(None, name)`. Non-symbols yield `None`. The split is
323
    /// on the single canonical `:` — `NS::NAME` was already folded to `NS:NAME`
324
    /// by the reader, so at most one `:` is ever present here.
325
    #[must_use]
326
    pub fn symbol_parts(&self) -> Option<(Option<&str>, &str)> {
327
        let name = self.as_symbol()?;
328
        Some(match name.split_once(':') {
329
            Some((ns, base)) => (Some(ns), base),
330
            None => (None, name),
331
        })
332
    }
333
}
334

            
335
/// Builds the canonical symbol-table key for a (possibly namespaced) name.
336
/// `NS:NAME` when a namespace is present, bare `NAME` otherwise — the single
337
/// source of truth for how a qualified name maps to its flat table key.
338
#[must_use]
339
329697
pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
340
329697
    match namespace {
341
329696
        Some(ns) => format!("{ns}:{name}"),
342
1
        None => name.to_string(),
343
    }
344
329697
}
345

            
346
#[derive(Debug, Clone, Default)]
347
pub struct Program {
348
    pub exprs: Vec<Expr>,
349
    pub annotations: Vec<Annotation>,
350
}
351

            
352
impl Program {
353
    #[must_use]
354
132884
    pub fn new(exprs: Vec<Expr>) -> Self {
355
132884
        Self {
356
132884
            exprs,
357
132884
            annotations: Vec::new(),
358
132884
        }
359
132884
    }
360

            
361
    #[must_use]
362
213928
    pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
363
213928
        Self { exprs, annotations }
364
213928
    }
365
}
366

            
367
#[cfg(test)]
368
mod tests {
369
    use super::*;
370

            
371
    #[test]
372
1
    fn test_expr_is_atom() {
373
1
        assert!(Expr::Nil.is_atom());
374
1
        assert!(Expr::Bool(true).is_atom());
375
1
        assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
376
1
        assert!(Expr::String("hello".into()).is_atom());
377
1
        assert!(Expr::Symbol("foo".into()).is_atom());
378
1
        assert!(Expr::Keyword("bar".into()).is_atom());
379
1
        assert!(!Expr::List(vec![]).is_atom());
380
1
        assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
381
1
        assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
382
1
    }
383

            
384
    #[test]
385
1
    fn test_expr_as_symbol() {
386
1
        assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
387
1
        assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
388
1
    }
389

            
390
    #[test]
391
1
    fn test_expr_as_list() {
392
1
        let list = Expr::List(vec![Expr::Symbol("a".into())]);
393
1
        assert!(list.as_list().is_some());
394
1
        assert_eq!(list.as_list().unwrap().len(), 1);
395
1
        assert!(Expr::Symbol("a".into()).as_list().is_none());
396
1
    }
397
}