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}