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
93
    pub fn type_name(self) -> &'static str {
35
93
        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
83
            Self::Condition => "nomi_condition",
44
        }
45
93
    }
46
}
47

            
48
impl fmt::Display for EntityKind {
49
86
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50
86
        write!(f, "{}", self.type_name())
51
86
    }
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
417
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84
417
        match self {
85
417
            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
417
    }
94
}
95

            
96
impl PairElement {
97
    /// The matching `WasmType` for a pair's car when extracted.
98
    #[must_use]
99
58850
    pub fn as_wasm_type(self) -> WasmType {
100
58850
        match self {
101
25896
            Self::I32 => WasmType::I32,
102
4897
            Self::Bool => WasmType::Bool,
103
18426
            Self::Ratio => WasmType::Ratio,
104
166
            Self::Commodity => WasmType::Commodity,
105
1494
            Self::StringRef => WasmType::StringRef,
106
2160
            Self::Entity(kind) => WasmType::EntityRef(kind),
107
5811
            Self::AnyRef => WasmType::AnyRef,
108
        }
109
58850
    }
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
80281
    pub fn from_wasm_type(ty: WasmType) -> Option<Self> {
117
80281
        match ty {
118
35368
            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
9794
            WasmType::Bool => Some(Self::Bool),
124
28723
            WasmType::Ratio => Some(Self::Ratio),
125
666
            WasmType::Commodity => Some(Self::Commodity),
126
5146
            WasmType::StringRef => Some(Self::StringRef),
127
            WasmType::EntityRef(kind) => Some(Self::Entity(kind)),
128
582
            WasmType::AnyRef => Some(Self::AnyRef),
129
2
            WasmType::PairRef(_) | WasmType::Closure(_) => None,
130
        }
131
80281
    }
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
34796
    pub fn widen(self, other: Self) -> Self {
138
34796
        if self == other { self } else { Self::AnyRef }
139
34796
    }
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` / `nil`. Wasm representation is identical to `I32` (0 = nil,
163
    /// nonzero = t), 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` / `T` 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
3326
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206
3326
        match self {
207
1162
            Self::I32 => write!(f, "i32"),
208
83
            Self::Bool => write!(f, "bool"),
209
747
            Self::Ratio => write!(f, "ratio"),
210
416
            Self::StringRef => write!(f, "string"),
211
            Self::Commodity => write!(f, "commodity"),
212
417
            Self::PairRef(elem) => write!(f, "pair<{elem}>"),
213
86
            Self::EntityRef(kind) => write!(f, "entity<{kind}>"),
214
            Self::Closure(sig) => write!(f, "closure<{sig}>"),
215
415
            Self::AnyRef => write!(f, "any"),
216
        }
217
3326
    }
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
8323839
    pub fn simple(params: Vec<String>) -> Self {
231
8323839
        Self {
232
8323839
            required: params,
233
8323839
            optional: Vec::new(),
234
8323839
            rest: None,
235
8323839
            key: Vec::new(),
236
8323839
            aux: Vec::new(),
237
8323839
        }
238
8323839
    }
