Skip to main content

elly_core/
parse.rs

1//! Parsing: Muon tree → Elly AST.
2//!
3//! A program is a single chain (`<expr>`). A chain's items are split on the
4//! first `&`-prefixed item into an application spine and a trailing
5//! abstraction, per `docs/elly-spec.md` and `docs/done/2026-07-06_elly-crate.md`.
6//! Comments are dropped.
7//!
8//! **Patterns** (see `docs/done/2026-07-26_elly-patterns-native.md`) are
9//! *faithful AST*: a `&`-header or a `let` binding's left-hand side parses into
10//! a [`Pattern`] node that the evaluator matches directly. Parsing does not compile patterns
11//! into eliminators — it only rephrases the Muon syntax into Elly's own
12//! structures. Every abstraction is an `Expr::Abs(Rc<Lambda>)`, a run of
13//! `&p0 &p1 …` collected into one multi-parameter [`Lambda`]; applying it matches
14//! the arguments against the head (refuting via `NoMatch` on a miss). Source
15//! application spines and spread calls flatten into one `Expr::App(Box<[Expr]>)`.
16
17use alloc::boxed::Box;
18use alloc::rc::Rc;
19use alloc::string::String;
20use alloc::vec::Vec;
21
22use muon::{Chain, Item, Seq};
23use num_bigint::BigInt;
24
25use crate::ast::{Captures, Expr, HomeLeaf, Lambda, MapKey, Pattern, ProtoRef, Rest, Text};
26use crate::eval::{Builtin, ModuleData};
27
28/// Copy a source string slice into an owned text leaf. The AST no longer borrows
29/// the Muon source (see `docs/todo/elly-impl-expr-repr-v1.md`), so every name /
30/// symbol / bound name captured at parse time is copied here (inline for short
31/// spellings, else a shared `Rc`). Takes `&str` so a `&&str` binding from a
32/// slice pattern deref-coerces at the call site.
33fn text(s: &str) -> Text {
34    Text::from(s)
35}
36
37// TODO: use thiserror?
38/// A parse failure: the input is a valid Muon tree but not a well-formed Elly
39/// program in this subset.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ParseError {
42    /// The top-level sequence held no chain (nothing to evaluate).
43    EmptyProgram,
44    /// The top-level sequence held more than one chain; top-level sequencing
45    /// is deferred (a program is a single expression).
46    MultipleExpressions,
47    /// A chain reduced to nothing (e.g. only comments).
48    EmptyChain,
49    /// A `&` binder with no body — `&x` alone is ill-formed (yet).
50    AbsWithoutBody,
51    /// A `&` prefix on something that is not a well-formed binder header (see
52    /// `BadPattern` for the pattern content itself).
53    BadBinder,
54    /// A binder appeared where an atomic expression was expected. Not reachable
55    /// via the split logic, kept so parsing never panics.
56    UnexpectedBinder,
57    /// A `.` sigil on something that is not a single name/number segment (e.g.
58    /// `.(…)`, `.&x`). `.foo` / `.0` are symbols; the record sugar `.(…)` and
59    /// other prefixed items are deferred / ill-formed.
60    BadSymbol,
61    /// A bare `.` used as an atom (`f . g`, a lone `.`). At the Muon layer this
62    /// is a `<punct>`; Elly reserves it for a future `.` / `|>` application
63    /// combinator, which is deferred.
64    CombinatorDeferred,
65    /// `_` used as a reference. `_` is the discard pattern (a binder that binds
66    /// nothing), never a name that can be read back.
67    DiscardReference,
68    /// A `&`-binder tried to introduce a name starting with `__`. Such names are
69    /// reserved for builtins / special forms and may be referenced but not bound.
70    ReservedName,
71    /// A `<sym>` that begins like a number but is not a valid integer literal
72    /// (`1a`, `0xZZ`, `1__2`, `0x`). Since a `<name>` may not start with a digit,
73    /// a digit-leading sym must be a well-formed `<int>` or it is an error.
74    MalformedNumber,
75    /// A stray `:` / `=` (or other) `<punct>` item at atom position. Muon parses
76    /// this punctuation, but outside a `let` binding (`=`) or a map entry (`:`)
77    /// it has no atomic reading. (A `<block>` `{…}` is a map literal and a
78    /// `<list>` `[…]` a runtime list value — neither is deferred.)
79    PunctAtAtom,
80    /// A map entry (`<block>` chain) whose key is missing or malformed: no `:`
81    /// after the key, or a key item that is not a bare atom, a `.`-symbol, a `(…)`
82    /// computed key, or a `[…]` list/unit key (`{ : 1 }`, `{ foo }`).
83    MalformedMapKey,
84    /// A `let` not followed by a binder group — `let 1 e`. An *empty* group is not
85    /// an error: `let () e` binds nothing and is `e` (see `parse_let`).
86    LetMissingBinders,
87    /// A `let (…)` with nothing after the binder group; the body is required.
88    LetMissingBody,
89    /// A `recur` not followed by a binder group — `recur 1 e`. As with `let`, an
90    /// empty group is not an error: `recur () e` is `e`.
91    RecurMissingBinders,
92    /// A `recur (…)` with nothing after the binder group; the body is required.
93    RecurMissingBody,
94    /// A `recur` binding whose name is already in scope. The group's bindings are
95    /// simultaneous, so inside it the name would mean the group's own binding and
96    /// never the outer one — the opposite of what the identical `let` line means.
97    /// Detected by the resolution pass (`resolve.rs`); carries the offending name.
98    RecurShadowsOuter(Text),
99    /// A binding inside a `let (…)` group is not `<pattern> "=" <expr>`: missing
100    /// or repeated top-level `=`, an empty pattern, or an empty value.
101    MalformedBinding,
102    /// A keyword (`let`, `with`) used as a reference or a binder. Keywords are
103    /// syntax, not names: they may be neither read nor bound.
104    ReservedKeyword,
105    /// A malformed pattern: an empty pattern, a comma-grouped `(a, b)` used as a
106    /// single pattern, a misplaced `...rest`, or any item with no pattern reading.
107    BadPattern,
108    /// A comparand after `=` / `<` / `>` that is not a single **atom** (a name or
109    /// a literal). A pattern is a closed grammar — `= f x`, `< (f x)`, `= [a]`
110    /// have no reading; write `= x`, `= 5`, or `.foo`.
111    ComparandNotAtom,
112    /// A bare **matching literal** used as an entire `&`-header or `let` binding
113    /// LHS: a number (`&42`), a symbol (`&.foo`), or a string (`&"foo"`).
114    /// Parenthesize it — `&(42)`, `&(.foo)`, `&("foo")`, `let ((42) = v)` — so the
115    /// "match this literal" intent is explicit (a bare literal reads as a value, or
116    /// as an intended binder). A literal *nested* in `[…]` / `{…}` / `(…)` / an
117    /// or-pattern needs no parens.
118    BareLiteralHeader,
119    /// A `<str>` literal with an **unpaired UTF-16 surrogate** escape (`"\uD83D"`,
120    /// a high or low `\uXXXX` not completing a pair). Muon accepts it lexically,
121    /// but UTF-8 cannot represent it, so Elly's decode rejects it at parse.
122    LoneSurrogate,
123    /// A bare top-level `|` in a `let` binding's LHS: `let ((x=.a) | (x=.b) = v)`.
124    /// Wrap the or-pattern — `let (((x=.a) | (x=.b)) = v) e` — so it does not
125    /// visually compete with the binding's own `=`.
126    LetOrUnparenthesized,
127    /// An `as <Type>` whose `<Type>` is not one of the closed kinds
128    /// `Int Sym List Map Fun`.
129    UnknownType,
130    /// An or-pattern `(p | q)` whose arms bind different sets of names.
131    OrBindersMismatch,
132    /// A `|>` pipe combinator with a missing operand — `|> f`, `x |>`, or
133    /// `x |> |> f`. Both sides of `|>` must be non-empty.
134    MalformedPipe,
135    /// A name reference that resolves to no enclosing `&`-binding — a free
136    /// variable. Detected by the resolution pass (`resolve.rs`) after parsing,
137    /// so it is a *static* error: the program is rejected before it runs, rather
138    /// than raising `.unbound_name` at eval. Carries the offending name (spans
139    /// are still a TODO). `__`-names never reach here — they are resolved to
140    /// [`Expr::Builtin`](crate::Expr) at parse.
141    UnboundName(Text),
142    /// A module (`parse_module`) binding whose left-hand side is not a single bare
143    /// name: a destructuring pattern, a literal, a discard, or a digit-leading
144    /// token. Module top-level keys are plain names in v0. A `__…` or keyword LHS
145    /// reports [`ParseError::ReservedName`] / [`ParseError::ReservedKeyword`] instead.
146    BadModuleItemName,
147    /// A module defines the same item name twice, or an `import`'s local name is
148    /// also an item name. A name means one thing inside a module, so both are
149    /// rejected here. The `Foo = import "spec"` form is the one place a name is
150    /// both an import and an item: it is a single declaration, and it makes them
151    /// the same thing.
152    DuplicateModuleItem,
153    /// An `import` that is neither `import "<spec>" as <Name>` nor
154    /// `<Name> = import "<spec>"`: a missing `as`, a spec that is not a string
155    /// literal, or trailing items. The spec is a literal in a fixed position —
156    /// which is what makes a module's imports a static, auditable list — so there
157    /// is nothing to compute here.
158    MalformedImport,
159    /// An `import` somewhere other than a module's top level — today, inside a
160    /// [`recur`](crate::Expr::Recur) group. An import is a frame slot the const
161    /// stage fills before the module exists, so it can only be written in a
162    /// module's own source. Written anywhere else, `import` is an ordinary keyword
163    /// and reports [`ParseError::ReservedKeyword`].
164    MisplacedImport,
165    /// A `const` declaration that is neither `const <name>`, `const (<name> …)`
166    /// nor `<name> = const`: a declaration that is not a bare name, a group whose
167    /// chains are not bare names, or a **payload** — `const x = 1`, `x = const 1`.
168    /// The payload forms are reserved for const-time expressions and rejected
169    /// until those exist (see `docs/done/2026-08-13_elly-modules-iotas.md`).
170    MalformedConst,
171    /// A `const` somewhere other than a module's top level — today, inside a
172    /// [`recur`](crate::Expr::Recur) group. A group is built while a program runs
173    /// and has no instance of its own to mint against, so an iota can only be
174    /// declared in a module's source. Written anywhere else, `const` is an
175    /// ordinary keyword and reports [`ParseError::ReservedKeyword`].
176    MisplacedConst,
177    /// A binder — a pattern binder, a `let` binding, a `recur` binding — taking a
178    /// name the enclosing module declares as an **import**. A declaration's name
179    /// means one thing over the whole module, so it may not be shadowed from
180    /// inside (see `docs/done/2026-08-13_elly-modules-iotas.md`). Carries the name.
181    ShadowsImport(Text),
182    /// A binder taking a name the enclosing module declares as a `const`
183    /// [iota](ParseError::ShadowsImport). Carries the name.
184    ShadowsIota(Text),
185    /// A `local` declaration that is neither `local <name> = <expr>` nor the
186    /// group `local (<name> = <expr> …)`: a left-hand side that is not a bare
187    /// name, a group chain that is not a binding, or a missing body. The
188    /// head-with-arguments form `local f x = <expr>` lands here too — it is a
189    /// template moditem, left open (see `docs/done/2026-08-13_elly-modules-local.md`).
190    MalformedLocal,
191    /// A `local` somewhere other than a module's top level — today, inside a
192    /// [`recur`](crate::Expr::Recur) group, where every binding is private
193    /// already, so the keyword would say nothing. Written anywhere else, `local`
194    /// is an ordinary keyword and reports [`ParseError::ReservedKeyword`].
195    MisplacedLocal,
196    /// A binder taking a name the enclosing module declares as a
197    /// [`local`](ParseError::ShadowsImport). Carries the name.
198    ShadowsLocal(Text),
199    /// A binder taking a name the enclosing module — or an enclosing
200    /// [`recur`](crate::Expr::Recur) group — declares as an **item**. Carries the
201    /// name. This is the direction opposite to
202    /// [`RecurShadowsOuter`](ParseError::RecurShadowsOuter), which stops a group
203    /// binding from shadowing a name already in scope; together they make a
204    /// declared name unshadowable from either side.
205    ShadowsItem(Text),
206    /// A `__` name the language does not define, in either position it can be
207    /// written: a module top-level left-hand side (`__mian = …`) or a reference
208    /// in an expression (`__mian`). The `__` namespace belongs to the language —
209    /// a source may write the builtins and the **reserved moditems** it defines
210    /// and nothing else — so a misspelling fails here, naming the namespace,
211    /// rather than becoming an item nobody reads or a name nothing binds.
212    /// Carries the name as written.
213    UnknownReserved(Text),
214    /// A module declaring the same reserved moditem twice (two `__main`s). A
215    /// reserved moditem is a single slot, not a table, so there is no second one
216    /// to hold. Carries the name.
217    DuplicateReserved(Text),
218    /// A reserved moditem somewhere other than a module's top level — today,
219    /// inside a [`recur`](crate::Expr::Recur) group. A group is an item table,
220    /// not a module source, and nothing would ever read the slot. Written
221    /// anywhere else, `__main` is an ordinary `__` name and reports
222    /// [`UnknownReserved`](ParseError::UnknownReserved).
223    MisplacedReserved,
224    /// A [home-module leaf](crate::HomeLeaf) — `__module`, `__new`, `__value`, or
225    /// the `__value <pat>` pattern — outside a module body. Each reads the module
226    /// its body is written in, and there is none: the term is not a module's, or
227    /// it is a [`recur`](crate::Expr::Recur) group in a program that is not a
228    /// module. One test covers both, since a group is not a module either.
229    /// Carries the name.
230    OutsideModule(Text),
231    /// An `as <Proto>` pattern whose reference is a `__` name that is not a
232    /// **prototype**: a builtin that is no module at all (`as __eq`), or `__Err`,
233    /// which is a namespace of operations that no value answers `__proto` with —
234    /// so the check could never hold. Rejected here rather than compiled into a
235    /// pattern that always refutes. Carries the name.
236    NotAPrototype(Text),
237}
238
239/// What a module's source writes to name an import — a path, a package-qualified
240/// name. Its grammar belongs to the host resolver, not to Elly.
241pub type ImportSpec = Text;
242
243/// What a resolver answers a spec with: one name per module, however many specs
244/// reach it. Module instances are deduplicated by this name, so a resolver that
245/// returns the spec unchanged would hand back two instances of one module for
246/// `"./foo"` and `"foo"` (see [`crate::load_module`]).
247pub type ModuleName = Text;
248
249/// One `import` declaration: the name the importing module reaches the instance
250/// by, and the spec its host resolver is handed.
251///
252/// The two written forms differ in whether the module also **re-exports** the
253/// instance. `import "spec" as Foo` is a private frame slot; `Foo = import "spec"`
254/// is the same slot plus an ordinary item `Foo` that forwards it. Both arrive here
255/// as one `ImportDecl` — the re-exporting form has additionally pushed its
256/// forwarding item into [`ModuleSyntax::items`] (see
257/// `docs/done/2026-08-11_elly-modules-import.md`).
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct ImportDecl {
260    /// The name the importing module's bodies read the instance by.
261    pub name: Text,
262    /// What the host resolver is handed.
263    pub spec: ImportSpec,
264}
265
266/// A module source's top-level declarations: its imports and its `const`
267/// declarations, in source order, and its items. The imports are the module's
268/// first frame slots — the const stage resolves, compiles and instantiates each
269/// one, then instantiates this module against the results (see
270/// [`crate::load_module`]). The iotas take no slot at all: an iota's identity is
271/// its instance plus its position, derived on reference (see
272/// `docs/done/2026-08-13_elly-modules-iotas.md`).
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct ModuleSyntax {
275    /// The `import` declarations, in source order, which is frame-slot order.
276    pub imports: Vec<ImportDecl>,
277    /// The names declared by `const`, in source order — the module's **iotas**.
278    /// Sorted before they reach [`ModuleData`](crate::ModuleData), where the
279    /// order is the address space a member reference counts against.
280    pub iotas: Vec<Text>,
281    /// The `name = body` items, in source order, bodies unresolved.
282    pub items: Vec<(Text, Expr)>,
283    /// The `local name = body` declarations, in source order, bodies unresolved
284    /// — the module's **private** bindings. They are members like the items, and
285    /// differ only in not being searched by name from outside (see
286    /// `docs/done/2026-08-13_elly-modules-local.md`).
287    pub locals: Vec<(Text, Expr)>,
288    /// What the module declared as `__main`, if anything — the **reserved
289    /// moditem** a runner evaluates and applies to the root capability.
290    ///
291    /// It is a slot rather than an entry in [`items`](Self::items), and that is
292    /// what makes it unnameable: the member index space it stays out of is the
293    /// only thing a body's reference or an outside `M.name` access can address.
294    /// See `docs/done/2026-08-14_elly-run.md`.
295    pub main: Option<Expr>,
296}
297
298/// Parse a whole parsed program (one top-level chain) to an expression.
299pub fn parse_program<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
300    match seq.0.as_slice() {
301        [] => Err(ParseError::EmptyProgram),
302        [chain] => parse_chain(chain),
303        _ => Err(ParseError::MultipleExpressions),
304    }
305}
306
307/// Parse a **module** source's top-level sequence into its declarations: one
308/// `name = body` item or one `import` per non-comment chain, in source order.
309/// Unlike [`parse_program`] a module is a *multi-chain* sequence, and a body is
310/// not resolved here — [`crate::compile_module`] runs
311/// [`crate::resolve::resolve_module_bodies`] afterwards so a body may refer to any
312/// sibling regardless of order. An item's left-hand side is a single bare name in
313/// v0 (no destructuring at module top level); duplicate names and non-name LHSs
314/// are rejected. See `docs/done/2026-08-07_elly-modules-v0.md`.
315///
316/// An `import` chain has no top-level `=` in its private form, so it is recognized
317/// before the `=` split rather than reported as a `MalformedBinding`. Its two
318/// forms are described on [`ImportDecl`]; `Foo = import "spec"` also pushes the
319/// item `Foo` that forwards the slot, since it is written as an item and should
320/// read as one from outside.
321///
322/// A `const` declaration is read the same way and follows the same privacy
323/// convention: `const false` and `const (false, true)` are private, `false =
324/// const` declares the iota *and* the item that exports it.
325///
326/// A `local` declaration is the ordinary binding under the same convention:
327/// `local x = <expr>` and the group `local (x = <expr> …)` bind a member the
328/// module's own bodies reach and nothing outside can, where the bare
329/// `x = <expr>` beside them is the export form.
330pub fn parse_module<'a>(seq: &Seq<'a>) -> Result<ModuleSyntax, ParseError> {
331    let mut items: Vec<(Text, Expr)> = Vec::new();
332    let mut imports: Vec<ImportDecl> = Vec::new();
333    let mut iotas: Vec<Text> = Vec::new();
334    let mut locals: Vec<(Text, Expr)> = Vec::new();
335    let mut main: Option<Expr> = None;
336    // The declarations that are also items, kept apart by kind, so the collision
337    // check below knows which shared names were written as one declaration and
338    // are therefore fine. A name shared by two *declarations* never is.
339    let mut reexported_imports: Vec<Text> = Vec::new();
340    let mut reexported_iotas: Vec<Text> = Vec::new();
341    for chain in &seq.0 {
342        let ci: Vec<&Item<'a>> = chain
343            .0
344            .iter()
345            .filter(|it| !matches!(it, Item::Comm(_)))
346            .collect();
347        if ci.is_empty() {
348            continue; // a blank / comment-only line separates items
349        }
350        // Split on the first top-level `=`, exactly as `parse_binding` does; a
351        // second top-level `=` is malformed (a nested `=` lives inside `(…)`/…).
352        let split = ci.iter().position(|it| matches!(it, Item::Punct("=")));
353        // `import "spec" as Foo` — a private slot, and the one module top-level
354        // chain with no `=` of its own. A chain that *has* one is a binding
355        // whatever it starts with, so `import = 3` still reports the keyword.
356        if split.is_none() && is_import_keyword(ci[0]) {
357            let (name, spec) = parse_private_import(&ci)?;
358            push_import(&mut imports, name, spec)?;
359            continue;
360        }
361        // `const false` / `const (false, true)` — a private declaration, and like
362        // a private import the whole chain is the declaration.
363        if split.is_none() && is_const_keyword(ci[0]) {
364            for name in parse_const_decl(&ci)? {
365                push_iota(&mut iotas, name)?;
366            }
367            continue;
368        }
369        // `local (x = 1, y = 2)` — the group form, whose bindings' `=`s all sit
370        // inside the tuple, so the chain has none of its own. The single form
371        // `local x = 1` does, and is read below with the other bindings.
372        if split.is_none() && is_local_keyword(ci[0]) {
373            for (name, body) in parse_local_group(&ci)? {
374                push_local(&mut locals, name, body)?;
375            }
376            continue;
377        }
378        let eq = split.ok_or(ParseError::MalformedBinding)?;
379        if ci[eq + 1..].iter().any(|it| matches!(it, Item::Punct("="))) {
380            return Err(ParseError::MalformedBinding);
381        }
382        let (head, tail) = (&ci[..eq], &ci[eq + 1..]);
383        if head.is_empty() || tail.is_empty() {
384            return Err(ParseError::MalformedBinding);
385        }
386        // `const x = <expr>`, the private const-time expression form the grammar
387        // reserves and nothing implements. A bare `const = 3` is not this — it
388        // falls through to `plain_name`, which reports the keyword.
389        if head.len() > 1 && is_const_keyword(head[0]) {
390            return Err(ParseError::MalformedConst);
391        }
392        // `local x = <expr>` — the single private binding. A bare `local = 3` is
393        // not this: it falls through to `plain_name`, which reports the keyword.
394        if head.len() > 1 && is_local_keyword(head[0]) {
395            let name = plain_name(&head[1..], ParseError::MalformedLocal)?;
396            push_local(&mut locals, name, parse_items(tail)?)?;
397            continue;
398        }
399        // `__main = <expr>` — a **reserved moditem**, recognized here because
400        // `plain_name` would otherwise stop at the `__` and report the name as
401        // reserved. It is not an item: it goes to its own slot, outside the
402        // member index space, so nothing can name it (see `ModuleSyntax::main`).
403        if let [Item::Sym(s)] = head {
404            if let Some(reserved) = reserved_moditem(s) {
405                if main.is_some() {
406                    return Err(ParseError::DuplicateReserved(text(reserved)));
407                }
408                main = Some(parse_items(tail)?);
409                continue;
410            }
411            if s.starts_with("__") {
412                return Err(ParseError::UnknownReserved(text(s)));
413            }
414        }
415        let name = plain_name(head, ParseError::BadModuleItemName)?;
416        if items.iter().any(|(n, _)| n.as_str() == name.as_str()) {
417            return Err(ParseError::DuplicateModuleItem);
418        }
419        // `Foo = import "spec"` — the same slot, re-exported. The item's body is
420        // the name itself: resolution looks imports up before items, so it reads
421        // the slot rather than looping back through this forwarding item.
422        if is_import_keyword(tail[0]) {
423            let spec = parse_import_spec(tail)?;
424            push_import(&mut imports, name.clone(), spec)?;
425            reexported_imports.push(name.clone());
426            items.push((name.clone(), Expr::Name(name)));
427            continue;
428        }
429        // `false = const` — the iota plus the item exporting it. There is nothing
430        // to the right of the keyword until const-time expressions exist, and the
431        // item's body is the name itself: a sibling reference finds the iota,
432        // since members are searched iotas-first (see `resolve_module_bodies`).
433        if is_const_keyword(tail[0]) {
434            if tail.len() != 1 {
435                return Err(ParseError::MalformedConst);
436            }
437            push_iota(&mut iotas, name.clone())?;
438            reexported_iotas.push(name.clone());
439            items.push((name.clone(), Expr::Name(name)));
440            continue;
441        }
442        let body = parse_items(tail)?;
443        items.push((name, body));
444    }
445    // A private declaration may not share a name with an item — inside the module
446    // the name would have to mean both. A re-exported one shares by construction.
447    // Two *declarations* may never share one, re-exported or not: neither form
448    // writes an import and an iota at once.
449    for imp in &imports {
450        if iotas.iter().any(|n| n.as_str() == imp.name.as_str()) {
451            return Err(ParseError::DuplicateModuleItem);
452        }
453        if reexported_imports
454            .iter()
455            .any(|n| n.as_str() == imp.name.as_str())
456        {
457            continue;
458        }
459        if items.iter().any(|(n, _)| n.as_str() == imp.name.as_str()) {
460            return Err(ParseError::DuplicateModuleItem);
461        }
462    }
463    for iota in &iotas {
464        if reexported_iotas.iter().any(|n| n.as_str() == iota.as_str()) {
465            continue;
466        }
467        if items.iter().any(|(n, _)| n.as_str() == iota.as_str()) {
468            return Err(ParseError::DuplicateModuleItem);
469        }
470    }
471    // A local shares with nothing: it is a declaration, and there is no form
472    // writing a local and something else at once.
473    for (name, _) in &locals {
474        let taken = items.iter().any(|(n, _)| n.as_str() == name.as_str())
475            || iotas.iter().any(|n| n.as_str() == name.as_str())
476            || imports.iter().any(|i| i.name.as_str() == name.as_str());
477        if taken {
478            return Err(ParseError::DuplicateModuleItem);
479        }
480    }
481    Ok(ModuleSyntax {
482        imports,
483        iotas,
484        items,
485        locals,
486        main,
487    })
488}
489
490/// The reserved moditems the language defines, as the names a module top-level
491/// left-hand side may spell in the `__` namespace. One so far; `__test` and
492/// `__doc` are anticipated, and each will be another slot on [`ModuleSyntax`]
493/// with a clause here (see `docs/done/2026-08-14_elly-run.md`).
494///
495/// Answers with the `'static` spelling so the caller reports the *defined* name
496/// rather than echoing the source, and `None` for a name that is not one — which
497/// the caller turns into [`ParseError::UnknownReserved`] rather than an item.
498fn reserved_moditem(name: &str) -> Option<&'static str> {
499    match name {
500        "__main" => Some("__main"),
501        _ => None,
502    }
503}
504
505/// Record one import, rejecting a name a previous import already took.
506fn push_import(
507    imports: &mut Vec<ImportDecl>,
508    name: Text,
509    spec: ImportSpec,
510) -> Result<(), ParseError> {
511    if imports.iter().any(|i| i.name.as_str() == name.as_str()) {
512        return Err(ParseError::DuplicateModuleItem);
513    }
514    imports.push(ImportDecl { name, spec });
515    Ok(())
516}
517
518/// Parse `import "<spec>" as <Name>` — the private form, whose whole chain is the
519/// declaration — to its name and spec.
520fn parse_private_import<'a>(ci: &[&Item<'a>]) -> Result<(Text, ImportSpec), ParseError> {
521    match ci {
522        [_import, Item::Str(spec), Item::Sym("as"), rest @ ..] if !rest.is_empty() => {
523            let name = plain_name(rest, ParseError::BadModuleItemName)?;
524            Ok((name, decode_str(spec)?))
525        }
526        _ => Err(ParseError::MalformedImport),
527    }
528}
529
530/// Record one iota, rejecting a name a previous `const` already declared.
531fn push_iota(iotas: &mut Vec<Text>, name: Text) -> Result<(), ParseError> {
532    if iotas.iter().any(|n| n.as_str() == name.as_str()) {
533        return Err(ParseError::DuplicateModuleItem);
534    }
535    iotas.push(name);
536    Ok(())
537}
538
539/// Parse a private `const` declaration — `const <name>`, or the group form
540/// `const (<name> …)` — to the names it declares.
541///
542/// The group is an ordinary `<tuple>` wrapping a `<seq>`, so its names may be
543/// newline- or comma-separated and it may carry comments, exactly as a `let`
544/// group may. An **empty group declares nothing**, the same degenerate reading
545/// `let ()` and `recur ()` take.
546fn parse_const_decl<'a>(ci: &[&Item<'a>]) -> Result<Vec<Text>, ParseError> {
547    match ci {
548        [_const, Item::Tuple(seq)] => {
549            let mut names = Vec::new();
550            for chain in &seq.0 {
551                let c: Vec<&Item<'a>> = chain
552                    .0
553                    .iter()
554                    .filter(|it| !matches!(it, Item::Comm(_)))
555                    .collect();
556                if c.is_empty() {
557                    continue; // a blank / comment-only line inside the group
558                }
559                names.push(plain_name(&c, ParseError::MalformedConst)?);
560            }
561            Ok(names)
562        }
563        [_const, rest @ ..] if !rest.is_empty() => {
564            Ok(alloc::vec![plain_name(rest, ParseError::MalformedConst)?])
565        }
566        _ => Err(ParseError::MalformedConst),
567    }
568}
569
570/// Record one local, rejecting a name a previous `local` already took.
571fn push_local(locals: &mut Vec<(Text, Expr)>, name: Text, body: Expr) -> Result<(), ParseError> {
572    if locals.iter().any(|(n, _)| n.as_str() == name.as_str()) {
573        return Err(ParseError::DuplicateModuleItem);
574    }
575    locals.push((name, body));
576    Ok(())
577}
578
579/// Parse the group form `local (<name> = <expr> …)` to its bindings.
580///
581/// The group is an ordinary `<tuple>` wrapping a `<seq>`, so its bindings may be
582/// newline- or comma-separated and it may carry comments, exactly as a `let`
583/// group may — and an **empty group declares nothing**, the degenerate reading
584/// `const ()` and `let ()` take. Anything else after the keyword is a malformed
585/// declaration: the single form `local x = <expr>` has a top-level `=` and never
586/// reaches here.
587fn parse_local_group<'a>(ci: &[&Item<'a>]) -> Result<Vec<(Text, Expr)>, ParseError> {
588    let [_local, Item::Tuple(seq)] = ci else {
589        return Err(ParseError::MalformedLocal);
590    };
591    let mut bindings = Vec::new();
592    for chain in &seq.0 {
593        let c: Vec<&Item<'a>> = chain
594            .0
595            .iter()
596            .filter(|it| !matches!(it, Item::Comm(_)))
597            .collect();
598        if c.is_empty() {
599            continue; // a blank / comment-only line inside the group
600        }
601        let eq = c
602            .iter()
603            .position(|it| matches!(it, Item::Punct("=")))
604            .ok_or(ParseError::MalformedLocal)?;
605        let (head, tail) = (&c[..eq], &c[eq + 1..]);
606        if head.is_empty() || tail.is_empty() {
607            return Err(ParseError::MalformedLocal);
608        }
609        if tail.iter().any(|it| matches!(it, Item::Punct("="))) {
610            return Err(ParseError::MalformedLocal);
611        }
612        bindings.push((
613            plain_name(head, ParseError::MalformedLocal)?,
614            parse_items(tail)?,
615        ));
616    }
617    Ok(bindings)
618}
619
620/// Parse the `import "<spec>"` right-hand side of a re-exporting import to its
621/// spec. There is no `as` here: the item's own left-hand side named it.
622fn parse_import_spec<'a>(tail: &[&Item<'a>]) -> Result<ImportSpec, ParseError> {
623    match tail {
624        [_import, Item::Str(spec)] => decode_str(spec),
625        _ => Err(ParseError::MalformedImport),
626    }
627}
628
629/// The single plain name a binding position may hold: exactly one `<sym>` that is
630/// not a keyword, a `__` name, a discard, or a number. A reserved token gets its
631/// own error; every other shape (a destructuring pattern, a literal, several
632/// items) gets the caller's `bad`, which names the position —
633/// [`ParseError::BadPattern`] for an at-pattern's LHS,
634/// [`ParseError::BadModuleItemName`] for a module item's.
635fn plain_name<'a>(items: &[&Item<'a>], bad: ParseError) -> Result<Text, ParseError> {
636    match items {
637        [Item::Sym(s)] if is_keyword(s) => Err(ParseError::ReservedKeyword),
638        [Item::Sym(s)] if s.starts_with("__") => Err(ParseError::ReservedName),
639        [Item::Sym(s)] if reads_as_number(s) || *s == "_" => Err(bad),
640        [Item::Sym(s)] => Ok(text(s)),
641        _ => Err(bad),
642    }
643}
644
645/// Would this non-empty `<sym>` read as a number rather than a reference? Either
646/// it is a valid integer literal — including a signed one, since `-`/`+` are
647/// symchars, so `-1` is one `<sym>` — or it is digit-leading and therefore a
648/// *malformed* number. A reference is neither (see the atom path above), so a
649/// binding may not take such a name: it could never be read back.
650fn reads_as_number(s: &str) -> bool {
651    parse_int(s).is_some() || s.as_bytes()[0].is_ascii_digit()
652}
653
654/// Parse one chain: drop comments, then split on the first binder.
655fn parse_chain<'a>(chain: &Chain<'a>) -> Result<Expr, ParseError> {
656    let items: Vec<&Item<'a>> = chain
657        .0
658        .iter()
659        .filter(|it| !matches!(it, Item::Comm(_)))
660        .collect();
661    parse_items(&items)
662}
663
664/// Parse a comment-free slice of items (a chain, or a suffix of one).
665fn parse_items<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
666    if items.is_empty() {
667        return Err(ParseError::EmptyChain);
668    }
669    // `|>` is the **loosest** operator: split on the last one so the chain is
670    // a left-associative pipe chain over **segments**, each parsed by
671    // `parse_segment`. A `&`/`let`/`recur` binder form can therefore never
672    // capture a pipe — `&x x |> f` is `f (&x x)`, and a pipe-bodied lambda is
673    // written `&x (x |> f)` — and a pipe's right operand is a whole segment,
674    // which may itself end in a trailing binder form (`x |> &y body`).
675    match items.iter().rposition(|it| matches!(it, Item::Punct("|>"))) {
676        None => parse_segment(items),
677        Some(k) => {
678            let (left, right) = (&items[..k], &items[k + 1..]);
679            if left.is_empty() || right.is_empty() {
680                return Err(ParseError::MalformedPipe);
681            }
682            let l = parse_items(left)?;
683            let r = parse_segment(right)?;
684            // Thread the left operand in as `r`'s final argument, extending its
685            // spine so `x |> f a b` stays one flattened call `f a b x`.
686            Ok(append_arg(r, l))
687        }
688    }
689}
690
691/// Parse one pipe segment — a `|>`-free item slice: an application spine whose
692/// trailing argument may be a `&`/`let`/`recur` binder form. A split point is a
693/// `&`-binder or the `let`/`recur` keyword: it has the lowest precedence within
694/// the segment and captures the rest of the segment as its body, so the spine
695/// before the first one is a left-folded application whose trailing argument is
696/// the abstraction / let / recur that follows. With no split point this is
697/// exactly `parse_app_spine`.
698fn parse_segment<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
699    match items.iter().position(|it| is_split_point(it)) {
700        // No split point: a plain application spine (with `()` spread).
701        None => parse_app_spine(items),
702        // Leading split point: a `let (…) body`, or `&<header> <rest>`
703        // abstracting a pattern over the rest.
704        Some(0) => {
705            if is_let_keyword(items[0]) {
706                parse_let(items)
707            } else if is_recur_keyword(items[0]) {
708                parse_recur(items)
709            } else {
710                if items.len() == 1 {
711                    return Err(ParseError::AbsWithoutBody);
712                }
713                let inner = match items[0] {
714                    Item::Prefixed { sigil: '&', item } => item.as_ref(),
715                    _ => return Err(ParseError::UnexpectedBinder),
716                };
717                let body = parse_segment(&items[1..])?;
718                build_binder(inner, body)
719            }
720        }
721        // Split point in the middle: the abstraction / let is the trailing
722        // argument of the application spine that precedes it.
723        Some(k) => {
724            let f = parse_segment(&items[..k])?;
725            let arg = parse_segment(&items[k..])?;
726            Ok(append_arg(f, arg))
727        }
728    }
729}
730
731/// Parse a `let (b0, b1, …) body`. `items[0]` is the `let` keyword; `items[1]`
732/// must be the binder-group tuple and `items[2..]` the (non-empty) body. The
733/// group's bindings are **sequential** — `let (a = va, b = vb) e` desugars to the
734/// nested `(&a ((&b e) vb)) va` — so we parse the body once and fold the bindings
735/// right-to-left, leaving the leftmost binding outermost.
736///
737/// An **empty group binds nothing**, so `let () e` is `e` — the fold over zero
738/// bindings, and the unit of the form. It is not an error: a group is a list, and
739/// a generated or commented-out one may legitimately come out empty.
740fn parse_let<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
741    let group = match items.get(1).map(|it| &**it) {
742        Some(Item::Tuple(seq)) => seq,
743        _ => return Err(ParseError::LetMissingBinders),
744    };
745    let body_items = &items[2..];
746    if body_items.is_empty() {
747        return Err(ParseError::LetMissingBody);
748    }
749    // Each binding is a chain of the group's `<seq>`; drop comment-only chains so
750    // a group can be laid out as a block with blank lines and comments.
751    let mut bindings: Vec<Vec<&Item<'a>>> = Vec::new();
752    for chain in &group.0 {
753        let ci: Vec<&Item<'a>> = chain
754            .0
755            .iter()
756            .filter(|it| !matches!(it, Item::Comm(_)))
757            .collect();
758        if !ci.is_empty() {
759            bindings.push(ci);
760        }
761    }
762    let mut acc = parse_segment(body_items)?;
763    for ci in bindings.iter().rev() {
764        let (pat, value) = parse_binding(ci)?;
765        acc = Expr::App(Box::new([abs(pat, acc), value]));
766    }
767    Ok(acc)
768}
769
770/// Parse a `recur (b0, b1, …) body`. Shaped like [`parse_let`] — `items[0]` is the
771/// keyword, `items[1]` the binder-group tuple, `items[2..]` the body — and unlike
772/// it in what the group means: the bindings are **simultaneous**, so they cannot
773/// desugar to nested abstractions and instead become a module item table that the
774/// bodies reach through a terminal.
775///
776/// The left-hand sides are plain names, not patterns. The group *is* an item table,
777/// so it follows [`parse_module`]'s rule (which is also what rejects a duplicate)
778/// rather than `let`'s, which does allow a destructuring pattern. That divergence
779/// falls out of the mechanism rather than being a choice.
780///
781/// The table is sorted here, at parse: the sort order *is* the index space a
782/// sibling [`Expr::ModItem`] counts against, and the resolver assigns those indices
783/// against the frozen [`ModuleData`].
784///
785/// As with `let`, an empty group is `body` — with the terminal dropped too, since a
786/// group with no items has nothing to reach through it.
787fn parse_recur<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
788    let group = match items.get(1).map(|it| &**it) {
789        Some(Item::Tuple(seq)) => seq,
790        _ => return Err(ParseError::RecurMissingBinders),
791    };
792    let body_items = &items[2..];
793    if body_items.is_empty() {
794        return Err(ParseError::RecurMissingBody);
795    }
796    let syntax = parse_module(group)?;
797    if !syntax.imports.is_empty() {
798        // A group is an item table, not a module source: it has no frame of its
799        // own for the const stage to fill, and it is built while a program runs.
800        return Err(ParseError::MisplacedImport);
801    }
802    if !syntax.iotas.is_empty() {
803        // Nor an instance of its own to mint iotas against.
804        return Err(ParseError::MisplacedConst);
805    }
806    if !syntax.locals.is_empty() {
807        // And every binding of a group is private already — it is reachable from
808        // the group's own bodies and its body, and from nowhere else — so the
809        // keyword would mark nothing.
810        return Err(ParseError::MisplacedLocal);
811    }
812    if syntax.main.is_some() {
813        // A reserved moditem is read by a tool that was pointed at a *source* —
814        // a runner, a test runner — and a group is not one, so the slot would
815        // never be looked at.
816        return Err(ParseError::MisplacedReserved);
817    }
818    let mut bindings = syntax.items;
819    let body = parse_segment(body_items)?;
820    if bindings.is_empty() {
821        return Ok(body);
822    }
823    bindings.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
824    // No frame, no imports, and no name: a group's bindings are written where
825    // they are used, so there is no source of its own for a host to name. The
826    // body goes in the module's own body slot — the same one a source fills with
827    // `__main`, differing only in who evaluates it and when.
828    Ok(Expr::Recur(Rc::new(ModuleData::from_bindings(
829        bindings,
830        Vec::new(),
831        Box::new([]),
832        Box::new([]),
833        Box::new([]),
834        Some(body),
835        None,
836    ))))
837}
838
839/// Parse one binding chain `<pattern> "=" <expr>` to its pattern and value. The
840/// separator is the **first** top-level `=` (`<punct>`); a pattern may carry its
841/// own `=` only inside `(…)`/`[…]`/`{…}` (a nested item, never top-level), so a
842/// second top-level `=` is malformed. Both the head (pattern) and tail (value)
843/// must be non-empty.
844fn parse_binding<'a>(items: &[&Item<'a>]) -> Result<(Pattern, Expr), ParseError> {
845    let mut eq = None;
846    for (i, it) in items.iter().enumerate() {
847        if matches!(it, Item::Punct("=")) {
848            if eq.is_some() {
849                return Err(ParseError::MalformedBinding); // a second top-level `=`
850            }
851            eq = Some(i);
852        }
853    }
854    let eq = eq.ok_or(ParseError::MalformedBinding)?;
855    let (head, tail) = (&items[..eq], &items[eq + 1..]);
856    if head.is_empty() || tail.is_empty() {
857        return Err(ParseError::MalformedBinding);
858    }
859    // A top-level `|` in the LHS must be parenthesized (an `&`-header can't reach
860    // this, since `&` prefixes one item), so the or-`|` and the binding `=` do
861    // not compete: `let (((x=.a) | (x=.b)) = v) e`.
862    if head.iter().any(|it| matches!(it, Item::Punct("|"))) {
863        return Err(ParseError::LetOrUnparenthesized);
864    }
865    // A bare matching literal as the whole LHS needs parens: `let ((42) = v)`,
866    // `let ((.foo) = v)`, `let (("s") = v)` (see `BareLiteralHeader`).
867    if let [it] = head {
868        if is_bare_literal_header(it) {
869            return Err(ParseError::BareLiteralHeader);
870        }
871    }
872    let pat = parse_pattern(head)?;
873    let value = parse_items(tail)?;
874    Ok((pat, value))
875}
876
877/// Parse a single item as an atomic expression (`<aexpr>`).
878fn parse_aexpr<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
879    match item {
880        Item::Sym(s) => Ok(parse_sym(s)?),
881        // A `.`-prefixed item is a symbol (`.foo`, `.0`); see parse_dot_symbol.
882        Item::Prefixed { sigil: '.', item } => parse_dot_symbol(item),
883        // A `[…]` list is a runtime list value (any arity, no 1-element
884        // collapse); a `(…)` group is grouping / spread folded into one expr.
885        Item::List(seq) => parse_list(seq),
886        Item::Tuple(seq) => parse_group(seq),
887        // A `<str>` literal, decoded at parse into an owned `Expr::Str`.
888        Item::Str(s) => Ok(Expr::Str(decode_str(s)?)),
889        // A `{…}` block is a map literal (see parse_map).
890        Item::Block(seq) => parse_map(seq),
891        // A bare `.` (composition combinator) is deferred; any other stray punct
892        // (`:`, `=`) has no atomic reading outside a map entry / `let` binding.
893        Item::Punct(".") => Err(ParseError::CombinatorDeferred),
894        Item::Punct(_) => Err(ParseError::PunctAtAtom),
895        // Filtered before we get here.
896        Item::Comm(_) => Err(ParseError::EmptyChain),
897        // A `&`-binder is never fed to parse_aexpr by the split logic above.
898        Item::Prefixed { .. } => Err(ParseError::UnexpectedBinder),
899    }
900}
901
902/// Decode a Muon `<str>`'s raw inner text (the bytes between the quotes, escapes
903/// unresolved) into an owned UTF-8 [`Text`]. Muon has already validated the escape
904/// *grammar* (only `\" \n \t \r \\ \/ \b \f` and four-hex-digit `\uXXXX`, no raw
905/// newline), so this resolves the escapes and — the higher-layer job Muon defers —
906/// **combines `\uXXXX` surrogate pairs** into one scalar. The one failure is an
907/// **unpaired surrogate** (a high not followed by a low, or a lone low), which
908/// UTF-8 cannot represent: [`ParseError::LoneSurrogate`].
909fn decode_str(raw: &str) -> Result<Text, ParseError> {
910    let b = raw.as_bytes();
911    let mut out = String::with_capacity(raw.len());
912    let mut i = 0;
913    let mut chunk = 0; // start of the current verbatim (unescaped) run
914    while i < b.len() {
915        if b[i] != b'\\' {
916            i += 1; // a non-escape byte (incl. any UTF-8 continuation) copies as-is
917            continue;
918        }
919        out.push_str(&raw[chunk..i]); // flush the verbatim run before this escape
920        match b[i + 1] {
921            b'"' => out.push('"'),
922            b'\\' => out.push('\\'),
923            b'/' => out.push('/'),
924            b'n' => out.push('\n'),
925            b't' => out.push('\t'),
926            b'r' => out.push('\r'),
927            b'b' => out.push('\u{8}'),
928            b'f' => out.push('\u{c}'),
929            b'u' => {
930                let hi = hex4(&b[i + 2..i + 6]);
931                i += 6;
932                if (0xD800..=0xDBFF).contains(&hi) {
933                    // A high surrogate must be completed by a `\uXXXX` low surrogate.
934                    let lo = match (b.get(i), b.get(i + 1)) {
935                        (Some(b'\\'), Some(b'u')) => hex4(&b[i + 2..i + 6]),
936                        _ => return Err(ParseError::LoneSurrogate),
937                    };
938                    if !(0xDC00..=0xDFFF).contains(&lo) {
939                        return Err(ParseError::LoneSurrogate);
940                    }
941                    let c = 0x1_0000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
942                    out.push(char::from_u32(c).expect("combined surrogate pair"));
943                    i += 6;
944                } else if (0xDC00..=0xDFFF).contains(&hi) {
945                    return Err(ParseError::LoneSurrogate); // a lone low surrogate
946                } else {
947                    out.push(char::from_u32(hi).expect("non-surrogate BMP scalar"));
948                }
949                chunk = i;
950                continue;
951            }
952            _ => unreachable!("Muon validated the <str> escape grammar"),
953        }
954        i += 2;
955        chunk = i;
956    }
957    out.push_str(&raw[chunk..]);
958    Ok(Text::from(out.as_str()))
959}
960
961/// Read exactly four ASCII hex-digit bytes (Muon-validated) as a `u32`.
962fn hex4(b: &[u8]) -> u32 {
963    b.iter()
964        .fold(0, |v, &c| v * 16 + (c as char).to_digit(16).unwrap())
965}
966
967/// Parse a `.`-prefixed item into a symbol. The inner item must be a single
968/// name or number segment (`.foo`, `.0`); a `.` glued to anything else — the
969/// record sugar `.(…)`, a nested sigil, a string — is deferred / ill-formed.
970fn parse_dot_symbol<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
971    match item {
972        // Stored *without* the leading dot; the dot is implied by Expr::Symbol
973        // and re-added when displayed / dumped.
974        Item::Sym(s) => Ok(Expr::Symbol(text(s))),
975        _ => Err(ParseError::BadSymbol),
976    }
977}
978
979/// Classify a `<sym>`: an integer literal, a name, or an error. Symbols are no
980/// longer `<sym>`s (a `.` is a Muon sigil now) — they arrive as `<prefixed>`
981/// items and are handled by parse_dot_symbol.
982fn parse_sym(s: &str) -> Result<Expr, ParseError> {
983    if is_keyword(s) {
984        // A keyword is syntax, never a reference. `let` at a chain head is
985        // handled before we get here; anywhere else it (and `with`) is an error.
986        return Err(ParseError::ReservedKeyword);
987    }
988    if s == "_" {
989        // `_` is the discard pattern (valid only as a binder), never a reference.
990        Err(ParseError::DiscardReference)
991    } else if let Some(n) = parse_int(s) {
992        Ok(Expr::Int(n))
993    } else if s.as_bytes()[0].is_ascii_digit() {
994        // Digit-leading but not a valid int, and a name can't start with a digit.
995        Err(ParseError::MalformedNumber)
996    } else if s.starts_with("__") {
997        // A `__` name is un-bindable, so resolve a known builtin here (a direct
998        // dispatch node) rather than leaving it to a failing env walk plus a name
999        // match on every call. The namespace is the language's, and closed, so an
1000        // unknown one is rejected here too — a misspelled `__mian` is a compile
1001        // error naming the namespace, not a name that happens to bind to nothing.
1002        // A reserved moditem (`__main`) is not a name at all and lands here as
1003        // well: it is a slot, and nothing reads it by reference.
1004        //
1005        // The three **home-module leaves** are the other clause: they read the
1006        // module the body is written in rather than naming a builtin, so they
1007        // become an `Expr::Home` whose depth the resolver measures (a leaf outside
1008        // any module body is [`ParseError::OutsideModule`] there).
1009        if let Some(leaf) = home_leaf(s) {
1010            return Ok(Expr::Home { leaf, depth: 0 });
1011        }
1012        match Builtin::from_name(s) {
1013            Some(op) => Ok(Expr::Builtin(op)),
1014            None => Err(ParseError::UnknownReserved(text(s))),
1015        }
1016    } else {
1017        Ok(Expr::Name(text(s)))
1018    }
1019}
1020
1021/// The [home-module leaf](HomeLeaf) a `__` name spells, if it is one. Kept beside
1022/// [`reserved_moditem`] and read the same way: the `__` namespace is the
1023/// language's and closed, so every name in it is defined in exactly one of these
1024/// small tables.
1025fn home_leaf(name: &str) -> Option<HomeLeaf> {
1026    match name {
1027        "__module" => Some(HomeLeaf::Module),
1028        "__new" => Some(HomeLeaf::New),
1029        "__value" => Some(HomeLeaf::Value),
1030        _ => None,
1031    }
1032}
1033
1034/// Parse an integer literal per the *integers* grammar: optional `-`/`+` sign,
1035/// then decimal / `0x` hex / `0b` binary digits, with `_` separating digit
1036/// groups (never leading, trailing, or doubled). `None` if `s` is not a literal.
1037fn parse_int(s: &str) -> Option<BigInt> {
1038    let (neg, body) = match s.as_bytes().first()? {
1039        b'-' => (true, &s[1..]),
1040        b'+' => (false, &s[1..]),
1041        _ => (false, s),
1042    };
1043    let (radix, digits) =
1044        if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
1045            (16u32, h)
1046        } else if let Some(b) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
1047            (2, b)
1048        } else {
1049            (10, body)
1050        };
1051    // Strip `_` separators while rejecting leading/trailing/doubled ones.
1052    let mut cleaned = String::new();
1053    let mut after_sep = true; // start "after a separator" → forbids a leading `_`
1054    for &c in digits.as_bytes() {
1055        if c == b'_' {
1056            if after_sep {
1057                return None;
1058            }
1059            after_sep = true;
1060        } else {
1061            cleaned.push(c as char);
1062            after_sep = false;
1063        }
1064    }
1065    if after_sep || cleaned.is_empty() {
1066        return None; // trailing `_`, or no digits at all (e.g. `0x`, `+`)
1067    }
1068    let mag = BigInt::parse_bytes(cleaned.as_bytes(), radix)?;
1069    Some(if neg { -mag } else { mag })
1070}
1071
1072/// Parse an application spine (a chain slice with no `&`/`let` split point and no
1073/// top-level `|>`), folding left. A `(…)` item does not contribute one argument —
1074/// it **spreads** (see `expand_arg`): `f (a, b)` folds to `((f a) b)`.
1075fn parse_app_spine<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
1076    let mut args: Vec<Expr> = Vec::new();
1077    for it in items {
1078        expand_arg(it, &mut args)?;
1079    }
1080    // `items` is non-empty and every item yields at least one arg (a `()` group
1081    // yields the unit), so `args` is non-empty. A single arg is not an
1082    // application; two or more flatten into one spine (callee then arguments).
1083    Ok(fold_spine(args))
1084}
1085
1086/// Fold a non-empty argument list into an expression: the lone element itself if
1087/// there is one, else a flattened `App` spine (element 0 the callee).
1088fn fold_spine(args: Vec<Expr>) -> Expr {
1089    if args.len() == 1 {
1090        args.into_iter().next().unwrap()
1091    } else {
1092        app(args)
1093    }
1094}
1095
1096/// Expand one chain item into argument expressions. A `(…)` group spreads its
1097/// chains (see `expand_group_args`); any other item is a single atom.
1098fn expand_arg<'a>(item: &Item<'a>, out: &mut Vec<Expr>) -> Result<(), ParseError> {
1099    match item {
1100        Item::Tuple(seq) => expand_group_args(seq, out),
1101        _ => {
1102            out.push(parse_aexpr(item)?);
1103            Ok(())
1104        }
1105    }
1106}
1107
1108/// Expand a `(…)` group's chains as arguments, on the rule **one chain → one
1109/// argument** (grouping is the one-chain case). An *empty* group is the nullary
1110/// marker: it feeds one unit value `[]` (an empty call still calls). Comment-only
1111/// chains are skipped and do not count toward emptiness.
1112fn expand_group_args<'a>(seq: &Seq<'a>, out: &mut Vec<Expr>) -> Result<(), ParseError> {
1113    let mut any = false;
1114    for chain in &seq.0 {
1115        if is_comment_only(chain) {
1116            continue;
1117        }
1118        out.push(parse_chain(chain)?);
1119        any = true;
1120    }
1121    if !any {
1122        out.push(Expr::List(Vec::new())); // nullary marker → unit []
1123    }
1124    Ok(())
1125}
1126
1127/// Parse a `(…)` group appearing as a standalone atom: expand it (grouping /
1128/// spread / nullary) and fold the pieces into one expression.
1129fn parse_group<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1130    let mut args: Vec<Expr> = Vec::new();
1131    expand_group_args(seq, &mut args)?;
1132    // `expand_group_args` always yields ≥1 arg (an empty group feeds unit `[]`).
1133    Ok(fold_spine(args))
1134}
1135
1136/// Parse a `[…]` list into a runtime list literal of *any* arity: `[]` (unit),
1137/// `[e]` (a genuine 1-list, not collapsed), `[e0, e1, …]`. Each non-comment
1138/// chain is one element expression.
1139fn parse_list<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1140    let mut elems: Vec<Expr> = Vec::new();
1141    for chain in &seq.0 {
1142        if is_comment_only(chain) {
1143            continue;
1144        }
1145        elems.push(parse_chain(chain)?);
1146    }
1147    Ok(Expr::List(elems))
1148}
1149
1150/// Parse a `{…}` block into a map literal: each non-comment chain of the `<seq>`
1151/// is one entry (see `parse_entry`); comment-only chains are skipped (so a map may
1152/// be laid out multi-line with blank lines and comments). Entry order is source
1153/// order — evaluation re-sorts by the value order.
1154fn parse_map<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1155    let mut entries: Vec<(Expr, Expr)> = Vec::new();
1156    for chain in &seq.0 {
1157        let items: Vec<&Item<'a>> = chain
1158            .0
1159            .iter()
1160            .filter(|it| !matches!(it, Item::Comm(_)))
1161            .collect();
1162        if items.is_empty() {
1163            continue;
1164        }
1165        entries.push(parse_entry(&items)?);
1166    }
1167    Ok(Expr::Map(entries))
1168}
1169
1170/// Parse one map entry `<key> ":" <value>`. The `:` is a single `<punct>` item at
1171/// position 1; the key is the single head item (`parse_key`) and the value is the
1172/// rest of the chain parsed as an `<expr>`. An *empty* value tail is sugar for the
1173/// unit value `[]` (so `{ .foo: }` == `{ .foo: [] }`).
1174fn parse_entry<'a>(items: &[&Item<'a>]) -> Result<(Expr, Expr), ParseError> {
1175    if items.len() < 2 || !matches!(items[1], Item::Punct(":")) {
1176        return Err(ParseError::MalformedMapKey);
1177    }
1178    let key = parse_key(items[0])?;
1179    let value_items = &items[2..];
1180    let value = if value_items.is_empty() {
1181        Expr::List(Vec::new()) // empty rest → the unit value []
1182    } else {
1183        parse_items(value_items)?
1184    };
1185    Ok((key, value))
1186}
1187
1188/// Parse a map-entry key item. A bare *non-digit* atom key is the **symbol** of
1189/// the same spelling — `one` == `.one` — mirroring `.foo` exactly; a key is just
1190/// a token. A **digit-leading** bare key (`0`, `5`) is rejected to avoid the
1191/// number/symbol confusion: write `.0` for the symbol or `(0)` for the integer.
1192/// Wrap a key as a `(…)` **computed** key to evaluate it: `(one)` is the *value*
1193/// of variable `one`, `(0)` the integer `0`. The remaining forms are a
1194/// `.`-prefixed symbol (`.one`) and a `[…]` list/unit key (`[]` is the unit key).
1195/// Anything else is a `MalformedMapKey`.
1196fn parse_key<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
1197    match item {
1198        // A bare non-digit `<sym>` key is the symbol of that spelling, read
1199        // exactly as `.<sym>` would be. A digit-leading key is rejected — write
1200        // `.5` (the symbol) or `(5)` (the computed integer key) to disambiguate.
1201        Item::Sym(s) if s.as_bytes()[0].is_ascii_digit() => Err(ParseError::MalformedMapKey),
1202        Item::Sym(s) => Ok(Expr::Symbol(text(s))),
1203        // `.sym` symbol key, `[…]` list/unit key, `(…)` computed key, `"…"` string
1204        // key — each reads exactly as the same item would at atom position.
1205        Item::Prefixed { sigil: '.', .. } | Item::List(_) | Item::Tuple(_) | Item::Str(_) => {
1206            parse_aexpr(item)
1207        }
1208        _ => Err(ParseError::MalformedMapKey),
1209    }
1210}
1211
1212// ===========================================================================
1213// patterns
1214// ===========================================================================
1215
1216/// `&<pattern> body` — the one binding form every abstraction (and `let`, and a
1217/// `__match` clause) is built from. Greedily **collects** into one multi-parameter
1218/// [`Lambda`]: when `body` is *directly* another abstraction (a run of `&p0 &p1 …`,
1219/// or a curried binder group), `pat` is prepended to its head so the whole run
1220/// shares one code node with a known arity. Collection stops at the first
1221/// non-abstraction body (a `&` nested inside an application does not extend the
1222/// arity) and at 255 parameters — a head that would exceed the `applied: u8`
1223/// counter starts a fresh nested `Lambda` instead of overflowing.
1224fn abs(pat: Pattern, body: Expr) -> Expr {
1225    match body {
1226        Expr::Abs(lam) if lam.head.len() < 255 => {
1227            // Prepend `pat` to the (freshly built, so uniquely owned) inner head.
1228            let lam = Rc::try_unwrap(lam).unwrap_or_else(|rc| (*rc).clone());
1229            let mut head = Vec::with_capacity(lam.head.len() + 1);
1230            head.push(pat);
1231            head.extend(Vec::from(lam.head));
1232            Expr::Abs(Rc::new(Lambda {
1233                head: head.into_boxed_slice(),
1234                body: lam.body,
1235                // Free variables are a resolution artifact: the parser leaves the
1236                // capture plan unresolved and `resolve.rs` fills it in.
1237                captures: Captures::Unresolved,
1238            }))
1239        }
1240        _ => Expr::Abs(Rc::new(Lambda {
1241            head: Box::new([pat]),
1242            body,
1243            captures: Captures::Unresolved,
1244        })),
1245    }
1246}
1247
1248/// Build an application node from a flat call spine `[callee, arg0, …]` (≥ 2
1249/// elements). A single-element `items` is *not* an application — the caller
1250/// returns that element directly.
1251fn app(items: Vec<Expr>) -> Expr {
1252    debug_assert!(
1253        items.len() >= 2,
1254        "an application has a callee and ≥1 argument"
1255    );
1256    Expr::App(items.into_boxed_slice())
1257}
1258
1259/// Apply `f` to one more `arg`, extending `f`'s call spine when it already is one
1260/// so `g x &y …` and `x |> f a` stay single flattened `App` nodes (a partial
1261/// applied to a trailing abstraction / pipe operand). Currying makes this
1262/// equivalent to a fresh binary application, and it re-renders identically.
1263fn append_arg(f: Expr, arg: Expr) -> Expr {
1264    match f {
1265        Expr::App(items) => {
1266            let mut v = Vec::from(items);
1267            v.push(arg);
1268            Expr::App(v.into_boxed_slice())
1269        }
1270        _ => Expr::App(Box::new([f, arg])),
1271    }
1272}
1273
1274/// Whether `item` is a bare **matching literal** — a number, a `.`-symbol, or a
1275/// string — used where a whole `&`-header or `let` LHS is expected. These are
1276/// equality patterns, not binders, so a bare one reads as a stray value or an
1277/// intended binding; Elly requires the wrap (`&(42)`, `&(.foo)`, `&("s")`; see
1278/// `BareLiteralHeader`). Nested in `[…]`/`{…}`/`(…)`/an or-pattern they are fine.
1279fn is_bare_literal_header(item: &Item<'_>) -> bool {
1280    match item {
1281        Item::Sym(s) => parse_int(s).is_some(),
1282        Item::Prefixed { sigil: '.', .. } => true,
1283        Item::Str(_) => true,
1284        _ => false,
1285    }
1286}
1287
1288/// Build the abstraction(s) an `&`-header `&<inner>` introduces over `body`. A
1289/// `(…)` inner is a **binder group** whose commas curry (`&(a, b)` → two params);
1290/// any other inner is a single-parameter pattern.
1291fn build_binder<'a>(inner: &Item<'a>, body: Expr) -> Result<Expr, ParseError> {
1292    match inner {
1293        Item::Tuple(seq) => build_group(seq, body),
1294        // A bare matching literal (number `&42`, symbol `&.foo`, string `&"s"`) as
1295        // the whole header must be parenthesized: `&(42)`, `&(.foo)`, `&("s")` (see
1296        // `BareLiteralHeader`). Nested literals reach `parse_atom_item` unaffected.
1297        it if is_bare_literal_header(it) => Err(ParseError::BareLiteralHeader),
1298        other => {
1299            let pat = parse_pattern(&[other])?;
1300            Ok(abs(pat, body))
1301        }
1302    }
1303}
1304
1305/// Build a curried binder group `&(p0, p1, …)`: each non-comment chain is one
1306/// parameter pattern, curried left-to-right (leftmost outermost). An empty group
1307/// `&()` is the nullary marker, desugaring to `&[]` — one param, the unit pattern.
1308fn build_group<'a>(seq: &Seq<'a>, body: Expr) -> Result<Expr, ParseError> {
1309    let params = chain_slices(seq);
1310    if params.is_empty() {
1311        // `&()` → `&[]`: assert the argument is unit.
1312        return Ok(abs(Pattern::List(Vec::new(), Rest::None), body));
1313    }
1314    let mut acc = body;
1315    for chain in params.iter().rev() {
1316        let pat = parse_pattern(chain)?;
1317        acc = abs(pat, acc);
1318    }
1319    Ok(acc)
1320}
1321
1322/// The non-comment chains of a `<seq>`, each as its non-comment items.
1323fn chain_slices<'a, 'b>(seq: &'b Seq<'a>) -> Vec<Vec<&'b Item<'a>>> {
1324    seq.0
1325        .iter()
1326        .filter(|c| !is_comment_only(c))
1327        .map(|c| {
1328            c.0.iter()
1329                .filter(|it| !matches!(it, Item::Comm(_)))
1330                .collect()
1331        })
1332        .collect()
1333}
1334
1335/// The single non-comment chain of a `<seq>` (its non-comment items), or `None`
1336/// if the seq holds zero or more than one chain (e.g. a comma-separated group).
1337fn single_chain<'a, 'b>(seq: &'b Seq<'a>) -> Option<Vec<&'b Item<'a>>> {
1338    let mut chains = chain_slices(seq).into_iter();
1339    let first = chains.next()?;
1340    if chains.next().is_some() {
1341        return None;
1342    }
1343    Some(first)
1344}
1345
1346/// Parse a pattern from a chain of items. Splits on the first top-level `|`
1347/// (or-pattern), then `=` (equality / at-pattern), else defers to the `as`/atom
1348/// levels.
1349fn parse_pattern<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
1350    if items.is_empty() {
1351        return Err(ParseError::BadPattern);
1352    }
1353    // `|` has the lowest precedence — split first so `=` and `as` bind tighter
1354    // inside each arm. Left-associative on multiple `|`.
1355    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Punct("|"))) {
1356        let left = parse_pattern(&items[..idx])?;
1357        let right = parse_pattern(&items[idx + 1..])?;
1358        // Both arms must bind the same names in the same order (see
1359        // `ParseError::OrBindersMismatch`); that agreement is checked by the
1360        // resolver, which has the binders' de Bruijn order naturally.
1361        return Ok(Pattern::Or(Box::new(left), Box::new(right)));
1362    }
1363    // Prefix ordering comparators `< <ref>` / `> <ref>` (the rest is an atom
1364    // comparand, like `= <ref>`); they only appear at the head of a pattern.
1365    match items[0] {
1366        Item::Punct("<") => return Ok(Pattern::Less(parse_comparand(&items[1..])?)),
1367        Item::Punct(">") => return Ok(Pattern::Greater(parse_comparand(&items[1..])?)),
1368        _ => {}
1369    }
1370    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Punct("="))) {
1371        if idx == 0 {
1372            // `= <ref>` — equality against an atom (name or literal).
1373            return Ok(Pattern::Equal(parse_comparand(&items[1..])?));
1374        }
1375        // `name = pat` — at-pattern (the rest is a *pattern*).
1376        let name = at_name(&items[..idx])?;
1377        let inner = parse_pattern(&items[idx + 1..])?;
1378        return Ok(Pattern::At(name, Box::new(inner)));
1379    }
1380    parse_as(items)
1381}
1382
1383/// Parse the right-hand side of an `=` / `<` / `>` comparand: exactly one
1384/// **atom** — a name (`= x`), an integer (`= 5`), or a symbol (`= .foo`). A
1385/// pattern is a *closed grammar*, so a comparand never recurses into the general
1386/// expression parser (`parse_items`); a compound `= f x` / `< (f x)` is rejected
1387/// (`ComparandNotAtom`). The atom keeps the ordinary expression default at match
1388/// time — a name is a lookup — which is the only non-literal a match evaluates.
1389fn parse_comparand<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
1390    match items {
1391        [it] => match it {
1392            Item::Sym(s) => parse_sym(s),
1393            Item::Prefixed { sigil: '.', item } => parse_dot_symbol(item),
1394            Item::Str(s) => Ok(Expr::Str(decode_str(s)?)),
1395            _ => Err(ParseError::ComparandNotAtom),
1396        },
1397        _ => Err(ParseError::ComparandNotAtom),
1398    }
1399}
1400
1401/// The bound name of an at-pattern's left-hand side: exactly one plain name (not
1402/// a keyword, a `__` name, a discard, or a number).
1403fn at_name<'a>(items: &[&Item<'a>]) -> Result<Text, ParseError> {
1404    plain_name(items, ParseError::BadPattern)
1405}
1406
1407/// The narrowing level: the `__value <pat>` unwrapping qualifier, the prefix
1408/// `(as <Proto>) <pat>`, the postfix `<pat> as <Proto>`, or a bare atom.
1409fn parse_as<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
1410    // `__value <pat>` — narrow to an object of the home module and match its
1411    // payload. It qualifies whatever follows, so the rest is a whole pattern and
1412    // `__value x as Int` reads as `__value (x as Int)`.
1413    if let Item::Sym("__value") = items[0] {
1414        if items.len() < 2 {
1415            // Nothing to unwrap *into*. `__value _` is how to say "an object of
1416            // mine, payload ignored" — spelled out, since a bare `__value` in a
1417            // binder is much more likely a slip.
1418            return Err(ParseError::BadPattern);
1419        }
1420        let inner = parse_pattern(&items[1..])?;
1421        return Ok(Pattern::Unwrap {
1422            depth: 0,
1423            inner: Box::new(inner),
1424        });
1425    }
1426    // Prefix: `(as <Proto>) <pat>` — items[0] is a `(as …)` group.
1427    if let Item::Tuple(seq) = items[0] {
1428        if let Some(chain) = single_chain(seq) {
1429            if !chain.is_empty() && matches!(chain[0], Item::Sym("as")) {
1430                let proto = parse_proto_ref(&chain[1..])?;
1431                let inner = parse_pattern(&items[1..])?;
1432                return Ok(Pattern::Type(proto, Box::new(inner)));
1433            }
1434        }
1435    }
1436    // Postfix: `<atom> as <Proto>`.
1437    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Sym("as"))) {
1438        let proto = parse_proto_ref(&items[idx + 1..])?;
1439        let inner = parse_atom(&items[..idx])?;
1440        return Ok(Pattern::Type(proto, Box::new(inner)));
1441    }
1442    parse_atom(items)
1443}
1444
1445/// Parse the prototype of an `as <Proto>` pattern: exactly one **name**. A
1446/// prototype is a value like any other now — `Int` is the module `__Int` under its
1447/// shadowable alias, not a keyword — so this is the same closed-grammar atom an
1448/// `=` comparand is, minus the literals: a module is named, never written out.
1449///
1450/// It stays a [`ProtoRef::Ref`] here even when the name is the `__`-spelling of a
1451/// builtin module. The alias spelling only becomes one at *resolve* (it is the
1452/// resolver's bottom tier), so folding both to the compiled [`ProtoRef::Kind`]
1453/// form belongs there, in one place.
1454fn parse_proto_ref(items: &[&Item<'_>]) -> Result<ProtoRef, ParseError> {
1455    let [Item::Sym(s)] = items else {
1456        return Err(ParseError::UnknownType);
1457    };
1458    match parse_sym(s)? {
1459        // A name (`Bool`, `Int`), a `__` builtin (`__Int`), or `__module`.
1460        e @ (Expr::Name(_) | Expr::Builtin(_) | Expr::Home { .. }) => Ok(ProtoRef::Ref(e)),
1461        // A literal: `as 5` names no module.
1462        _ => Err(ParseError::UnknownType),
1463    }
1464}
1465
1466/// Parse an atomic pattern: a single item (or a `(…)` grouping of one pattern).
1467fn parse_atom<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
1468    match items {
1469        [it] => parse_atom_item(it),
1470        _ => Err(ParseError::BadPattern),
1471    }
1472}
1473
1474/// Parse a single atomic-pattern item.
1475fn parse_atom_item<'a>(item: &Item<'a>) -> Result<Pattern, ParseError> {
1476    match item {
1477        Item::Sym("_") => Ok(Pattern::Discard),
1478        Item::Sym(s) if is_keyword(s) => Err(ParseError::ReservedKeyword),
1479        Item::Sym(s) if s.starts_with("__") => Err(ParseError::ReservedName),
1480        // A numeric literal is an equality pattern (`= n` sugar, mirroring the
1481        // `.sym → Equal(Symbol)` sugar below); a digit-leading non-integer is a
1482        // malformed number, and everything else is a binder name.
1483        Item::Sym(s) => match parse_int(s) {
1484            Some(n) => Ok(Pattern::Equal(Expr::Int(n))),
1485            None if s.as_bytes()[0].is_ascii_digit() => Err(ParseError::MalformedNumber),
1486            None => Ok(Pattern::Bind(text(s))),
1487        },
1488        // `.sym` literal → `= .sym`.
1489        Item::Prefixed { sigil: '.', item } => Ok(Pattern::Equal(parse_dot_symbol(item)?)),
1490        // A `<str>` literal is an equality pattern — the textual twin of `.sym`.
1491        Item::Str(s) => Ok(Pattern::Equal(Expr::Str(decode_str(s)?))),
1492        Item::List(seq) => parse_list_pattern(seq),
1493        // `(…)` grouping — a single subpattern; commas (multiple chains) are an
1494        // error (they only curry after `&`).
1495        Item::Tuple(seq) => match single_chain(seq) {
1496            Some(chain) if !chain.is_empty() => parse_pattern(&chain),
1497            _ => Err(ParseError::BadPattern),
1498        },
1499        Item::Block(seq) => parse_map_pattern(seq),
1500        _ => Err(ParseError::BadPattern),
1501    }
1502}
1503
1504/// Parse a `{ k0: p0, … }` map pattern: each entry is `<key> ":" <value-pattern>`,
1505/// with an optional trailing `...`/`...rest`; `Rest::None` (no rest) is a
1506/// **closed** key set. The **key is itself a pattern**, but must denote a concrete
1507/// value to probe. A **lookup** key names a concrete value: `.sym` (the literal),
1508/// bare `sym` (the literal), or the equality pattern `(= <expr>)` / `= <expr>` for
1509/// a computed key. A grouped **binder** pattern in key position is instead a
1510/// **capture** — `(k)` / `(_)` peels the smallest remaining entry, binding its key
1511/// (see `match_pattern` in `eval.rs`); this is why the computed key is `(= k)`,
1512/// not `(k)`. The entry splits on its first top-level `:`.
1513fn parse_map_pattern<'a>(seq: &Seq<'a>) -> Result<Pattern, ParseError> {
1514    let chains = chain_slices(seq);
1515    let n = chains.len();
1516    let mut entries: Vec<(MapKey, Pattern)> = Vec::new();
1517    let mut rest = Rest::None;
1518    for (i, chain) in chains.iter().enumerate() {
1519        if let Some(r) = as_rest(chain) {
1520            if i != n - 1 {
1521                return Err(ParseError::BadPattern); // rest must be trailing
1522            }
1523            rest = r;
1524        } else {
1525            let colon = chain
1526                .iter()
1527                .position(|it| matches!(it, Item::Punct(":")))
1528                .ok_or(ParseError::MalformedMapKey)?;
1529            let val_items = &chain[colon + 1..];
1530            if colon == 0 || val_items.is_empty() {
1531                return Err(ParseError::MalformedMapKey); // empty key or value
1532            }
1533            // A bare single `<sym>` key is the literal symbol (like a map literal);
1534            // otherwise the key is a pattern — an equality form is a concrete-key
1535            // lookup, any other (binder) pattern is a key capture.
1536            let key_items = &chain[..colon];
1537            let bare_sym = match key_items {
1538                [Item::Sym(s)] => Some(*s),
1539                _ => None,
1540            };
1541            let key = if let Some(s) = bare_sym {
1542                // A bare non-digit sym key is the literal symbol (`five` → `.five`);
1543                // a digit-leading key is rejected — write `.5` (the symbol) or
1544                // `(5)` / `(= 5)` (the integer key) to disambiguate.
1545                if s.as_bytes()[0].is_ascii_digit() {
1546                    return Err(ParseError::MalformedMapKey);
1547                }
1548                MapKey::Lookup(Expr::Symbol(text(s)))
1549            } else {
1550                match parse_pattern(key_items)? {
1551                    Pattern::Equal(expr) => MapKey::Lookup(expr),
1552                    kpat => MapKey::Capture(Box::new(kpat)),
1553                }
1554            };
1555            let valpat = parse_pattern(val_items)?;
1556            entries.push((key, valpat));
1557        }
1558    }
1559    Ok(Pattern::Map(entries, rest))
1560}
1561
1562/// Parse a `[…]` list pattern: fixed-arity element patterns with an optional
1563/// trailing `...`/`...rest`.
1564fn parse_list_pattern<'a>(seq: &Seq<'a>) -> Result<Pattern, ParseError> {
1565    let chains = chain_slices(seq);
1566    let n = chains.len();
1567    let mut elems: Vec<Pattern> = Vec::new();
1568    let mut rest = Rest::None;
1569    for (i, chain) in chains.iter().enumerate() {
1570        if let Some(r) = as_rest(chain) {
1571            if i != n - 1 {
1572                return Err(ParseError::BadPattern); // rest must be trailing
1573            }
1574            rest = r;
1575        } else {
1576            elems.push(parse_pattern(chain)?);
1577        }
1578    }
1579    Ok(Pattern::List(elems, rest))
1580}
1581
1582/// Recognize a trailing-rest element chain: `...rest` (a name) or `...` (anon).
1583/// Both lex as nested `.` sigils (three deep), so no new Muon token is needed.
1584fn as_rest<'a>(chain: &[&Item<'a>]) -> Option<Rest> {
1585    if chain.len() != 1 {
1586        return None;
1587    }
1588    // `it = .( .( … ) )`: two `.` sigils, then either `.name` (→ named) or a bare
1589    // `.` punct (→ anon).
1590    if let Item::Prefixed {
1591        sigil: '.',
1592        item: a,
1593    } = chain[0]
1594    {
1595        if let Item::Prefixed {
1596            sigil: '.',
1597            item: b,
1598        } = a.as_ref()
1599        {
1600            match b.as_ref() {
1601                Item::Prefixed {
1602                    sigil: '.',
1603                    item: c,
1604                } => {
1605                    if let Item::Sym(name) = c.as_ref() {
1606                        return Some(Rest::Named(text(name)));
1607                    }
1608                }
1609                Item::Punct(".") => return Some(Rest::Anon),
1610                _ => {}
1611            }
1612        }
1613    }
1614    None
1615}
1616
1617/// Does this chain hold only comments (so it contributes no expression)?
1618fn is_comment_only(chain: &Chain) -> bool {
1619    chain.0.iter().all(|it| matches!(it, Item::Comm(_)))
1620}
1621
1622/// Is `s` an Elly keyword (reserved syntax, neither a reference nor bindable)?
1623/// `as` leads the type-narrowing pattern qualifier (recognized structurally
1624/// before this check); `with`/`when`/`match`/`case`/`of` are parked for future
1625/// forms (a parallel binding, a guard, and the surface `case`/`match` sugar over
1626/// the `__match` builtin).
1627fn is_keyword(s: &str) -> bool {
1628    matches!(
1629        s,
1630        "let"
1631            | "recur"
1632            | "import"
1633            | "const"
1634            | "local"
1635            | "with"
1636            | "as"
1637            | "when"
1638            | "match"
1639            | "case"
1640            | "of"
1641    )
1642}
1643
1644/// Is this item the `import` keyword (the sym `import`)? Reserving the word is
1645/// what stops `import = 3` and a local named `import`, so the only place it can
1646/// appear is where [`parse_module`] looks for it.
1647fn is_import_keyword(item: &Item) -> bool {
1648    matches!(item, Item::Sym("import"))
1649}
1650
1651/// Is this item the `const` keyword (the sym `const`)? Reserved for the same
1652/// reason `import` is: the word is syntax, so `const = 3` and a local named
1653/// `const` are rejected, and the only place it can appear is where
1654/// [`parse_module`] looks for it.
1655fn is_const_keyword(item: &Item) -> bool {
1656    matches!(item, Item::Sym("const"))
1657}
1658
1659/// Is this item the `local` keyword (the sym `local`)? Reserved for the same
1660/// reason `import` and `const` are: the word is syntax, so `local = 3` and a
1661/// binder named `local` are rejected, and the only place it can appear is where
1662/// [`parse_module`] looks for it.
1663fn is_local_keyword(item: &Item) -> bool {
1664    matches!(item, Item::Sym("local"))
1665}
1666
1667/// Can `name` be bound and then referenced from Elly source as a plain name? It
1668/// must be a Muon `<sym>` and pass the same tests `plain_name` applies to a
1669/// binding's left-hand side: not a keyword, not a `__` reserved name, not the
1670/// discard `_`, and not something that `reads_as_number` accepts.
1671///
1672/// This is the rule for names a *host* binds from outside a program — the PyO3
1673/// `elly.Env` prelude — where there is no left-hand side to parse but source must
1674/// still be able to read the binding back. Keep it in step with `plain_name`;
1675/// `crates/elly-core/tests/module.rs` pins the two together.
1676pub fn is_bindable_name(name: &str) -> bool {
1677    muon::is_sym(name)
1678        && !is_keyword(name)
1679        && !name.starts_with("__")
1680        && name != "_"
1681        && !reads_as_number(name)
1682}
1683
1684/// Is this item a chain split point — a `&`-binder, or a `let` / `recur` keyword?
1685fn is_split_point(item: &Item) -> bool {
1686    is_binder(item) || is_let_keyword(item) || is_recur_keyword(item)
1687}
1688
1689/// Is this item the `let` keyword (the sym `let`)?
1690fn is_let_keyword(item: &Item) -> bool {
1691    matches!(item, Item::Sym("let"))
1692}
1693
1694/// Is this item the `recur` keyword (the sym `recur`)?
1695fn is_recur_keyword(item: &Item) -> bool {
1696    matches!(item, Item::Sym("recur"))
1697}
1698
1699/// Is this item a `&`-prefixed binder?
1700fn is_binder(item: &Item) -> bool {
1701    matches!(item, Item::Prefixed { sigil: '&', .. })
1702}