Skip to main content

elly_core/
ast.rs

1//! The Elly AST and its s-expression dump.
2//!
3//! `to_sexp` provides a compact parenthesized form for parse golden tests. Leaf text
4//! uses [`Text`] (inline/`Rc` handle) to make the AST `'static` and independent of
5//! Muon source lifetimes; structure is `alloc`ated.
6//!
7//! Patterns are first-class AST nodes (see `docs/done/2026-07-26_elly-patterns-native.md`)
8//! used by `&`-abstractions, `let` bindings, and `__match` clauses. The evaluator
9//! interprets [`Pattern`] nodes directly; the AST rephrases Muon syntax without
10//! compiling patterns, which is deferred to the VM.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::rc::Rc;
15use alloc::string::{String, ToString};
16use alloc::vec::Vec;
17
18use num_bigint::BigInt;
19
20pub use crate::eval::Builtin;
21pub use crate::module::ModuleData;
22
23/// An owned, cheap-clone text leaf using hipstr's `Local` (`Rc`) backend.
24/// Short strings (≤ 23 bytes on 64-bit) are inlined; longer strings use a shared
25/// `Rc<str>` slice. This ensures `Expr` and `Value` are `'static` and independent
26/// of the Muon source. Host tags are stored via [`Text::from_static`].
27pub type Text = hipstr::LocalHipStr<'static>;
28
29/// An Elly expression for the initial subset. Strings and records are deferred
30/// per the spec.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Expr {
33    /// Arbitrary-precision integer literal parsed from tokens (`0`, `-123`, `0xCAFE`).
34    Int(BigInt),
35    /// The unit value `()`. Used as a nullary marker in chains (`f ()`), standalone,
36    /// or as empty-value sugar in maps (`{ .k: }`). Matches [`Pattern::Unit`].
37    /// Rendered as `()`.
38    Unit,
39    /// Runtime list literal from Muon `#`-prefixed brackets (`#[…]`). Supports
40    /// any arity, including empty `#[ ]` or single-element `#[e]`. Grouping
41    /// parentheses `(…)` are handled at parse time.
42    List(Vec<Expr>),
43    /// Unresolved reference to a binding or free name. The resolver rewrites
44    /// in-scope names to [`Expr::Local`] and rejects others. Evaluator only
45    /// sees `Local`.
46    Name(Text),
47    /// Resolved reference using a de Bruijn `index`. The environment is a cons
48    /// list of bindings (innermost first); `index` is the distance from the top.
49    /// The original `name` is boxed and kept for rendering to keep `Expr` at four words.
50    Local { name: Box<Text>, index: u32 },
51    /// A `__`-builtin resolved at parse time via `Builtin::from_name`. Since `__`
52    /// names are unbindable, this avoids environment lookups at runtime. Renders
53    /// as the `Name` it replaces.
54    Builtin(Builtin),
55    /// Reference to an item in the enclosing module. Resolved by
56    /// `resolve_module_bodies`. Evaluated via the home module in the environment.
57    /// `index` is the item's position in the sorted item table. `depth` is the
58    /// number of [`Module`](crate::EnvNode::Module) terminals to skip. `name` is
59    /// boxed for size.
60    ModItem {
61        name: Box<Text>,
62        index: u32,
63        depth: u32,
64    },
65    /// Recursive binding group: `recur (a = va, b = vb) e`. Bindings are simultaneous
66    /// and mutually visible. The group is treated as a module where bindings are
67    /// items and the scoped expression is the module body. See
68    /// `docs/done/2026-08-13_elly-recur.md`.
69    Recur(Rc<ModuleData>),
70    /// Dotted literal (`.foo`, `.0`), stored without the leading dot.
71    Symbol(Text),
72    /// UTF-8 string literal decoded from Muon `<str>`. Evaluates to `Value::Str`.
73    /// Rendered as a quoted literal.
74    Str(Text),
75    /// Flattened call spine: index 0 is the callee, `[1..]` are arguments. Covers
76    /// both standard and spread calls. Evaluated via `apply_n` in `eval.rs`. Holds
77    /// at least two elements; currying is handled by the evaluator.
78    App(Box<[Expr]>),
79    /// Abstraction `&<pattern> body`. The `Lambda` (patterns and body) is shared
80    /// via `Rc`. Multiple `&` parameters are collected into one `Lambda`. Evaluator
81    /// creates closures by capturing the environment. Renders as nested `(abs …)`
82    /// for stability.
83    Abs(Rc<Lambda>),
84    /// Map literal from Muon `#{ … }`. Entries are (key, value) pairs in source
85    /// order. Keys are evaluated at runtime.
86    Map(Vec<(Expr, Expr)>),
87    /// Reference to the home module: `Self`, `#Self(…)`, or `x.Self`. Found via the
88    /// environment's terminal. `depth` is the number of `Module` terminals to skip.
89    /// See `docs/done/2026-09-11_elly-v1-self.md`.
90    Home { leaf: HomeLeaf, depth: u32 },
91    /// Sequence `{ c0, c1, …, e }`, also the target for `let`. Clauses are evaluated
92    /// sequentially in a shared scope; the last clause's value is the block's result.
93    /// Represented explicitly to support future effects (e.g., `break`, `defer`).
94    /// Boxed to keep `Expr` at four words.
95    Block(Box<[(Option<Pattern>, Expr)]>),
96    /// Matching construct `case <subject> { &p0 b0, ... }`. Evaluates subject once
97    /// and executes the first matching arm's body. Raises `.no_match` if no arms
98    /// match. Subjectless cases are lowered to `&__arg (case __arg { ... })`.
99    Case {
100        subject: Rc<Expr>,
101        cases: Box<[(Pattern, Expr)]>,
102    },
103}
104
105/// Home-module leaf types: the module itself (`Self`), construction
106/// (`#Self(…)`), or payload read (`x.Self`). These share the same resolution
107/// machinery (one walk to the terminal) and differ only in their eval result.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum HomeLeaf {
110    /// `Self` — the home module as a `Value::Module`.
111    Module,
112    /// `#Self(…)` / `#Self …` — the object constructor for the home module.
113    /// Always the callee of an [`App`](Expr::App). Renders as `(new …)`.
114    New,
115    /// `x.Self` — the payload reader for the home module. Always the callee
116    /// of an [`App`](Expr::App). In patterns, spelled as [`Pattern::Unwrap`].
117    Value,
118}
119
120impl HomeLeaf {
121    /// The name reported by the resolver for leaves outside a module body.
122    pub fn name(self) -> &'static str {
123        "Self"
124    }
125}
126// TODO: annotate expressions with source spans
127// TODO: do not hardcode `Vec`/`Box`
128
129/// Returns the body of a `recur` group. Required by grammar; `ModuleData`
130/// uses `Option` because standalone modules may lack one.
131pub(crate) fn recur_body(data: &ModuleData) -> &Expr {
132    data.body().expect("a recur group always has a body")
133}
134
135/// The code of an abstraction: parameter patterns and body. Shared via `Rc`
136/// between [`Expr::Abs`] and `Value::Closure`. Arity is `head.len()`, capped
137/// at 255. Every abstraction binds at least one parameter.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct Lambda {
140    pub head: Box<[Pattern]>,
141    pub body: Expr,
142    /// Environment capture strategy, determined by the resolver.
143    pub captures: Captures,
144}
145
146impl Lambda {
147    /// Number of parameters bound before the body executes.
148    pub fn arity(&self) -> usize {
149        self.head.len()
150    }
151}
152
153/// Environment capture strategy for a [`Lambda`] closure.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum Captures {
156    /// Unresolved state from the parser. Refused by the evaluator.
157    Unresolved,
158    /// Share the defining environment by reference. Used when the closure
159    /// cannot outlive its creator (e.g., direct applications, `__match` arms,
160    /// or `__Int.for` callbacks), avoiding frame allocation.
161    Chain,
162    /// Capture free variables into a fresh frame, sorted by `outer` index.
163    /// This is the default for escaping closures. An empty plan is a closed lambda.
164    Frame(Box<[Capture]>),
165}
166
167/// A captured free variable: `outer` is the de Bruijn index in the enclosing
168/// activation at evaluation; `slot` is its position in the closure's frame.
169/// The plan is sorted by `outer` so the evaluator fills the frame in one
170/// linear walk of the environment.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct Capture {
173    pub outer: u32,
174    pub slot: u32,
175}
176
177/// A pattern: the left-hand side of a binding (`&<pat> …`, `let (<pat> = …)`,
178/// or a `__match` clause). Matched by the evaluator's native matcher
179/// (`match_pattern` in `eval.rs`). `Bind`/`Discard` are irrefutable.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum Pattern {
182    /// `_` — matches any value without binding.
183    Discard,
184    /// `name` — binds the subject to `name`. A cell is consed onto the environment;
185    /// names are resolved to de Bruijn indices at parse time.
186    Bind(Text),
187    /// `name = <pat>` — binds the subject to `name`, then matches `<pat>` against
188    /// the same subject.
189    At(Text, Box<Pattern>),
190    /// `= <expr>` — matches if the subject equals the value of `<expr>`.
191    /// Evaluation errors propagate; they are not refutations.
192    Equal(Expr),
193    /// `< <expr>` — matches if the subject is ordered before the value of `<expr>`.
194    /// Only defined for comparable kinds (e.g., `Int`); others refute with `.not_int`.
195    Less(Expr),
196    /// `> <expr>` — matches if the subject is ordered after the value of `<expr>`.
197    Greater(Expr),
198    /// `<pat> as <Proto>` — matches if the subject's prototype is `<Proto>`, then
199    /// matches `<pat>` against the subject.
200    Type(ProtoRef, Box<Pattern>),
201    /// `Self <pat>` — matches if the subject is an object of the home module and
202    /// its payload matches `<pat>`. Prototype check is implicit.
203    /// `depth` is the distance to the home module terminal.
204    Unwrap { depth: u32, inner: Box<Pattern> },
205    /// `(p1 | p2)` — tries arms sequentially. Both arms must bind the same names.
206    /// Once an arm matches, the pattern is committed (no backtracking).
207    Or(Box<Pattern>, Box<Pattern>),
208    /// `&()` — matches only the unit value.
209    Unit,
210    /// `[p0, ...]` list pattern. `rest` handles the remainder: `None` for exact
211    /// arity, `Some(Discard)` for minimum arity, or `Some(Bind(name))` to bind.
212    List {
213        elems: Vec<Pattern>,
214        rest: Option<Box<Pattern>>,
215    },
216    /// `{ k0: p0, ... }` map pattern. `rest` handles the remainder similarly to lists.
217    Map {
218        entries: Vec<(MapKey, Pattern)>,
219        rest: Option<Box<Pattern>>,
220    },
221    /// `<pat> when (<cond>)` guarded pattern. Matches `inner`, then the guard.
222    /// Plain guards must be `Bool`; pattern guards match `gpat` against the condition.
223    When {
224        inner: Box<Pattern>,
225        when: (Option<Box<Pattern>>, Expr),
226    },
227}
228
229/// Map-pattern key. `Lookup` probes a concrete key; `Capture` peels the first
230/// remaining entry and matches its key against the pattern.
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum MapKey {
233    Lookup(Expr),
234    Capture(Box<Pattern>),
235}
236
237/// Prototype used in `as <Proto>` patterns.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub enum ProtoRef {
240    /// Fast form: a reference to a builtin module prototype (e.g., `Int`).
241    /// Compiles to a discriminant test [`TyKind`].
242    Kind(TyKind),
243    /// Any other prototype, evaluated at match time and required to be a module.
244    Ref(Expr),
245}
246
247impl ProtoRef {
248    /// Render the reference: a kind by its bare name (`Int`), so the existing
249    /// goldens are unchanged, and anything else as the expression it is.
250    fn write_sexp(&self, out: &mut String) {
251        match self {
252            ProtoRef::Kind(ty) => out.push_str(ty.name()),
253            ProtoRef::Ref(expr) => expr.write_sexp(out),
254        }
255    }
256
257    fn write_json(&self, out: &mut String) {
258        match self {
259            ProtoRef::Kind(ty) => json_str(ty.name(), out),
260            ProtoRef::Ref(expr) => expr.write_json(out),
261        }
262    }
263}
264
265/// Value kinds with builtin module prototypes, allowing `as <Proto>` to
266/// compile to a discriminant test (see [`ProtoRef::Kind`]).
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum TyKind {
269    Int,
270    Sym,
271    Str,
272    List,
273    Map,
274    Fun,
275    /// Module value (prototype `__Mod`).
276    Mod,
277    /// Unit value (prototype `__Unit`).
278    Unit,
279    /// Boolean (iota of `__Bool`).
280    Bool,
281}
282
283impl TyKind {
284    /// Spelling used in `as <Proto>` surface and dumps.
285    pub fn name(self) -> &'static str {
286        match self {
287            TyKind::Int => "Int",
288            TyKind::Sym => "Sym",
289            TyKind::Str => "Str",
290            TyKind::List => "List",
291            TyKind::Map => "Map",
292            TyKind::Fun => "Fun",
293            TyKind::Mod => "Mod",
294            TyKind::Unit => "Unit",
295            TyKind::Bool => "Bool",
296        }
297    }
298}
299
300impl Expr {
301    /// Serialize to a compact s-expression, e.g. `(app (name f) (name x))`.
302    pub fn to_sexp(&self) -> String {
303        let mut out = String::new();
304        self.write_sexp(&mut out);
305        out
306    }
307
308    // TODO: replace with impl de::Deserialize, impl ser::Serialize
309    /// Serialize to compact, tagged JSON, e.g. `{"app":[{"name":"f"},{"name":"x"}]}`.
310    /// Used by the `elly-wasm` playground to render the AST.
311    pub fn to_json(&self) -> String {
312        let mut out = String::new();
313        self.write_json(&mut out);
314        out
315    }
316
317    fn write_json(&self, out: &mut String) {
318        match self {
319            Expr::Int(n) => {
320                // Emit as bare number if it fits in f64 [-2^53, 2^53] exactly;
321                // otherwise emit as a tagged decimal string to avoid precision loss.
322                if fits_f64_exactly(n) {
323                    out.push_str(&n.to_string());
324                } else {
325                    out.push_str("{\"Int\":");
326                    json_str(&n.to_string(), out);
327                    out.push('}');
328                }
329            }
330            // Name, Local, and Builtin all render by name.
331            Expr::Name(n) => {
332                out.push_str("{\"name\":");
333                json_str(n, out);
334                out.push('}');
335            }
336            Expr::Local { name, .. } => {
337                out.push_str("{\"name\":");
338                json_str(name.as_str(), out);
339                out.push('}');
340            }
341            Expr::Builtin(op) => {
342                out.push_str("{\"name\":");
343                json_str(op.name(), out);
344                out.push('}');
345            }
346            // Home leaves render as `{"self":true}`, `{"new":true}`, or `{"value":true}`.
347            // Depth is derived and not rendered.
348            Expr::Home { leaf, .. } => match leaf {
349                HomeLeaf::Module => out.push_str("{\"self\":true}"),
350                HomeLeaf::New => out.push_str("{\"new\":true}"),
351                HomeLeaf::Value => out.push_str("{\"value\":true}"),
352            },
353            Expr::ModItem { name, .. } => {
354                out.push_str("{\"moditem\":");
355                json_str(name.as_str(), out);
356                out.push('}');
357            }
358            Expr::Recur(data) => {
359                out.push_str("{\"recur\":[");
360                for (i, (name, body)) in data.items().iter().enumerate() {
361                    if i > 0 {
362                        out.push(',');
363                    }
364                    out.push('[');
365                    json_str(name.as_str(), out);
366                    out.push(',');
367                    body.write_json(out);
368                    out.push(']');
369                }
370                out.push_str("],\"body\":");
371                recur_body(data).write_json(out);
372                out.push('}');
373            }
374            Expr::Symbol(s) => {
375                let mut dotted = String::from(".");
376                dotted.push_str(s);
377                out.push_str("{\"sym\":");
378                json_str(&dotted, out);
379                out.push('}');
380            }
381            Expr::Str(s) => {
382                out.push_str("{\"str\":");
383                json_str(s, out);
384                out.push('}');
385            }
386            // Render the call spine flat to preserve stored arity.
387            Expr::App(items) => write_app_json(items, out),
388            // Render collected head as nested single-parameter abstractions.
389            Expr::Abs(code) => write_abs_json(&code.head, &code.body, out),
390            Expr::Unit => out.push_str("{\"unit\":true}"),
391            Expr::List(elems) => {
392                out.push_str("{\"list\":[");
393                for (i, e) in elems.iter().enumerate() {
394                    if i > 0 {
395                        out.push(',');
396                    }
397                    e.write_json(out);
398                }
399                out.push_str("]}");
400            }
401            Expr::Map(entries) => {
402                out.push_str("{\"map\":[");
403                for (i, (k, v)) in entries.iter().enumerate() {
404                    if i > 0 {
405                        out.push(',');
406                    }
407                    out.push('[');
408                    k.write_json(out);
409                    out.push(',');
410                    v.write_json(out);
411                    out.push(']');
412                }
413                out.push_str("]}");
414            }
415            // `{"block":[<clause>…]}`. Binds are `{"bind":{"pat":…,"value":…}}`,
416            // others are the expression itself, in evaluation order.
417            Expr::Block(clauses) => {
418                out.push_str("{\"block\":[");
419                for (i, (pat, value)) in clauses.iter().enumerate() {
420                    if i > 0 {
421                        out.push(',');
422                    }
423                    if let Some(pat) = pat {
424                        out.push_str("{\"bind\":{\"pat\":");
425                        pat.write_json(out);
426                        out.push_str(",\"value\":");
427                        value.write_json(out);
428                        out.push_str("}}");
429                    } else {
430                        value.write_json(out);
431                    }
432                }
433                out.push_str("]}");
434            }
435            // `{"case":{"subject":…,"arms":[{"pat":…,"body":…}, …]}}`.
436            Expr::Case { subject, cases } => {
437                out.push_str("{\"case\":{\"subject\":");
438                subject.write_json(out);
439                out.push_str(",\"arms\":[");
440                for (i, (pat, body)) in cases.iter().enumerate() {
441                    if i > 0 {
442                        out.push(',');
443                    }
444                    out.push_str("{\"pat\":");
445                    pat.write_json(out);
446                    out.push_str(",\"body\":");
447                    body.write_json(out);
448                    out.push('}');
449                }
450                out.push_str("]}}");
451            }
452        }
453    }
454
455    fn write_sexp(&self, out: &mut String) {
456        match self {
457            Expr::Int(n) => {
458                out.push_str("(int ");
459                out.push_str(&n.to_string());
460                out.push(')');
461            }
462            // Name, Local, and Builtin all render by name.
463            Expr::Name(n) => {
464                out.push_str("(name ");
465                out.push_str(n);
466                out.push(')');
467            }
468            Expr::Local { name, .. } => {
469                out.push_str("(name ");
470                out.push_str(name.as_str());
471                out.push(')');
472            }
473            Expr::Builtin(op) => {
474                out.push_str("(name ");
475                out.push_str(op.name());
476                out.push(')');
477            }
478            // Home leaves render as `(self)`, `(new)`, or `(value)`.
479            // Depth is derived and not rendered.
480            Expr::Home { leaf, .. } => match leaf {
481                HomeLeaf::Module => out.push_str("(self)"),
482                HomeLeaf::New => out.push_str("(new)"),
483                HomeLeaf::Value => out.push_str("(value)"),
484            },
485            Expr::ModItem { name, .. } => {
486                out.push_str("(moditem ");
487                out.push_str(name.as_str());
488                out.push(')');
489            }
490            // `(recur (bind a …) (bind b …) <body>)` — bindings in sorted order, then body.
491            Expr::Recur(data) => {
492                out.push_str("(recur");
493                for (name, body) in data.items() {
494                    out.push_str(" (bind ");
495                    out.push_str(name.as_str());
496                    out.push(' ');
497                    body.write_sexp(out);
498                    out.push(')');
499                }
500                out.push(' ');
501                recur_body(data).write_sexp(out);
502                out.push(')');
503            }
504            Expr::Symbol(s) => {
505                out.push_str("(sym .");
506                out.push_str(s);
507                out.push(')');
508            }
509            // Strings are rendered as bare quoted literals.
510            Expr::Str(s) => json_str(s, out),
511            // Render the call spine flat to preserve stored arity.
512            Expr::App(items) => write_app_sexp(items, out),
513            // Render collected head as nested single-parameter abstractions.
514            Expr::Abs(code) => write_abs_sexp(&code.head, &code.body, out),
515            Expr::Unit => out.push_str("(unit)"),
516            Expr::List(elems) => {
517                out.push_str("(list");
518                for e in elems {
519                    out.push(' ');
520                    e.write_sexp(out);
521                }
522                out.push(')');
523            }
524            Expr::Map(entries) => {
525                out.push_str("(map");
526                for (k, v) in entries {
527                    out.push_str(" (entry ");
528                    k.write_sexp(out);
529                    out.push(' ');
530                    v.write_sexp(out);
531                    out.push(')');
532                }
533                out.push(')');
534            }
535            // `(block (bind <pat> <value>) … <value>)` — clauses in evaluation order.
536            Expr::Block(clauses) => {
537                out.push_str("(block");
538                for (pat, expr) in clauses.iter() {
539                    out.push(' ');
540                    if let Some(pat) = pat {
541                        out.push_str("(bind ");
542                        pat.write_sexp(out);
543                        out.push(' ');
544                        expr.write_sexp(out);
545                        out.push(')');
546                    } else {
547                        expr.write_sexp(out);
548                    }
549                }
550                out.push(')');
551            }
552            // `(case <subject> (arm <pat> <body>) …)`.
553            Expr::Case { subject, cases } => {
554                out.push_str("(case ");
555                subject.write_sexp(out);
556                for (pat, body) in cases.iter() {
557                    out.push_str(" (arm ");
558                    pat.write_sexp(out);
559                    out.push(' ');
560                    body.write_sexp(out);
561                    out.push(')');
562                }
563                out.push(')');
564            }
565        }
566    }
567}
568
569/// Renders a flat call spine `[callee, a0, a1, …]` as `(app callee a0 a1)`, preserving
570/// stored arity. `#Self(…)` and `x.Self` render as `(new a0)` and `(value a0)`.
571fn write_app_sexp(items: &[Expr], out: &mut String) {
572    let (open, body) = match &items[0] {
573        Expr::Home {
574            leaf: HomeLeaf::New,
575            ..
576        } => ("(new", &items[1..]),
577        Expr::Home {
578            leaf: HomeLeaf::Value,
579            ..
580        } => ("(value", &items[1..]),
581        _ => ("(app", items),
582    };
583    out.push_str(open);
584    for it in body {
585        out.push(' ');
586        it.write_sexp(out);
587    }
588    out.push(')');
589}
590
591/// Renders a collected head `[p0, p1, …]` over `body` as nested `(abs p0 (abs p1 body))`.
592fn write_abs_sexp(head: &[Pattern], body: &Expr, out: &mut String) {
593    out.push_str("(abs ");
594    head[0].write_sexp(out);
595    out.push(' ');
596    if head.len() == 1 {
597        body.write_sexp(out);
598    } else {
599        write_abs_sexp(&head[1..], body, out);
600    }
601    out.push(')');
602}
603
604/// JSON twin of [`write_app_sexp`]: `{"app":[callee, a0, a1, …]}`, with the same
605/// special cases for `new` and `value` spines.
606fn write_app_json(items: &[Expr], out: &mut String) {
607    let (tag, body) = match &items[0] {
608        Expr::Home {
609            leaf: HomeLeaf::New,
610            ..
611        } => ("new", &items[1..]),
612        Expr::Home {
613            leaf: HomeLeaf::Value,
614            ..
615        } => ("value", &items[1..]),
616        _ => ("app", items),
617    };
618    out.push_str("{\"");
619    out.push_str(tag);
620    out.push_str("\":[");
621    for (i, it) in body.iter().enumerate() {
622        if i > 0 {
623            out.push(',');
624        }
625        it.write_json(out);
626    }
627    out.push_str("]}");
628}
629
630/// JSON twin of [`write_abs_sexp`]: nested `{"abs":{"param":…,"body":…}}`.
631fn write_abs_json(head: &[Pattern], body: &Expr, out: &mut String) {
632    out.push_str("{\"abs\":{\"param\":");
633    head[0].write_json(out);
634    out.push_str(",\"body\":");
635    if head.len() == 1 {
636        body.write_json(out);
637    } else {
638        write_abs_json(&head[1..], body, out);
639    }
640    out.push_str("}}");
641}
642
643impl Pattern {
644    /// Serialize a pattern to a compact s-expression. `Bind`/`Discard` render as names;
645    /// structured patterns are tagged (e.g., `(list a ...rest)`).
646    fn write_sexp(&self, out: &mut String) {
647        match self {
648            Pattern::Discard => out.push('_'),
649            Pattern::Unit => out.push_str("(unit)"),
650            Pattern::Bind(name) => out.push_str(name),
651            Pattern::At(name, inner) => {
652                out.push_str("(at ");
653                out.push_str(name);
654                out.push(' ');
655                inner.write_sexp(out);
656                out.push(')');
657            }
658            Pattern::Equal(expr) => {
659                out.push_str("(eq ");
660                expr.write_sexp(out);
661                out.push(')');
662            }
663            Pattern::Less(expr) => {
664                out.push_str("(lt ");
665                expr.write_sexp(out);
666                out.push(')');
667            }
668            Pattern::Greater(expr) => {
669                out.push_str("(gt ");
670                expr.write_sexp(out);
671                out.push(')');
672            }
673            Pattern::Type(proto, inner) => {
674                out.push_str("(as ");
675                proto.write_sexp(out);
676                out.push(' ');
677                inner.write_sexp(out);
678                out.push(')');
679            }
680            Pattern::Unwrap { inner, .. } => {
681                out.push_str("(unwrap ");
682                inner.write_sexp(out);
683                out.push(')');
684            }
685            Pattern::Or(left, right) => {
686                out.push_str("(or ");
687                left.write_sexp(out);
688                out.push(' ');
689                right.write_sexp(out);
690                out.push(')');
691            }
692            Pattern::List { elems, rest } => {
693                out.push_str("(list");
694                for e in elems {
695                    out.push(' ');
696                    e.write_sexp(out);
697                }
698                write_rest_sexp(rest, out);
699                out.push(')');
700            }
701            Pattern::Map { entries, rest } => {
702                out.push_str("(map");
703                for (key, vpat) in entries {
704                    out.push_str(" (entry ");
705                    key.write_sexp(out);
706                    out.push(' ');
707                    vpat.write_sexp(out);
708                    out.push(')');
709                }
710                write_rest_sexp(rest, out);
711                out.push(')');
712            }
713            // `(when <inner> <cond>)` for plain guards; `(when <inner> (bind <gpat> <cond>))`
714            // for pattern guards.
715            Pattern::When {
716                inner,
717                when: (guard, cond),
718            } => {
719                out.push_str("(when ");
720                inner.write_sexp(out);
721                out.push(' ');
722                match guard {
723                    None => cond.write_sexp(out),
724                    Some(gpat) => {
725                        out.push_str("(bind ");
726                        gpat.write_sexp(out);
727                        out.push(' ');
728                        cond.write_sexp(out);
729                        out.push(')');
730                    }
731                }
732                out.push(')');
733            }
734        }
735    }
736
737    fn write_json(&self, out: &mut String) {
738        match self {
739            Pattern::Discard => out.push_str("{\"discard\":true}"),
740            Pattern::Unit => out.push_str("{\"unit\":true}"),
741            Pattern::Bind(name) => {
742                out.push_str("{\"bind\":");
743                json_str(name, out);
744                out.push('}');
745            }
746            Pattern::At(name, inner) => {
747                out.push_str("{\"at\":[");
748                json_str(name, out);
749                out.push(',');
750                inner.write_json(out);
751                out.push_str("]}");
752            }
753            Pattern::Equal(expr) => {
754                out.push_str("{\"eq\":");
755                expr.write_json(out);
756                out.push('}');
757            }
758            Pattern::Less(expr) => {
759                out.push_str("{\"lt\":");
760                expr.write_json(out);
761                out.push('}');
762            }
763            Pattern::Greater(expr) => {
764                out.push_str("{\"gt\":");
765                expr.write_json(out);
766                out.push('}');
767            }
768            Pattern::Type(proto, inner) => {
769                out.push_str("{\"as\":[");
770                proto.write_json(out);
771                out.push(',');
772                inner.write_json(out);
773                out.push_str("]}");
774            }
775            Pattern::Unwrap { inner, .. } => {
776                out.push_str("{\"unwrap\":");
777                inner.write_json(out);
778                out.push('}');
779            }
780            Pattern::Or(left, right) => {
781                out.push_str("{\"or\":[");
782                left.write_json(out);
783                out.push(',');
784                right.write_json(out);
785                out.push_str("]}");
786            }
787            Pattern::List { elems, rest } => {
788                out.push_str("{\"listpat\":{\"elems\":[");
789                for (i, e) in elems.iter().enumerate() {
790                    if i > 0 {
791                        out.push(',');
792                    }
793                    e.write_json(out);
794                }
795                out.push_str("],\"rest\":");
796                write_rest_json(rest, out);
797                out.push_str("}}");
798            }
799            Pattern::Map { entries, rest } => {
800                out.push_str("{\"mappat\":{\"entries\":[");
801                for (i, (key, vpat)) in entries.iter().enumerate() {
802                    if i > 0 {
803                        out.push(',');
804                    }
805                    out.push('[');
806                    key.write_json(out);
807                    out.push(',');
808                    vpat.write_json(out);
809                    out.push(']');
810                }
811                out.push_str("],\"rest\":");
812                write_rest_json(rest, out);
813                out.push_str("}}");
814            }
815            Pattern::When {
816                inner,
817                when: (guard, cond),
818            } => {
819                out.push_str("{\"when\":{\"inner\":");
820                inner.write_json(out);
821                out.push_str(",\"guard\":");
822                match guard {
823                    None => out.push_str("null"),
824                    Some(gpat) => gpat.write_json(out),
825                }
826                out.push_str(",\"cond\":");
827                cond.write_json(out);
828                out.push_str("}}");
829            }
830        }
831    }
832}
833
834/// Renders the trailing rest as `...` followed by the pattern (if not `Discard`).
835fn write_rest_sexp(rest: &Option<Box<Pattern>>, out: &mut String) {
836    if let Some(p) = rest {
837        out.push_str(" ...");
838        if !matches!(**p, Pattern::Discard) {
839            p.write_sexp(out);
840        }
841    }
842}
843
844/// Renders `null` for closed patterns, otherwise the rest pattern's JSON.
845fn write_rest_json(rest: &Option<Box<Pattern>>, out: &mut String) {
846    match rest {
847        None => out.push_str("null"),
848        Some(p) => p.write_json(out),
849    }
850}
851
852impl MapKey {
853    fn write_sexp(&self, out: &mut String) {
854        match self {
855            // Lookup key: a plain expression.
856            MapKey::Lookup(expr) => expr.write_sexp(out),
857            // Capture key: matches the peeled key against a pattern.
858            MapKey::Capture(kpat) => {
859                out.push_str("(capture ");
860                kpat.write_sexp(out);
861                out.push(')');
862            }
863        }
864    }
865
866    fn write_json(&self, out: &mut String) {
867        match self {
868            MapKey::Lookup(expr) => {
869                out.push_str("{\"lookup\":");
870                expr.write_json(out);
871                out.push('}');
872            }
873            MapKey::Capture(kpat) => {
874                out.push_str("{\"capture\":");
875                kpat.write_json(out);
876                out.push('}');
877            }
878        }
879    }
880}
881
882/// Returns true if `n` is exactly representable as an `f64` (within [-2^53, 2^53]).
883fn fits_f64_exactly(n: &BigInt) -> bool {
884    const LIMIT: i64 = 1 << 53; // 2^53 == 9_007_199_254_740_992
885    *n >= BigInt::from(-LIMIT) && *n <= BigInt::from(LIMIT)
886}
887
888/// Writes `s` as a quoted and escaped JSON string literal.
889pub(crate) fn json_str(s: &str, out: &mut String) {
890    out.push('"');
891    for ch in s.chars() {
892        match ch {
893            '"' => out.push_str("\\\""),
894            '\\' => out.push_str("\\\\"),
895            '\n' => out.push_str("\\n"),
896            '\r' => out.push_str("\\r"),
897            '\t' => out.push_str("\\t"),
898            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
899            c => out.push(c),
900        }
901    }
902    out.push('"');
903}