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
    /// 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

            
29
impl 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
81
    pub fn type_name(self) -> &'static str {
35
81
        match self {
36
3
            Self::Account => "account",
37
2
            Self::Commodity => "commodity_entity",
38
1
            Self::Transaction => "transaction",
39
1
            Self::Split => "split",
40
1
            Self::Tag => "tag_entity",
41
1
            Self::Price => "price",
42
1
            Self::SshKey => "ssh_key",
43
71
            Self::Condition => "nomi_condition",
44
        }
45
81
    }
46
}
47

            
48
impl fmt::Display for EntityKind {
49
74
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50
74
        write!(f, "{}", self.type_name())
51
74
    }
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)]
61
pub 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

            
82
impl fmt::Display for PairElement {
83
426
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84
426
        match self {
85
142
            Self::I32 => write!(f, "i32"),
86
            Self::Bool => write!(f, "bool"),
87
213
            Self::Ratio => write!(f, "ratio"),
88
            Self::Commodity => write!(f, "commodity"),
89
71
            Self::StringRef => write!(f, "string"),
90
            Self::Entity(kind) => write!(f, "{kind}"),
91
            Self::AnyRef => write!(f, "any"),
92
        }
93
426
    }
94
}
95

            
96
impl PairElement {
97
    /// The matching `WasmType` for a pair's car when extracted.
98
    #[must_use]
99
24356
    pub fn as_wasm_type(self) -> WasmType {
100
24356
        match self {
101
10295
            Self::I32 => WasmType::I32,
102
1633
            Self::Bool => WasmType::Bool,
103
8307
            Self::Ratio => WasmType::Ratio,
104
            Self::Commodity => WasmType::Commodity,
105
639
            Self::StringRef => WasmType::StringRef,
106
1848
            Self::Entity(kind) => WasmType::EntityRef(kind),
107
1634
            Self::AnyRef => WasmType::AnyRef,
108
        }
109
24356
    }
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
47152
    pub fn from_wasm_type(ty: WasmType) -> Option<Self> {
117
47152
        match ty {
118
19740
            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
4118
            WasmType::Bool => Some(Self::Bool),
124
19741
            WasmType::Ratio => Some(Self::Ratio),
125
286
            WasmType::Commodity => Some(Self::Commodity),
126
3195
            WasmType::StringRef => Some(Self::StringRef),
127
            WasmType::EntityRef(kind) => Some(Self::Entity(kind)),
128
72
            WasmType::AnyRef => Some(Self::AnyRef),
129
            WasmType::PairRef(_) | WasmType::Closure(_) => None,
130
        }
131
47152
    }
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
22303
    pub fn widen(self, other: Self) -> Self {
138
22303
        if self == other { self } else { Self::AnyRef }
139
22303
    }
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)]
150
pub struct ClosureSigId(pub u32);
151

            
152
impl 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)]
159
pub 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

            
204
impl fmt::Display for WasmType {
205
2844
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206
2844
        match self {
207
923
            Self::I32 => write!(f, "i32"),
208
142
            Self::Bool => write!(f, "bool"),
209
639
            Self::Ratio => write!(f, "ratio"),
210
356
            Self::StringRef => write!(f, "string"),
211
            Self::Commodity => write!(f, "commodity"),
212
355
            Self::PairRef(elem) => write!(f, "pair<{elem}>"),
213
74
            Self::EntityRef(kind) => write!(f, "entity<{kind}>"),
214
            Self::Closure(sig) => write!(f, "closure<{sig}>"),
215
355
            Self::AnyRef => write!(f, "any"),
216
        }
217
2844
    }