239
}
240

            
241
#[derive(Debug, Clone, PartialEq)]
242
pub enum Expr {
243
    Nil,
244
    T,
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
933
    pub fn cons(car: Expr, cdr: Expr) -> Self {
266
933
        Expr::Cons(Box::new(car), Box::new(cdr))
267
933
    }
268

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

            
278
    /// True if this is a runtime value (`WasmRuntime` or `WasmLocal`).
279
    #[must_use]
280
208347
    pub fn is_wasm_runtime(&self) -> bool {
281
208347
        matches!(self, Self::WasmRuntime(_) | Self::WasmLocal(_, _))
282
208347
    }
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
    /// A constant list datum: the empty list is `nil`, anything else is quoted.
293
    #[must_use]
294
614438
    pub fn quoted_list(items: Vec<Expr>) -> Self {
295
614438
        if items.is_empty() {
296
2243
            Expr::Nil
297
        } else {
298
612195
            Expr::Quote(Box::new(Expr::List(items)))
299
        }
300
614438
    }
301

            
302
    /// The single compile-time truth test: a value is false exactly when it is nil.
303
    #[must_use]
304
59677
    pub fn is_nil(&self) -> bool {
305
59677
        matches!(self, Expr::Nil)
306
59677
    }
307

            
308
    #[must_use]
309
9
    pub fn is_atom(&self) -> bool {
310
9
        !matches!(self, Expr::List(_) | Expr::Cons(_, _))
311
9
    }
312

            
313
    #[must_use]
314
5316068
    pub fn as_symbol(&self) -> Option<&str> {
315
5316068
        match self {
316
5315154
            Expr::Symbol(s) => Some(s),
317
914
            _ => None,
318
        }
319
5316068
    }
320

            
321
    /// The elements of a list; `nil` is the empty list.
322
    #[must_use]
323
1166087
    pub fn as_list(&self) -> Option<&[Expr]> {
324
1166087
        match self {
325
1160607
            Expr::List(l) => Some(l),
326
5396
            Expr::Nil => Some(&[]),
327
84
            _ => None,
328
        }
329
1166087
    }
330

            
331
    /// Splits a `Symbol` into `(namespace, name)`. A qualified symbol is
332
    /// stored canonically as `NS:NAME` (ADR-0029); an unqualified one has no
333
    /// `:` and yields `(None, name)`. Non-symbols yield `None`. The split is
334
    /// on the single canonical `:` — `NS::NAME` was already folded to `NS:NAME`
335
    /// by the reader, so at most one `:` is ever present here.
336
    #[must_use]
337
    pub fn symbol_parts(&self) -> Option<(Option<&str>, &str)> {
338
        let name = self.as_symbol()?;
339
        Some(match name.split_once(':') {
340
            Some((ns, base)) => (Some(ns), base),
341
            None => (None, name),
342
        })
343
    }
344
}
345

            
346
/// Builds the canonical symbol-table key for a (possibly namespaced) name.
347
/// `NS:NAME` when a namespace is present, bare `NAME` otherwise — the single
348
/// source of truth for how a qualified name maps to its flat table key.
349
#[must_use]
350
493285
pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
351
493285
    match namespace {
352
493284
        Some(ns) => format!("{ns}:{name}"),
353
1
        None => name.to_string(),
354
    }
355
493285
}
356

            
357
#[derive(Debug, Clone, Default)]
358
pub struct Program {
359
    pub exprs: Vec<Expr>,
360
    pub annotations: Vec<Annotation>,
361
}
362

            
363
impl Program {
364
    #[must_use]
365
371769
    pub fn new(exprs: Vec<Expr>) -> Self {
366
371769
        Self {
367
371769
            exprs,
368
371769
            annotations: Vec::new(),
369
371769
        }
370
371769
    }
371

            
372
    #[must_use]
373
349987
    pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
374
349987
        Self { exprs, annotations }
375
349987
    }
376
}
377

            
378
#[cfg(test)]
379
mod tests {
380
    use super::*;
381

            
382
    #[test]
383
1
    fn test_expr_is_atom() {
384
1
        assert!(Expr::Nil.is_atom());
385
1
        assert!(Expr::T.is_atom());
386
1
        assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
387
1
        assert!(Expr::String("hello".into()).is_atom());
388
1
        assert!(Expr::Symbol("foo".into()).is_atom());
389
1
        assert!(Expr::Keyword("bar".into()).is_atom());
390
1
        assert!(!Expr::List(vec![]).is_atom());
391
1
        assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
392
1
        assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
393
1
    }
394

            
395
    #[test]
396
1
    fn quoted_list_of_nothing_is_nil() {
397
1
        assert_eq!(Expr::quoted_list(vec![]), Expr::Nil);
398
1
        assert_eq!(
399
1
            Expr::quoted_list(vec![Expr::T]),
400
1
            Expr::Quote(Box::new(Expr::List(vec![Expr::T])))
401
        );
402
1
    }
403

            
404
    #[test]
405
1
    fn test_expr_as_symbol() {
406
1
        assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
407
1
        assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
408
1
    }
409

            
410
    #[test]
411
1
    fn test_expr_as_list() {
412
1
        let list = Expr::List(vec![Expr::Symbol("a".into())]);
413
1
        assert!(list.as_list().is_some());
414
1
        assert_eq!(list.as_list().unwrap().len(), 1);
415
1
        assert!(Expr::Symbol("a".into()).as_list().is_none());
416
1
        assert_eq!(Expr::Nil.as_list(), Some(&[][..]));
417
1
    }
418
}