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` / `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
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 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
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 /// A constant list datum: the empty list is `nil`, anything else is quoted.
293 #[must_use]
294 pub fn quoted_list(items: Vec<Expr>) -> Self {
295 if items.is_empty() {
296 Expr::Nil
297 } else {
298 Expr::Quote(Box::new(Expr::List(items)))
299 }
300 }
301
302 /// The single compile-time truth test: a value is false exactly when it is nil.
303 #[must_use]
304 pub fn is_nil(&self) -> bool {
305 matches!(self, Expr::Nil)
306 }
307
308 #[must_use]
309 pub fn is_atom(&self) -> bool {
310 !matches!(self, Expr::List(_) | Expr::Cons(_, _))
311 }
312
313 #[must_use]
314 pub fn as_symbol(&self) -> Option<&str> {
315 match self {
316 Expr::Symbol(s) => Some(s),
317 _ => None,
318 }
319 }
320
321 /// The elements of a list; `nil` is the empty list.
322 #[must_use]
323 pub fn as_list(&self) -> Option<&[Expr]> {
324 match self {
325 Expr::List(l) => Some(l),
326 Expr::Nil => Some(&[]),
327 _ => None,
328 }
329 }
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]
350pub fn canonical_symbol(namespace: Option<&str>, name: &str) -> String {
351 match namespace {
352 Some(ns) => format!("{ns}:{name}"),
353 None => name.to_string(),
354 }
355}
356
357#[derive(Debug, Clone, Default)]
358pub struct Program {
359 pub exprs: Vec<Expr>,
360 pub annotations: Vec<Annotation>,
361}
362
363impl Program {
364 #[must_use]
365 pub fn new(exprs: Vec<Expr>) -> Self {
366 Self {
367 exprs,
368 annotations: Vec::new(),
369 }
370 }
371
372 #[must_use]
373 pub fn with_annotations(exprs: Vec<Expr>, annotations: Vec<Annotation>) -> Self {
374 Self { exprs, annotations }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_expr_is_atom() {
384 assert!(Expr::Nil.is_atom());
385 assert!(Expr::T.is_atom());
386 assert!(Expr::Number(Fraction::from_integer(42)).is_atom());
387 assert!(Expr::String("hello".into()).is_atom());
388 assert!(Expr::Symbol("foo".into()).is_atom());
389 assert!(Expr::Keyword("bar".into()).is_atom());
390 assert!(!Expr::List(vec![]).is_atom());
391 assert!(!Expr::cons(Expr::Nil, Expr::Nil).is_atom());
392 assert!(Expr::Lambda(LambdaParams::simple(vec![]), Box::new(Expr::Nil)).is_atom());
393 }
394
395 #[test]
396 fn quoted_list_of_nothing_is_nil() {
397 assert_eq!(Expr::quoted_list(vec![]), Expr::Nil);
398 assert_eq!(
399 Expr::quoted_list(vec![Expr::T]),
400 Expr::Quote(Box::new(Expr::List(vec![Expr::T])))
401 );
402 }
403
404 #[test]
405 fn test_expr_as_symbol() {
406 assert_eq!(Expr::Symbol("foo".into()).as_symbol(), Some("foo"));
407 assert_eq!(Expr::Number(Fraction::from_integer(1)).as_symbol(), None);
408 }
409
410 #[test]
411 fn test_expr_as_list() {
412 let list = Expr::List(vec![Expr::Symbol("a".into())]);
413 assert!(list.as_list().is_some());
414 assert_eq!(list.as_list().unwrap().len(), 1);
415 assert!(Expr::Symbol("a".into()).as_list().is_none());
416 assert_eq!(Expr::Nil.as_list(), Some(&[][..]));
417 }
418}