218
}
219

            
220
#[derive(Debug, Clone, PartialEq)]
221
pub 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

            
229
impl LambdaParams {
230
6167584
    pub fn simple(params: Vec<String>) -> Self {
231
6167584
        Self {
232
6167584
            required: params,
233
6167584
            optional: Vec::new(),
234
6167584
            rest: None,
235
6167584
            key: Vec::new(),
236
6167584
            aux: Vec::new(),
237
6167584
        }
238
6167584
    }
239
}
240

            
241
#[derive(Debug, Clone, PartialEq)]
242
pub 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

            
263
impl Expr {
264
    #[must_use]
265
154
    pub fn cons(car: Expr, cdr: Expr) -> Self {
266
154
        Expr::Cons(Box::new(car), Box::new(cdr))
267
154
    }
268

            
269
    /// Returns the `WasmType` if this expression is a runtime value (`WasmRuntime` or `WasmLocal`).
270
    #[must_use]
271
344589
    pub fn wasm_type(&self) -> Option<WasmType> {
272
344589
        match self {
273
284512
            Self::WasmRuntime(ty) | Self::WasmLocal(_, ty) => Some(*ty),
274
60077
            _ => None,
275
        }
276
344589
    }
277

            
278
    /// True if this is a runtime value (`WasmRuntime` or `WasmLocal`).
279
    #[must_use]
280
141733
    pub fn is_wasm_runtime(&self) -> bool {
281
141733
        matches!(self, Self::WasmRuntime(_) | Self::WasmLocal(_, _))
282
141733
    }
283
}
284

            
285
#[derive(Debug, Clone, PartialEq)]
286
pub struct Annotation {
287
    pub name: String,
288
    pub value: Expr,
289
}
290

            
291
impl Expr {
292
    #[must_use]
293
9
    pub fn is_atom(&self) -> bool {
294
9
        !matches!(self, Expr::List(_) | Expr::Cons(_, _))
295
9
    }
296

            
297
    #[must_use]
298
3936065
    pub fn as_symbol(&self) -> Option<&str> {
299
3936065
        match self {
300
3935283
            Expr::Symbol(s) => Some(s),
301
782
            _ => None,
302
        }
303
3936065
    }
304

            
305
    #[must_use]
306
386885
    pub fn as_list(&self) -> Option<&[Expr]> {
307
386885
        match self {
308
386813
            Expr::List(l) => Some(l),
309
72
            _ => None,
310
        }
311
386885
    }
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]
332
356645
pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
333
356645
    match namespace {
334
356644
        Some(ns) => format!("{ns}:{name}"),
335
1
        None => name.to_string(),
336
    }
337
356645
}
338

            
339
#[derive(Debug, Clone, Default)]
340
pub struct Program {
341
    pub exprs: Vec<Expr>,
342
    pub annotations: Vec<Annotation>,
343
}
344

            
345
impl Program {
346
    #[must_use]
347
162673
    pub fn new(exprs: Vec<Expr>) -> Self {
348
162673
        Self {
349
162673
            exprs,
350
162673
            annotations: Vec::new(),
351
162673
        }
352
162673
    }
353

            
354
    #[must_use]
355
254451
    pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
356
254451
        Self { exprs, annotations }
357
254451
    }
358
}
359

            
360
#[cfg(test)]
361
mod tests {
362
    use super::*;
363

            
364
    #[test]
365
1
    fn test_expr_is_atom() {
366
1
        assert!(Expr::Nil.is_atom());
367
1
        assert!(Expr::Bool(true).is_atom());
368
1
        assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
369
1
        assert!(Expr::String("hello".into()).is_atom());
370
1
        assert!(Expr::Symbol("foo".into()).is_atom());
371
1
        assert!(Expr::Keyword("bar".into()).is_atom());
372
1
        assert!(!Expr::List(vec![]).is_atom());
373
1
        assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
374
1
        assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
375
1
    }
376

            
377
    #[test]
378
1
    fn test_expr_as_symbol() {
379
1
        assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
380
1
        assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
381
1
    }
382

            
383
    #[test]
384
1
    fn test_expr_as_list() {
385
1
        let list = Expr::List(vec![Expr::Symbol("a".into())]);
386
1
        assert!(list.as_list().is_some());
387
1
        assert_eq!(list.as_list().unwrap().len(), 1);
388
1
        assert!(Expr::Symbol("a".into()).as_list().is_none());
389
1
    }
390
}