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