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, Clause, Expr, HomeLeaf, Lambda, MapKey, Pattern, ProtoRef, 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 `#{…}` map literal and a `#[…]` list value are
78    /// their own atoms; the bare `{…}` / `[…]` forms are rejected, not deferred.)
79    PunctAtAtom,
80    /// A map entry (`<braces>` 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 `#[…]` / `#{…}` literal 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) — `Self`, `#Self(…)`, `x.Self`, or
225    /// the `Self <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 — `Self`, the one word every form is spelled with.
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    /// A [`&`](muon::Item::Prefixed)-headed **run** (`&x.0`, `&(a, b).0`) — a
238    /// binder glued to a following atom with no whitespace. A binder is one
239    /// prefixed atom, so the glued tail has no reading; write it spaced
240    /// (`&x .0`) to apply, or restructure. See `docs/todo/elly-parse-runs.md` §C.
241    BinderRunGlued,
242    /// An `<int>`-then-`.<digits>` run — the float-like run `3.14` (Muon groups
243    /// the two atoms, see `docs/done/2026-09-08_muon-runs.md`). Float literals do
244    /// not exist yet; this holds the notation open for them (a real float is a
245    /// later `Num` extension) while giving a clear error now instead of the
246    /// runtime `.not_applicable` a spaced `3 .14` projection raises.
247    FloatLiteralUnsupported,
248    /// A [`#`](muon::Item::Prefixed)-prefixed form other than the three that
249    /// read: `#[…]` (a list), `#{…}` (a map), and `#Self(…)` / `#Self …`
250    /// (construction). The `#` sigil marks a **literal notation**, and the
251    /// remaining shapes — `#<sym>`, `#<str>`, `#(…)`, `#.name` — are reserved
252    /// for future literal kinds with no reading yet. A bare `#Self` lands here
253    /// as well: construction is a form, not a value, so it may only head a
254    /// spine (see `parse_app_spine`). See `docs/todo/elly-syntax-v1.md` §2.
255    ReservedLiteralForm,
256    /// A bare `[…]` where a list literal or list pattern is written. After the
257    /// reshuffle (phase 3), the list literal uses the `#`-prefixed spelling
258    /// exclusively: write `#[…]`. See `docs/todo/elly-syntax-v1.md` § 3–5.
259    BareListLiteral,
260    /// A bare `{…}` carrying a `:`-clause where a map literal or map pattern is
261    /// written. After the reshuffle, the map literal uses the `#`-prefixed
262    /// spelling exclusively: write `#{…}`. A `:`-clause has no reading in a block
263    /// (nothing legal in a block starts `<key> :`), so a brace group with one is
264    /// this error in every position; a brace group *without* a `:`-clause is a
265    /// block (step 6), and the empty `{}` is the unit value.
266    /// See `docs/todo/elly-syntax-v1.md` § 3–5.
267    BareMapLiteral,
268    /// A block `{…}` whose final clause is a binding (`{ …, x = v }`). A block's
269    /// value is its last clause's value and its bindings do not escape, so a
270    /// trailing binding would bind a name nothing can read and leave no value to
271    /// return — as a `let` with no body does. End the block with an expression.
272    /// See `docs/todo/elly-syntax-v1.md` § 5 (review) and step 6.
273    BlockTrailingBinding,
274    /// A `case` with no `{…}` arm block — `case x` or `case` alone, or a trailing
275    /// item that is not a brace group. The block is where the clauses live, so a
276    /// `case` without one has nothing to match against and no reading.
277    CaseMissingArms,
278    /// A `case` subject that is more than one item — `case f x { … }`. The subject
279    /// is exactly one item so the arm block's `{…}` is unambiguous; group a
280    /// compound subject (`case (f x) { … }`) or pipe it in (`f x |> case { … }`).
281    CaseSubjectExtra,
282    /// A `case` arm whose chain is not headed by `&` — a clause that is not a
283    /// binder. Every arm is `& <pattern> … <body>`; a bare expression is not one.
284    ArmNotBinder,
285    /// A `case` arm with a pattern (and any guards) but no body after them —
286    /// `& x` or `& x when (c)` alone. An arm's body is required.
287    ArmMissingBody,
288    /// A `when` guard with no `(…)` condition group — `when` at the end of a
289    /// header, or `when` followed by something other than a parenthesized group.
290    WhenMissingCond,
291}
292
293/// What a module's source writes to name an import — a path, a package-qualified
294/// name. Its grammar belongs to the host resolver, not to Elly.
295pub type ImportSpec = Text;
296
297/// What a resolver answers a spec with: one name per module, however many specs
298/// reach it. Module instances are deduplicated by this name, so a resolver that
299/// returns the spec unchanged would hand back two instances of one module for
300/// `"./foo"` and `"foo"` (see [`crate::load_module`]).
301pub type ModuleName = Text;
302
303/// One `import` declaration: the name the importing module reaches the instance
304/// by, and the spec its host resolver is handed.
305///
306/// The two written forms differ in whether the module also **re-exports** the
307/// instance. `import "spec" as Foo` is a private frame slot; `Foo = import "spec"`
308/// is the same slot plus an ordinary item `Foo` that forwards it. Both arrive here
309/// as one `ImportDecl` — the re-exporting form has additionally pushed its
310/// forwarding item into [`ModuleSyntax::items`] (see
311/// `docs/done/2026-08-11_elly-modules-import.md`).
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct ImportDecl {
314    /// The name the importing module's bodies read the instance by.
315    pub name: Text,
316    /// What the host resolver is handed.
317    pub spec: ImportSpec,
318}
319
320/// A module source's top-level declarations: its imports and its `const`
321/// declarations, in source order, and its items. The imports are the module's
322/// first frame slots — the const stage resolves, compiles and instantiates each
323/// one, then instantiates this module against the results (see
324/// [`crate::load_module`]). The iotas take no slot at all: an iota's identity is
325/// its instance plus its position, derived on reference (see
326/// `docs/done/2026-08-13_elly-modules-iotas.md`).
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct ModuleSyntax {
329    /// The `import` declarations, in source order, which is frame-slot order.
330    pub imports: Vec<ImportDecl>,
331    /// The names declared by `const`, in source order — the module's **iotas**.
332    /// Sorted before they reach [`ModuleData`](crate::ModuleData), where the
333    /// order is the address space a member reference counts against.
334    pub iotas: Vec<Text>,
335    /// The `name = body` items, in source order, bodies unresolved.
336    pub items: Vec<(Text, Expr)>,
337    /// The `local name = body` declarations, in source order, bodies unresolved
338    /// — the module's **private** bindings. They are members like the items, and
339    /// differ only in not being searched by name from outside (see
340    /// `docs/done/2026-08-13_elly-modules-local.md`).
341    pub locals: Vec<(Text, Expr)>,
342    /// What the module declared as `__main`, if anything — the **reserved
343    /// moditem** a runner evaluates and applies to the root capability.
344    ///
345    /// It is a slot rather than an entry in [`items`](Self::items), and that is
346    /// what makes it unnameable: the member index space it stays out of is the
347    /// only thing a body's reference or an outside `M.name` access can address.
348    /// See `docs/done/2026-08-14_elly-run.md`.
349    pub main: Option<Expr>,
350}
351
352/// Parse a whole parsed program (one top-level chain) to an expression.
353pub fn parse_program<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
354    // Chain joining runs first, so a program written as `&x` ⏎ `body` (or any
355    // other continuation) is one expression, not `MultipleExpressions`.
356    match chain_slices(seq).as_slice() {
357        [] => Err(ParseError::EmptyProgram),
358        [items] => parse_items(items),
359        _ => Err(ParseError::MultipleExpressions),
360    }
361}
362
363/// Parse a **module** source's top-level sequence into its declarations: one
364/// `name = body` item or one `import` per non-comment chain, in source order.
365/// Unlike [`parse_program`] a module is a *multi-chain* sequence, and a body is
366/// not resolved here — [`crate::compile_module`] runs
367/// [`crate::resolve::resolve_module_bodies`] afterwards so a body may refer to any
368/// sibling regardless of order. An item's left-hand side is a single bare name in
369/// v0 (no destructuring at module top level); duplicate names and non-name LHSs
370/// are rejected. See `docs/done/2026-08-07_elly-modules-v0.md`.
371///
372/// An `import` chain has no top-level `=` in its private form, so it is recognized
373/// before the `=` split rather than reported as a `MalformedBinding`. Its two
374/// forms are described on [`ImportDecl`]; `Foo = import "spec"` also pushes the
375/// item `Foo` that forwards the slot, since it is written as an item and should
376/// read as one from outside.
377///
378/// A `const` declaration is read the same way and follows the same privacy
379/// convention: `const false` and `const (false, true)` are private, `false =
380/// const` declares the iota *and* the item that exports it.
381///
382/// A `local` declaration is the ordinary binding under the same convention:
383/// `local x = <expr>` and the group `local (x = <expr> …)` bind a member the
384/// module's own bodies reach and nothing outside can, where the bare
385/// `x = <expr>` beside them is the export form.
386pub fn parse_module<'a>(seq: &Seq<'a>) -> Result<ModuleSyntax, ParseError> {
387    let mut items: Vec<(Text, Expr)> = Vec::new();
388    let mut imports: Vec<ImportDecl> = Vec::new();
389    let mut iotas: Vec<Text> = Vec::new();
390    let mut locals: Vec<(Text, Expr)> = Vec::new();
391    let mut main: Option<Expr> = None;
392    // The declarations that are also items, kept apart by kind, so the collision
393    // check below knows which shared names were written as one declaration and
394    // are therefore fine. A name shared by two *declarations* never is.
395    let mut reexported_imports: Vec<Text> = Vec::new();
396    let mut reexported_iotas: Vec<Text> = Vec::new();
397    for ci in chain_slices(seq) {
398        // `chain_slices` drops comment-only chains and applies chain joining, so
399        // `ci` is a non-empty, already-joined declaration chain.
400        // Split on the first top-level `=`, exactly as `parse_binding` does; a
401        // second top-level `=` is malformed (a nested `=` lives inside `(…)`/…).
402        let split = ci.iter().position(|it| matches!(it, Item::Punct("=")));
403        // `import "spec" as Foo` — a private slot, and the one module top-level
404        // chain with no `=` of its own. A chain that *has* one is a binding
405        // whatever it starts with, so `import = 3` still reports the keyword.
406        if split.is_none() && head_keyword(ci[0]) == Some("import") {
407            let (name, spec) = parse_private_import(&ci)?;
408            push_import(&mut imports, name, spec)?;
409            continue;
410        }
411        // `const false` / `const (false, true)` — a private declaration, and like
412        // a private import the whole chain is the declaration.
413        if split.is_none() && head_keyword(ci[0]) == Some("const") {
414            for name in parse_const_decl(&ci)? {
415                push_iota(&mut iotas, name)?;
416            }
417            continue;
418        }
419        // `local (x = 1, y = 2)` — the group form, whose bindings' `=`s all sit
420        // inside the tuple, so the chain has none of its own. The single form
421        // `local x = 1` does, and is read below with the other bindings.
422        if split.is_none() && head_keyword(ci[0]) == Some("local") {
423            for (name, body) in parse_local_group(&ci)? {
424                push_local(&mut locals, name, body)?;
425            }
426            continue;
427        }
428        let eq = split.ok_or(ParseError::MalformedBinding)?;
429        if ci[eq + 1..].iter().any(|it| matches!(it, Item::Punct("="))) {
430            return Err(ParseError::MalformedBinding);
431        }
432        let (head, tail) = (&ci[..eq], &ci[eq + 1..]);
433        if head.is_empty() || tail.is_empty() {
434            return Err(ParseError::MalformedBinding);
435        }
436        // `const x = <expr>`, the private const-time expression form the grammar
437        // reserves and nothing implements. A bare `const = 3` is not this — it
438        // falls through to `plain_name`, which reports the keyword.
439        if head.len() > 1 && head_keyword(head[0]) == Some("const") {
440            return Err(ParseError::MalformedConst);
441        }
442        // `local x = <expr>` — the single private binding. A bare `local = 3` is
443        // not this: it falls through to `plain_name`, which reports the keyword.
444        if head.len() > 1 && head_keyword(head[0]) == Some("local") {
445            let name = plain_name(&head[1..], ParseError::MalformedLocal)?;
446            push_local(&mut locals, name, parse_items(tail)?)?;
447            continue;
448        }
449        // `__main = <expr>` — a **reserved moditem**, recognized here because
450        // `plain_name` would otherwise stop at the `__` and report the name as
451        // reserved. It is not an item: it goes to its own slot, outside the
452        // member index space, so nothing can name it (see `ModuleSyntax::main`).
453        if let [Item::Sym(s)] = head {
454            if let Some(reserved) = reserved_moditem(s) {
455                if main.is_some() {
456                    return Err(ParseError::DuplicateReserved(text(reserved)));
457                }
458                main = Some(parse_items(tail)?);
459                continue;
460            }
461            if s.starts_with("__") {
462                return Err(ParseError::UnknownReserved(text(s)));
463            }
464        }
465        let name = plain_name(head, ParseError::BadModuleItemName)?;
466        if items.iter().any(|(n, _)| n.as_str() == name.as_str()) {
467            return Err(ParseError::DuplicateModuleItem);
468        }
469        // `Foo = import "spec"` — the same slot, re-exported. The item's body is
470        // the name itself: resolution looks imports up before items, so it reads
471        // the slot rather than looping back through this forwarding item.
472        if head_keyword(tail[0]) == Some("import") {
473            let spec = parse_import_spec(tail)?;
474            push_import(&mut imports, name.clone(), spec)?;
475            reexported_imports.push(name.clone());
476            items.push((name.clone(), Expr::Name(name)));
477            continue;
478        }
479        // `false = const` — the iota plus the item exporting it. There is nothing
480        // to the right of the keyword until const-time expressions exist, and the
481        // item's body is the name itself: a sibling reference finds the iota,
482        // since members are searched iotas-first (see `resolve_module_bodies`).
483        if head_keyword(tail[0]) == Some("const") {
484            if tail.len() != 1 {
485                return Err(ParseError::MalformedConst);
486            }
487            push_iota(&mut iotas, name.clone())?;
488            reexported_iotas.push(name.clone());
489            items.push((name.clone(), Expr::Name(name)));
490            continue;
491        }
492        let body = parse_items(tail)?;
493        items.push((name, body));
494    }
495    // A private declaration may not share a name with an item — inside the module
496    // the name would have to mean both. A re-exported one shares by construction.
497    // Two *declarations* may never share one, re-exported or not: neither form
498    // writes an import and an iota at once.
499    for imp in &imports {
500        if iotas.iter().any(|n| n.as_str() == imp.name.as_str()) {
501            return Err(ParseError::DuplicateModuleItem);
502        }
503        if reexported_imports
504            .iter()
505            .any(|n| n.as_str() == imp.name.as_str())
506        {
507            continue;
508        }
509        if items.iter().any(|(n, _)| n.as_str() == imp.name.as_str()) {
510            return Err(ParseError::DuplicateModuleItem);
511        }
512    }
513    for iota in &iotas {
514        if reexported_iotas.iter().any(|n| n.as_str() == iota.as_str()) {
515            continue;
516        }
517        if items.iter().any(|(n, _)| n.as_str() == iota.as_str()) {
518            return Err(ParseError::DuplicateModuleItem);
519        }
520    }
521    // A local shares with nothing: it is a declaration, and there is no form
522    // writing a local and something else at once.
523    for (name, _) in &locals {
524        let taken = items.iter().any(|(n, _)| n.as_str() == name.as_str())
525            || iotas.iter().any(|n| n.as_str() == name.as_str())
526            || imports.iter().any(|i| i.name.as_str() == name.as_str());
527        if taken {
528            return Err(ParseError::DuplicateModuleItem);
529        }
530    }
531    Ok(ModuleSyntax {
532        imports,
533        iotas,
534        items,
535        locals,
536        main,
537    })
538}
539
540/// The reserved moditems the language defines, as the names a module top-level
541/// left-hand side may spell in the `__` namespace. One so far; `__test` and
542/// `__doc` are anticipated, and each will be another slot on [`ModuleSyntax`]
543/// with a clause here (see `docs/done/2026-08-14_elly-run.md`).
544///
545/// Answers with the `'static` spelling so the caller reports the *defined* name
546/// rather than echoing the source, and `None` for a name that is not one — which
547/// the caller turns into [`ParseError::UnknownReserved`] rather than an item.
548fn reserved_moditem(name: &str) -> Option<&'static str> {
549    match name {
550        "__main" => Some("__main"),
551        _ => None,
552    }
553}
554
555/// Record one import, rejecting a name a previous import already took.
556fn push_import(
557    imports: &mut Vec<ImportDecl>,
558    name: Text,
559    spec: ImportSpec,
560) -> Result<(), ParseError> {
561    if imports.iter().any(|i| i.name.as_str() == name.as_str()) {
562        return Err(ParseError::DuplicateModuleItem);
563    }
564    imports.push(ImportDecl { name, spec });
565    Ok(())
566}
567
568/// Parse `import "<spec>" as <Name>` — the private form, whose whole chain is the
569/// declaration — to its name and spec. The keyword may be a bare sym or a glued
570/// run head (§D); in the run case only the spec glues into the run (`import"spec"`)
571/// while `as <Name>` stays in the chain after it, so the two are concatenated back
572/// into the one item stream the spaced form already is.
573fn parse_private_import<'a>(ci: &[&Item<'a>]) -> Result<(Text, ImportSpec), ParseError> {
574    // Drop the keyword head. When it is a glued run head (§D) the spec sits inside
575    // the run (`import"spec"`), but `as <Name>` is space-separated and so stays in
576    // the chain after the run — so the run's members and the chain tail concatenate
577    // into one `"spec" as Name` stream, the same one the spaced form is.
578    let rest: Vec<&Item<'a>> = match ci[0] {
579        Item::Run(members) => members[1..]
580            .iter()
581            .map(|it| it as &Item<'a>)
582            .chain(ci[1..].iter().copied())
583            .collect(),
584        _ => ci[1..].to_vec(),
585    };
586    match rest.as_slice() {
587        [Item::Str(spec), Item::Sym("as"), rest @ ..] if !rest.is_empty() => {
588            let name = plain_name(rest, ParseError::BadModuleItemName)?;
589            Ok((name, decode_str(spec)?))
590        }
591        _ => Err(ParseError::MalformedImport),
592    }
593}
594
595/// Record one iota, rejecting a name a previous `const` already declared.
596fn push_iota(iotas: &mut Vec<Text>, name: Text) -> Result<(), ParseError> {
597    if iotas.iter().any(|n| n.as_str() == name.as_str()) {
598        return Err(ParseError::DuplicateModuleItem);
599    }
600    iotas.push(name);
601    Ok(())
602}
603
604/// Parse a private `const` declaration — `const <name>`, or the group form
605/// `const (<name> …)` — to the names it declares.
606///
607/// The group is an ordinary `<parens>` wrapping a `<seq>`, so its names may be
608/// newline- or comma-separated and it may carry comments, exactly as a `let`
609/// group may. An **empty group declares nothing**, the same degenerate reading
610/// `let ()` and `recur ()` take.
611fn parse_const_decl<'a>(ci: &[&Item<'a>]) -> Result<Vec<Text>, ParseError> {
612    // The keyword may be a bare sym or a glued run head (§D); the declared names
613    // are read from what follows it (the run's members in the run case).
614    let rest: Vec<&Item<'a>> = match ci[0] {
615        Item::Run(members) => members[1..].iter().map(|it| it as &Item<'a>).collect(),
616        _ => ci[1..].to_vec(),
617    };
618    match rest.as_slice() {
619        [Item::Parens(seq), ..] => {
620            let mut names = Vec::new();
621            for c in chain_slices(seq) {
622                names.push(plain_name(&c, ParseError::MalformedConst)?);
623            }
624            Ok(names)
625        }
626        rest if !rest.is_empty() => Ok(alloc::vec![plain_name(rest, ParseError::MalformedConst)?]),
627        _ => Err(ParseError::MalformedConst),
628    }
629}
630
631/// Record one local, rejecting a name a previous `local` already took.
632fn push_local(locals: &mut Vec<(Text, Expr)>, name: Text, body: Expr) -> Result<(), ParseError> {
633    if locals.iter().any(|(n, _)| n.as_str() == name.as_str()) {
634        return Err(ParseError::DuplicateModuleItem);
635    }
636    locals.push((name, body));
637    Ok(())
638}
639
640/// Parse the group form `local (<name> = <expr> …)` to its bindings.
641///
642/// The group is an ordinary `<parens>` wrapping a `<seq>`, so its bindings may be
643/// newline- or comma-separated and it may carry comments, exactly as a `let`
644/// group may — and an **empty group declares nothing**, the degenerate reading
645/// `const ()` and `let ()` take. Anything else after the keyword is a malformed
646/// declaration: the single form `local x = <expr>` has a top-level `=` and never
647/// reaches here.
648fn parse_local_group<'a>(ci: &[&Item<'a>]) -> Result<Vec<(Text, Expr)>, ParseError> {
649    // The `local` keyword may be a bare sym or a glued run head (§D); the group is
650    // the item after it (the run's second member in the run case).
651    // The group is the item after the keyword: the run's second member in the run
652    // case, the next chain item otherwise.
653    let seq = match ci[0] {
654        Item::Run(members) => {
655            let Some(Item::Parens(seq)) = members.get(1) else {
656                return Err(ParseError::MalformedLocal);
657            };
658            seq
659        }
660        _ => {
661            let Some(Item::Parens(seq)) = ci.get(1) else {
662                return Err(ParseError::MalformedLocal);
663            };
664            seq
665        }
666    };
667    let mut bindings = Vec::new();
668    for c in chain_slices(seq) {
669        let eq = c
670            .iter()
671            .position(|it| matches!(it, Item::Punct("=")))
672            .ok_or(ParseError::MalformedLocal)?;
673        let (head, tail) = (&c[..eq], &c[eq + 1..]);
674        if head.is_empty() || tail.is_empty() {
675            return Err(ParseError::MalformedLocal);
676        }
677        if tail.iter().any(|it| matches!(it, Item::Punct("="))) {
678            return Err(ParseError::MalformedLocal);
679        }
680        bindings.push((
681            plain_name(head, ParseError::MalformedLocal)?,
682            parse_items(tail)?,
683        ));
684    }
685    Ok(bindings)
686}
687
688/// Parse the `import "<spec>"` right-hand side of a re-exporting import to its
689/// spec. There is no `as` here: the item's own left-hand side named it. The
690/// keyword may be spaced from the spec (two chain items) or glued to it (§D — one
691/// run whose second member is the spec), the two spellings meaning the same.
692fn parse_import_spec<'a>(tail: &[&Item<'a>]) -> Result<ImportSpec, ParseError> {
693    let spec = match tail {
694        [_import, Item::Str(spec)] => spec,
695        [Item::Run(members)] => match members.as_slice() {
696            [_import, Item::Str(spec)] => spec,
697            _ => return Err(ParseError::MalformedImport),
698        },
699        _ => return Err(ParseError::MalformedImport),
700    };
701    decode_str(spec)
702}
703
704/// The single plain name a binding position may hold: exactly one `<sym>` that is
705/// not a keyword, a `__` name, a discard, or a number. A reserved token gets its
706/// own error; every other shape (a destructuring pattern, a literal, several
707/// items) gets the caller's `bad`, which names the position —
708/// [`ParseError::BadPattern`] for an at-pattern's LHS,
709/// [`ParseError::BadModuleItemName`] for a module item's.
710fn plain_name<'a>(items: &[&Item<'a>], bad: ParseError) -> Result<Text, ParseError> {
711    match items {
712        [Item::Sym(s)] if is_keyword(s) => Err(ParseError::ReservedKeyword),
713        [Item::Sym(s)] if s.starts_with("__") => Err(ParseError::ReservedName),
714        [Item::Sym(s)] if reads_as_number(s) || *s == "_" => Err(bad),
715        [Item::Sym(s)] => Ok(text(s)),
716        _ => Err(bad),
717    }
718}
719
720/// Would this non-empty `<sym>` read as a number rather than a reference? Either
721/// it is a valid integer literal — including a signed one, since `-`/`+` are
722/// symchars, so `-1` is one `<sym>` — or it is digit-leading and therefore a
723/// *malformed* number. A reference is neither (see the atom path above), so a
724/// binding may not take such a name: it could never be read back.
725fn reads_as_number(s: &str) -> bool {
726    parse_int(s).is_some() || s.as_bytes()[0].is_ascii_digit()
727}
728
729/// Parse a comment-free slice of items (a chain, or a suffix of one).
730fn parse_items<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
731    if items.is_empty() {
732        return Err(ParseError::EmptyChain);
733    }
734    // `|>` is the **loosest** operator: split on the last one so the chain is
735    // a left-associative pipe chain over **segments**, each parsed by
736    // `parse_segment`. A `&`/`let`/`recur` binder form can therefore never
737    // capture a pipe — `&x x |> f` is `f (&x x)`, and a pipe-bodied lambda is
738    // written `&x (x |> f)` — and a pipe's right operand is a whole segment,
739    // which may itself end in a trailing binder form (`x |> &y body`).
740    match items.iter().rposition(|it| matches!(it, Item::Punct("|>"))) {
741        None => parse_segment(items),
742        Some(k) => {
743            let (left, right) = (&items[..k], &items[k + 1..]);
744            if left.is_empty() || right.is_empty() {
745                return Err(ParseError::MalformedPipe);
746            }
747            let l = parse_items(left)?;
748            let r = parse_segment(right)?;
749            // Thread the left operand in as `r`'s final argument, extending its
750            // spine so `x |> f a b` stays one flattened call `f a b x`.
751            Ok(append_arg(r, l))
752        }
753    }
754}
755
756/// Parse one pipe segment — a `|>`-free item slice: an application spine whose
757/// trailing argument may be a `&`/`let`/`recur` binder form. A split point is a
758/// `&`-binder or the `let`/`recur` keyword (or a `let`/`recur` **run head**, §D):
759/// it has the lowest precedence within the segment and captures the rest of the
760/// segment as its body, so the spine before the first one is a left-folded
761/// application whose trailing argument is the abstraction / let / recur that
762/// follows. With no split point this is exactly `parse_app_spine`.
763fn parse_segment<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
764    match items.iter().position(|it| is_split_point(it)) {
765        // No split point: a plain application spine (with `()` spread).
766        None => parse_app_spine(items),
767        // Leading split point: a `let (…) body`, a `recur (…) body`, or
768        // `&<header> <rest>` abstracting a pattern over the rest.
769        Some(0) => {
770            match head_keyword(items[0]) {
771                // A `let`/`recur` written glued to its group reads the group from
772                // inside the run (§D): `let(x = 1) e` == `let (x = 1) e`.
773                Some("let") | Some("recur") => parse_let_or_recur(items[0], items),
774                // `case <subject>? { arms }`
775                Some("case") => parse_case(items),
776                _ => {
777                    // A `&` binder (never `&`-headed-run, which is a run, not a
778                    // `Prefixed`).
779                    if items.len() == 1 {
780                        return Err(ParseError::AbsWithoutBody);
781                    }
782                    let inner = match items[0] {
783                        Item::Prefixed { sigil: '&', item } => item.as_ref(),
784                        _ => return Err(ParseError::UnexpectedBinder),
785                    };
786                    // A `&<pat> when (…) …` binder carries a guard on its pattern,
787                    // the same guard a `case` arm takes (see `peel_when`); the body
788                    // is what remains after the guards. Guards attach only to a
789                    // single (non-group) pattern header — a `&(a, b)` curried group
790                    // with a `when` is not supported and falls through to
791                    // `build_binder`, where the `when` lands in the body and errors.
792                    if matches!(items.get(1), Some(it) if is_when_kw(it))
793                        && !matches!(inner, Item::Parens(_))
794                    {
795                        if is_bare_literal_header(inner) {
796                            return Err(ParseError::BareLiteralHeader);
797                        }
798                        let base = parse_pattern(&[inner])?;
799                        let (pat, rest) = peel_when(base, &items[1..])?;
800                        if rest.is_empty() {
801                            return Err(ParseError::AbsWithoutBody);
802                        }
803                        let body = parse_segment(rest)?;
804                        return Ok(abs(pat, body));
805                    }
806                    let body = parse_segment(&items[1..])?;
807                    build_binder(inner, body)
808                }
809            }
810        }
811        // Split point in the middle: the abstraction / let is the trailing
812        // argument of the application spine that precedes it.
813        Some(k) => {
814            let f = parse_segment(&items[..k])?;
815            let arg = parse_segment(&items[k..])?;
816            Ok(append_arg(f, arg))
817        }
818    }
819}
820
821/// Parse a leading `let`/`recur` form, reading the binder group from the item's
822/// **second member** when the keyword is a glued run (§D) or from the next chain
823/// item when it is a bare sym. `let(x = 1) e` and `let (x = 1) e` are the same.
824/// The group must be a `(…)` tuple; a keyword with no group, or a glued run with
825/// more than the keyword and one group member (`let(a)(b)`), is `…MissingBinders`.
826fn parse_let_or_recur<'a>(head: &Item<'a>, items: &[&Item<'a>]) -> Result<Expr, ParseError> {
827    // A keyword is `let` or `recur` (the only heads that reach here).
828    let is_recur = head_keyword(head) == Some("recur");
829    let missing = || {
830        if is_recur {
831            ParseError::RecurMissingBinders
832        } else {
833            ParseError::LetMissingBinders
834        }
835    };
836    let (group_item, body_items): (&Item<'_>, &[&Item<'_>]) = match head {
837        // Glued run: the group is the run's lone second member (a longer run is
838        // malformed); the body is the chain after the run.
839        Item::Run(members) if members.len() == 2 => (&members[1], &items[1..]),
840        Item::Run(_) => return Err(missing()),
841        // Bare keyword: the group is the next chain item, the body what follows it.
842        _ => (*items.get(1).ok_or_else(missing)?, &items[2..]),
843    };
844    let group = match group_item {
845        Item::Parens(seq) => seq,
846        _ => return Err(missing()),
847    };
848    if is_recur {
849        parse_recur_inner(group, body_items)
850    } else {
851        parse_let_inner(group, body_items)
852    }
853}
854
855/// The body of a `let`: fold the group's **sequential** bindings
856/// (`let (a = va, b = vb) e` desugars to the nested `(&a ((&b e) vb)) va`)
857/// right-to-left over `body_items`, leaving the leftmost binding outermost.
858/// Reached through [`parse_let_or_recur`], which reads the group from the chain
859/// (a bare `let (…)` keyword) or from inside a glued run (`let(…)`).
860///
861/// An **empty group binds nothing**, so `let () e` is `e` — the fold over zero
862/// bindings, and the unit of the form. It is not an error: a group is a list, and
863/// a generated or commented-out one may legitimately come out empty.
864fn parse_let_inner<'a>(group: &Seq<'a>, body_items: &[&Item<'a>]) -> Result<Expr, ParseError> {
865    if body_items.is_empty() {
866        return Err(ParseError::LetMissingBody);
867    }
868    // `let (a = va, b = vb) e` **is** the block `{ a = va, b = vb, e }`: the group's
869    // (joined, comment-free) binding chains followed by the body as the final
870    // clause. `build_block` produces the shared `Expr::Block`, collapsing an empty
871    // group (`let () e` is `e`) and a lone body the same way a `{…}` block does.
872    let mut clauses = chain_slices(group);
873    // A `let` group holds **bindings only** — a chain without a top-level `=` is a
874    // malformed binding (unlike a block, whose non-final clauses may be bare
875    // expressions). Enforce it here so `let (x) x` stays `MalformedBinding`.
876    for ci in &clauses {
877        if !clause_is_binding(ci) {
878            return Err(ParseError::MalformedBinding);
879        }
880    }
881    clauses.push(body_items.to_vec());
882    build_block(clauses)
883}
884
885/// Parse a `recur (b0, b1, …) body`. Shaped like [`parse_let`] — `items[0]` is the
886/// keyword, `items[1]` the binder-group tuple, `items[2..]` the body — and unlike
887/// it in what the group means: the bindings are **simultaneous**, so they cannot
888/// desugar to nested abstractions and instead become a module item table that the
889/// bodies reach through a terminal.
890///
891/// The left-hand sides are plain names, not patterns. The group *is* an item table,
892/// so it follows [`parse_module`]'s rule (which is also what rejects a duplicate)
893/// rather than `let`'s, which does allow a destructuring pattern. That divergence
894/// falls out of the mechanism rather than being a choice.
895///
896/// The table is sorted here, at parse: the sort order *is* the index space a
897/// sibling [`Expr::ModItem`] counts against, and the resolver assigns those indices
898/// against the frozen [`ModuleData`].
899///
900/// As with `let`, an empty group is `body` — with the terminal dropped too, since a
901/// group with no items has nothing to reach through it.
902fn parse_recur_inner<'a>(group: &Seq<'a>, body_items: &[&Item<'a>]) -> Result<Expr, ParseError> {
903    if body_items.is_empty() {
904        return Err(ParseError::RecurMissingBody);
905    }
906    let syntax = parse_module(group)?;
907    if !syntax.imports.is_empty() {
908        // A group is an item table, not a module source: it has no frame of its
909        // own for the const stage to fill, and it is built while a program runs.
910        return Err(ParseError::MisplacedImport);
911    }
912    if !syntax.iotas.is_empty() {
913        // Nor an instance of its own to mint iotas against.
914        return Err(ParseError::MisplacedConst);
915    }
916    if !syntax.locals.is_empty() {
917        // And every binding of a group is private already — it is reachable from
918        // the group's own bodies and its body, and from nowhere else — so the
919        // keyword would mark nothing.
920        return Err(ParseError::MisplacedLocal);
921    }
922    if syntax.main.is_some() {
923        // A reserved moditem is read by a tool that was pointed at a *source* —
924        // a runner, a test runner — and a group is not one, so the slot would
925        // never be looked at.
926        return Err(ParseError::MisplacedReserved);
927    }
928    let mut bindings = syntax.items;
929    let body = parse_segment(body_items)?;
930    if bindings.is_empty() {
931        return Ok(body);
932    }
933    bindings.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
934    // No frame, no imports, and no name: a group's bindings are written where
935    // they are used, so there is no source of its own for a host to name. The
936    // body goes in the module's own body slot — the same one a source fills with
937    // `__main`, differing only in who evaluates it and when.
938    Ok(Expr::Recur(Rc::new(ModuleData::from_bindings(
939        bindings,
940        Vec::new(),
941        Box::new([]),
942        Box::new([]),
943        Box::new([]),
944        Some(body),
945        None,
946    ))))
947}
948
949/// Parse one binding chain `<pattern> "=" <expr>` to its pattern and value. The
950/// separator is the **first** top-level `=` (`<punct>`); a pattern may carry its
951/// own `=` only inside `(…)`/`[…]`/`{…}` (a nested item, never top-level), so a
952/// second top-level `=` is malformed. Both the head (pattern) and tail (value)
953/// must be non-empty.
954fn parse_binding<'a>(items: &[&Item<'a>]) -> Result<(Pattern, Expr), ParseError> {
955    let mut eq = None;
956    for (i, it) in items.iter().enumerate() {
957        if matches!(it, Item::Punct("=")) {
958            if eq.is_some() {
959                return Err(ParseError::MalformedBinding); // a second top-level `=`
960            }
961            eq = Some(i);
962        }
963    }
964    let eq = eq.ok_or(ParseError::MalformedBinding)?;
965    let (head, tail) = (&items[..eq], &items[eq + 1..]);
966    if head.is_empty() || tail.is_empty() {
967        return Err(ParseError::MalformedBinding);
968    }
969    // A top-level `|` in the LHS must be parenthesized (an `&`-header can't reach
970    // this, since `&` prefixes one item), so the or-`|` and the binding `=` do
971    // not compete: `let (((x=.a) | (x=.b)) = v) e`.
972    if head.iter().any(|it| matches!(it, Item::Punct("|"))) {
973        return Err(ParseError::LetOrUnparenthesized);
974    }
975    // A bare matching literal as the whole LHS needs parens: `let ((42) = v)`,
976    // `let ((.foo) = v)`, `let (("s") = v)` (see `BareLiteralHeader`).
977    if let [it] = head {
978        if is_bare_literal_header(it) {
979            return Err(ParseError::BareLiteralHeader);
980        }
981    }
982    let pat = parse_pattern(head)?;
983    let value = parse_items(tail)?;
984    Ok((pat, value))
985}
986
987/// Parse a single item as an atomic expression (`<aexpr>`).
988fn parse_aexpr<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
989    match item {
990        Item::Sym(s) => Ok(parse_sym(s)?),
991        // A `.`-prefixed item is a symbol (`.foo`, `.0`); see parse_dot_symbol.
992        Item::Prefixed { sigil: '.', item } => parse_dot_symbol(item),
993        // A `[…]` list is a runtime list value (any arity, no 1-element
994        // collapse); a `(…)` group is grouping / spread folded into one expr.
995        // After the reshuffle (phase 3), bare `[…]` is rejected: write `#[…]`.
996        Item::Brackets(_) => Err(ParseError::BareListLiteral),
997        Item::Parens(seq) => parse_group(seq),
998        // A `<str>` literal, decoded at parse into an owned `Expr::Str`.
999        Item::Str(s) => Ok(Expr::Str(decode_str(s)?)),
1000        // A `{…}` brace group is a **block expression** (step 6), never a map:
1001        // the map literal is `#{…}` exclusively. `parse_block` reads the empty
1002        // `{}` as unit and refuses a stale `:`-clause with the `#{…}` nudge.
1003        Item::Braces(seq) => parse_block(seq),
1004        // A bare `.` (composition combinator) is deferred; any other stray punct
1005        // (`:`, `=`) has no atomic reading outside a map entry / `let` binding.
1006        Item::Punct(".") => Err(ParseError::CombinatorDeferred),
1007        Item::Punct(_) => Err(ParseError::PunctAtAtom),
1008        // Filtered before we get here.
1009        Item::Comm(_) => Err(ParseError::EmptyChain),
1010        // `#[…]` / `#{…}` are the explicit spellings of the list / map literals,
1011        // reading exactly as the bare `[…]` / `{…}` forms (reshuffle phase 1);
1012        // any other `#`-prefixed shape — including a bare `#Self`, which is a
1013        // form, not a value (see `parse_app_spine`) — is a reserved literal
1014        // notation (§2).
1015        Item::Prefixed { sigil: '#', item } => match item.as_ref() {
1016            Item::Brackets(seq) => parse_list(seq),
1017            Item::Braces(seq) => parse_map(seq),
1018            _ => Err(ParseError::ReservedLiteralForm),
1019        },
1020        // A `&`-binder is never fed to parse_aexpr by the split logic above.
1021        Item::Prefixed { .. } => Err(ParseError::UnexpectedBinder),
1022        // A **run** of glued atoms is one chain item; lower it to the single
1023        // expression its members would form (see `parse_run`).
1024        Item::Run(members) => parse_run(members),
1025    }
1026}
1027
1028/// Lower a **run** — two or more atoms written with no whitespace — to the single
1029/// expression its members would form as one chain. The members are all atoms (a
1030/// `<punct>` or `<comm>` ends a run, never joins one), so they are exactly a
1031/// `<chain>`: parse them with the chain parser.
1032///
1033/// Two members that glue are either a **path** (a reference head followed by
1034/// `.`-segments) or an application (`f(x)`, `Main .A.S`). Both are the same
1035/// lowering — the members as a left-folded chain — so one `parse_items` call
1036/// serves both. A `&`-headed run is a binder slip and is refused outright (§C),
1037/// and a float-like run (`3.14`) is a rejected literal (§G).
1038fn parse_run<'a>(members: &[Item<'a>]) -> Result<Expr, ParseError> {
1039    check_run_head(members)?;
1040    parse_items(
1041        members
1042            .iter()
1043            .map(|it| it as &Item<'_>)
1044            .collect::<Vec<_>>()
1045            .as_slice(),
1046    )
1047}
1048
1049/// Reject the two run shapes that have no reading in **any** position, so the
1050/// argument lowering ([`parse_run`]) and the spine-head flatten ([`parse_app_spine`])
1051/// refuse them alike:
1052///
1053/// - §C — a `&`-headed run is a binder glued to a following atom (`&x.0` is one
1054///   prefixed atom with a tail, and the tail has no binder reading);
1055/// - §G — an `<int>` immediately followed by a `.<digits>` segment is the float
1056///   shape (`3.14`), refused rather than read as the projection `3 .14` applied,
1057///   until a float literal exists.
1058///
1059/// A run always has ≥ 2 members (Muon never wraps a lone atom), so indexing the
1060/// head is sound.
1061fn check_run_head(members: &[Item<'_>]) -> Result<(), ParseError> {
1062    if matches!(&members[0], Item::Prefixed { sigil: '&', .. }) {
1063        return Err(ParseError::BinderRunGlued);
1064    }
1065    if let (
1066        Item::Sym(head),
1067        Some(Item::Prefixed {
1068            sigil: '.',
1069            item: seg,
1070        }),
1071    ) = (&members[0], members.get(1))
1072    {
1073        if parse_int(head).is_some() && is_dot_number(seg) {
1074            return Err(ParseError::FloatLiteralUnsupported);
1075        }
1076    }
1077    Ok(())
1078}
1079
1080/// Is this a `.`-prefixed **number** segment (the second half of a float run)?
1081/// `3.14` is `Run[3, .14]`; a `.name` segment (`.foo`) is a path member, not a
1082/// float tail.
1083fn is_dot_number(item: &Item<'_>) -> bool {
1084    match item {
1085        Item::Sym(s) => parse_int(s).is_some(),
1086        _ => false,
1087    }
1088}
1089
1090/// Decode a Muon `<str>`'s raw inner text (the bytes between the quotes, escapes
1091/// unresolved) into an owned UTF-8 [`Text`]. Muon has already validated the escape
1092/// *grammar* (only `\" \n \t \r \\ \/ \b \f` and four-hex-digit `\uXXXX`, no raw
1093/// newline), so this resolves the escapes and — the higher-layer job Muon defers —
1094/// **combines `\uXXXX` surrogate pairs** into one scalar. The one failure is an
1095/// **unpaired surrogate** (a high not followed by a low, or a lone low), which
1096/// UTF-8 cannot represent: [`ParseError::LoneSurrogate`].
1097fn decode_str(raw: &str) -> Result<Text, ParseError> {
1098    let b = raw.as_bytes();
1099    let mut out = String::with_capacity(raw.len());
1100    let mut i = 0;
1101    let mut chunk = 0; // start of the current verbatim (unescaped) run
1102    while i < b.len() {
1103        if b[i] != b'\\' {
1104            i += 1; // a non-escape byte (incl. any UTF-8 continuation) copies as-is
1105            continue;
1106        }
1107        out.push_str(&raw[chunk..i]); // flush the verbatim run before this escape
1108        match b[i + 1] {
1109            b'"' => out.push('"'),
1110            b'\\' => out.push('\\'),
1111            b'/' => out.push('/'),
1112            b'n' => out.push('\n'),
1113            b't' => out.push('\t'),
1114            b'r' => out.push('\r'),
1115            b'b' => out.push('\u{8}'),
1116            b'f' => out.push('\u{c}'),
1117            b'u' => {
1118                let hi = hex4(&b[i + 2..i + 6]);
1119                i += 6;
1120                if (0xD800..=0xDBFF).contains(&hi) {
1121                    // A high surrogate must be completed by a `\uXXXX` low surrogate.
1122                    let lo = match (b.get(i), b.get(i + 1)) {
1123                        (Some(b'\\'), Some(b'u')) => hex4(&b[i + 2..i + 6]),
1124                        _ => return Err(ParseError::LoneSurrogate),
1125                    };
1126                    if !(0xDC00..=0xDFFF).contains(&lo) {
1127                        return Err(ParseError::LoneSurrogate);
1128                    }
1129                    let c = 0x1_0000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
1130                    out.push(char::from_u32(c).expect("combined surrogate pair"));
1131                    i += 6;
1132                } else if (0xDC00..=0xDFFF).contains(&hi) {
1133                    return Err(ParseError::LoneSurrogate); // a lone low surrogate
1134                } else {
1135                    out.push(char::from_u32(hi).expect("non-surrogate BMP scalar"));
1136                }
1137                chunk = i;
1138                continue;
1139            }
1140            _ => unreachable!("Muon validated the <str> escape grammar"),
1141        }
1142        i += 2;
1143        chunk = i;
1144    }
1145    out.push_str(&raw[chunk..]);
1146    Ok(Text::from(out.as_str()))
1147}
1148
1149/// Read exactly four ASCII hex-digit bytes (Muon-validated) as a `u32`.
1150fn hex4(b: &[u8]) -> u32 {
1151    b.iter()
1152        .fold(0, |v, &c| v * 16 + (c as char).to_digit(16).unwrap())
1153}
1154
1155/// Parse a `.`-prefixed item into a symbol. The inner item must be a single
1156/// name or number segment (`.foo`, `.0`); a `.` glued to anything else — the
1157/// record sugar `.(…)`, a nested sigil, a string — is deferred / ill-formed.
1158fn parse_dot_symbol<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
1159    match item {
1160        // Stored *without* the leading dot; the dot is implied by Expr::Symbol
1161        // and re-added when displayed / dumped.
1162        Item::Sym(s) => Ok(Expr::Symbol(text(s))),
1163        _ => Err(ParseError::BadSymbol),
1164    }
1165}
1166
1167/// Classify a `<sym>`: an integer literal, a name, or an error. Symbols are no
1168/// longer `<sym>`s (a `.` is a Muon sigil now) — they arrive as `<prefixed>`
1169/// items and are handled by parse_dot_symbol.
1170fn parse_sym(s: &str) -> Result<Expr, ParseError> {
1171    // `Self` — the home module. It is a keyword (un-bindable, and the `Self <pat>`
1172    // pattern form needs a reserved head), but a *reference* to it is the module
1173    // leaf, so the short-circuit sits before the keyword guard. `as Self` reaches
1174    // here through `parse_proto_ref`, so the nominal check is the same leaf.
1175    if s == "Self" {
1176        return Ok(Expr::Home {
1177            leaf: HomeLeaf::Module,
1178            depth: 0,
1179        });
1180    }
1181    if is_keyword(s) {
1182        // A keyword is syntax, never a reference. `let` at a chain head is
1183        // handled before we get here; anywhere else it (and `with`) is an error.
1184        return Err(ParseError::ReservedKeyword);
1185    }
1186    if s == "_" {
1187        // `_` is the discard pattern (valid only as a binder), never a reference.
1188        Err(ParseError::DiscardReference)
1189    } else if let Some(n) = parse_int(s) {
1190        Ok(Expr::Int(n))
1191    } else if s.as_bytes()[0].is_ascii_digit() {
1192        // Digit-leading but not a valid int, and a name can't start with a digit.
1193        Err(ParseError::MalformedNumber)
1194    } else if s.starts_with("__") {
1195        // A `__` name is un-bindable, so resolve a known builtin here (a direct
1196        // dispatch node) rather than leaving it to a failing env walk plus a name
1197        // match on every call. The namespace is the language's, and closed, so an
1198        // unknown one is rejected here too — a misspelled `__mian` is a compile
1199        // error naming the namespace, not a name that happens to bind to nothing.
1200        // A reserved moditem (`__main`) is not a name at all and lands here as
1201        // well: it is a slot, and nothing reads it by reference. The former
1202        // home-module leaves are retired spellings like any other misspelling
1203        // (`Self` / `#Self` / `x.Self` are the readings now, see
1204        // `docs/done/2026-09-11_elly-v1-self.md`), so they land here too.
1205        match Builtin::from_name(s) {
1206            Some(op) => Ok(Expr::Builtin(op)),
1207            None => Err(ParseError::UnknownReserved(text(s))),
1208        }
1209    } else {
1210        Ok(Expr::Name(text(s)))
1211    }
1212}
1213
1214/// Parse an integer literal per the *integers* grammar: optional `-`/`+` sign,
1215/// then decimal / `0x` hex / `0b` binary digits, with `_` separating digit
1216/// groups (never leading, trailing, or doubled). `None` if `s` is not a literal.
1217pub(crate) fn parse_int(s: &str) -> Option<BigInt> {
1218    let (neg, body) = match s.as_bytes().first()? {
1219        b'-' => (true, &s[1..]),
1220        b'+' => (false, &s[1..]),
1221        _ => (false, s),
1222    };
1223    let (radix, digits) =
1224        if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
1225            (16u32, h)
1226        } else if let Some(b) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
1227            (2, b)
1228        } else {
1229            (10, body)
1230        };
1231    // Strip `_` separators while rejecting leading/trailing/doubled ones.
1232    let mut cleaned = String::new();
1233    let mut after_sep = true; // start "after a separator" → forbids a leading `_`
1234    for &c in digits.as_bytes() {
1235        if c == b'_' {
1236            if after_sep {
1237                return None;
1238            }
1239            after_sep = true;
1240        } else {
1241            cleaned.push(c as char);
1242            after_sep = false;
1243        }
1244    }
1245    if after_sep || cleaned.is_empty() {
1246        return None; // trailing `_`, or no digits at all (e.g. `0x`, `+`)
1247    }
1248    let mag = BigInt::parse_bytes(cleaned.as_bytes(), radix)?;
1249    Some(if neg { -mag } else { mag })
1250}
1251
1252/// Parse an application spine (a chain slice with no `&`/`let` split point and no
1253/// top-level `|>`), folding left. A `(…)` item does not contribute one argument —
1254/// it **spreads** (see `expand_arg`): `f (a, b)` folds to `((f a) b)`.
1255fn parse_app_spine<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
1256    let mut args: Vec<Expr> = Vec::new();
1257    for (i, it) in items.iter().enumerate() {
1258        // The **head** run flattens into the spine (§B): `f(x) y` keeps the one
1259        // flat `App` node `f x y` it is today, not `((f x) y)`. A run anywhere but
1260        // the head never does — that is where the run is one grouped argument (§A).
1261        if i == 0 {
1262            match *it {
1263                Item::Run(members) => {
1264                    // The run still has no reading if it is `&`-headed or float-like
1265                    // (§C / §G) — flattening the head must not smuggle it past the
1266                    // guard the argument lowering applies.
1267                    check_run_head(members)?;
1268                    // `#Self(x)` — construction glued to its payload: the run's
1269                    // head member is the `#`-prefixed keyword (`Run[#Self, (x)]`),
1270                    // so it contributes the constructor leaf and the members
1271                    // after it the payload. Reads as the spaced `#Self x` below.
1272                    if is_self_head(members) {
1273                        args.push(Expr::Home {
1274                            leaf: HomeLeaf::New,
1275                            depth: 0,
1276                        });
1277                        for m in members[1..].iter() {
1278                            expand_arg(m, &mut args)?;
1279                        }
1280                    } else {
1281                        for m in members.iter() {
1282                            expand_arg(m, &mut args)?;
1283                        }
1284                    }
1285                }
1286                // `#Self x` — construction heading the spine: the constructor leaf
1287                // becomes the callee and the payload(s) follow as ordinary spine
1288                // items. A `#Self` anywhere else is `ReservedLiteralForm` (via
1289                // `parse_aexpr`), and a bare one — the loop's single item, refused
1290                // below — is too: construction is a form, not a value, so it may
1291                // only be applied, never bound or handed out. A module that means
1292                // to give construction away writes the eta-expansion
1293                // `mint = &n #Self(n)`.
1294                Item::Prefixed { sigil: '#', item }
1295                    if matches!(item.as_ref(), Item::Sym("Self")) =>
1296                {
1297                    args.push(Expr::Home {
1298                        leaf: HomeLeaf::New,
1299                        depth: 0,
1300                    });
1301                }
1302                _ => expand_arg(it, &mut args)?,
1303            }
1304        } else {
1305            expand_arg(it, &mut args)?;
1306        }
1307    }
1308    // `#Self` with nothing after it — the constructor leaf alone, which the
1309    // grammar above never leaves standing as a value.
1310    if matches!(
1311        args.as_slice(),
1312        [Expr::Home {
1313            leaf: HomeLeaf::New,
1314            ..
1315        }]
1316    ) {
1317        return Err(ParseError::ReservedLiteralForm);
1318    }
1319    // `items` is non-empty and every item yields at least one arg (a `()` group
1320    // yields the unit), so `args` is non-empty. A single arg is not an
1321    // application; two or more flatten into one spine (callee then arguments).
1322    Ok(fold_spine(args))
1323}
1324
1325/// Fold a non-empty argument list into an expression: the lone element itself if
1326/// there is one, else a flattened `App` spine (element 0 the callee).
1327fn fold_spine(args: Vec<Expr>) -> Expr {
1328    if args.len() == 1 {
1329        args.into_iter().next().unwrap()
1330    } else {
1331        app(args)
1332    }
1333}
1334
1335/// Is this run the glued `#Self(…)` construction — i.e. headed by the
1336/// `#`-prefixed `Self` keyword as its first member? (`#Self(5)` fuses into
1337/// `Run[#Self, (5)]`; the spaced `#Self 5` is two chain items and never a run.)
1338fn is_self_head(members: &[Item<'_>]) -> bool {
1339    matches!(
1340        members.first(),
1341        Some(Item::Prefixed { sigil: '#', item })
1342            if matches!(item.as_ref(), Item::Sym("Self"))
1343    )
1344}
1345
1346/// Expand one chain item into argument expressions. A `(…)` group spreads its
1347/// chains (see `expand_group_args`); any other item is a single atom.
1348fn expand_arg<'a>(item: &'a Item<'a>, out: &mut Vec<Expr>) -> Result<(), ParseError> {
1349    match item {
1350        Item::Parens(seq) => expand_group_args(seq, out),
1351        // `.Self` — the payload read, a postfix over everything applied so far:
1352        // `f x .Self` is `(f x).Self`, the same left-associative step a `.N`
1353        // projection is (so a member reached by dot can no longer be named
1354        // `Self`). Bare — nothing accumulated before it — it stays the symbol
1355        // literal `.Self`: `parse_dot_symbol` never consults the keywords.
1356        Item::Prefixed { sigil: '.', item }
1357            if matches!(item.as_ref(), Item::Sym("Self")) && !out.is_empty() =>
1358        {
1359            let acc = fold_spine(core::mem::take(out));
1360            out.push(Expr::App(Box::new([
1361                Expr::Home {
1362                    leaf: HomeLeaf::Value,
1363                    depth: 0,
1364                },
1365                acc,
1366            ])));
1367            Ok(())
1368        }
1369        _ => {
1370            out.push(parse_aexpr(item)?);
1371            Ok(())
1372        }
1373    }
1374}
1375
1376/// Expand a `(…)` group's chains as arguments, on the rule **one chain → one
1377/// argument** (grouping is the one-chain case). An *empty* group is the nullary
1378/// marker: it feeds one unit value `()` (an empty call still calls). Comment-only
1379/// chains are skipped and do not count toward emptiness.
1380fn expand_group_args<'a>(seq: &Seq<'a>, out: &mut Vec<Expr>) -> Result<(), ParseError> {
1381    let mut any = false;
1382    for items in chain_slices(seq) {
1383        out.push(parse_items(&items)?);
1384        any = true;
1385    }
1386    if !any {
1387        out.push(Expr::Unit); // nullary marker → the unit value ()
1388    }
1389    Ok(())
1390}
1391
1392/// Parse a `(…)` group appearing as a standalone atom: expand it (grouping /
1393/// spread / nullary) and fold the pieces into one expression.
1394fn parse_group<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1395    let mut args: Vec<Expr> = Vec::new();
1396    expand_group_args(seq, &mut args)?;
1397    // `expand_group_args` always yields ≥1 arg (an empty group feeds unit `()`).
1398    Ok(fold_spine(args))
1399}
1400
1401/// Parse a `[…]` list into a runtime list literal of *any* arity: `[]` (the
1402/// empty list, not unit), `[e]` (a genuine 1-list, not collapsed), `[e0, e1, …]`.
1403/// Each non-comment chain is one element expression.
1404fn parse_list<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1405    let mut elems: Vec<Expr> = Vec::new();
1406    for items in chain_slices(seq) {
1407        elems.push(parse_items(&items)?);
1408    }
1409    Ok(Expr::List(elems))
1410}
1411
1412/// Parse a `{…}` brace group as a **block expression** (syntax v1 step 6): a
1413/// sequence of clauses whose value is the last clause's. A block lowers onto the
1414/// same nested-abstraction chain a `let` group produces — `{ a = va, e }` is
1415/// `let (a = va) e`, i.e. `(&a e) va` — so it adds no evaluator node, and a
1416/// block *is* a scope because each binding becomes a lambda parameter that the
1417/// following clauses (and nothing outside) can see.
1418///
1419/// - The empty block `{}` is the unit value `()`, and `{ e }` is `e` — the
1420///   degenerate-case identities.
1421/// - Each **non-final** clause is a binding (`<pat> = <expr>`, a top-level `=`,
1422///   bound over the rest) or a bare expression (evaluated for its effect, its
1423///   value discarded — lowered to a `_ =` binding, today's `_ = io.print(…)`).
1424/// - The **final** clause is the block's value and must be an expression; a
1425///   trailing binding is [`BlockTrailingBinding`](ParseError::BlockTrailingBinding).
1426///
1427/// A `:`-clause (a stale bare-map entry) has no reading in a block and is refused
1428/// with the `#{…}` nudge, as it is in every brace position. Clauses are the
1429/// `<seq>`'s joined non-comment chains, so a block may be laid out multi-line
1430/// with blank lines, comments, and the chain-continuation joins.
1431fn parse_block<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1432    // A `:`-clause is the one shape a block shares with the old bare map; keep
1433    // the pointed error rather than letting it fall through as a bad expression.
1434    if braces_contain_colon(seq) {
1435        return Err(ParseError::BareMapLiteral);
1436    }
1437    build_block(chain_slices(seq))
1438}
1439
1440/// Build an [`Expr::Block`] from a block's (already joined, comment-free) clause
1441/// chains — shared by the `{…}` block and by `let (…) e` (its group's binding
1442/// chains plus its body as the final clause). Each clause becomes a
1443/// [`Clause::Bind`] (a chain with a top-level `=`) or a [`Clause::Do`] (a bare
1444/// expression). The degenerate blocks collapse rather than build a node: an empty
1445/// clause list is the unit value `()`, and a single expression clause is that
1446/// expression (`{ e }` is `e`). A trailing binding is rejected — a block's value
1447/// is its last clause's, and a binding leaves none (and its name would not
1448/// escape) — so the final clause is always a `Do`.
1449fn build_block<'a>(clauses: Vec<Vec<&Item<'a>>>) -> Result<Expr, ParseError> {
1450    let Some((last, leading)) = clauses.split_last() else {
1451        return Ok(Expr::Unit); // `{}` / `let () e` with no body chain — unit
1452    };
1453    if clause_is_binding(last) {
1454        return Err(ParseError::BlockTrailingBinding);
1455    }
1456    if leading.is_empty() {
1457        return parse_items(last); // `{ e }` is `e`
1458    }
1459    let mut built: Vec<Clause> = Vec::with_capacity(leading.len() + 1);
1460    for ci in leading {
1461        if clause_is_binding(ci) {
1462            let (pat, value) = parse_binding(ci)?;
1463            built.push(Clause::Bind(pat, value));
1464        } else {
1465            built.push(Clause::Do(parse_items(ci)?));
1466        }
1467    }
1468    built.push(Clause::Do(parse_items(last)?));
1469    Ok(Expr::Block(built.into_boxed_slice()))
1470}
1471
1472/// Whether a block clause is a **binding** — it carries a top-level `=` punct.
1473/// A nested `=` lives inside a `(…)`/`[…]`/`{…}` item, never as a top-level
1474/// `Item::Punct`, so this is exactly the `<binding>` vs `<chain>` split the
1475/// `let` group makes (and [`parse_binding`] then splits at the first `=`).
1476fn clause_is_binding(items: &[&Item<'_>]) -> bool {
1477    items.iter().any(|it| matches!(it, Item::Punct("=")))
1478}
1479
1480/// Parse a `{…}` block into a map literal: each non-comment chain of the `<seq>`
1481/// is one entry (see `parse_entry`); comment-only chains are skipped (so a map may
1482/// be laid out multi-line with blank lines and comments). Entry order is source
1483/// order — evaluation re-sorts by the value order.
1484fn parse_map<'a>(seq: &Seq<'a>) -> Result<Expr, ParseError> {
1485    let mut entries: Vec<(Expr, Expr)> = Vec::new();
1486    for items in chain_slices(seq) {
1487        entries.push(parse_entry(&items)?);
1488    }
1489    Ok(Expr::Map(entries))
1490}
1491
1492/// Parse one map entry `<key> ":" <value>`. The `:` is a single `<punct>` item at
1493/// position 1; the key is the single head item (`parse_key`) and the value is the
1494/// rest of the chain parsed as an `<expr>`. An *empty* value tail is sugar for the
1495/// unit value `()` (so `{ .foo: }` == `{ .foo: () }`).
1496fn parse_entry<'a>(items: &[&Item<'a>]) -> Result<(Expr, Expr), ParseError> {
1497    if items.len() < 2 || !matches!(items[1], Item::Punct(":")) {
1498        return Err(ParseError::MalformedMapKey);
1499    }
1500    let key = parse_key(items[0])?;
1501    let value_items = &items[2..];
1502    let value = if value_items.is_empty() {
1503        Expr::Unit // empty rest → the unit value ()
1504    } else {
1505        parse_items(value_items)?
1506    };
1507    Ok((key, value))
1508}
1509
1510/// Parse a map-entry key item. A bare *non-digit* atom key is the **symbol** of
1511/// the same spelling — `one` == `.one` — mirroring `.foo` exactly; a key is just
1512/// a token. A **digit-leading** bare key (`0`, `5`) is rejected to avoid the
1513/// number/symbol confusion: write `.0` for the symbol or `(0)` for the integer.
1514/// Wrap a key as a `(…)` **computed** key to evaluate it: `(one)` is the *value*
1515/// of variable `one`, `(0)` the integer `0`; an *empty* `()` is the unit key.
1516/// The remaining forms are a `.`-prefixed symbol (`.one`) and a `#[…]` / `#{…}`
1517/// literal key (`#[]` the empty-list key). A bare `[…]` / `{…}` key routes to the
1518/// same place only to be refused with its `#`-spelling nudge (`BareListLiteral` /
1519/// `BareMapLiteral`); anything else is a `MalformedMapKey`.
1520fn parse_key<'a>(item: &Item<'a>) -> Result<Expr, ParseError> {
1521    match item {
1522        // A bare non-digit `<sym>` key is the symbol of that spelling, read
1523        // exactly as `.<sym>` would be. A digit-leading key is rejected — write
1524        // `.5` (the symbol) or `(5)` (the computed integer key) to disambiguate.
1525        Item::Sym(s) if s.as_bytes()[0].is_ascii_digit() => Err(ParseError::MalformedMapKey),
1526        Item::Sym(s) => Ok(Expr::Symbol(text(s))),
1527        // A bare `{…}` in key position is a block expression at the atom layer,
1528        // which has no reading as a key — refuse it with the `#{…}` nudge rather
1529        // than silently keying on a block value (write `#{…}` for a map key).
1530        Item::Braces(_) => Err(ParseError::BareMapLiteral),
1531        // `.sym` symbol key, `(…)` computed key (empty `()` the unit key), `"…"`
1532        // string key, and the explicit `#[…]` / `#{…}` literal keys — each reads
1533        // exactly as the same item would at atom position. A bare `[…]` routes
1534        // here too, only to be refused with its `#[…]`-spelling nudge.
1535        Item::Prefixed { sigil: '.', .. }
1536        | Item::Prefixed { sigil: '#', .. }
1537        | Item::Brackets(_)
1538        | Item::Parens(_)
1539        | Item::Str(_) => parse_aexpr(item),
1540        // A **path** key (`{ Mod.k: v }`) denotes a concrete value by reference,
1541        // so it is a lookup key alongside the `.sym` / bare-sym forms (§E).
1542        Item::Run(members) => parse_path(members).map_err(|_| ParseError::MalformedMapKey),
1543        _ => Err(ParseError::MalformedMapKey),
1544    }
1545}
1546
1547// ===========================================================================
1548// patterns
1549// ===========================================================================
1550
1551/// `&<pattern> body` — the one binding form every abstraction (and `let`, and a
1552/// `__match` clause) is built from. Greedily **collects** into one multi-parameter
1553/// [`Lambda`]: when `body` is *directly* another abstraction (a run of `&p0 &p1 …`,
1554/// or a curried binder group), `pat` is prepended to its head so the whole run
1555/// shares one code node with a known arity. Collection stops at the first
1556/// non-abstraction body (a `&` nested inside an application does not extend the
1557/// arity) and at 255 parameters — a head that would exceed the `applied: u8`
1558/// counter starts a fresh nested `Lambda` instead of overflowing.
1559fn abs(pat: Pattern, body: Expr) -> Expr {
1560    match body {
1561        Expr::Abs(lam) if lam.head.len() < 255 => {
1562            // Prepend `pat` to the (freshly built, so uniquely owned) inner head.
1563            let lam = Rc::try_unwrap(lam).unwrap_or_else(|rc| (*rc).clone());
1564            let mut head = Vec::with_capacity(lam.head.len() + 1);
1565            head.push(pat);
1566            head.extend(Vec::from(lam.head));
1567            Expr::Abs(Rc::new(Lambda {
1568                head: head.into_boxed_slice(),
1569                body: lam.body,
1570                // Free variables are a resolution artifact: the parser leaves the
1571                // capture plan unresolved and `resolve.rs` fills it in.
1572                captures: Captures::Unresolved,
1573            }))
1574        }
1575        _ => Expr::Abs(Rc::new(Lambda {
1576            head: Box::new([pat]),
1577            body,
1578            captures: Captures::Unresolved,
1579        })),
1580    }
1581}
1582
1583/// Build an application node from a flat call spine `[callee, arg0, …]` (≥ 2
1584/// elements). A single-element `items` is *not* an application — the caller
1585/// returns that element directly.
1586fn app(items: Vec<Expr>) -> Expr {
1587    debug_assert!(
1588        items.len() >= 2,
1589        "an application has a callee and ≥1 argument"
1590    );
1591    Expr::App(items.into_boxed_slice())
1592}
1593
1594/// Apply `f` to one more `arg`, extending `f`'s call spine when it already is one
1595/// so `g x &y …` and `x |> f a` stay single flattened `App` nodes (a partial
1596/// applied to a trailing abstraction / pipe operand). Currying makes this
1597/// equivalent to a fresh binary application, and it re-renders identically.
1598fn append_arg(f: Expr, arg: Expr) -> Expr {
1599    match f {
1600        Expr::App(items) => {
1601            let mut v = Vec::from(items);
1602            v.push(arg);
1603            Expr::App(v.into_boxed_slice())
1604        }
1605        _ => Expr::App(Box::new([f, arg])),
1606    }
1607}
1608
1609/// Whether `item` is a bare **matching literal** — a number, a `.`-symbol, or a
1610/// string — used where a whole `&`-header or `let` LHS is expected. These are
1611/// equality patterns, not binders, so a bare one reads as a stray value or an
1612/// intended binding; Elly requires the wrap (`&(42)`, `&(.foo)`, `&("s")`; see
1613/// `BareLiteralHeader`). Nested in `[…]`/`{…}`/`(…)`/an or-pattern they are fine.
1614fn is_bare_literal_header(item: &Item<'_>) -> bool {
1615    match item {
1616        Item::Sym(s) => parse_int(s).is_some(),
1617        Item::Prefixed { sigil: '.', .. } => true,
1618        Item::Str(_) => true,
1619        _ => false,
1620    }
1621}
1622
1623/// Build the abstraction(s) an `&`-header `&<inner>` introduces over `body`. A
1624/// `(…)` inner is a **binder group** whose commas curry (`&(a, b)` → two params);
1625/// any other inner is a single-parameter pattern.
1626fn build_binder<'a>(inner: &Item<'a>, body: Expr) -> Result<Expr, ParseError> {
1627    match inner {
1628        Item::Parens(seq) => build_group(seq, body),
1629        // A bare matching literal (number `&42`, symbol `&.foo`, string `&"s"`) as
1630        // the whole header must be parenthesized: `&(42)`, `&(.foo)`, `&("s")` (see
1631        // `BareLiteralHeader`). Nested literals reach `parse_atom_item` unaffected.
1632        it if is_bare_literal_header(it) => Err(ParseError::BareLiteralHeader),
1633        other => {
1634            let pat = parse_pattern(&[other])?;
1635            Ok(abs(pat, body))
1636        }
1637    }
1638}
1639
1640/// Build a curried binder group `&(p0, p1, …)`: each non-comment chain is one
1641/// parameter pattern, curried left-to-right (leftmost outermost). An empty group
1642/// `&()` is the nullary marker — one param, the unit pattern that matches `()`.
1643fn build_group<'a>(seq: &Seq<'a>, body: Expr) -> Result<Expr, ParseError> {
1644    let params = chain_slices(seq);
1645    if params.is_empty() {
1646        // `&()` → the unit pattern: assert the argument is unit `()`.
1647        return Ok(abs(Pattern::Unit, body));
1648    }
1649    let mut acc = body;
1650    for chain in params.iter().rev() {
1651        let pat = parse_pattern(chain)?;
1652        acc = abs(pat, acc);
1653    }
1654    Ok(acc)
1655}
1656
1657/// Parse `case <subject>? { arms }`. The subject is exactly one item (atom, group, run)
1658/// or empty. Subjectless `case { ... }` is lowered to `&__arg (case __arg { ... })`
1659/// as an [`Expr::Abs`] using the reserved name `__arg`.
1660fn parse_case<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
1661    // Gather the pieces after the `case` keyword: a glued run head contributes
1662    // its tail members, then the following chain items, forming `[subject?, {…}]`.
1663    let mut rest: Vec<&Item<'a>> = Vec::new();
1664    if let Item::Run(members) = items[0] {
1665        for m in &members[1..] {
1666            rest.push(m);
1667        }
1668    }
1669    rest.extend_from_slice(&items[1..]);
1670    // The arm block is the trailing `{…}`; what precedes it is the subject.
1671    let (last, subject_items) = rest.split_last().ok_or(ParseError::CaseMissingArms)?;
1672    let arms_seq = match last {
1673        Item::Braces(seq) => seq,
1674        _ => return Err(ParseError::CaseMissingArms),
1675    };
1676    let cases = parse_arms(arms_seq)?;
1677    match subject_items {
1678        // Subjectless: lower to `&__arg (case __arg { … })`.
1679        [] => {
1680            let subject = Rc::new(Expr::Name(text("__arg")));
1681            Ok(abs(
1682                Pattern::Bind(text("__arg")),
1683                Expr::Case { subject, cases },
1684            ))
1685        }
1686        // `case <subject> { … }` — the subject is exactly one item, an atom.
1687        [subj] => Ok(Expr::Case {
1688            subject: Rc::new(parse_aexpr(subj)?),
1689            cases,
1690        }),
1691        _ => Err(ParseError::CaseSubjectExtra),
1692    }
1693}
1694
1695/// Parse `case` arms from a block. One arm per non-comment chain.
1696/// Empty blocks raise [`CaseMissingArms`].
1697fn parse_arms<'a>(seq: &Seq<'a>) -> Result<Box<[(Pattern, Expr)]>, ParseError> {
1698    let chains = chain_slices(seq);
1699    if chains.is_empty() {
1700        return Err(ParseError::CaseMissingArms);
1701    }
1702    let mut arms = Vec::with_capacity(chains.len());
1703    for chain in &chains {
1704        arms.push(parse_arm(chain)?);
1705    }
1706    Ok(arms.into_boxed_slice())
1707}
1708
1709/// Parse an arm: `& <pattern> (when (<cond>))* <body>`.
1710/// Header must start with `&`. Guards are folded using [`peel_when`].
1711fn parse_arm<'a>(items: &[&Item<'a>]) -> Result<(Pattern, Expr), ParseError> {
1712    let inner = match items.first() {
1713        Some(Item::Prefixed { sigil: '&', item }) => item.as_ref(),
1714        _ => return Err(ParseError::ArmNotBinder),
1715    };
1716    // A bare matching literal needs parens, as everywhere a header does.
1717    if is_bare_literal_header(inner) {
1718        return Err(ParseError::BareLiteralHeader);
1719    }
1720    let base = parse_pattern(&[inner])?;
1721    let (pat, rest) = peel_when(base, &items[1..])?;
1722    if rest.is_empty() {
1723        return Err(ParseError::ArmMissingBody);
1724    }
1725    let body = parse_items(rest)?;
1726    Ok((pat, body))
1727}
1728
1729/// Fold `when (<cond>)` guards onto `base`. Guards are nested as [`Pattern::When`]
1730/// and tried in order. Conditions must be `(…)` groups.
1731fn peel_when<'a, 'b>(
1732    mut base: Pattern,
1733    mut items: &'b [&'b Item<'a>],
1734) -> Result<(Pattern, &'b [&'b Item<'a>]), ParseError> {
1735    while !items.is_empty() && is_when_kw(items[0]) {
1736        let (group, next): (&Seq<'a>, &[&Item<'a>]) = match items[0] {
1737            // Glued run `when(…)`: the group is the run's lone second member.
1738            Item::Run(members) if members.len() == 2 => match &members[1] {
1739                Item::Parens(seq) => (seq, &items[1..]),
1740                _ => return Err(ParseError::WhenMissingCond),
1741            },
1742            Item::Run(_) => return Err(ParseError::WhenMissingCond),
1743            // Bare `when`: the group is the next chain item.
1744            _ => match items.get(1) {
1745                Some(Item::Parens(seq)) => (seq, &items[2..]),
1746                _ => return Err(ParseError::WhenMissingCond),
1747            },
1748        };
1749        let cond = parse_when_cond(group)?;
1750        base = Pattern::When {
1751            inner: Box::new(base),
1752            when: cond,
1753        };
1754        items = next;
1755    }
1756    Ok((base, items))
1757}
1758
1759/// Is `item` the `when` guard keyword — bare, or a glued run head (§D)?
1760fn is_when_kw(item: &Item) -> bool {
1761    head_keyword(item) == Some("when")
1762}
1763
1764/// Parse a `when` condition. Plain guards `(<chain>)` use `None`; pattern guards
1765/// `(<pattern> = <chain>)` use `Some(pattern)`.
1766fn parse_when_cond<'a>(seq: &Seq<'a>) -> Result<(Option<Box<Pattern>>, Expr), ParseError> {
1767    let chain = single_chain(seq).ok_or(ParseError::WhenMissingCond)?;
1768    if chain.is_empty() {
1769        return Err(ParseError::WhenMissingCond);
1770    }
1771    if chain.iter().any(|it| matches!(it, Item::Punct("="))) {
1772        let (pat, expr) = parse_binding(&chain)?;
1773        Ok((Some(Box::new(pat)), expr))
1774    } else {
1775        Ok((None, parse_items(&chain)?))
1776    }
1777}
1778
1779/// The non-comment chains of a `<seq>`, each as its non-comment items, with the
1780/// [chain-joining pass](join_chains) applied.
1781fn chain_slices<'a, 'b>(seq: &'b Seq<'a>) -> Vec<Vec<&'b Item<'a>>> {
1782    let chains = seq
1783        .0
1784        .iter()
1785        .filter(|c| !is_comment_only(c))
1786        .map(|c| {
1787            c.0.iter()
1788                .filter(|it| !matches!(it, Item::Comm(_)))
1789                .collect()
1790        })
1791        .collect();
1792    join_chains(chains)
1793}
1794
1795/// Whether a `<braces>` seq contains any chain whose second non-comment item is
1796/// `:` — the shape a block shares with the old bare-map spelling. Such a group is
1797/// rejected with the `#{…}` nudge ([`parse_block`]); a brace group with no
1798/// `:`-clause is a block (step 6).
1799fn braces_contain_colon(seq: &Seq<'_>) -> bool {
1800    for chain in seq.0.iter().filter(|c| !is_comment_only(c)) {
1801        let items: Vec<&Item<'_>> = chain
1802            .0
1803            .iter()
1804            .filter(|it| !matches!(it, Item::Comm(_)))
1805            .collect();
1806        if items.len() >= 2 && matches!(items[1], Item::Punct(":")) {
1807            return true;
1808        }
1809    }
1810    false
1811}
1812
1813/// The chain-joining pass (`docs/todo/elly-syntax-v1.md` step 2, review §1): over
1814/// a `<seq>`'s non-comment chains, fold a chain into the one before it whenever
1815/// the pair reads as one continued expression that is *ill-formed as two separate
1816/// chains today* — so no existing program changes meaning. A chain joins onto its
1817/// predecessor when
1818///
1819/// - the predecessor ends with a trailing continuation — a `&`-binder header
1820///   (`AbsWithoutBody`), the punct `=` (`MalformedBinding`), the punct `|>`, or a
1821///   `when (…)` guard with no body — or
1822/// - the chain itself opens with a leading continuation — the punct `|>`, or one
1823///   of the continuation keywords `elif` / `else` / `catch`.
1824///
1825/// Joins cascade: a folded chain is re-examined, so `x =` ⏎ `&y` ⏎ `body` collapses
1826/// to the single chain `x = &y body`. A leading continuation at the very start of a
1827/// seq has no predecessor and is left alone (it fails downstream as it does today).
1828fn join_chains<'a, 'b>(chains: Vec<Vec<&'b Item<'a>>>) -> Vec<Vec<&'b Item<'a>>> {
1829    let mut out: Vec<Vec<&'b Item<'a>>> = Vec::with_capacity(chains.len());
1830    for chain in chains {
1831        let join = matches!(out.last(), Some(prev) if trailing_join(prev) || leading_join(&chain));
1832        if join {
1833            // `out` is non-empty because `join` implies a predecessor.
1834            out.last_mut().unwrap().extend(chain);
1835        } else {
1836            out.push(chain);
1837        }
1838    }
1839    out
1840}
1841
1842/// Whether a chain ends with a trailing continuation (see [`join_chains`]).
1843fn trailing_join(chain: &[&Item]) -> bool {
1844    match chain.last() {
1845        Some(Item::Prefixed { sigil: '&', .. }) => true,
1846        Some(Item::Punct("=")) | Some(Item::Punct("|>")) => true,
1847        // A `when (…)` guard with no body: the arm's body is on the next line.
1848        _ => {
1849            let n = chain.len();
1850            n >= 2
1851                && matches!(chain[n - 2], Item::Sym("when"))
1852                && matches!(chain[n - 1], Item::Parens(_))
1853        }
1854    }
1855}
1856
1857/// Whether a chain opens with a leading continuation (see [`join_chains`]).
1858fn leading_join(chain: &[&Item]) -> bool {
1859    match chain.first() {
1860        Some(Item::Punct("|>")) => true,
1861        Some(first) => matches!(head_keyword(first), Some("elif" | "else" | "catch")),
1862        None => false,
1863    }
1864}
1865
1866/// The single non-comment chain of a `<seq>` (its non-comment items), or `None`
1867/// if the seq holds zero or more than one chain (e.g. a comma-separated group).
1868fn single_chain<'a, 'b>(seq: &'b Seq<'a>) -> Option<Vec<&'b Item<'a>>> {
1869    let mut chains = chain_slices(seq).into_iter();
1870    let first = chains.next()?;
1871    if chains.next().is_some() {
1872        return None;
1873    }
1874    Some(first)
1875}
1876
1877/// Parse a pattern from a chain of items. Splits on the first top-level `|`
1878/// (or-pattern), then `=` (equality / at-pattern), else defers to the `as`/atom
1879/// levels.
1880fn parse_pattern<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
1881    if items.is_empty() {
1882        return Err(ParseError::BadPattern);
1883    }
1884    // `|` has the lowest precedence — split first so `=` and `as` bind tighter
1885    // inside each arm. Left-associative on multiple `|`.
1886    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Punct("|"))) {
1887        let left = parse_pattern(&items[..idx])?;
1888        let right = parse_pattern(&items[idx + 1..])?;
1889        // Both arms must bind the same names in the same order (see
1890        // `ParseError::OrBindersMismatch`); that agreement is checked by the
1891        // resolver, which has the binders' de Bruijn order naturally.
1892        return Ok(Pattern::Or(Box::new(left), Box::new(right)));
1893    }
1894    // Prefix ordering comparators `< <ref>` / `> <ref>` (the rest is an atom
1895    // comparand, like `= <ref>`); they only appear at the head of a pattern.
1896    match items[0] {
1897        Item::Punct("<") => return Ok(Pattern::Less(parse_comparand(&items[1..])?)),
1898        Item::Punct(">") => return Ok(Pattern::Greater(parse_comparand(&items[1..])?)),
1899        _ => {}
1900    }
1901    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Punct("="))) {
1902        if idx == 0 {
1903            // `= <ref>` — equality against an atom (name or literal).
1904            return Ok(Pattern::Equal(parse_comparand(&items[1..])?));
1905        }
1906        // `name = pat` — at-pattern (the rest is a *pattern*).
1907        let name = at_name(&items[..idx])?;
1908        let inner = parse_pattern(&items[idx + 1..])?;
1909        return Ok(Pattern::At(name, Box::new(inner)));
1910    }
1911    parse_as(items)
1912}
1913
1914/// Parse the right-hand side of an `=` / `<` / `>` comparand: exactly one
1915/// **atom** — a name (`= x`), an integer (`= 5`), or a symbol (`= .foo`). A
1916/// pattern is a *closed grammar*, so a comparand never recurses into the general
1917/// expression parser (`parse_items`); a compound `= f x` / `< (f x)` is rejected
1918/// (`ComparandNotAtom`). The atom keeps the ordinary expression default at match
1919/// time — a name is a lookup — which is the only non-literal a match evaluates.
1920fn parse_comparand<'a>(items: &[&Item<'a>]) -> Result<Expr, ParseError> {
1921    match items {
1922        [it] => match it {
1923            Item::Sym(s) => parse_sym(s),
1924            Item::Prefixed { sigil: '.', item } => parse_dot_symbol(item),
1925            Item::Str(s) => Ok(Expr::Str(decode_str(s)?)),
1926            // A **path** (`Bool.false`) is one atom denoting a value by reference
1927            // — a lookup, not a computation, so it is a legitimate comparand (§E).
1928            Item::Run(members) => parse_path(members),
1929            _ => Err(ParseError::ComparandNotAtom),
1930        },
1931        _ => Err(ParseError::ComparandNotAtom),
1932    }
1933}
1934
1935/// The bound name of an at-pattern's left-hand side: exactly one plain name (not
1936/// a keyword, a `__` name, a discard, or a number).
1937fn at_name<'a>(items: &[&Item<'a>]) -> Result<Text, ParseError> {
1938    plain_name(items, ParseError::BadPattern)
1939}
1940
1941/// The narrowing level: the `Self <pat>` unwrapping qualifier, the prefix
1942/// `(as <Proto>) <pat>`, the postfix `<pat> as <Proto>`, or a bare atom.
1943fn parse_as<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
1944    // `Self <pat>` — narrow to an object of the home module and match its
1945    // payload. It qualifies whatever follows, so the rest is a whole pattern and
1946    // `Self x as Int` reads as `Self (x as Int)`. It is a `<pat>`, not a
1947    // top-level `<pattern>`, so a binder has to parenthesize it: `&(Self inner) …`
1948    // is valid, `&Self inner …` is not (`&` prefixes one item, so the latter
1949    // never reaches here with an inner at all).
1950    if let Item::Sym("Self") = items[0] {
1951        if items.len() < 2 {
1952            // Nothing to unwrap *into*. `Self _` is how to say "an object of
1953            // mine, payload ignored" — spelled out, since a bare `Self` in a
1954            // binder is much more likely a slip.
1955            return Err(ParseError::BadPattern);
1956        }
1957        let inner = parse_pattern(&items[1..])?;
1958        return Ok(Pattern::Unwrap {
1959            depth: 0,
1960            inner: Box::new(inner),
1961        });
1962    }
1963    // Prefix: `(as <Proto>) <pat>` — items[0] is a `(as …)` group.
1964    if let Item::Parens(seq) = items[0] {
1965        if let Some(chain) = single_chain(seq) {
1966            if !chain.is_empty() && matches!(chain[0], Item::Sym("as")) {
1967                let proto = parse_proto_ref(&chain[1..])?;
1968                let inner = parse_pattern(&items[1..])?;
1969                return Ok(Pattern::Type(proto, Box::new(inner)));
1970            }
1971        }
1972    }
1973    // Postfix: `<atom> as <Proto>`.
1974    if let Some(idx) = items.iter().position(|it| matches!(it, Item::Sym("as"))) {
1975        let proto = parse_proto_ref(&items[idx + 1..])?;
1976        let inner = parse_atom(&items[..idx])?;
1977        return Ok(Pattern::Type(proto, Box::new(inner)));
1978    }
1979    parse_atom(items)
1980}
1981
1982/// Parse the prototype of an `as <Proto>` pattern: exactly one **name**. A
1983/// prototype is a value like any other now — `Int` is the module `__Int` under its
1984/// shadowable alias, not a keyword — so this is the same closed-grammar atom an
1985/// `=` comparand is, minus the literals: a module is named, never written out.
1986///
1987/// It stays a [`ProtoRef::Ref`] here even when the name is the `__`-spelling of a
1988/// builtin module. The alias spelling only becomes one at *resolve* (it is the
1989/// resolver's bottom tier), so folding both to the compiled [`ProtoRef::Kind`]
1990/// form belongs there, in one place.
1991fn parse_proto_ref(items: &[&Item<'_>]) -> Result<ProtoRef, ParseError> {
1992    let e = match items {
1993        [Item::Sym(s)] => parse_sym(s)?,
1994        // A **path** names a qualified prototype (`as Mod.Proto`), lowered by the
1995        // shared `parse_path` (§E).
1996        [Item::Run(members)] => parse_path(members)?,
1997        _ => return Err(ParseError::UnknownType),
1998    };
1999    match e {
2000        // A name (`Bool`, `Int`), a `__` builtin (`__Int`), `Self`, or a path
2001        // that lowers to a reference / application of references.
2002        e @ (Expr::Name(_) | Expr::Builtin(_) | Expr::Home { .. } | Expr::App(_)) => {
2003            Ok(ProtoRef::Ref(e))
2004        }
2005        // A literal: `as 5` names no module.
2006        _ => Err(ParseError::UnknownType),
2007    }
2008}
2009
2010/// Parse a **path**: a run whose head is an **identifier** and whose every
2011/// following atom is a `.`-prefixed name-or-number segment (`Bool.false`,
2012/// `a.b.c`, `pair.0`, `__Int.add`). It lowers to a left fold of the head applied
2013/// to symbol literals — the same node member access and projection share — so it
2014/// is the reference the closed positions (a comparand, a map key, an `as <Proto>`)
2015/// accept by name.
2016///
2017/// The head admits a plain name, a `__` builtin, or a home leaf — references only.
2018/// A number head is excluded, which is what keeps `3.14` out (it is the rejected
2019/// float run, §G); a string or group head is a run but not a reference, so not a
2020/// path; a `(…)` call member anywhere is computation, not reference, and
2021/// disqualifies. A path is a run (≥ 2 members); the bare-name degenerate case is
2022/// the ordinary atom arm at the call site.
2023fn parse_path<'a>(members: &[Item<'a>]) -> Result<Expr, ParseError> {
2024    if members.len() < 2 {
2025        return Err(ParseError::ComparandNotAtom);
2026    }
2027    // The head is a reference: a plain name, a `__` builtin, or a home leaf.
2028    let head = match &members[0] {
2029        Item::Sym(s) => parse_sym(s)?,
2030        _ => return Err(ParseError::ComparandNotAtom),
2031    };
2032    // Every following member is a `.`-segment (a symbol or a number projection).
2033    for m in &members[1..] {
2034        if !matches!(m, Item::Prefixed { sigil: '.', .. }) {
2035            return Err(ParseError::ComparandNotAtom);
2036        }
2037    }
2038    // Left fold: the head applied to each `.`-segment's symbol in turn.
2039    let mut acc = head;
2040    for m in &members[1..] {
2041        if let Item::Prefixed { sigil: '.', item } = m {
2042            acc = Expr::App(Box::new([acc, parse_dot_symbol(item)?]));
2043        }
2044    }
2045    Ok(acc)
2046}
2047
2048/// Parse an atomic pattern: a single item (or a `(…)` grouping of one pattern).
2049fn parse_atom<'a>(items: &[&Item<'a>]) -> Result<Pattern, ParseError> {
2050    match items {
2051        [it] => parse_atom_item(it),
2052        _ => Err(ParseError::BadPattern),
2053    }
2054}
2055
2056/// Parse a single atomic-pattern item.
2057fn parse_atom_item<'a>(item: &Item<'a>) -> Result<Pattern, ParseError> {
2058    match item {
2059        Item::Sym("_") => Ok(Pattern::Discard),
2060        Item::Sym(s) if is_keyword(s) => Err(ParseError::ReservedKeyword),
2061        Item::Sym(s) if s.starts_with("__") => Err(ParseError::ReservedName),
2062        // A numeric literal is an equality pattern (`= n` sugar, mirroring the
2063        // `.sym → Equal(Symbol)` sugar below); a digit-leading non-integer is a
2064        // malformed number, and everything else is a binder name.
2065        Item::Sym(s) => match parse_int(s) {
2066            Some(n) => Ok(Pattern::Equal(Expr::Int(n))),
2067            None if s.as_bytes()[0].is_ascii_digit() => Err(ParseError::MalformedNumber),
2068            None => Ok(Pattern::Bind(text(s))),
2069        },
2070        // `.sym` literal → `= .sym`.
2071        Item::Prefixed { sigil: '.', item } => Ok(Pattern::Equal(parse_dot_symbol(item)?)),
2072        // A `<str>` literal is an equality pattern — the textual twin of `.sym`.
2073        Item::Str(s) => Ok(Pattern::Equal(Expr::Str(decode_str(s)?))),
2074        // Bare `[…]` / `{…}` in pattern position are always rejected (reshuffle):
2075        // write `#[…]` / `#{…}`. No `{…}` block exists in a pattern and the empty
2076        // `{}` is no longer the empty-map pattern, so every bare brace is refused —
2077        // exactly as every bare bracket is.
2078        Item::Brackets(_) => Err(ParseError::BareListLiteral),
2079        // `(…)` grouping — a single subpattern; commas (multiple chains) are an
2080        // error (they only curry after `&`).
2081        Item::Parens(seq) => match single_chain(seq) {
2082            Some(chain) if !chain.is_empty() => parse_pattern(&chain),
2083            _ => Err(ParseError::BadPattern),
2084        },
2085        Item::Braces(_) => Err(ParseError::BareMapLiteral),
2086        // `#[…]` / `#{…}` — the explicit spellings of the list / map patterns,
2087        // reading exactly as the bare forms (reshuffle phase 1); any other
2088        // `#`-prefixed shape is a reserved literal notation (§2).
2089        Item::Prefixed { sigil: '#', item } => match item.as_ref() {
2090            Item::Brackets(seq) => parse_list_pattern(seq),
2091            Item::Braces(seq) => parse_map_pattern(seq),
2092            _ => Err(ParseError::ReservedLiteralForm),
2093        },
2094        _ => Err(ParseError::BadPattern),
2095    }
2096}
2097
2098/// Parse a `{ k0: p0, … }` map pattern: each entry is `<key> ":" <value-pattern>`,
2099/// with an optional trailing `...`/`...rest`; `Rest::None` (no rest) is a
2100/// **closed** key set. The **key is itself a pattern**, but must denote a concrete
2101/// value to probe. A **lookup** key names a concrete value: `.sym` (the literal),
2102/// bare `sym` (the literal), or the equality pattern `(= <expr>)` / `= <expr>` for
2103/// a computed key. A grouped **binder** pattern in key position is instead a
2104/// **capture** — `(k)` / `(_)` peels the smallest remaining entry, binding its key
2105/// (see `match_pattern` in `eval.rs`); this is why the computed key is `(= k)`,
2106/// not `(k)`. The entry splits on its first top-level `:`.
2107fn parse_map_pattern<'a>(seq: &Seq<'a>) -> Result<Pattern, ParseError> {
2108    let chains = chain_slices(seq);
2109    let n = chains.len();
2110    let mut entries: Vec<(MapKey, Pattern)> = Vec::new();
2111    let mut rest: Option<Box<Pattern>> = None;
2112    for (i, chain) in chains.iter().enumerate() {
2113        if let Some(r) = as_rest(chain) {
2114            if i != n - 1 {
2115                return Err(ParseError::BadPattern); // rest must be trailing
2116            }
2117            rest = Some(r);
2118        } else {
2119            let colon = chain
2120                .iter()
2121                .position(|it| matches!(it, Item::Punct(":")))
2122                .ok_or(ParseError::MalformedMapKey)?;
2123            let val_items = &chain[colon + 1..];
2124            if colon == 0 || val_items.is_empty() {
2125                return Err(ParseError::MalformedMapKey); // empty key or value
2126            }
2127            // A bare single `<sym>` key is the literal symbol (like a map literal);
2128            // otherwise the key is a pattern — an equality form is a concrete-key
2129            // lookup, any other (binder) pattern is a key capture.
2130            let key_items = &chain[..colon];
2131            let bare_sym = match key_items {
2132                [Item::Sym(s)] => Some(*s),
2133                _ => None,
2134            };
2135            let key = if let Some(s) = bare_sym {
2136                // A bare non-digit sym key is the literal symbol (`five` → `.five`);
2137                // a digit-leading key is rejected — write `.5` (the symbol) or
2138                // `(5)` / `(= 5)` (the integer key) to disambiguate.
2139                if s.as_bytes()[0].is_ascii_digit() {
2140                    return Err(ParseError::MalformedMapKey);
2141                }
2142                MapKey::Lookup(Expr::Symbol(text(s)))
2143            } else if let [Item::Run(members)] = key_items {
2144                // A **path** key (`{ Mod.k: p }`) denotes a concrete value by
2145                // reference — a lookup, like the `.sym` / bare-sym forms (§E).
2146                MapKey::Lookup(parse_path(members)?)
2147            } else {
2148                match parse_pattern(key_items)? {
2149                    Pattern::Equal(expr) => MapKey::Lookup(expr),
2150                    kpat => MapKey::Capture(Box::new(kpat)),
2151                }
2152            };
2153            let valpat = parse_pattern(val_items)?;
2154            entries.push((key, valpat));
2155        }
2156    }
2157    Ok(Pattern::Map { entries, rest })
2158}
2159
2160/// Parse a `[…]` list pattern: fixed-arity element patterns with an optional
2161/// trailing `...`/`...rest`.
2162fn parse_list_pattern<'a>(seq: &Seq<'a>) -> Result<Pattern, ParseError> {
2163    let chains = chain_slices(seq);
2164    let n = chains.len();
2165    let mut elems: Vec<Pattern> = Vec::new();
2166    let mut rest: Option<Box<Pattern>> = None;
2167    for (i, chain) in chains.iter().enumerate() {
2168        if let Some(r) = as_rest(chain) {
2169            if i != n - 1 {
2170                return Err(ParseError::BadPattern); // rest must be trailing
2171            }
2172            rest = Some(r);
2173        } else {
2174            elems.push(parse_pattern(chain)?);
2175        }
2176    }
2177    Ok(Pattern::List { elems, rest })
2178}
2179
2180/// Recognize a trailing-rest element chain: `...rest` (a name) or `...` (anon).
2181/// Both lex as nested `.` sigils (three deep), so no new Muon token is needed.
2182fn as_rest<'a>(chain: &[&Item<'a>]) -> Option<Box<Pattern>> {
2183    if chain.len() != 1 {
2184        return None;
2185    }
2186    // `it = .( .( … ) )`: two `.` sigils, then either `.name` (→ a `Bind` over the
2187    // remainder) or a bare `.` punct (→ a `Discard` over it).
2188    if let Item::Prefixed {
2189        sigil: '.',
2190        item: a,
2191    } = chain[0]
2192    {
2193        if let Item::Prefixed {
2194            sigil: '.',
2195            item: b,
2196        } = a.as_ref()
2197        {
2198            match b.as_ref() {
2199                Item::Prefixed {
2200                    sigil: '.',
2201                    item: c,
2202                } => {
2203                    if let Item::Sym(name) = c.as_ref() {
2204                        return Some(Box::new(Pattern::Bind(text(name))));
2205                    }
2206                }
2207                Item::Punct(".") => return Some(Box::new(Pattern::Discard)),
2208                _ => {}
2209            }
2210        }
2211    }
2212    None
2213}
2214
2215/// Does this chain hold only comments (so it contributes no expression)?
2216fn is_comment_only(chain: &Chain) -> bool {
2217    chain.0.iter().all(|it| matches!(it, Item::Comm(_)))
2218}
2219
2220/// Is `s` an Elly keyword (reserved syntax, neither a reference nor bindable)?
2221/// `as` leads the type-narrowing pattern qualifier (recognized structurally
2222/// before this check); `Self` is the home module — a *reference* to it is the
2223/// module leaf (see `parse_sym`), and the reservation is what keeps no binder
2224/// from taking the name and what lets `Self <pat>` head the unwrap pattern;
2225/// `with`/`when`/`match`/`case`/`of` are parked for future forms (a parallel
2226/// binding, a guard, and the surface `case`/`match` sugar over the `__match`
2227/// builtin).
2228fn is_keyword(s: &str) -> bool {
2229    matches!(
2230        s,
2231        "Self"
2232            | "let"
2233            | "recur"
2234            | "import"
2235            | "const"
2236            | "local"
2237            | "with"
2238            | "as"
2239            | "when"
2240            | "match"
2241            | "case"
2242            | "of"
2243            | "if"
2244            | "elif"
2245            | "else"
2246            | "catch"
2247    )
2248}
2249
2250/// Can `name` be bound and then referenced from Elly source as a plain name? It
2251/// must be a Muon `<sym>` and pass the same tests `plain_name` applies to a
2252/// binding's left-hand side: not a keyword, not a `__` reserved name, not the
2253/// discard `_`, and not something that `reads_as_number` accepts.
2254///
2255/// This is the rule for names a *host* binds from outside a program — the PyO3
2256/// `elly.Env` prelude — where there is no left-hand side to parse but source must
2257/// still be able to read the binding back. Keep it in step with `plain_name`;
2258/// `crates/elly-core/tests/module.rs` pins the two together.
2259pub fn is_bindable_name(name: &str) -> bool {
2260    muon::is_sym(name)
2261        && !is_keyword(name)
2262        && !name.starts_with("__")
2263        && name != "_"
2264        && !reads_as_number(name)
2265}
2266
2267/// Is this item a chain split point — a `&`-binder, or a `let` / `recur` keyword
2268/// (bare or as a glued **run head**, §D)? A keyword run is a split point because
2269/// it is the keyword form reading its group from inside the run; `head_keyword`
2270/// covers the bare-sym case too, so no separate bare check is needed.
2271fn is_split_point(item: &Item) -> bool {
2272    is_binder(item)
2273        || matches!(
2274            head_keyword(item),
2275            Some("let") | Some("recur") | Some("case")
2276        )
2277}
2278
2279/// Is this item a `&`-prefixed binder?
2280fn is_binder(item: &Item) -> bool {
2281    matches!(item, Item::Prefixed { sigil: '&', .. })
2282}
2283
2284/// The **keyword head** of an item, if it is one: a bare keyword sym, or the head
2285/// sym of a **keyword-headed run** (§D). A keyword written glued to its group —
2286/// `let(x = 1) e`, `import"spec"`, `const(a, b)` — fuses into one run whose head
2287/// is the keyword, and the group comes from the run's second member rather than
2288/// the next chain item. Reading the head is what makes `let(x=1) e` equal
2289/// `let (x=1) e`.
2290fn head_keyword<'a>(item: &Item<'a>) -> Option<&'a str> {
2291    match item {
2292        Item::Sym(s) if is_keyword(s) => Some(s),
2293        Item::Run(members) => match members.first() {
2294            Some(Item::Sym(s)) if is_keyword(s) => Some(s),
2295            _ => None,
2296        },
2297        _ => None,
2298    }
2299}