elly_core/ast.rs
1//! The Elly AST and its s-expression dump.
2//!
3//! `to_sexp` emits a compact parenthesized form used by the parse golden
4//! tests so they read close to `docs/elly-spec.md`. Leaves own their text as
5//! [`Text`] (a cheap-clone inline/`Rc` handle), so the AST is `'static` — no
6//! longer tied to the Muon source lifetime; structure is `alloc`ated.
7//!
8//! **Patterns are first-class AST** (see `docs/done/2026-07-26_elly-patterns-native.md`):
9//! an `&`-abstraction, a `let` binding, and a `__match` clause all bind through
10//! the same [`Pattern`] node, which the evaluator interprets directly (a native
11//! matcher, not a lowering to eliminator builtins). The AST *rephrases* Muon
12//! into Elly's own syntax; it does not *compile* patterns into anything else —
13//! that is a later (VM) pass's job.
14
15use alloc::boxed::Box;
16use alloc::format;
17use alloc::rc::Rc;
18use alloc::string::{String, ToString};
19use alloc::vec::Vec;
20
21use num_bigint::BigInt;
22
23use crate::eval::{Builtin, ModuleData};
24
25/// An owned, cheap-clone text leaf — hipstr's single-threaded `Local` (`Rc`)
26/// backend: **inline** for short strings (≤ 23 bytes on 64-bit), else a shared
27/// `Rc<str>` slice, an **O(1) clone** either way. Replaces the former `&'a str`
28/// source borrow so `Expr`/`Value` are `'static` and no longer tied to the Muon
29/// source (see `docs/todo/elly-impl-expr-repr-v1.md`). A `'static` host tag is
30/// stored for free via [`Text::from_static`](hipstr::string::HipStr::from_static).
31pub type Text = hipstr::LocalHipStr<'static>;
32
33/// An Elly expression in the first subset. Strings and records are deferred
34/// (see the spec), so there is no variant for them yet.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Expr {
37 /// An arbitrary-precision integer literal, parsed from the token (`0`, `-123`,
38 /// `0xCAFE`); see the *integers* section of the spec.
39 Int(BigInt),
40 /// A runtime list literal from a Muon `<list>` (`[…]`), of *any* arity: `[]`
41 /// (the empty list, which is unit), `[e]` (a genuine 1-list, distinct from
42 /// `e`), or `[e0, e1, …]`. Parens `(…)` never produce this — they are
43 /// grouping / argument spread at parse time (see `parse.rs`).
44 List(Vec<Expr>),
45 /// A bare name: a reference to an enclosing `&`-binding, else a free name.
46 /// Produced by the parser as the *unresolved* form; the resolution pass
47 /// (`resolve.rs`) rewrites every in-scope `Name` into a [`Expr::Local`] and
48 /// rejects the rest. Tooling that wants the raw, pre-resolution tree
49 /// (`parse_open`) keeps seeing `Name`; the evaluator only ever sees `Local`.
50 Name(Text),
51 /// A **resolved** reference: a flat de Bruijn `index` produced by the resolver
52 /// from a [`Expr::Name`]. The environment is a nameless cons list of bindings
53 /// (one cell per bound name, innermost first), and `index` counts cells to walk
54 /// from the top (0 = the most recent binding) — so the evaluator's lookup is a
55 /// pointer chase with no string compare and no per-link clone. The original
56 /// `name` is kept only for rendering, **boxed** so it does not widen `Expr` past
57 /// four words (it is read solely by `to_sexp` / `to_json`): a `Local` then
58 /// prints exactly like the `Name` it replaced (`(name foo)` / `{"name":"foo"}`),
59 /// so the goldens are unchanged.
60 Local { name: Box<Text>, index: u32 },
61 /// A `__`-builtin resolved **at parse** (see `Builtin::from_name`). `__` names
62 /// are un-bindable, so a reference like `__Int.add` never depends on the
63 /// environment; resolving it here turns each call into a direct dispatch rather
64 /// than a failing env walk plus a name match at eval. Rendered exactly like the
65 /// `Name` it replaces (`(name __Int.add)` / `{"name":"__Int.add"}`) so the
66 /// goldens are unchanged.
67 Builtin(Builtin),
68 /// A **module sibling reference**: a name that resolved not to a lexical
69 /// binding but to another item of the enclosing module (see
70 /// `docs/done/2026-08-07_elly-modules-v0.md`). Produced only by the module resolver
71 /// (`resolve::resolve_module_bodies`), never by `parse`; it is a *sink* — it
72 /// carries no module reference and is resolved at eval through the home module
73 /// carried in the environment (`EnvNode::Module`).
74 ///
75 /// `index` is the item's position in the home module's (name-sorted) item
76 /// table, assigned at resolve so eval indexes straight into it with no name
77 /// compare — the module-tier analogue of a `Local`'s de Bruijn index. `name` is
78 /// kept for rendering only (boxed to keep `Expr` at four words). Renders
79 /// `(moditem factorial)` / `{"moditem":"factorial"}`.
80 /// `depth` is how many [`Module`](crate::EnvNode::Module) terminals the walk
81 /// **skips** before the one that owns this item. A program with one module has
82 /// no nesting and every `ModItem` carries `0`; a [`recur`](Expr::Recur) inside
83 /// a module body puts a second terminal on the chain, and a reference to the
84 /// module's own item from inside the `recur` has to step over the `recur`'s.
85 /// It is the barrier-stack distance the resolver measured, not rendered (like
86 /// a `Local`'s index, it is derived addressing).
87 ModItem {
88 name: Box<Text>,
89 index: u32,
90 depth: u32,
91 },
92 /// A **recursive binding group as an expression**: `recur (a = va, b = vb) e`.
93 /// The bindings are *simultaneous* — each sees all of them, itself included —
94 /// which is what separates it from `let`, whose group is sequential and
95 /// desugars to nested [`App`](Expr::App)/[`Abs`](Expr::Abs) with no node of its
96 /// own.
97 ///
98 /// The group is a module — the whole of one, body included: the bindings are
99 /// its (name-sorted) item table, and they reach each other as
100 /// [`ModItem`](Expr::ModItem)s *through the terminal* rather than by
101 /// capturing references to each other, the edge that would cycle. The
102 /// expression the group scopes over is the [`ModuleData`'s own
103 /// body](crate::ModuleData::body), the slot a module source fills with
104 /// `__main`. Evaluating this builds the terminal over the current environment
105 /// (the cons-shaped frame) and runs that body under it; see
106 /// `docs/done/2026-08-13_elly-recur.md` and `docs/done/2026-08-14_elly-run.md`.
107 Recur(Rc<ModuleData>),
108 /// A `.`-prefixed literal (`.foo`, `.0`), stored *without* the dot — the dot
109 /// is the Muon sigil and is re-added when displayed / dumped.
110 Symbol(Text),
111 /// A string literal, **decoded** at parse from a Muon `<str>` (JSON/Muon
112 /// escapes resolved, `\uXXXX` surrogate pairs combined) into owned UTF-8. It
113 /// is self-evaluating: eval clones the handle into `Value::Str`. Rendered as
114 /// the bare quoted, re-escaped literal (`"foo"`) — the quotes already mark it
115 /// apart from `.foo` / `foo`, so no `(str …)` wrapper.
116 Str(Text),
117 /// Application: a **flattened call spine** whose element 0 is the callee and
118 /// `[1..]` the arguments. A source spine `f a b c` and a spread call `f(a, b)`
119 /// both parse into one node with a known call-site arity, so the evaluator
120 /// binds a whole call's arguments in one pass (see `apply_n` in `eval.rs`).
121 /// Always holds at least a callee and one argument (two elements); currying is
122 /// recovered by `apply_n`'s over-application loop, so a flat spine is
123 /// semantically identical to the left-nested binary applications it replaced —
124 /// which is exactly how it is re-rendered in `to_sexp` / `to_json`.
125 App(Box<[Expr]>),
126 /// Abstraction `&<pattern> body`: the one universal binding form, its *code*
127 /// (`Lambda`: the parameter patterns and the body) shared behind `Rc`. A run
128 /// of `&p0 &p1 …` is collected at parse into one multi-parameter `Lambda`
129 /// (arity `head.len()`), so applying it *matches* a whole call's arguments in
130 /// one pass. The evaluator builds a closure by cloning the `Rc<Lambda>` and
131 /// capturing the environment, rather than deep-cloning the subtree. Rendered as
132 /// nested `(abs p0 (abs p1 …))` — the curried reading the collected head stands
133 /// for — to keep the goldens stable.
134 Abs(Rc<Lambda>),
135 /// A map literal from a Muon `<block>` (`{ … }`): its entries as
136 /// (key-expr, value-expr) pairs, in source order. Keys stay expressions
137 /// until eval, so computed keys (`(expr)`) just evaluate like any other
138 /// expression; see the *maps* section of the spec.
139 Map(Vec<(Expr, Expr)>),
140 /// A **home-module leaf**: `__module`, `__new` or `__value`, each reading the
141 /// module the body it sits in was written in (see [`HomeLeaf`]). Like a
142 /// [`ModItem`](Expr::ModItem) it is a *sink* — it carries no module reference
143 /// and finds its module through the environment's terminal at eval — and like
144 /// one it costs a leaf and no capture slot.
145 ///
146 /// `depth` is how many [`Module`](crate::EnvNode::Module) terminals to step
147 /// over before the module: a leaf names the nearest terminal *written as a
148 /// module*, so a [`recur`](Expr::Recur) between the leaf and its module is
149 /// stepped over and a method that loops internally still constructs objects of
150 /// its own type. The resolver measures it (the same barrier-stack distance a
151 /// `ModItem` carries) and rejects a leaf with no module on the stack; it is
152 /// derived addressing, so it is not rendered. See
153 /// `docs/done/2026-08-15_elly-objects-v0.md`.
154 Home { leaf: HomeLeaf, depth: u32 },
155}
156
157/// Which of the three [home-module leaves](Expr::Home) a reference is. They share
158/// one variant because they share the whole of their machinery — one walk to the
159/// terminal, one parse clause each, no capture — and differ only in what they
160/// evaluate to once it is found.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum HomeLeaf {
163 /// `__module` — the home module itself, a `Value::Module`.
164 Module,
165 /// `__new` — the object constructor over the home module: the object builtin
166 /// with the module already applied, so what is left takes the payload.
167 New,
168 /// `__value` — the payload reader over the home module, the same shape. In
169 /// *pattern* position the name is [`Pattern::Unwrap`] instead.
170 Value,
171}
172
173impl HomeLeaf {
174 /// The reserved name this leaf is written and rendered as.
175 pub fn name(self) -> &'static str {
176 match self {
177 HomeLeaf::Module => "__module",
178 HomeLeaf::New => "__new",
179 HomeLeaf::Value => "__value",
180 }
181 }
182}
183// TODO: annotate expressions with source spans
184// TODO: do not hardcode `Vec`/`Box`
185
186/// The body of a [`recur`](Expr::Recur) group. It is an `Option` on
187/// [`ModuleData`] because a *module* may decline to declare one, but a group's is
188/// required by the grammar ([`ParseError::RecurMissingBody`]) — so every
189/// `Expr::Recur` has one, and the two readers that would otherwise both unwrap
190/// (`to_sexp`/`to_json` here, [`eval`](crate::eval) there) say why once.
191///
192/// [`ParseError::RecurMissingBody`]: crate::ParseError::RecurMissingBody
193pub(crate) fn recur_body(data: &ModuleData) -> &Expr {
194 data.body().expect("a recur group always has a body")
195}
196
197/// The *code* of an abstraction: the parameter patterns and the body. Shared
198/// behind `Rc` between the [`Expr::Abs`] that builds a closure and the
199/// `Value::Closure` that captures an environment over it, so a partial
200/// application is one `Rc::clone` (plus a bumped `applied` counter and an
201/// extended environment) rather than a fresh `Lambda` allocation. The **arity is
202/// `head.len()`** — collected greedily at parse from a run of `&p0 &p1 …` — and
203/// is capped at 255 (see `parse.rs`). `head` is non-empty: every abstraction
204/// binds at least one parameter.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Lambda {
207 pub head: Box<[Pattern]>,
208 pub body: Expr,
209 /// How a closure over this lambda gets its environment: see [`Captures`].
210 /// Derived data, not syntax — it is not rendered by `to_sexp` / `to_json`.
211 pub captures: Captures,
212}
213
214impl Lambda {
215 /// The number of parameters this abstraction binds before its body runs.
216 pub fn arity(&self) -> usize {
217 self.head.len()
218 }
219}
220
221/// How a closure over a [`Lambda`] gets the environment its body runs in — decided
222/// per abstraction by the resolver, and the *only* thing that distinguishes the two
223/// closure representations (both are semantically identical; environments are
224/// immutable, so reading a value now or later gives the same answer).
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum Captures {
227 /// Not yet resolved — the state the parser leaves behind. The evaluator refuses
228 /// it rather than silently reading the wrong slots (the same posture as an
229 /// unresolved [`Expr::Name`] at eval).
230 Unresolved,
231 /// **Share** the defining environment by reference: no frame, no allocation, and
232 /// the body's indices keep counting into the enclosing activation's chain.
233 ///
234 /// Chosen where a closure cannot outlive the call that creates it, so retaining
235 /// the chain retains nothing observable: the callee of a direct application
236 /// (every `let`), and a literal `&`-abstraction handed straight to a builtin that
237 /// only *applies* it — a `__match` clause, an `__Err.catch` or `__eq` branch, a
238 /// `__Int.for` / `__Map.for` / `__Map.merge` callback. Elly mints these on every
239 /// call, and making each one copy a frame is what a measured 10–28% regression
240 /// was made of (see `docs/done/2026-08-14_elly-perf-closure-capture.md`).
241 Chain,
242 /// **Capture** these free variables into a fresh frame, one [`Capture`] each,
243 /// **sorted by `outer`** (see `Capture` for why). The closure then retains only
244 /// what its body uses, and a reference into an enclosing scope is one index into
245 /// the frame (see `EnvNode::Frame` in `eval.rs`). This is the default — every
246 /// abstraction whose closure may escape. An empty plan is a *closed* lambda: it
247 /// captures nothing and allocates no frame.
248 Frame(Box<[Capture]>),
249}
250
251/// One captured free variable: **where to read it** (`outer` — a de Bruijn index in
252/// the *enclosing* activation, at the point the [`Expr::Abs`] is evaluated) and
253/// **where it goes** (`slot` — its position in the closure's frame).
254///
255/// The two are separate because the resolver fixes `slot` at a free variable's
256/// first use, while `outer` only falls out of resolving it in the enclosing scopes;
257/// re-ordering the frame to match would mean renumbering references already emitted
258/// into the body. Instead the resolver sorts the *plan* by `outer`, so the
259/// evaluator fills the whole frame in **one** walk of the defining environment
260/// (ascending indices, never restarting) and writes each value to its `slot`.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub struct Capture {
263 pub outer: u32,
264 pub slot: u32,
265}
266
267/// A **pattern**: the left-hand side of any binding (`&<pat> …`, `let (<pat> =
268/// …)`, a `__match` clause). Matched against a value by the evaluator's native
269/// matcher (`match_pattern` in `eval.rs`), which either extends the environment
270/// or refutes. `Bind`/`Discard` are irrefutable; every other shape may refute.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub enum Pattern {
273 /// `_` — matches anything, binds nothing.
274 Discard,
275 /// `name` — binds the subject to `name`. The matcher conses one cell carrying
276 /// the subject onto the environment; the name itself is not stored at runtime
277 /// (references to it were resolved to a de Bruijn index).
278 Bind(Text),
279 /// `name = <pat>` — binds the subject to `name`, then matches `<pat>` against
280 /// the same subject.
281 At(Text, Box<Pattern>),
282 /// `= <expr>` (and its `.sym` sugar) — matches iff the subject equals the
283 /// value of `<expr>`; binds nothing. Evaluating `<expr>` may raise a real
284 /// error, which propagates (it is not a refutation).
285 Equal(Expr),
286 /// `< <expr>` — matches iff the subject is (strictly) ordered before the value
287 /// of `<expr>`; binds nothing. Ordering is defined only within a comparable
288 /// kind (`Int` for now, later `Str`), so a subject/bound that are not both of
289 /// one such kind refute with `.not_int` (a refutation, not a host raise).
290 Less(Expr),
291 /// `> <expr>` — the mirror of [`Less`](Pattern::Less): matches iff the subject
292 /// is ordered after the value of `<expr>`.
293 Greater(Expr),
294 /// `<pat> as <Proto>` / `(as <Proto>) <pat>` — matches iff the subject's
295 /// prototype is `<Proto>`, then matches `<pat>` against the (narrowed)
296 /// subject. See [`ProtoRef`] for the two forms the prototype takes.
297 Type(ProtoRef, Box<Pattern>),
298 /// `__value <pat>` — matches iff the subject is an **object of the home
299 /// module** and its payload matches `<pat>`, binding what the inner pattern
300 /// binds. It is the pattern side of the [`__value`](HomeLeaf::Value) leaf, and
301 /// the prototype check is implicit for the same reason the leaf's is:
302 /// unwrapping a foreign object has no sound reading.
303 ///
304 /// It holds no reference — the module is implicit, exactly as it is for
305 /// `__new` — so the pattern grammar stays closed. `depth` is the leaf's, and
306 /// the matcher walks the environment to that terminal and compares the
307 /// prototype by pointer before descending.
308 Unwrap { depth: u32, inner: Box<Pattern> },
309 /// `(p1 | p2)` — try the left arm, on refutation try the right. Both arms bind
310 /// the same set of names (checked at parse time). **Committed**: once an arm's
311 /// pattern matches, a later failure does not backtrack into the other arm.
312 Or(Box<Pattern>, Box<Pattern>),
313 /// `[p0, …]` with an optional trailing rest — a fixed-arity list pattern.
314 List(Vec<Pattern>, Rest),
315 /// `{ k0: p0, … }` — a map pattern of (key, value pattern) entries.
316 /// `Rest::None` is **closed** (exactly these keys); `Rest::Anon`/`Named` open
317 /// it (at least these keys; `Named` binds the remainder map).
318 Map(Vec<(MapKey, Pattern)>, Rest),
319}
320
321/// A map-pattern entry's key. A **lookup** key (`.sym`, bare `sym`, `(= expr)`)
322/// names a concrete key to probe. A **capture** key (a binder pattern in key
323/// position, e.g. `(k)` / `(_)`) instead *peels* the smallest remaining entry and
324/// matches its key against the pattern — the map analogue of `[a, ...rest]`.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub enum MapKey {
327 Lookup(Expr),
328 Capture(Box<Pattern>),
329}
330
331/// A pattern's trailing rest: none, an anonymous `...` (ignore the remainder), or
332/// a named `...rest` (bind it to the remainder). For a list, `None` is an exact
333/// arity; for a map, `None` is a closed key set — either way a rest opens it.
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub enum Rest {
336 None,
337 Anon,
338 /// `...name` — binds the remainder (list tail / leftover map) to `name`; the
339 /// matcher conses one cell carrying the remainder onto the environment.
340 Named(Text),
341}
342
343/// The prototype an `as <Proto>` pattern narrows to. One rule covers both forms —
344/// the subject matches iff its prototype *is* the named module — and they differ
345/// only in how much of the work is left to run time.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub enum ProtoRef {
348 /// The **compiled fast form**: a reference the resolver recognized as the
349 /// builtin module that is some value kind's prototype, so the check is the
350 /// discriminant test [`TyKind`] always was rather than an eval plus a pointer
351 /// compare. `as Int` takes this path, alias or `__`-spelling alike.
352 Kind(TyKind),
353 /// Any other prototype, named by an **atom** — a module item, a local, a
354 /// frame slot, or `__module`. Resolved like an [`Equal`](Pattern::Equal)
355 /// comparand, evaluated at match time, and required to be a module value.
356 Ref(Expr),
357}
358
359impl ProtoRef {
360 /// Render the reference: a kind by its bare name (`Int`), so the existing
361 /// goldens are unchanged, and anything else as the expression it is.
362 fn write_sexp(&self, out: &mut String) {
363 match self {
364 ProtoRef::Kind(ty) => out.push_str(ty.name()),
365 ProtoRef::Ref(expr) => expr.write_sexp(out),
366 }
367 }
368
369 fn write_json(&self, out: &mut String) {
370 match self {
371 ProtoRef::Kind(ty) => json_str(ty.name(), out),
372 ProtoRef::Ref(expr) => expr.write_json(out),
373 }
374 }
375}
376
377/// The value kinds whose prototype is a **builtin module**, so that `as <Proto>`
378/// against it compiles to a discriminant test (see [`ProtoRef::Kind`]).
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub enum TyKind {
381 Int,
382 Sym,
383 Str,
384 List,
385 Map,
386 Fun,
387 /// A module value, whose prototype is `__Mod`. It has no `as Mod` history —
388 /// the kind set was closed before modules existed — and is here because
389 /// `__Mod` is a builtin module like the rest, so the fast form covers it.
390 Mod,
391}
392
393impl TyKind {
394 /// The spelling used in the `as <Proto>` surface and the dumps.
395 pub fn name(self) -> &'static str {
396 match self {
397 TyKind::Int => "Int",
398 TyKind::Sym => "Sym",
399 TyKind::Str => "Str",
400 TyKind::List => "List",
401 TyKind::Map => "Map",
402 TyKind::Fun => "Fun",
403 TyKind::Mod => "Mod",
404 }
405 }
406}
407
408impl Expr {
409 /// Serialize to a compact s-expression, e.g. `(app (name f) (name x))`.
410 pub fn to_sexp(&self) -> String {
411 let mut out = String::new();
412 self.write_sexp(&mut out);
413 out
414 }
415
416 // TODO: replace with impl de::Deserialize, impl ser::Serialize
417 /// Serialize to compact, tagged JSON, e.g.
418 /// `{"app":[{"name":"f"},{"name":"x"}]}`. Parallels `muon::Seq::to_json`;
419 /// used by the `elly-wasm` playground to render the AST as a tree.
420 pub fn to_json(&self) -> String {
421 let mut out = String::new();
422 self.write_json(&mut out);
423 out
424 }
425
426 fn write_json(&self, out: &mut String) {
427 match self {
428 Expr::Int(n) => {
429 // f64 (JSON's only number type) represents every integer in
430 // [-2^53, 2^53] exactly. Within that range emit a bare number so
431 // the playground shows it as-is; outside it, fall back to a
432 // decimal *string* tagged `Int` to avoid precision loss.
433 if fits_f64_exactly(n) {
434 out.push_str(&n.to_string());
435 } else {
436 out.push_str("{\"Int\":");
437 json_str(&n.to_string(), out);
438 out.push('}');
439 }
440 }
441 // A `Name` (unresolved) and a `Local` (resolved) both render by name.
442 Expr::Name(n) => {
443 out.push_str("{\"name\":");
444 json_str(n, out);
445 out.push('}');
446 }
447 Expr::Local { name, .. } => {
448 out.push_str("{\"name\":");
449 json_str(name.as_str(), out);
450 out.push('}');
451 }
452 // A parse-resolved builtin renders as the `Name` it stands in for.
453 Expr::Builtin(op) => {
454 out.push_str("{\"name\":");
455 json_str(op.name(), out);
456 out.push('}');
457 }
458 // A home-module leaf renders as the reserved name it was written as,
459 // for the reason a `Local` and a `Builtin` do: the depth beside it is
460 // derived addressing, not syntax.
461 Expr::Home { leaf, .. } => {
462 out.push_str("{\"name\":");
463 json_str(leaf.name(), out);
464 out.push('}');
465 }
466 Expr::ModItem { name, .. } => {
467 out.push_str("{\"moditem\":");
468 json_str(name.as_str(), out);
469 out.push('}');
470 }
471 Expr::Recur(data) => {
472 out.push_str("{\"recur\":[");
473 for (i, (name, body)) in data.items().iter().enumerate() {
474 if i > 0 {
475 out.push(',');
476 }
477 out.push('[');
478 json_str(name.as_str(), out);
479 out.push(',');
480 body.write_json(out);
481 out.push(']');
482 }
483 out.push_str("],\"body\":");
484 recur_body(data).write_json(out);
485 out.push('}');
486 }
487 Expr::Symbol(s) => {
488 let mut dotted = String::from(".");
489 dotted.push_str(s);
490 out.push_str("{\"sym\":");
491 json_str(&dotted, out);
492 out.push('}');
493 }
494 Expr::Str(s) => {
495 out.push_str("{\"str\":");
496 json_str(s, out);
497 out.push('}');
498 }
499 // Render the call spine flat, preserving the arity the AST stores.
500 Expr::App(items) => write_app_json(items, out),
501 // Re-render the collected head as nested single-parameter abstractions.
502 Expr::Abs(code) => write_abs_json(&code.head, &code.body, out),
503 Expr::List(elems) => {
504 out.push_str("{\"list\":[");
505 for (i, e) in elems.iter().enumerate() {
506 if i > 0 {
507 out.push(',');
508 }
509 e.write_json(out);
510 }
511 out.push_str("]}");
512 }
513 Expr::Map(entries) => {
514 out.push_str("{\"map\":[");
515 for (i, (k, v)) in entries.iter().enumerate() {
516 if i > 0 {
517 out.push(',');
518 }
519 out.push('[');
520 k.write_json(out);
521 out.push(',');
522 v.write_json(out);
523 out.push(']');
524 }
525 out.push_str("]}");
526 }
527 }
528 }
529
530 fn write_sexp(&self, out: &mut String) {
531 match self {
532 Expr::Int(n) => {
533 out.push_str("(int ");
534 out.push_str(&n.to_string());
535 out.push(')');
536 }
537 // A `Name` (unresolved) and a `Local` (resolved) both render by name.
538 Expr::Name(n) => {
539 out.push_str("(name ");
540 out.push_str(n);
541 out.push(')');
542 }
543 Expr::Local { name, .. } => {
544 out.push_str("(name ");
545 out.push_str(name.as_str());
546 out.push(')');
547 }
548 // A parse-resolved builtin renders as the `Name` it stands in for.
549 Expr::Builtin(op) => {
550 out.push_str("(name ");
551 out.push_str(op.name());
552 out.push(')');
553 }
554 Expr::Home { leaf, .. } => {
555 out.push_str("(name ");
556 out.push_str(leaf.name());
557 out.push(')');
558 }
559 Expr::ModItem { name, .. } => {
560 out.push_str("(moditem ");
561 out.push_str(name.as_str());
562 out.push(')');
563 }
564 // `(recur (bind a …) (bind b …) <body>)` — the bindings in the table's
565 // sorted order, then the body, so the group reads as one form.
566 Expr::Recur(data) => {
567 out.push_str("(recur");
568 for (name, body) in data.items() {
569 out.push_str(" (bind ");
570 out.push_str(name.as_str());
571 out.push(' ');
572 body.write_sexp(out);
573 out.push(')');
574 }
575 out.push(' ');
576 recur_body(data).write_sexp(out);
577 out.push(')');
578 }
579 Expr::Symbol(s) => {
580 out.push_str("(sym .");
581 out.push_str(s);
582 out.push(')');
583 }
584 // A string leaf is the bare quoted, re-escaped literal — the quotes
585 // already set it apart from `(sym .foo)` / `(name foo)`, so no wrapper.
586 Expr::Str(s) => json_str(s, out),
587 // Render the call spine flat, preserving the arity the AST stores.
588 Expr::App(items) => write_app_sexp(items, out),
589 // Re-render the collected head as nested single-parameter abstractions.
590 Expr::Abs(code) => write_abs_sexp(&code.head, &code.body, out),
591 Expr::List(elems) => {
592 out.push_str("(list");
593 for e in elems {
594 out.push(' ');
595 e.write_sexp(out);
596 }
597 out.push(')');
598 }
599 Expr::Map(entries) => {
600 out.push_str("(map");
601 for (k, v) in entries {
602 out.push_str(" (entry ");
603 k.write_sexp(out);
604 out.push(' ');
605 v.write_sexp(out);
606 out.push(')');
607 }
608 out.push(')');
609 }
610 }
611 }
612}
613
614/// Render a flat call spine `[callee, a0, a1, …]` (≥ 2 elements) as the flat
615/// s-expression `(app callee a0 a1)` — the arity is shown as the AST stores it,
616/// not re-curried into left-nested binary `(app (app callee a0) a1)` forms.
617fn write_app_sexp(items: &[Expr], out: &mut String) {
618 out.push_str("(app");
619 for it in items {
620 out.push(' ');
621 it.write_sexp(out);
622 }
623 out.push(')');
624}
625
626/// Render a collected head `[p0, p1, …]` (non-empty) over `body` as the nested
627/// `(abs p0 (abs p1 body))` it stands for.
628fn write_abs_sexp(head: &[Pattern], body: &Expr, out: &mut String) {
629 out.push_str("(abs ");
630 head[0].write_sexp(out);
631 out.push(' ');
632 if head.len() == 1 {
633 body.write_sexp(out);
634 } else {
635 write_abs_sexp(&head[1..], body, out);
636 }
637 out.push(')');
638}
639
640/// The tagged-JSON twin of [`write_app_sexp`]: `{"app":[callee, a0, a1, …]}`, the
641/// whole spine flat so the AST view shows the call's arity as stored.
642fn write_app_json(items: &[Expr], out: &mut String) {
643 out.push_str("{\"app\":[");
644 for (i, it) in items.iter().enumerate() {
645 if i > 0 {
646 out.push(',');
647 }
648 it.write_json(out);
649 }
650 out.push_str("]}");
651}
652
653/// The tagged-JSON twin of [`write_abs_sexp`]: `{"abs":{"param":…,"body":…}}` nested.
654fn write_abs_json(head: &[Pattern], body: &Expr, out: &mut String) {
655 out.push_str("{\"abs\":{\"param\":");
656 head[0].write_json(out);
657 out.push_str(",\"body\":");
658 if head.len() == 1 {
659 body.write_json(out);
660 } else {
661 write_abs_json(&head[1..], body, out);
662 }
663 out.push_str("}}");
664}
665
666impl Pattern {
667 /// Serialize a pattern to the same compact s-expression family as `Expr`.
668 /// A bare `Bind`/`Discard` renders as just its name (`a`, `_`) so the common
669 /// `&x` reads `(abs x …)`; structured patterns are tagged forms
670 /// (`(list a ...rest)`, `(at n <p>)`, `(as Int <p>)`, `(eq <expr>)`, …).
671 fn write_sexp(&self, out: &mut String) {
672 match self {
673 Pattern::Discard => out.push('_'),
674 Pattern::Bind(name) => out.push_str(name),
675 Pattern::At(name, inner) => {
676 out.push_str("(at ");
677 out.push_str(name);
678 out.push(' ');
679 inner.write_sexp(out);
680 out.push(')');
681 }
682 Pattern::Equal(expr) => {
683 out.push_str("(eq ");
684 expr.write_sexp(out);
685 out.push(')');
686 }
687 Pattern::Less(expr) => {
688 out.push_str("(lt ");
689 expr.write_sexp(out);
690 out.push(')');
691 }
692 Pattern::Greater(expr) => {
693 out.push_str("(gt ");
694 expr.write_sexp(out);
695 out.push(')');
696 }
697 Pattern::Type(proto, inner) => {
698 out.push_str("(as ");
699 proto.write_sexp(out);
700 out.push(' ');
701 inner.write_sexp(out);
702 out.push(')');
703 }
704 Pattern::Unwrap { inner, .. } => {
705 out.push_str("(unwrap ");
706 inner.write_sexp(out);
707 out.push(')');
708 }
709 Pattern::Or(left, right) => {
710 out.push_str("(or ");
711 left.write_sexp(out);
712 out.push(' ');
713 right.write_sexp(out);
714 out.push(')');
715 }
716 Pattern::List(elems, rest) => {
717 out.push_str("(list");
718 for e in elems {
719 out.push(' ');
720 e.write_sexp(out);
721 }
722 rest.write_sexp(out);
723 out.push(')');
724 }
725 Pattern::Map(entries, rest) => {
726 out.push_str("(map");
727 for (key, vpat) in entries {
728 out.push_str(" (entry ");
729 key.write_sexp(out);
730 out.push(' ');
731 vpat.write_sexp(out);
732 out.push(')');
733 }
734 rest.write_sexp(out);
735 out.push(')');
736 }
737 }
738 }
739
740 fn write_json(&self, out: &mut String) {
741 match self {
742 Pattern::Discard => out.push_str("{\"discard\":true}"),
743 Pattern::Bind(name) => {
744 out.push_str("{\"bind\":");
745 json_str(name, out);
746 out.push('}');
747 }
748 Pattern::At(name, inner) => {
749 out.push_str("{\"at\":[");
750 json_str(name, out);
751 out.push(',');
752 inner.write_json(out);
753 out.push_str("]}");
754 }
755 Pattern::Equal(expr) => {
756 out.push_str("{\"eq\":");
757 expr.write_json(out);
758 out.push('}');
759 }
760 Pattern::Less(expr) => {
761 out.push_str("{\"lt\":");
762 expr.write_json(out);
763 out.push('}');
764 }
765 Pattern::Greater(expr) => {
766 out.push_str("{\"gt\":");
767 expr.write_json(out);
768 out.push('}');
769 }
770 Pattern::Type(proto, inner) => {
771 out.push_str("{\"as\":[");
772 proto.write_json(out);
773 out.push(',');
774 inner.write_json(out);
775 out.push_str("]}");
776 }
777 Pattern::Unwrap { inner, .. } => {
778 out.push_str("{\"unwrap\":");
779 inner.write_json(out);
780 out.push('}');
781 }
782 Pattern::Or(left, right) => {
783 out.push_str("{\"or\":[");
784 left.write_json(out);
785 out.push(',');
786 right.write_json(out);
787 out.push_str("]}");
788 }
789 Pattern::List(elems, rest) => {
790 out.push_str("{\"listpat\":{\"elems\":[");
791 for (i, e) in elems.iter().enumerate() {
792 if i > 0 {
793 out.push(',');
794 }
795 e.write_json(out);
796 }
797 out.push_str("],\"rest\":");
798 rest.write_json(out);
799 out.push_str("}}");
800 }
801 Pattern::Map(entries, rest) => {
802 out.push_str("{\"mappat\":{\"entries\":[");
803 for (i, (key, vpat)) in entries.iter().enumerate() {
804 if i > 0 {
805 out.push(',');
806 }
807 out.push('[');
808 key.write_json(out);
809 out.push(',');
810 vpat.write_json(out);
811 out.push(']');
812 }
813 out.push_str("],\"rest\":");
814 rest.write_json(out);
815 out.push_str("}}");
816 }
817 }
818 }
819}
820
821impl Rest {
822 /// Append a trailing rest to an s-expression list/map form: nothing for
823 /// `None`, ` ...` for `Anon`, ` ...name` for `Named`.
824 fn write_sexp(&self, out: &mut String) {
825 match self {
826 Rest::None => {}
827 Rest::Anon => out.push_str(" ..."),
828 Rest::Named(name) => {
829 out.push_str(" ...");
830 out.push_str(name);
831 }
832 }
833 }
834
835 fn write_json(&self, out: &mut String) {
836 match self {
837 Rest::None => out.push_str("null"),
838 Rest::Anon => out.push_str("\"...\""),
839 Rest::Named(name) => {
840 out.push_str("{\"named\":");
841 json_str(name, out);
842 out.push('}');
843 }
844 }
845 }
846}
847
848impl MapKey {
849 fn write_sexp(&self, out: &mut String) {
850 match self {
851 // A lookup key is a plain expression (`.a`, a computed `(name k)`).
852 MapKey::Lookup(expr) => expr.write_sexp(out),
853 // A capture key wraps a pattern that matches the peeled key.
854 MapKey::Capture(kpat) => {
855 out.push_str("(capture ");
856 kpat.write_sexp(out);
857 out.push(')');
858 }
859 }
860 }
861
862 fn write_json(&self, out: &mut String) {
863 match self {
864 MapKey::Lookup(expr) => {
865 out.push_str("{\"lookup\":");
866 expr.write_json(out);
867 out.push('}');
868 }
869 MapKey::Capture(kpat) => {
870 out.push_str("{\"capture\":");
871 kpat.write_json(out);
872 out.push('}');
873 }
874 }
875 }
876}
877
878/// Whether `n` is exactly representable as an `f64`: every integer in
879/// `[-2^53, 2^53]` is (2^53 is the largest magnitude with no representation gap).
880fn fits_f64_exactly(n: &BigInt) -> bool {
881 const LIMIT: i64 = 1 << 53; // 2^53 == 9_007_199_254_740_992
882 *n >= BigInt::from(-LIMIT) && *n <= BigInt::from(LIMIT)
883}
884
885/// Write `s` as a JSON string literal (quoted and escaped). Shared with `eval.rs`
886/// so a `Value::Str`'s display re-escapes identically to an `Expr::Str`'s dumps.
887pub(crate) fn json_str(s: &str, out: &mut String) {
888 out.push('"');
889 for ch in s.chars() {
890 match ch {
891 '"' => out.push_str("\\\""),
892 '\\' => out.push_str("\\\\"),
893 '\n' => out.push_str("\\n"),
894 '\r' => out.push_str("\\r"),
895 '\t' => out.push_str("\\t"),
896 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
897 c => out.push(c),
898 }
899 }
900 out.push('"');
901}