Skip to main content

rustyfi_lang/v1/
lower.rs

1//! Structural CST-to-CST transcription: `cst_v1::ast` -> `cst::ast`.
2//!
3//! Rather than widen `elaborate.rs`'s ~30 expression-lowering helpers to walk a
4//! second node type, this converts a parsed [`cst_v1::FileV1`] into ordinary
5//! [`cst::TopBinding`]s / [`cst::ast::Expr`]; the caller assembles one
6//! synthetic [`cst::File`] — the shape `merge_program` already produces for
7//! 0.0.6 — and hands it to the untouched elaborate -> typecheck -> compile
8//! pipeline. Both sides import the same `rustyfi_syntax::leaf` token types, so
9//! most of this moves tokens rather than re-encoding them. The one non-1:1
10//! seam is `lower_cmd_tail`: `cst_v1` kept the older "one application-chain
11//! `Expr`" argument encoding, `cst.rs` has since moved to a flat `AppArg`
12//! list.
13//!
14//! What does NOT lower — a real [`LowerError`], never a panic:
15//!
16//! - a labeled-optional BUNDLE on an inline/block/math *command* parameter,
17//!   and `?(…)` at the TYPE level. Ordinary labeled-optional arguments and
18//!   parameter bundles do lower; the 0.0.6 `?:`/`?*` positional markers do not
19//!   exist under V0_1 (the lexer emits only `?` = `OptionalType`);
20//! - a module-qualified command name (`\Mod.cmd`/`+Mod.cmd`) in BINDING
21//!   position — the target field is a bare `HorzCmdTok`/`VertCmdTok`;
22//! - a type constructor applied to more than one argument: 0.0.6's
23//!   `cst::ast::TypeApp` is single-argument postfix, 0.1's prefix form is
24//!   n-ary, so `lower_type_app` accepts arity 0/1 and rejects arity >= 2;
25//! - a `:>` whose left side is neither a `struct … end` literal nor a bare
26//!   module name, and functors — both need a static module environment this
27//!   port does not build.
28//!
29//! **`type` names are pre-qualified here**, rewritten to their
30//! `qualify_key`-identical fully-qualified string (`"M.t"`) at three sites —
31//! the declared name, synonym/payload references, and applied-constructor
32//! heads — via `TypeNameEnv`. Constructor names stay unqualified. A dotted
33//! `M.t` reference is a FOURTH site with a different formula:
34//! `qualify_type_key` on the token's own `mods`/`name`, bypassing
35//! `TypeNameEnv::qualify`, because an absolute reference has nothing to
36//! resolve against the local module's names. Whether the result names a
37//! concrete type, an abstract stamp or an error is undecidable here — lowering
38//! runs before any seal table exists.
39//!
40//! **`sig_annot` lowers to NOTHING.** A sealed module produces the
41//! byte-identical `TopBinding::Module { sig: None, .. }` its unsealed twin
42//! does; width/depth matching, sealing and hiding are entirely
43//! `v1/module_check.rs`'s job, read back off the original `cst_v1` tree. The
44//! ordering is load-bearing: the struct BODY lowers first, so a body error
45//! surfaces with its own message whether or not the module carries a `:>` —
46//! pinned by `sig_annot_body_still_lowers_first`.
47//!
48//! **`include M` splices at the includer's OWN path**, its member copies
49//! unwrapped from `StructDecl` boxes — contrast `lower_module_alias`, which
50//! wraps them in a synthetic module at a lengthened path. Resolution is frozen
51//! at surface-build time (`v1/surface.rs`), so an unresolved, forward-
52//! referencing or self-including target is a precise error, never a panic.
53
54use crate::v1::functor;
55use crate::v1::surface::{self, ModSurface, SurfaceEnv};
56use rustyfi_syntax::cst;
57use rustyfi_syntax::cst_v1::{self, ast as ast_v1};
58use rustyfi_syntax::leaf::*;
59use rustyfi_syntax::Span;
60
61/// A 0.1 construct this port deliberately does not lower yet. A real user
62/// error (not a panic): points at the construct and why it isn't supported yet.
63#[derive(Debug, Clone, thiserror::Error)]
64#[error(
65    "{span}: SATySFi 0.1 construct not supported yet in this port's Slice 1: {construct} ({hint})"
66)]
67pub struct LowerError {
68    pub construct: &'static str,
69    pub hint: &'static str,
70    pub span: Span,
71}
72
73fn unsupported(construct: &'static str, hint: &'static str, span: Span) -> LowerError {
74    LowerError {
75        construct,
76        hint,
77        span,
78    }
79}
80
81/// The lowering-time
82/// RELATIVE-SIBLING absolutization rule — a [`functor::HeadRewrite`]
83/// impl sharing `v1/functor.rs`'s clone+rewrite walker with its functor-
84/// parameter substitution (the module doc comment's risk-6 "one head-splice"
85/// guard). Where [`functor::ParamSubstRewrite`] splices a parameter's
86/// argument path, this rewrite resolves a nested-sibling module head
87/// (`Impl.add`/`Console.scheme`, set.satyg's/code.satyh's shape) to its
88/// ABSOLUTE qualified path via an OUTWARD search against `surfaces` — the
89/// same search [`surface::resolve_module`] already performs for module-level
90/// `Var`/`Coerce`/`App` bodies (which is why THOSE sites never needed this
91/// fix: they already resolve outward at the surface-build pass). A head that
92/// already names itself (a top-level dependency, or an already-absolute
93/// path) resolves to the SAME bare segment and is left unchanged; a head
94/// that resolves to nothing (unknown, or simply not a module reference at
95/// all — a local value/type name, a bound variable, …) is ALSO left
96/// unchanged — never invent, matching `v1/surface.rs`'s own posture.
97struct AbsolutizeRewrite<'a, 's> {
98    surfaces: &'a SurfaceEnv<'s>,
99}
100
101impl functor::HeadRewrite for AbsolutizeRewrite<'_, '_> {
102    fn rewrite(
103        &self,
104        mods: &[String],
105        path: &[String],
106        _span: Span,
107    ) -> Result<Option<Vec<String>>, LowerError> {
108        let Some(head) = mods.first() else {
109            return Ok(None);
110        };
111        let Some((resolved, _)) = surface::resolve_module(self.surfaces, path, head) else {
112            return Ok(None);
113        };
114        if resolved == *head {
115            // Already exactly what was written (a top-level dependency
116            // name, or an already-absolute reference) — no splice needed.
117            return Ok(None);
118        }
119        let mut out: Vec<String> = resolved.split('.').map(str::to_string).collect();
120        out.extend(mods[1..].iter().cloned());
121        Ok(Some(out))
122    }
123
124    // A bare single-segment slot (`let open Foo in …`, `Foo :> S`) cannot
125    // grammatically hold a multi-segment absolutized path, and needs no
126    // rewrite: those sites already resolve correctly via `surface.rs`'s own
127    // outward search (module-level aliases/coercions are resolved there,
128    // never re-walked textually here). Signature bodies are deliberately
129    // never absolutized.
130    fn rewrite_bare_names(&self) -> bool {
131        false
132    }
133
134    fn walk_signatures(&self) -> bool {
135        false
136    }
137
138    // A `ModExpr::Functor` node encountered while
139    // absolutizing ORDINARY binds is an everyday sibling-level functor
140    // DEFINITION (never itself lowered directly — only an application's
141    // substituted body is absolutized, fresh, at the instantiated site) —
142    // not a curried/nested one; leave it untouched rather than reject it.
143    fn reject_nested_functor_literals(&self) -> bool {
144        false
145    }
146}
147
148/// Module-path pre-qualification of 0.1 `type` names. Maps a
149/// locally-visible bare type name to its fully-qualified nominal key, using
150/// EXACTLY `elaborate::qualify_key`'s scheme (`elaborate.rs:289-295`):
151/// `"M.N.t"` for `type t` inside `module M = struct module N = … end`.
152/// `elaborate::qualify_key` itself is private to that module, so
153/// [`qualify_type_key`] reproduces the identical formula here rather than
154/// importing it — the two must never drift; both are one-liners.
155///
156/// **Ctor carve-out:** constructor names (`Known`, `Some`) are NEVER looked
157/// up here — they stay surfaced unqualified (`UserTypeDecl.ctors`,
158/// `elaborate.rs:147-154`), referenced as bare `Atomic::Ctor`/`PatBot::Ctor`
159/// strings. Cross-package ctor collisions remain a latent 0.0.6-inherited
160/// limitation.
161///
162/// `pub(crate)`: `v1/module_check.rs` reuses this exact type (and
163/// [`TypeNameEnv::child`]/`qualify`) to pre-qualify a sig's own `val` type
164/// annotations against the SAME module-local `type` names the struct body
165/// resolves against — a single-implementation guarantee that the two walks
166/// (body lowering here, sig elaboration there) can never drift apart on what
167/// a bare type name means.
168#[derive(Clone, Default)]
169pub(crate) struct TypeNameEnv(std::collections::HashMap<String, String>);
170
171/// `elaborate::qualify_key`'s formula (`elaborate.rs:289-295`), reproduced
172/// here since that function is private to the `elaborate` module — see
173/// [`TypeNameEnv`]'s doc comment. `pub(crate)`: also the formula
174/// `v1/module_check.rs` uses to key `StaticEnv::seals`/`hidden` by qualified
175/// member name — value names and type names share the same qualification
176/// scheme.
177pub(crate) fn qualify_type_key(mod_path: &[String], local: &str) -> String {
178    if mod_path.is_empty() {
179        local.to_string()
180    } else {
181        format!("{}.{}", mod_path.join("."), local)
182    }
183}
184
185impl TypeNameEnv {
186    pub(crate) fn qualify(&self, bare: &str) -> String {
187        self.0
188            .get(bare)
189            .cloned()
190            .unwrap_or_else(|| bare.to_string())
191    }
192
193    /// Child env for one module body: parent mappings (outer types stay
194    /// visible inside, per ordinary module scoping) overlaid with this
195    /// body's own `type` names — collected by a pre-scan over ALL of the
196    /// body's `Bind::Type` arms (first + ands) BEFORE any bind is
197    /// lowered, so forward and mutual references inside the module map
198    /// correctly. Inner declarations shadow outer same-named ones.
199    /// `mod_path` is the FULL path at which `binds` lives (i.e. already
200    /// includes this module's own name — the caller, [`lower_module_bind`],
201    /// computes it before calling this).
202    ///
203    /// `surfaces` is threaded so an `include M` bind's
204    /// spliced TYPE members join the map too — a later `val f (x : t) = …`
205    /// in the SAME body must read bare `t` as the included copy
206    /// (`⟨mod_path⟩.t`), exactly as if `type t = M.t` had been written
207    /// directly. Consults the SAME frozen `include_targets` answer
208    /// `v1/surface.rs::build_binds` recorded (never re-resolving) — see
209    /// [`surface::frozen_include_target`]'s doc comment. Insertion order
210    /// along the bind walk still preserves shadowing (a later `type t`
211    /// overwrites an included `t`'s mapping, and vice versa, since `binds`
212    /// is walked in source order below).
213    pub(crate) fn child<'a, 's>(
214        &self,
215        mod_path: &[String],
216        binds: impl Iterator<Item = &'a cst_v1::Bind>,
217        surfaces: &SurfaceEnv<'s>,
218    ) -> Self {
219        let mut map = self.0.clone();
220        for b in binds {
221            match b {
222                cst_v1::Bind::Type { first, ands, .. } => {
223                    map.insert(
224                        first.name.name.clone(),
225                        qualify_type_key(mod_path, &first.name.name),
226                    );
227                    for a in ands {
228                        map.insert(
229                            a.bind.name.name.clone(),
230                            qualify_type_key(mod_path, &a.bind.name.name),
231                        );
232                    }
233                }
234                cst_v1::Bind::Include { kw, body } => match &*body.0 {
235                    ast_v1::ModExpr::Var(_) => {
236                        if let Some(Some(target)) =
237                            surface::frozen_include_target(surfaces, mod_path, kw.0)
238                        {
239                            if let Some(target_surf) = surfaces.modules.get(target) {
240                                for (t, _) in &target_surf.types {
241                                    map.insert(t.clone(), qualify_type_key(mod_path, t));
242                                }
243                            }
244                        }
245                    }
246                    // `include Make Arg`'s substituted body's
247                    // own `type` names join the map too. The instantiated
248                    // body's DECLARED type names are exactly the functor
249                    // body's OWN — substitution only ever touches REFERENCES,
250                    // never declared names — so this reads straight off the
251                    // functor body's binds, not off a separately-registered
252                    // target surface (there isn't one for a fresh
253                    // instantiation).
254                    ast_v1::ModExpr::App { func, arg: _ } => {
255                        let app_span = mod_chain_span(func);
256                        if let Some(Some(surface::AppResolution { functor_path, .. })) =
257                            surface::frozen_app_target(surfaces, mod_path, app_span)
258                        {
259                            if let Some(fdef) = surfaces.functors.get(functor_path) {
260                                if let Some(body_binds) = functor::functor_body_binds(fdef.body) {
261                                    for b in body_binds.iter().map(|sb| sb.0.as_ref()) {
262                                        if let cst_v1::Bind::Type { first, ands, .. } = b {
263                                            map.insert(
264                                                first.name.name.clone(),
265                                                qualify_type_key(mod_path, &first.name.name),
266                                            );
267                                            for a in ands {
268                                                map.insert(
269                                                    a.bind.name.name.clone(),
270                                                    qualify_type_key(mod_path, &a.bind.name.name),
271                                                );
272                                            }
273                                        }
274                                    }
275                                }
276                            }
277                        }
278                    }
279                    ast_v1::ModExpr::Struct { .. }
280                    | ast_v1::ModExpr::Coerce { .. }
281                    | ast_v1::ModExpr::Functor { .. } => {}
282                },
283                _ => {}
284            }
285        }
286        TypeNameEnv(map)
287    }
288
289    /// The
290    /// [`Self::child`] twin for a synthetic seal over an
291    /// `ImplView::Surface` (an alias/coerce/application-result body, or a
292    /// `Decl::Module` member recursed via a target's own already-computed
293    /// [`ModSurface`]) — no real `cst_v1::Bind`s are available to scan (the
294    /// target's own types live elsewhere in the tree, or came from an
295    /// OWNED, substituted functor-codomain instantiation), but the target's
296    /// OWN type NAMES are already known (`ModSurface::types`) and qualify
297    /// EXACTLY like a real `Bind::Type` at this path would
298    /// (`qualify_type_key(mod_path, name)` — the formula is name-only, not
299    /// mechanism-dependent). Does not thread `include`s (an alias/app
300    /// result's own type surface is already flattened by
301    /// [`crate::v1::surface::build_binds`]'s own include-splicing, so no
302    /// further include-awareness is needed here).
303    pub(crate) fn child_from_names(
304        &self,
305        mod_path: &[String],
306        names: impl Iterator<Item = String>,
307    ) -> Self {
308        let mut map = self.0.clone();
309        for n in names {
310            let q = qualify_type_key(mod_path, &n);
311            map.insert(n, q);
312        }
313        TypeNameEnv(map)
314    }
315}
316
317/// Lower one dependency library (`module Name = struct binds end`) to a
318/// single real [`cst::TopBinding::Module`] — names exported **qualified**
319/// (see the module doc comment). `FileV1::Document` input is a
320/// caller bug (the loader's `DocumentAsDependency` check already rejects it
321/// before this is ever reached): a `LowerError`, not a panic.
322pub fn lower_file_v1(file: &cst_v1::FileV1) -> Result<Vec<cst::TopBinding>, LowerError> {
323    // Back-compat single-file wrapper (every existing caller outside
324    // `lib.rs`'s dep loop — the crate's own unit tests, and every OTHER
325    // integration test file that has no cross-file alias/named-signature
326    // need): build a throwaway `SurfaceEnv` from THIS file alone. A
327    // single-file `SurfaceEnv` still resolves every alias/named-signature
328    // reference *within* the same library (the ordering rule is a
329    // within-file/bind-order rule first) — only a reference reaching an
330    // EARLIER, SEPARATELY-LOADED dependency file needs the threaded
331    // `lower_file_v1_with_surfaces` sibling below, which `lib.rs`'s real
332    // pipeline uses instead.
333    let mut surfaces = SurfaceEnv::default();
334    surface::build_file_surface(file, &mut surfaces);
335    lower_file_v1_with_surfaces(file, &surfaces)
336}
337
338/// The threaded sibling
339/// `lib.rs`'s dep loop uses — `surfaces` must already contain every EARLIER
340/// dependency file's surface (built via [`surface::build_file_surface`] in
341/// load order) AND, after [`surface::build_file_surface`] has ALSO been
342/// called on `file` itself, `file`'s own surface (so this file's own
343/// internal aliases/named-signature references resolve too — see that
344/// function's doc comment on why `build_file_surface` runs BEFORE lowering,
345/// not interleaved with it: it's a pure syntactic walk with no dependency on
346/// anything lowering produces).
347pub(crate) fn lower_file_v1_with_surfaces<'a>(
348    file: &'a cst_v1::FileV1,
349    surfaces: &SurfaceEnv<'a>,
350) -> Result<Vec<cst::TopBinding>, LowerError> {
351    match file {
352        cst_v1::FileV1::Library {
353            module_kw,
354            name,
355            sig_annot: _,
356            eq,
357            struct_kw,
358            binds,
359            end_kw,
360            ..
361        } => {
362            // The seal rule (module doc comment): lower the struct body
363            // FIRST, exactly as if there were no annotation; `sig_annot` is
364            // then simply DROPPED.
365            let module = lower_module_bind(
366                module_kw,
367                name,
368                eq,
369                struct_kw,
370                binds,
371                end_kw,
372                &[],
373                &TypeNameEnv::default(),
374                surfaces,
375            )?;
376            Ok(vec![module])
377        }
378        cst_v1::FileV1::Document { eoi, .. } => Err(unsupported(
379            "a document file used as a dependency library",
380            "the loader's DocumentAsDependency check should have rejected \
381             this before lowering ever ran",
382            eoi.0,
383        )),
384    }
385}
386
387/// Shared by [`lower_file_v1`] (the top-level library, whose `binds` field
388/// is a plain `Vec<Bind>`) and `lower_bind_v1`'s `Bind::Module` arm (a
389/// nested `module N = struct … end` bind, whose `binds` field is a
390/// `Vec<StructBindV1>` — see [`cst_v1::StructBindV1`]'s doc comment for why
391/// the two differ). Both produce a `cst::TopBinding::Module` with `sig:
392/// None` wrapping the lowered binds. `mod_path` is the path of the ENCLOSING
393/// scope (`[]` at the top level); this function extends it with `name` itself
394/// before pre-scanning `binds` for `type` names ([`TypeNameEnv::child`]'s
395/// pre-qualification) and lowering each bind against the
396/// extended env/path — `flat_map`, preserving source order, since a `Type`
397/// `and`-chain lowers to N consecutive `TopBinding`s.
398fn lower_module_bind<'a, 's>(
399    module_kw: &KwModule,
400    name: &CtorTok,
401    eq: &DefEqTok,
402    struct_kw: &KwStruct,
403    binds: impl IntoIterator<Item = &'a cst_v1::Bind>,
404    end_kw: &KwEnd,
405    mod_path: &[String],
406    tyenv: &TypeNameEnv,
407    surfaces: &SurfaceEnv<'s>,
408) -> Result<cst::TopBinding, LowerError> {
409    let mut child_path = mod_path.to_vec();
410    child_path.push(name.name.clone());
411    // Absolutize relative-sibling module
412    // heads BEFORE lowering — an owned clone, consumed immediately below.
413    // Applies uniformly to plain modules, alias targets' synthesized trees
414    // (no-op — copies are already absolute), and instantiated functor bodies
415    // (the `Bind::Module`/`Bind::Include` App arms below run parameter
416    // substitution FIRST, then call this function on the substituted binds —
417    // parameter heads are gone before absolutization, so the two rewrites
418    // cannot interfere, module doc comment's risk-6 guard).
419    let absolutized = functor::rewrite_binds(binds, &AbsolutizeRewrite { surfaces }, &child_path)?;
420    let child_tyenv = tyenv.child(&child_path, absolutized.iter(), surfaces);
421    let mut decls = Vec::new();
422    for b in &absolutized {
423        for tb in lower_bind_v1(b, &child_path, &child_tyenv, surfaces)? {
424            decls.push(cst::StructDecl(Box::new(tb)));
425        }
426    }
427    Ok(cst::TopBinding::Module {
428        kw: module_kw.clone(),
429        name: name.clone(),
430        sig: None,
431        eq: eq.clone(),
432        struct_kw: struct_kw.clone(),
433        decls,
434        end_kw: end_kw.clone(),
435    })
436}
437
438/// Lower the entry document's body expression. `FileV1::Library` input is
439/// the mirror-image caller bug (the loader's `LibraryAsEntry` check already
440/// rejects it): a `LowerError`, not a panic.
441pub fn lower_document_v1(file: &cst_v1::FileV1) -> Result<cst::ast::Expr, LowerError> {
442    match file {
443        cst_v1::FileV1::Document { body, .. } => lower_expr(body),
444        cst_v1::FileV1::Library { end_kw, .. } => Err(unsupported(
445            "a library file used as the entry document",
446            "the loader's LibraryAsEntry check should have rejected this \
447             before lowering ever ran",
448            end_kw.0,
449        )),
450    }
451}
452
453// ---- Bind ---------------------------------------------------------------
454
455/// `mod_path` is the path of the SCOPE `b` itself lives in (not including
456/// any module `b` might itself introduce — see [`lower_module_bind`]'s doc
457/// comment); `tyenv` is that same scope's `type`-name pre-qualification env.
458/// Returns a `Vec` (not a single `TopBinding`) since the `Type`
459/// arm's `and`-chain lowers to N consecutive `TopBinding::Type`s —
460/// 1-element for every other arm.
461fn lower_bind_v1<'s>(
462    b: &cst_v1::Bind,
463    mod_path: &[String],
464    tyenv: &TypeNameEnv,
465    surfaces: &SurfaceEnv<'s>,
466) -> Result<Vec<cst::TopBinding>, LowerError> {
467    match b {
468        cst_v1::Bind::Value {
469            kw,
470            stage,
471            name,
472            params,
473            eq,
474            body,
475        } => {
476            let (ps, value) = lower_param_units(params, lower_expr(body)?)?;
477            Ok(vec![cst::TopBinding::Let(cst::TopLet {
478                let_kw: KwLet(kw.0),
479                // `val ~x` / `val persistent ~x` — the stage travels ON the
480                // binding, which is what lets it survive being nested inside
481                // `module … = struct … end` (`elaborate.rs`'s index-keyed
482                // `ItemOrigins::stages` cannot name a binding in there, and
483                // every 0.1 `val` is in there).
484                stage: stage.as_ref().map(lower_bind_stage),
485                name: name.clone(),
486                ascription: None,
487                // 0.1's grammar has no `nonrecdecargpart` bar to lower.
488                leading_bar: None,
489                params: ps,
490                eq: eq.clone(),
491                value,
492            })])
493        }
494        cst_v1::Bind::ValueInline {
495            kw,
496            stage,
497            ctx,
498            cmd,
499            params,
500            eq,
501            body,
502            ..
503        } => Ok(vec![cst::TopBinding::LetInline {
504            kw: KwLetHorz(kw.0),
505            stage: stage.as_ref().map(lower_bind_stage),
506            ctx: ctx.clone(),
507            cmd: plain_horz(cmd)?,
508            params: lower_command_params(params)?,
509            eq: eq.clone(),
510            value: lower_expr(body)?,
511        }]),
512        cst_v1::Bind::ValueBlock {
513            kw,
514            stage,
515            ctx,
516            cmd,
517            params,
518            eq,
519            body,
520            ..
521        } => Ok(vec![cst::TopBinding::LetBlock {
522            kw: KwLetVert(kw.0),
523            stage: stage.as_ref().map(lower_bind_stage),
524            ctx: ctx.clone(),
525            cmd: plain_vert(cmd)?,
526            params: lower_command_params(params)?,
527            eq: eq.clone(),
528            value: lower_expr(body)?,
529        }]),
530        cst_v1::Bind::ValueMath {
531            kw,
532            stage,
533            ctx,
534            cmd,
535            params,
536            scripts,
537            eq,
538            body,
539            ..
540        } => Ok(vec![lower_value_math(
541            kw, stage, ctx, cmd, params, scripts, eq, body,
542        )?]),
543        cst_v1::Bind::ValueRec {
544            kw,
545            stage,
546            first,
547            ands,
548            ..
549        } => Ok(vec![cst::TopBinding::LetRec {
550            kw: KwLetRec(kw.0),
551            stage: stage.as_ref().map(lower_bind_stage),
552            first: lower_rec_clause(first)?,
553            ands: ands
554                .iter()
555                .map(|a| {
556                    Ok(cst::ast::AndBinding {
557                        and_kw: a.and_kw.clone(),
558                        binding: lower_rec_clause(&a.clause)?,
559                    })
560                })
561                .collect::<Result<_, LowerError>>()?,
562        }]),
563        cst_v1::Bind::ValueMutable {
564            kw,
565            stage,
566            name,
567            arrow,
568            value,
569            ..
570        } => Ok(vec![cst::TopBinding::LetMutable {
571            kw: KwLetMutable(kw.0),
572            stage: stage.as_ref().map(lower_bind_stage),
573            name: name.clone(),
574            arrow: arrow.clone(),
575            value: lower_expr(value)?,
576        }]),
577        cst_v1::Bind::Type { kw, first, ands } => {
578            let mut out = Vec::with_capacity(1 + ands.len());
579            out.push(lower_type_single(kw, first, tyenv)?);
580            for a in ands {
581                out.push(lower_type_single(kw, &a.bind, tyenv)?);
582            }
583            Ok(out)
584        }
585        cst_v1::Bind::Module {
586            module_kw,
587            name,
588            sig_annot: _,
589            eq,
590            body,
591        } => match &*body.0 {
592            ast_v1::ModExpr::Struct {
593                struct_kw,
594                binds,
595                end_kw,
596            } => {
597                // The seal rule (module doc comment): the struct-literal body
598                // lowers to a real TopBinding::Module regardless of the
599                // annotation, which is DROPPED here.
600                let module = lower_module_bind(
601                    module_kw,
602                    name,
603                    eq,
604                    struct_kw,
605                    binds.iter().map(|b| b.0.as_ref()),
606                    end_kw,
607                    mod_path,
608                    tyenv,
609                    surfaces,
610                )?;
611                Ok(vec![module])
612            }
613            // `module M = N` / `module M = A.B.C` —
614            // member-copy alias expansion (`lower_module_alias`). The
615            // annotation (if any, `module M :> S = N`) is dropped here —
616            // same seal rule as the struct-literal case above — and
617            // enforced by `v1/module_check.rs`'s `ImplView::Alias`.
618            ast_v1::ModExpr::Var(chain) => Ok(vec![lower_module_alias(
619                module_kw,
620                name,
621                eq,
622                mod_path,
623                surfaces,
624                &chain.render(),
625                mod_chain_span(chain),
626            )?]),
627            // `module M = Make Arg` —
628            // instantiate the functor generatively at `M`'s own path, then
629            // WRAP the substituted body in one synthetic
630            // `TopBinding::Module` (the `lower_module_alias`/
631            // `alias_member_decls` wrap precedent). `Arg` may itself be an
632            // ENCLOSING functor's own parameter (`set.satyg`'s `Map.Make
633            // Elem` shape) — `v1/surface.rs`'s `ParamSubst` stack already
634            // resolves that case when the enclosing functor is applied, so
635            // `frozen_app_target` returning anything other than
636            // `Some(Some(_))` means a genuinely unknown functor/argument name
637            // (or a non-struct functor body) — a precise `LowerError`, never
638            // a panic (the `frozen_include_target` posture).
639            ast_v1::ModExpr::App { func, arg: _ } => {
640                let app_span = mod_chain_span(func);
641                match surface::frozen_app_target(surfaces, mod_path, app_span) {
642                    Some(Some(surface::AppResolution {
643                        functor_path,
644                        arg_path,
645                    })) => {
646                        let fdef = surfaces
647                            .functors
648                            .get(functor_path)
649                            .expect("a frozen app target always names a registered functor");
650                        let body_binds = functor::functor_body_binds(fdef.body).expect(
651                            "a frozen app target's functor body is always struct-shaped \
652                             (a non-struct body never freezes a resolution)",
653                        );
654                        let arg_segs: Vec<String> =
655                            arg_path.split('.').map(str::to_string).collect();
656                        let substituted =
657                            functor::substitute_binds(body_binds, &fdef.param, &arg_segs)?;
658                        let span = name.span;
659                        let module = lower_module_bind(
660                            module_kw,
661                            name,
662                            eq,
663                            &KwStruct(span),
664                            substituted.iter().map(|sb| sb.0.as_ref()),
665                            &KwEnd(span),
666                            mod_path,
667                            tyenv,
668                            surfaces,
669                        )?;
670                        Ok(vec![module])
671                    }
672                    _ => Err(unsupported(
673                        "a functor application whose functor or argument is unknown",
674                        "an application must name an earlier, concrete module already \
675                         in scope (or an enclosing functor's own parameter, once that \
676                         functor is itself applied)",
677                        mod_chain_span(func),
678                    )),
679                }
680            }
681            // `module M = fun (X:S) -> body` — a functor
682            // DEFINITION emits ZERO runtime bindings (it is not a value —
683            // exactly `Bind::Signature`'s posture, `:583` below). The
684            // `FunctorDef` itself was already registered by
685            // `v1/surface.rs::build_binds` — nothing left to do here.
686            ast_v1::ModExpr::Functor { .. } => Ok(Vec::new()),
687            // `module M = N :> S` — lowers EXACTLY
688            // like `Var` (the full surface of `N` copied, not `S`-filtered
689            // — the seal HIDES undeclared members via the established
690            // non-commit path, byte-matching the struct-literal behavior);
691            // `sig_` is dropped here and enforced by `v1/module_check.rs`.
692            ast_v1::ModExpr::Coerce { name: target, .. } => Ok(vec![lower_module_alias(
693                module_kw,
694                name,
695                eq,
696                mod_path,
697                surfaces,
698                &target.name,
699                target.span,
700            )?]),
701        },
702        // A signature has no runtime/expression-spine
703        // content at all — only `v1/surface.rs`'s `SurfaceEnv`/
704        // `v1/module_check.rs`'s static environment record it.
705        cst_v1::Bind::Signature { .. } => Ok(Vec::new()),
706        // `include M` splices copies of ALL of `M`'s exported
707        // members directly into THIS body — the alias member-copy generator
708        // (`alias_member_decls`) called with `alias_path` = `mod_path` (the
709        // INCLUDER's OWN path, not a fresh sub-path), unwrapped from its
710        // `StructDecl` boxes into this arm's `Vec<TopBinding>` so the copies
711        // splice inline (contrast `lower_module_alias`, which wraps its
712        // copies in ONE synthetic `TopBinding::Module`). Only a `Var` body
713        // resolves (an earlier module already in scope); every other shape is
714        // a precise error — `App`/`Functor` reuse the EXISTING functor
715        // wordings verbatim (reachable through `include`, e.g. `include Make
716        // Int`), `Struct`/`Coerce` require the target be named first.
717        cst_v1::Bind::Include { kw, body } => match &*body.0 {
718            ast_v1::ModExpr::Var(chain) => {
719                match surface::frozen_include_target(surfaces, mod_path, kw.0) {
720                    Some(Some(target_path)) => {
721                        let target_surf = surfaces
722                            .modules
723                            .get(target_path)
724                            .expect("a frozen include target is always a registered module");
725                        let decls = alias_member_decls(kw.0, mod_path, target_path, target_surf)?;
726                        Ok(decls.into_iter().map(|sd| *sd.0).collect())
727                    }
728                    _ => Err(unsupported(
729                        "an `include M` binding naming an unknown module",
730                        "an include must name an earlier module already in scope",
731                        mod_chain_span(chain),
732                    )),
733                }
734            }
735            // `include Make Arg` (map.satyg's `include
736            // Make Int` shape) — instantiate the functor generatively at
737            // THIS includer's OWN path, then splice the substituted body's
738            // lowered binds UNWRAPPED (the `Var` arm's unwrap-`StructDecl`-
739            // boxes pattern above, contrast the `Bind::Module` App arm's
740            // wrap). Each substituted bind is lowered via `lower_bind_v1`
741            // directly (not `lower_module_bind`) since there is no fresh
742            // sub-path to wrap into — exactly how the `Var` arm's copies
743            // splice flat.
744            ast_v1::ModExpr::App { func, arg: _ } => {
745                let app_span = mod_chain_span(func);
746                match surface::frozen_app_target(surfaces, mod_path, app_span) {
747                    Some(Some(surface::AppResolution {
748                        functor_path,
749                        arg_path,
750                    })) => {
751                        let fdef = surfaces
752                            .functors
753                            .get(functor_path)
754                            .expect("a frozen app target always names a registered functor");
755                        let body_binds = functor::functor_body_binds(fdef.body).expect(
756                            "a frozen app target's functor body is always struct-shaped \
757                             (a non-struct body never freezes a resolution)",
758                        );
759                        let arg_segs: Vec<String> =
760                            arg_path.split('.').map(str::to_string).collect();
761                        let substituted =
762                            functor::substitute_binds(body_binds, &fdef.param, &arg_segs)?;
763                        let mut out = Vec::new();
764                        for b in substituted.iter().map(|sb| sb.0.as_ref()) {
765                            out.extend(lower_bind_v1(b, mod_path, tyenv, surfaces)?);
766                        }
767                        Ok(out)
768                    }
769                    _ => Err(unsupported(
770                        "an `include` of a functor application whose functor or argument \
771                         is unknown",
772                        "an include must name an earlier, concrete module already in \
773                         scope (or an enclosing functor's own parameter, once that \
774                         functor is itself applied)",
775                        mod_chain_span(func),
776                    )),
777                }
778            }
779            // A functor LITERAL cannot itself be `include`d (there is no
780            // module value to splice — upstream-faithfully; you cannot
781            // `include` a functor either) — stays a precise error.
782            ast_v1::ModExpr::Functor { fun_kw, .. } => Err(unsupported(
783                "an `include` of a functor literal (`fun (X : S) -> ...`)",
784                "a functor is not a module value — apply it first \
785                 (`include Make Arg`), or name the application \
786                 (`module M = Make Arg  include M`)",
787                fun_kw.0,
788            )),
789            ast_v1::ModExpr::Struct { struct_kw, .. } => Err(unsupported(
790                "an `include` of an inline `struct … end` literal",
791                "name the module first: `module N = struct … end  include N`",
792                struct_kw.0,
793            )),
794            ast_v1::ModExpr::Coerce { name: target, .. } => Err(unsupported(
795                "an `include` of a coerced module",
796                "seal a named module first, then include it",
797                target.span,
798            )),
799        },
800    }
801}
802
803/// Any reasonable span off a [`ast_v1::ModChainV1`] — used only to point a
804/// [`LowerError`] at the offending module-path/functor-operand token.
805fn mod_chain_span(c: &ast_v1::ModChainV1) -> Span {
806    match c {
807        ast_v1::ModChainV1::Long(t) => t.span,
808        ast_v1::ModChainV1::Single(t) => t.span,
809    }
810}
811
812// ---- Module aliases (`module M = N`, `module M = N :> S`) — lowering-time member-copy expansion from `v1/surface.rs`'s
813// syntactic `SurfaceEnv`. ---------------------------------------------------
814
815/// `module M = N` / `module M = A.B.C` (`ModExpr::Var`) and `module M = N
816/// :> S` (`ModExpr::Coerce`, `chain_rendered`/`chain_span` naming just the
817/// bare target `N` — coercion applies to a bare name only, upstream-
818/// faithfully): resolve the target outward from `mod_path` against
819/// `surfaces`, then emit ONE synthetic `cst::TopBinding::Module` whose decls
820/// are member-copies of the target's (already seal-filtered, if sealed)
821/// surface, in the target's own source order. An unresolved target — unknown
822/// name, or a forward reference to a module not yet registered — is a
823/// precise `LowerError`, not a panic.
824fn lower_module_alias(
825    module_kw: &KwModule,
826    name: &CtorTok,
827    eq: &DefEqTok,
828    mod_path: &[String],
829    surfaces: &SurfaceEnv,
830    _chain_rendered: &str,
831    chain_span: Span,
832) -> Result<cst::TopBinding, LowerError> {
833    let span = name.span;
834    let mut alias_path = mod_path.to_vec();
835    alias_path.push(name.name.clone());
836    // Consult the FROZEN, in-source-order resolution `v1/surface.rs`
837    // recorded when it walked this alias bind: a forward reference
838    // (target defined LATER in the same body) was `None` there and stays a
839    // `LowerError` here, even though `surfaces.modules` now contains the
840    // later target. `frozen_alias_target` returning `None` at all would
841    // mean the surface builder never saw this alias — impossible on the
842    // real pipeline (it always runs first), so treat it as unresolved too.
843    let target_path = match surface::frozen_alias_target(surfaces, &alias_path) {
844        Some(Some(t)) => t.clone(),
845        _ => {
846            return Err(unsupported(
847                "a module alias/path binding (`module M = N`) naming an unknown module",
848                "a module alias must name an earlier module already in scope",
849                chain_span,
850            ));
851        }
852    };
853    let surface = surfaces
854        .modules
855        .get(&target_path)
856        .expect("a frozen alias target is always a registered module");
857    let decls = alias_member_decls(span, &alias_path, &target_path, surface)?;
858    Ok(cst::TopBinding::Module {
859        kw: module_kw.clone(),
860        name: name.clone(),
861        sig: None,
862        eq: eq.clone(),
863        struct_kw: KwStruct(span),
864        decls,
865        end_kw: KwEnd(span),
866    })
867}
868
869/// The member-copy list itself (alias expansion), recursing into
870/// `surface.mods` for each nested module re-export. Every fabricated node
871/// reuses `span` throughout (never re-unparsed — the same "fabricate
872/// `cst::ast` nodes directly" precedent as `val math`'s synthesis helpers,
873/// `var_tok`/`apply_chain`/`fun1` above) and every reference is the
874/// target's FULL ABSOLUTE path (`target_path`, e.g. `"Lib.N"`) — the
875/// elaborator's exact-join lookup makes an absolute reference the only kind
876/// that can ever resolve there.
877fn alias_member_decls(
878    span: Span,
879    alias_path: &[String],
880    target_path: &str,
881    surface: &ModSurface,
882) -> Result<Vec<cst::StructDecl>, LowerError> {
883    let mut out = Vec::with_capacity(surface.vals.len() + surface.types.len() + surface.mods.len());
884    for x in &surface.vals {
885        let target_ref = apply_chain(
886            cst::ast::Atomic::VarWithMod(VarWithModTok {
887                mods: vec![target_path.to_string()],
888                name: x.clone(),
889                span,
890            }),
891            Vec::new(),
892        );
893        out.push(cst::StructDecl(Box::new(cst::TopBinding::Let(
894            cst::TopLet {
895                let_kw: KwLet(span),
896                // A synthesized alias re-export is written at the default
897                // stage, whatever the aliased member declared.
898                stage: None,
899                name: cst::BindName::from(var_tok(x, span)),
900                ascription: None,
901                // 0.1's grammar has no `nonrecdecargpart` bar to lower.
902                leading_bar: None,
903                params: Vec::new(),
904                eq: DefEqTok(span),
905                value: target_ref,
906            },
907        ))));
908    }
909    for (tname, arity) in &surface.types {
910        let ctor = var_tok(&format!("{target_path}.{tname}"), span);
911        let (tyvars, ty) = match *arity {
912            0 => (
913                Vec::new(),
914                cst::ast::TypeExpr::Atom(cst::ast::TypeProd {
915                    first: cst::ast::TypeApp {
916                        head: cst::ast::TypeAtom::Name(ctor),
917                        rest: Vec::new(),
918                    },
919                    rest: Vec::new(),
920                }),
921            ),
922            1 => {
923                let tv = TypeVarTok {
924                    name: "a".to_string(),
925                    span,
926                };
927                (
928                    vec![tv.clone()],
929                    cst::ast::TypeExpr::Atom(cst::ast::TypeProd {
930                        first: cst::ast::TypeApp {
931                            head: cst::ast::TypeAtom::Var(tv),
932                            rest: vec![cst::ast::TypeAtom::Name(ctor)],
933                        },
934                        rest: Vec::new(),
935                    }),
936                )
937            }
938            _ => {
939                return Err(unsupported(
940                    "an alias copy of a type member with arity >= 2",
941                    "the 0.0.6 cst target (`TypeApp`) is single-argument \
942                     (cst.rs:1297-1312) — widen it only when a real \
943                     package needs arity >= 2",
944                    span,
945                ));
946            }
947        };
948        out.push(cst::StructDecl(Box::new(cst::TopBinding::Type(
949            cst::TypeDecl {
950                kw: KwType(span),
951                tyvars,
952                name: var_tok(&qualify_type_key(alias_path, tname), span),
953                eq: DefEqTok(span),
954                body: cst::TypeDeclBody::Synonym(ty),
955                ands: Vec::new(),
956            },
957        ))));
958    }
959    for (qname, child) in &surface.mods {
960        let mut child_alias_path = alias_path.to_vec();
961        child_alias_path.push(qname.clone());
962        let child_target = format!("{target_path}.{qname}");
963        let child_decls = alias_member_decls(span, &child_alias_path, &child_target, child)?;
964        out.push(cst::StructDecl(Box::new(cst::TopBinding::Module {
965            kw: KwModule(span),
966            name: CtorTok {
967                name: qname.clone(),
968                span,
969            },
970            sig: None,
971            eq: DefEqTok(span),
972            struct_kw: KwStruct(span),
973            decls: child_decls,
974            end_kw: KwEnd(span),
975        })));
976    }
977    // Signature members: zero runtime/type-spine residue — the
978    // surface env re-exports the name (`M.S` resolves to the same
979    // `SigDef`); nothing to emit here.
980    Ok(out)
981}
982
983/// The shared clause bridge for `val rec`/`let rec` — the `RecBinding`
984/// reshape the module doc anticipated, field-by-field against the real
985/// `cst::ast::RecBinding` (`cst.rs:753-762`).
986fn lower_rec_clause(c: &ast_v1::RecClauseV1) -> Result<cst::ast::RecBinding, LowerError> {
987    let value_expr = lower_expr(&c.value.0)?;
988    // `RecBinding.params` is `Vec<PatBot>` (not `Vec<Param>`). All-plain
989    // clauses lower their patterns directly (byte-identical). A clause with a
990    // `?(l = x, …)` bundle desugars to `params: []` + a nested lambda-chain
991    // value — the LetRec lambda-body invariant holds because the chain head
992    // is itself a lambda.
993    let (params, value) = if c.params.iter().all(|p| p.opts.is_none()) {
994        let ps = c
995            .params
996            .iter()
997            .map(|p| lower_param_body(&p.body))
998            .collect::<Result<_, _>>()?;
999        (ps, value_expr)
1000    } else {
1001        let (_, chain) = lower_param_units(&c.params, value_expr)?;
1002        (Vec::new(), chain)
1003    };
1004    Ok(cst::ast::RecBinding {
1005        // `BindName` == `BindName`: shared leaf-level type, clone not
1006        // re-encode (see the module doc comment).
1007        name: c.name.clone(),
1008        // No `: ty` form in 0.1's `bind_value_nonrec`.
1009        ascription: None,
1010        // No multi-clause sugar in 0.1.
1011        leading_bar: None,
1012        params,
1013        eq: c.eq.clone(),
1014        value: erase_expr(value),
1015        // `RecClause` continuations are a 0.0.6-only surface (cst.rs:779-786).
1016        extra: Vec::new(),
1017    })
1018}
1019
1020// ---- type binds -----------------------------------------
1021
1022fn lower_type_single(
1023    kw: &KwType,
1024    s: &cst_v1::TypeBindSingleV1,
1025    tyenv: &TypeNameEnv,
1026) -> Result<cst::TopBinding, LowerError> {
1027    Ok(cst::TopBinding::Type(cst::TypeDecl {
1028        // One shared `type` span per chain.
1029        kw: kw.clone(),
1030        // Field REORDER: 0.1 postfix (`type t 'a`) → cst prefix slot.
1031        tyvars: s.tyvars.clone(),
1032        name: VarTok {
1033            name: tyenv.qualify(&s.name.name),
1034            span: s.name.span,
1035        },
1036        eq: s.eq.clone(),
1037        body: match &s.body {
1038            cst_v1::TypeBodyV1::Variant {
1039                leading_bar,
1040                first,
1041                rest,
1042            } => cst::TypeDeclBody::Variant {
1043                leading_bar: leading_bar.clone(),
1044                first: lower_variant_def(first, tyenv)?,
1045                rest: rest
1046                    .iter()
1047                    .map(|b| {
1048                        Ok(cst::BarVariantDef {
1049                            bar: b.bar.clone(),
1050                            def: lower_variant_def(&b.def, tyenv)?,
1051                        })
1052                    })
1053                    .collect::<Result<_, LowerError>>()?,
1054            },
1055            cst_v1::TypeBodyV1::Synonym(ty) => {
1056                cst::TypeDeclBody::Synonym(lower_type_expr(ty, tyenv)?)
1057            }
1058        },
1059        // 0.1's `type … and …` chain is lowered to CONSECUTIVE 0.0.6
1060        // `TopBinding::Type`s (one per clause), so each carries no own `and`.
1061        ands: Vec::new(),
1062    }))
1063}
1064
1065fn lower_variant_def(
1066    v: &cst_v1::VariantDefV1,
1067    tyenv: &TypeNameEnv,
1068) -> Result<cst::VariantDef, LowerError> {
1069    Ok(cst::VariantDef {
1070        // Ctors stay UNQUALIFIED — the `TypeNameEnv` carve-out.
1071        ctor: v.ctor.clone(),
1072        of_ty: v
1073            .of_ty
1074            .as_ref()
1075            .map(|o| {
1076                Ok(cst::OfType {
1077                    of_kw: o.of_kw.clone(),
1078                    ty: lower_type_expr(&o.ty, tyenv)?,
1079                })
1080            })
1081            .transpose()?,
1082    })
1083}
1084
1085pub(crate) fn lower_type_expr(
1086    t: &ast_v1::TypeExpr,
1087    tyenv: &TypeNameEnv,
1088) -> Result<cst::ast::TypeExpr, LowerError> {
1089    Ok(match t {
1090        ast_v1::TypeExpr::Fun { dom, arrow, cod } => cst::ast::TypeExpr::Fun {
1091            // 0.1 has no `?->` domain-suffix syntax at all (that's 0.0.6's
1092            // own fused sigil, dropped in 0.1) — `opts` here is always empty.
1093            // The 0.1 `?(…) ->` PREFIX form is `ast_v1::TypeExpr::OptRowFun`,
1094            // a separate arm below.
1095            opts: Vec::new(),
1096            dom: lower_type_prod(dom, tyenv)?,
1097            arrow: arrow.clone(),
1098            cod: Box::new(lower_type_expr(cod, tyenv)?),
1099        },
1100        ast_v1::TypeExpr::Atom(p) => cst::ast::TypeExpr::Atom(lower_type_prod(p, tyenv)?),
1101        // `?(l1 : ty1, … [| ?'r]) dom -> cod`. A row-variable tail is
1102        // parsed but rejected here: it needs
1103        // signature-level row quantification (`rowquant`/`quant`,
1104        // `parser_v1.mly:631-633`), which this port does not implement
1105        // (contrast the record-type row-tail below, which is accepted
1106        // — a bare record type has no `quant`-list obligation
1107        // to satisfy).
1108        ast_v1::TypeExpr::OptRowFun {
1109            opt_dom,
1110            dom,
1111            arrow,
1112            cod,
1113        } => {
1114            if let Some(tail) = &opt_dom.inner.row_tail {
1115                return Err(unsupported(
1116                    "a row-variable tail in an optional-argument type domain (`| ?'r`)",
1117                    "row quantification arrives with signature enforcement — \
1118                     roadmap L4 / Sub-slice 2d",
1119                    tail.var.span,
1120                ));
1121            }
1122            if opt_dom.inner.entries.is_empty() {
1123                return Err(unsupported(
1124                    "an empty `?()` optional-argument type domain",
1125                    "a `?(…)` domain must bind at least one label",
1126                    opt_dom.q.0,
1127                ));
1128            }
1129            cst::ast::TypeExpr::OptRowFun {
1130                opt_dom: cst::ast::CstTypeOptDom {
1131                    q: opt_dom.q.clone(),
1132                    paren: clone_paren(&opt_dom.paren),
1133                    entries: opt_dom
1134                        .inner
1135                        .entries
1136                        .iter()
1137                        .map(|e| {
1138                            Ok(cst::ast::CstTypeOptEntry {
1139                                label: e.label.clone(), // labels are NOT type names
1140                                colon: e.colon.clone(),
1141                                ty: cst::TyErased(Box::new(lower_type_expr(&e.ty.0, tyenv)?)),
1142                                comma: e.comma.clone(),
1143                            })
1144                        })
1145                        .collect::<Result<_, LowerError>>()?,
1146                },
1147                dom: lower_type_prod(dom, tyenv)?,
1148                arrow: arrow.clone(),
1149                cod: Box::new(lower_type_expr(cod, tyenv)?),
1150            }
1151        }
1152    })
1153}
1154
1155fn lower_type_prod(
1156    p: &ast_v1::TypeProd,
1157    tyenv: &TypeNameEnv,
1158) -> Result<cst::ast::TypeProd, LowerError> {
1159    Ok(cst::ast::TypeProd {
1160        first: lower_type_app(&p.first, tyenv)?,
1161        rest: p
1162            .rest
1163            .iter()
1164            .map(|s| {
1165                Ok(cst::ast::StarType {
1166                    star: s.star.clone(),
1167                    ty: lower_type_app(&s.ty, tyenv)?,
1168                })
1169            })
1170            .collect::<Result<_, LowerError>>()?,
1171    })
1172}
1173
1174/// The PREFIX → POSTFIX bridge: 0.1 `list int` ↔ cst `int
1175/// list`. A real `LowerError` (not a panic) on arity ≥ 2 — the cst target
1176/// (`cst::ast::TypeApp`) is single-argument by design (`cst.rs:1297-1312`,
1177/// "N-ary applied constructors are not [supported]"). This also adds
1178/// `InlineCmdTy`/`BlockCmdTy` (→ `cst::ast::TypeAtom::Cmd`, wrapped in
1179/// `TypeApp::Atom` — a command type is never itself "applied") and
1180/// `AppliedLong` (the same prefix→postfix bridge as `Applied`, minus
1181/// `tyenv.qualify`: a `LONG_LOWER` head is already absolute).
1182fn lower_type_app(
1183    a: &ast_v1::TypeApp,
1184    tyenv: &TypeNameEnv,
1185) -> Result<cst::ast::TypeApp, LowerError> {
1186    match a {
1187        ast_v1::TypeApp::InlineCmdTy { kw, ilist: list, args } => Ok(cst::ast::TypeApp {
1188            head: cst::ast::TypeAtom::Cmd {
1189                list: list.clone(),
1190                args: lower_type_cmd_args(args, tyenv)?,
1191                kind: cst::ast::CmdTypeKind::Inline(HorzCmdTypeTok(kw.0)),
1192            },
1193            rest: Vec::new(),
1194        }),
1195        ast_v1::TypeApp::BlockCmdTy { kw, blist: list, args } => Ok(cst::ast::TypeApp {
1196            head: cst::ast::TypeAtom::Cmd {
1197                list: list.clone(),
1198                args: lower_type_cmd_args(args, tyenv)?,
1199                kind: cst::ast::CmdTypeKind::Block(VertCmdTypeTok(kw.0)),
1200            },
1201            rest: Vec::new(),
1202        }),
1203        // `math […]`. `lower_type_atom`'s Cmd
1204        // arm in `typecheck.rs` already produces `MonoType::MathCmd` for
1205        // `CmdTypeKind::Math` and `unify.rs` already dispatches it — no
1206        // typecheck/unify change needed for the head itself.
1207        ast_v1::TypeApp::MathCmdTy { kw, mlist: list, args } => Ok(cst::ast::TypeApp {
1208            head: cst::ast::TypeAtom::Cmd {
1209                list: list.clone(),
1210                args: lower_type_cmd_args(args, tyenv)?,
1211                kind: cst::ast::CmdTypeKind::Math(MathCmdTypeTok(kw.0)),
1212            },
1213            rest: Vec::new(),
1214        }),
1215        // `arg1 … argN M.ctor` — the 0.0.6 target is now an N-ary atom run
1216        // (`TypeApp { head, rest }`), so the full argument list lowers with no
1217        // arity ceiling. NO `tyenv.qualify` on a `LONG_LOWER` head: an `M.t`
1218        // ctor is already an absolute dotted reference.
1219        ast_v1::TypeApp::AppliedLong { ctor, first, rest } => lower_applied(
1220            first,
1221            rest,
1222            qualify_type_key(&ctor.mods, &ctor.name),
1223            ctor.span,
1224            tyenv,
1225        ),
1226        ast_v1::TypeApp::Applied { ctor, first, rest } => {
1227            let name = tyenv.qualify(&ctor.name);
1228            lower_applied(first, rest, name, ctor.span, tyenv)
1229        }
1230        ast_v1::TypeApp::Atom(at) => Ok(cst::ast::TypeApp {
1231            head: lower_type_atom(at, tyenv)?,
1232            rest: Vec::new(),
1233        }),
1234    }
1235}
1236
1237/// Lower a v1 applied type constructor (`first`/`rest` arguments + a resolved
1238/// `ctor` name) to a 0.0.6 [`cst::ast::TypeApp`] atom run whose last atom is
1239/// the constructor — see [`cst::ast::TypeApp`]'s doc comment.
1240fn lower_applied(
1241    first: &ast_v1::TypeAtom,
1242    rest: &[ast_v1::TypeAtom],
1243    ctor_name: String,
1244    ctor_span: Span,
1245    tyenv: &TypeNameEnv,
1246) -> Result<cst::ast::TypeApp, LowerError> {
1247    let head = lower_type_atom(first, tyenv)?;
1248    let mut out_rest: Vec<cst::ast::TypeAtom> = Vec::with_capacity(rest.len() + 1);
1249    for a in rest {
1250        out_rest.push(lower_type_atom(a, tyenv)?);
1251    }
1252    out_rest.push(cst::ast::TypeAtom::Name(VarTok {
1253        name: ctor_name,
1254        span: ctor_span,
1255    }));
1256    Ok(cst::ast::TypeApp {
1257        head,
1258        rest: out_rest,
1259    })
1260}
1261
1262/// Each `[…]`-bracketed command-type slot lowers to one
1263/// `cst::ast::TypeCmdArgItem` (`opt: None` — this grammar has no `?`
1264/// suffix of its own; `semi: None` — these synthetic items are never
1265/// re-unparsed, only fed to `elaborate`/`typecheck`, so the `;`-separator
1266/// token is immaterial). `opt_labels`
1267/// carries this slot's `?(l:τ,…)` prefix bundle, if any — a flat,
1268/// *surface-order* list; `typecheck.rs`'s `lower_type_atom` `Cmd` arm is
1269/// responsible for sorting it into the closed map's canonical order (kept
1270/// unsorted here, matching every other lowering site in this file, which
1271/// never itself imposes a canonical order on anything — that's a
1272/// typecheck-time concern).
1273fn lower_type_cmd_args(
1274    args: &[ast_v1::TypeCmdArgItemV1],
1275    tyenv: &TypeNameEnv,
1276) -> Result<Vec<cst::ast::TypeCmdArgItem>, LowerError> {
1277    args.iter()
1278        .map(|a| {
1279            Ok(cst::ast::TypeCmdArgItem {
1280                opt_labels: match &a.opts {
1281                    None => Vec::new(),
1282                    Some(dom) => {
1283                        if dom.entries.is_empty() {
1284                            return Err(unsupported(
1285                                "an empty `?()` command-type optional-label bundle",
1286                                "a `?(…)` bundle must bind at least one label",
1287                                dom.q.0,
1288                            ));
1289                        }
1290                        dom.entries
1291                            .iter()
1292                            .map(|e| {
1293                                Ok(cst::ast::TypeCmdOptField {
1294                                    label: e.label.clone(),
1295                                    colon: e.colon.clone(),
1296                                    ty: cst::TyErased(Box::new(lower_type_expr(&e.ty.0, tyenv)?)),
1297                                    comma: e.comma.clone(),
1298                                })
1299                            })
1300                            .collect::<Result<_, LowerError>>()?
1301                    }
1302                },
1303                ty: cst::TyErased(Box::new(lower_type_expr(&a.ty.0, tyenv)?)),
1304                opt: None,
1305                semi: None,
1306            })
1307        })
1308        .collect()
1309}
1310
1311fn lower_type_atom(
1312    a: &ast_v1::TypeAtom,
1313    tyenv: &TypeNameEnv,
1314) -> Result<cst::ast::TypeAtom, LowerError> {
1315    Ok(match a {
1316        ast_v1::TypeAtom::Paren { paren, inner } => cst::ast::TypeAtom::Paren {
1317            paren: paren.clone(),
1318            inner: cst::TyErased(Box::new(lower_type_expr(&inner.0, tyenv)?)),
1319        },
1320        // Closed form (`row_tail: None`) transcribes to
1321        // `cst::ast::TypeAtom::Record`. Open form (`row_tail: Some(_)`)
1322        // transcribes to the additive
1323        // `cst::ast::TypeAtom::RecordOpen` instead — a fresh row variable at
1324        // the `typecheck.rs` end, not the SAME variable across occurrences
1325        // (this models one open record type at a time, not
1326        // cross-signature shared-row polymorphism).
1327        ast_v1::TypeAtom::Record { rec, inner } if inner.row_tail.is_none() => {
1328            cst::ast::TypeAtom::Record {
1329                rec: rec.clone(),
1330                fields: inner
1331                    .fields
1332                    .iter()
1333                    .map(|f| {
1334                        Ok(cst::ast::TypeRecordField {
1335                            name: f.name.clone(),   // labels are NOT type names —
1336                            colon: f.colon.clone(), // no `tyenv.qualify` on `name`
1337                            ty: cst::TyErased(Box::new(lower_type_expr(&f.ty.0, tyenv)?)),
1338                            // `,` dropped (`semi: None`) — synthetic tree is
1339                            // never unparsed; `lower_record_field`/
1340                            // `lower_type_cmd_args` precedent.
1341                            semi: None,
1342                        })
1343                    })
1344                    .collect::<Result<_, LowerError>>()?,
1345            }
1346        }
1347        ast_v1::TypeAtom::Record { rec, inner } => {
1348            let tail = inner.row_tail.as_ref().expect("guarded by the arm above");
1349            cst::ast::TypeAtom::RecordOpen {
1350                orec: rec.clone(),
1351                inner: cst::ast::CstRecordOpenInner {
1352                    fields: inner
1353                        .fields
1354                        .iter()
1355                        .map(|f| {
1356                            Ok(cst::ast::CstRecordOpenField {
1357                                name: f.name.clone(),
1358                                colon: f.colon.clone(),
1359                                ty: cst::TyErased(Box::new(lower_type_expr(&f.ty.0, tyenv)?)),
1360                                comma: None,
1361                            })
1362                        })
1363                        .collect::<Result<_, LowerError>>()?,
1364                    bar: tail.bar.clone(),
1365                    var: tail.var.clone(),
1366                },
1367            }
1368        }
1369        ast_v1::TypeAtom::Var(v) => cst::ast::TypeAtom::Var(v.clone()),
1370        // NO `tyenv.qualify` — see `AppliedLong`'s doc comment above; the
1371        // same "already absolute" argument applies bare.
1372        ast_v1::TypeAtom::LongName(t) => cst::ast::TypeAtom::Name(VarTok {
1373            name: qualify_type_key(&t.mods, &t.name),
1374            span: t.span,
1375        }),
1376        ast_v1::TypeAtom::Name(n) => cst::ast::TypeAtom::Name(VarTok {
1377            name: tyenv.qualify(&n.name),
1378            span: n.span,
1379        }),
1380    })
1381}
1382
1383
1384fn plain_horz(name: &AnyHorzCmdTok) -> Result<HorzCmdTok, LowerError> {
1385    match name {
1386        AnyHorzCmdTok::Plain(t) => Ok(t.clone()),
1387        AnyHorzCmdTok::Mod(t) => Err(unsupported(
1388            "a module-qualified command name in binding position",
1389            "the cst target field (`LetInline::cmd`) is a bare `HorzCmdTok` \
1390             — not valid 0.1 syntax",
1391            t.span,
1392        )),
1393    }
1394}
1395
1396fn plain_vert(name: &AnyVertCmdTok) -> Result<VertCmdTok, LowerError> {
1397    match name {
1398        AnyVertCmdTok::Plain(t) => Ok(t.clone()),
1399        AnyVertCmdTok::Mod(t) => Err(unsupported(
1400            "a module-qualified command name in binding position",
1401            "the cst target field (`LetBlock::cmd`) is a bare `VertCmdTok` \
1402             — not valid 0.1 syntax",
1403            t.span,
1404        )),
1405    }
1406}
1407
1408/// Lower a `param_unit` list plus the (already-lowered) binding body into a
1409/// `(cst params, cst value)` pair (upstream `curry_lambda_abstraction`, one
1410/// `UTFunction` per `param_unit`).
1411///
1412/// - **Every unit plain** (`opts: None`): the params lower directly and
1413///   `body` is returned unchanged.
1414/// - **Any unit bundled** (`?(l = x, …)`): the whole list right-folds into a
1415///   nested `FunRows`/`Fun` lambda chain, and the returned param list is
1416///   empty — so the target `TopLet`/`RecBinding`/`LetIn` shape stays frozen
1417///   (a bundled binding is `params: []` + a lambda-chain value). This is the
1418///   `f p = e ≡ f = fun p -> e` identity applied per unit.
1419fn lower_param_units(
1420    params: &[cst_v1::Param],
1421    body: cst::ast::Expr,
1422) -> Result<(Vec<cst::ast::Param>, cst::ast::Expr), LowerError> {
1423    if params.iter().all(|p| p.opts.is_none()) {
1424        let ps = params
1425            .iter()
1426            .map(|p| Ok(cst::ast::Param::Pat(lower_param_body(&p.body)?)))
1427            .collect::<Result<_, LowerError>>()?;
1428        return Ok((ps, body));
1429    }
1430    let mut chain = body;
1431    for p in params.iter().rev() {
1432        let param_pat = lower_param_body(&p.body)?;
1433        chain = match &p.opts {
1434            Some(opts) => cst::ast::Expr::FunRows {
1435                kw: KwFun(opts.q.0),
1436                opts: lower_opt_binders(opts)?,
1437                param: param_pat,
1438                arrow: ArrowTok(opts.q.0),
1439                body: Box::new(chain),
1440            },
1441            None => cst::ast::Expr::Fun {
1442                kw: KwFun(Span::default()),
1443                params: vec![param_pat],
1444                arrow: ArrowTok(Span::default()),
1445                body: Box::new(chain),
1446            },
1447        };
1448    }
1449    Ok((Vec::new(), chain))
1450}
1451
1452/// Lower an inline/block/math command binding's own `Param` list, preserving
1453/// order 1:1 (each `cst_v1::Param` maps to exactly one `cst::ast::Param` —
1454/// unlike the value-level `FunRows` desugar, which right-folds a bundled
1455/// unit into a lambda chain and returns an EMPTY param list, a command
1456/// binding's `params` vec carries order straight into `curry_cmd_params_v1`).
1457/// A plain (non-bundled) unit lowers to `Param::Pat` as before; a
1458/// `?(l = x, …)`-bundled unit lowers to the additive
1459/// `cst::ast::Param::Bundled`, consumed by `elaborate.rs`'s bundle-aware
1460/// `curry_cmd_params_v1`. Shared by `ValueInline`/`ValueBlock` (which accept
1461/// a bundle freely) AND `ValueMath` (`lower_value_math` rejects a bundle
1462/// itself, BEFORE calling this — math command parameter bundles use the
1463/// same `?(name=…)` syntax on `val math ctx \derive`).
1464fn lower_command_params(params: &[cst_v1::Param]) -> Result<Vec<cst::ast::Param>, LowerError> {
1465    params
1466        .iter()
1467        .map(|p| match &p.opts {
1468            None => Ok(cst::ast::Param::Pat(lower_param_body(&p.body)?)),
1469            Some(opts) => Ok(cst::ast::Param::Bundled {
1470                opts: lower_opt_binders(opts)?,
1471                body: lower_param_body(&p.body)?,
1472            }),
1473        })
1474        .collect()
1475}
1476
1477/// Transcribe a `?(l = x, …)` parameter-binder bundle to its cst twin. An
1478/// empty bundle (`?()`) is a lowering error.
1479fn lower_opt_binders(opts: &ast_v1::OptParamsV1) -> Result<cst::ast::CstOptBinders, LowerError> {
1480    if opts.entries.is_empty() {
1481        return Err(unsupported(
1482            "an empty `?()` optional-parameter bundle",
1483            "a `?(…)` bundle must bind at least one label",
1484            opts.q.0,
1485        ));
1486    }
1487    Ok(cst::ast::CstOptBinders {
1488        q: opts.q.clone(),
1489        paren: clone_paren(&opts.paren),
1490        entries: opts
1491            .entries
1492            .iter()
1493            .map(|e| cst::ast::CstOptBinderEntry {
1494                label: e.label.clone(),
1495                eq: e.eq.clone(),
1496                var: e.var.clone(),
1497                comma: e.comma.clone(),
1498            })
1499            .collect(),
1500    })
1501}
1502
1503/// Transcribe a `?(l = e, …)` application-argument bundle to its cst twin
1504/// (entry values are full expressions, lowered + erased). Empty (`?()`) is an
1505/// error.
1506fn lower_opt_args(opts: &ast_v1::OptArgsV1) -> Result<cst::ast::CstOptArgs, LowerError> {
1507    if opts.entries.is_empty() {
1508        return Err(unsupported(
1509            "an empty `?()` optional-argument bundle",
1510            "a `?(…)` bundle must supply at least one label",
1511            opts.q.0,
1512        ));
1513    }
1514    Ok(cst::ast::CstOptArgs {
1515        q: opts.q.clone(),
1516        paren: clone_paren(&opts.paren),
1517        entries: opts
1518            .entries
1519            .iter()
1520            .map(|e| {
1521                Ok(cst::ast::CstOptArgEntry {
1522                    label: e.label.clone(),
1523                    eq: e.eq.clone(),
1524                    value: erase_expr(lower_expr(&e.value.0)?),
1525                    comma: e.comma.clone(),
1526                })
1527            })
1528            .collect::<Result<_, LowerError>>()?,
1529    })
1530}
1531
1532/// Clone an empty `ParenGroup<()>` marker (open/close tokens only). The
1533/// `#[group]`-payload slot is `()`, so this just carries the delimiter spans.
1534fn clone_paren(p: &ParenGroup<()>) -> ParenGroup<()> {
1535    ParenGroup {
1536        open: p.open.clone(),
1537        slot: (),
1538        close: p.close.clone(),
1539    }
1540}
1541
1542// ---- `val math`: the target — the EXISTING
1543// `cst::TopBinding::LetMath` — is deliberately NOT extended with ctx/scripts
1544// fields (a back-compat break on the 0.0.6 grammar the same derive parses);
1545// instead the ctx/sub/sup binders are synthesized directly into the bind's
1546// own VALUE as ordinary `cst::ast::Expr::Fun`/application nodes, reusing the
1547// bind's own spans. The synthesis helpers below (`var_atomic`/`atom_expr`/
1548// `paren_atomic`/`apply_chain`/`fun1`) build exactly the small slice of
1549// `cst::ast::Expr` shapes this needs — a bare variable reference, a
1550// parenthesized sub-expression, an application chain, and a one-parameter
1551// lambda — by hand, since this is the one place `v1/lower.rs` needs to
1552// PRODUCE `cst::ast::Expr` nodes rather than transcribe them from a parsed
1553// `cst_v1::ast::Expr`. ----
1554
1555fn var_tok(name: &str, span: Span) -> VarTok {
1556    VarTok {
1557        name: name.to_string(),
1558        span,
1559    }
1560}
1561
1562fn var_atomic(name: &str, span: Span) -> cst::ast::Atomic {
1563    cst::ast::Atomic::Var(var_tok(name, span))
1564}
1565
1566/// `( expr )` as an `Atomic::Paren` — needed to embed an arbitrary `Expr`
1567/// (the `val math` bind's own body) as ONE application argument, since
1568/// `AppArg::Atom.atom: Atomic` can't hold a full `Expr` directly.
1569fn paren_atomic(expr: cst::ast::Expr, span: Span) -> cst::ast::Atomic {
1570    cst::ast::Atomic::Paren {
1571        paren: ParenGroup {
1572            open: LParenTok(span),
1573            slot: (),
1574            close: RParenTok(span),
1575        },
1576        inner: Box::new(cst::ast::ParenBody {
1577            first: cst::ExprErased(Box::new(expr)),
1578            rest: Vec::new(),
1579        }),
1580    }
1581}
1582
1583/// `head arg1 arg2 …` as one `Expr::Ops(OpChain)` node — a curried
1584/// application chain with `args.len()` atomic arguments, one `AppExpr`.
1585fn apply_chain(head: cst::ast::Atomic, args: Vec<cst::ast::Atomic>) -> cst::ast::Expr {
1586    let app_args = args
1587        .into_iter()
1588        .map(|atom| cst::ast::AppArg::Atom {
1589            stage: None,
1590            excl: None,
1591            atom,
1592            accesses: Vec::new(),
1593        })
1594        .collect();
1595    cst::ast::Expr::Ops(cst::ast::OpChain {
1596        head: cst::ast::AppExpr {
1597            minus: None,
1598            stage: None,
1599            excl: None,
1600            head,
1601            head_accesses: Vec::new(),
1602            args: app_args,
1603        },
1604        tail: Vec::new(),
1605        before: None,
1606    })
1607}
1608
1609/// `fun <param> -> <body>` — a one-parameter lambda.
1610fn fun1(param_name: &str, span: Span, body: cst::ast::Expr) -> cst::ast::Expr {
1611    cst::ast::Expr::Fun {
1612        kw: KwFun(span),
1613        params: vec![cst::ast::PatBot::Var(var_tok(param_name, span))],
1614        arrow: ArrowTok(span),
1615        body: Box::new(body),
1616    }
1617}
1618
1619/// Lower one `Bind::ValueMath`: `val math
1620/// <ctx> \cmd <param>* [with <sub> <sup>] = <body>`. Target: the existing
1621/// `cst::TopBinding::LetMath` — `elaborate_let_math` (unchanged) curries the
1622/// user's own `params` around the synthesized `fun ctx -> fun sub -> fun sup
1623/// -> …` chain built here and emits `Ast::LetMathIn`; `Checker::infer_
1624/// binding`'s `LetMath` arm (`typecheck.rs`) then branches on `self.version`
1625/// to apply `math_command_scheme_v01` instead of the 0.0.6 rule — no
1626/// elaborate/eval edit needed at all.
1627///
1628/// - **with `with sub sup`**: the user's own binders become the `sub`/`sup`
1629///   lambda parameters directly; the body is used as written (upstream
1630///   WithScripts: closure run with ctx, body's result returned raw).
1631/// - **without**: hidden binders `%sub`/`%sup` (unlexable — `%` starts a
1632///   comment, so unreachable from real source, the same trick upstream's own
1633///   `"%context"` uses), and the body wrapped as `%math-attach-scripts <ctx>
1634///   (<body>) %sub %sup` (upstream Simple: closure run with ctx, then
1635///   scripts appended under `enter_script`).
1636fn lower_value_math(
1637    kw: &KwVal,
1638    stage: &Option<cst_v1::BindStageV1>,
1639    ctx: &VarTok,
1640    cmd: &AnyHorzCmdTok,
1641    params: &[cst_v1::Param],
1642    scripts: &Option<cst_v1::ScriptsParamV1>,
1643    eq: &DefEqTok,
1644    body: &ast_v1::Expr,
1645) -> Result<cst::TopBinding, LowerError> {
1646    // A `?(l = x, …)` bundle on a `val math` parameter (`val math ctx \derive
1647    // ?(name = …) …`) works: `lower_command_params`
1648    // (shared with `ValueInline`/`ValueBlock`) already
1649    // accepts it freely, `curry_cmd_params_v1` already emits `Ast::LambdaOpt`
1650    // for it (elaborate.rs), and `math_command_scheme_v01` (typecheck.rs) now
1651    // harvests the resulting `Row` into the command's closed label map, same
1652    // as `command_scheme`'s V0_1 branch. No rejection needed here.
1653    let body = lower_expr(body)?;
1654    let span = eq.0;
1655    let (sub_name, sup_name, wrapped_body) = match scripts {
1656        Some(sp) => (sp.sub.name.clone(), sp.sup.name.clone(), body),
1657        None => (
1658            "%sub".to_string(),
1659            "%sup".to_string(),
1660            apply_chain(
1661                var_atomic("%math-attach-scripts", span),
1662                vec![
1663                    var_atomic(&ctx.name, ctx.span),
1664                    paren_atomic(body, span),
1665                    var_atomic("%sub", span),
1666                    var_atomic("%sup", span),
1667                ],
1668            ),
1669        ),
1670    };
1671    let value = fun1(
1672        &ctx.name,
1673        ctx.span,
1674        fun1(&sub_name, span, fun1(&sup_name, span, wrapped_body)),
1675    );
1676    Ok(cst::TopBinding::LetMath {
1677        kw: KwLetMath(kw.0),
1678        stage: stage.as_ref().map(lower_bind_stage),
1679        cmd: plain_horz(cmd)?,
1680        params: lower_command_params(params)?,
1681        eq: eq.clone(),
1682        value,
1683    })
1684}
1685
1686// ---- Expr ---------------------------------------------------------------
1687
1688fn lower_expr(e: &ast_v1::Expr) -> Result<cst::ast::Expr, LowerError> {
1689    match e {
1690        ast_v1::Expr::LetRecIn {
1691            let_kw,
1692            first,
1693            ands,
1694            in_kw,
1695            body,
1696            ..
1697        } => Ok(cst::ast::Expr::LetRecIn {
1698            // Span-lossy on `rec` (synthetic tree, never unparsed).
1699            kw: KwLetRec(let_kw.0),
1700            first: lower_rec_clause(first)?,
1701            ands: ands
1702                .iter()
1703                .map(|a| {
1704                    Ok(cst::ast::AndBinding {
1705                        and_kw: a.and_kw.clone(),
1706                        binding: lower_rec_clause(&a.clause)?,
1707                    })
1708                })
1709                .collect::<Result<_, LowerError>>()?,
1710            in_kw: in_kw.clone(),
1711            body: Box::new(lower_expr(body)?),
1712        }),
1713        ast_v1::Expr::LetMutableIn {
1714            let_kw,
1715            name,
1716            arrow,
1717            init,
1718            in_kw,
1719            body,
1720            ..
1721        } => Ok(cst::ast::Expr::LetMutableIn {
1722            kw: KwLetMutable(let_kw.0),
1723            name: name.clone(),
1724            arrow: arrow.clone(),
1725            init: Box::new(lower_expr(init)?),
1726            in_kw: in_kw.clone(),
1727            body: Box::new(lower_expr(body)?),
1728        }),
1729        ast_v1::Expr::LetIn {
1730            kw,
1731            name,
1732            params,
1733            eq,
1734            value,
1735            in_kw,
1736            body,
1737        } => {
1738            let (ps, value_expr) = lower_param_units(params, lower_expr(value)?)?;
1739            Ok(cst::ast::Expr::LetIn {
1740                kw: kw.clone(),
1741                name: name.clone(),
1742                ascription: None,
1743                // 0.1's grammar has no `nonrecdecargpart` bar to lower.
1744                leading_bar: None,
1745                params: ps,
1746                eq: eq.clone(),
1747                value: Box::new(value_expr),
1748                in_kw: in_kw.clone(),
1749                body: Box::new(lower_expr(body)?),
1750            })
1751        }
1752        ast_v1::Expr::LetPatternIn {
1753            kw,
1754            pat,
1755            eq,
1756            value,
1757            in_kw,
1758            body,
1759        } => Ok(cst::ast::Expr::LetPatternIn {
1760            kw: kw.clone(),
1761            pat: erase_pat_non_var(lower_pattern(pat)?),
1762            eq: eq.clone(),
1763            value: Box::new(lower_expr(value)?),
1764            in_kw: in_kw.clone(),
1765            body: Box::new(lower_expr(body)?),
1766        }),
1767        ast_v1::Expr::OpenIn {
1768            open_kw,
1769            name,
1770            in_kw,
1771            body,
1772            ..
1773        } => Ok(cst::ast::Expr::OpenIn {
1774            kw: open_kw.clone(),
1775            name: name.clone(),
1776            in_kw: in_kw.clone(),
1777            body: Box::new(lower_expr(body)?),
1778        }),
1779        ast_v1::Expr::If {
1780            kw,
1781            cond,
1782            then_kw,
1783            then_branch,
1784            else_kw,
1785            else_branch,
1786        } => Ok(cst::ast::Expr::If {
1787            kw: kw.clone(),
1788            cond: Box::new(lower_expr(cond)?),
1789            then_kw: then_kw.clone(),
1790            then_branch: Box::new(lower_expr(then_branch)?),
1791            else_kw: else_kw.clone(),
1792            else_branch: Box::new(lower_expr(else_branch)?),
1793        }),
1794        ast_v1::Expr::Fun {
1795            kw,
1796            params,
1797            arrow,
1798            body,
1799        } => {
1800            let body_expr = lower_expr(body)?;
1801            if params.iter().all(|p| p.opts.is_none()) {
1802                Ok(cst::ast::Expr::Fun {
1803                    kw: kw.clone(),
1804                    params: params
1805                        .iter()
1806                        .map(|p| lower_param_body(&p.body))
1807                        .collect::<Result<_, _>>()?,
1808                    arrow: arrow.clone(),
1809                    body: Box::new(body_expr),
1810                })
1811            } else {
1812                // Any `?(l = x, …)` bundle: the whole `fun` desugars to a
1813                // nested `FunRows`/`Fun` lambda chain (returned directly as
1814                // the lambda expression).
1815                let (_, chain) = lower_param_units(params, body_expr)?;
1816                Ok(chain)
1817            }
1818        }
1819        ast_v1::Expr::Match {
1820            kw,
1821            scrutinee,
1822            with_kw,
1823            leading_bar,
1824            first,
1825            rest,
1826            ..
1827        } => Ok(cst::ast::Expr::Match {
1828            kw: kw.clone(),
1829            scrutinee: Box::new(lower_expr(scrutinee)?),
1830            with_kw: with_kw.clone(),
1831            leading_bar: leading_bar.clone(),
1832            first: lower_match_arm(first)?,
1833            rest: rest.iter().map(lower_bar_arm).collect::<Result<_, _>>()?,
1834        }),
1835        ast_v1::Expr::Overwrite { name, arrow, value } => Ok(cst::ast::Expr::Overwrite {
1836            name: name.clone(),
1837            arrow: arrow.clone(),
1838            value: erase_expr(lower_expr(value)?),
1839        }),
1840        ast_v1::Expr::Ops(chain) => Ok(cst::ast::Expr::Ops(lower_op_chain(chain)?)),
1841    }
1842}
1843
1844fn lower_match_arm(a: &ast_v1::MatchArm) -> Result<cst::ast::MatchArm, LowerError> {
1845    Ok(cst::ast::MatchArm {
1846        pat: erase_pat(lower_pattern(&a.pat)?),
1847        guard: None,
1848        arrow: a.arrow.clone(),
1849        body: erase_expr(lower_expr(&a.body)?),
1850    })
1851}
1852
1853fn lower_bar_arm(a: &ast_v1::BarArm) -> Result<cst::ast::BarArm, LowerError> {
1854    Ok(cst::ast::BarArm {
1855        bar: a.bar.clone(),
1856        arm: lower_match_arm(&a.arm)?,
1857    })
1858}
1859
1860fn lower_op_chain(c: &ast_v1::OpChain) -> Result<cst::ast::OpChain, LowerError> {
1861    Ok(cst::ast::OpChain {
1862        head: lower_app_expr(&c.head)?,
1863        tail: c.tail.iter().map(lower_op_rhs).collect::<Result<_, _>>()?,
1864        before: None,
1865    })
1866}
1867
1868fn lower_op_rhs(r: &ast_v1::OpRhs) -> Result<cst::ast::OpRhs, LowerError> {
1869    Ok(cst::ast::OpRhs {
1870        op: r.op.clone(),
1871        rhs: lower_app_expr(&r.rhs)?,
1872    })
1873}
1874
1875/// `~` / `persistent ~` (`cst_v1::BindStageV1` -> `cst::TopStage`) — a
1876/// `val`'s own stage qualifier. Token-identical types on both sides
1877/// (`cst::TopStage` exists ONLY to receive this), so, like
1878/// [`lower_stage_prefix`], this moves tokens.
1879fn lower_bind_stage(s: &cst_v1::BindStageV1) -> cst::TopStage {
1880    cst::TopStage {
1881        persistent: s.persistent.clone(),
1882        tilde: s.tilde.clone(),
1883    }
1884}
1885
1886/// `&`/`~` (`cst_v1::ast::StagePrefix` -> `cst::ast::StagePrefix`). 0.1 has
1887/// staging BOTH per binding (`val ~x = e`) and per operand (`&e`/`~e`,
1888/// `parser_v1.mly:870-873`) — the two productions are character-identical to
1889/// 0.0.6's, so this moves tokens and nothing else.
1890fn lower_stage_prefix(s: &ast_v1::StagePrefix) -> cst::ast::StagePrefix {
1891    match s {
1892        ast_v1::StagePrefix::Next(t) => cst::ast::StagePrefix::Next(t.clone()),
1893        ast_v1::StagePrefix::Prev(t) => cst::ast::StagePrefix::Prev(t.clone()),
1894    }
1895}
1896
1897fn lower_app_expr(e: &ast_v1::AppExpr) -> Result<cst::ast::AppExpr, LowerError> {
1898    Ok(cst::ast::AppExpr {
1899        minus: e.minus.clone(),
1900        stage: e.stage.as_ref().map(lower_stage_prefix),
1901        excl: e.excl.clone(),
1902        head: lower_atomic(&e.head)?,
1903        head_accesses: e.head_accesses.iter().map(lower_access_seg).collect(),
1904        args: e.args.iter().map(lower_app_arg).collect::<Result<_, _>>()?,
1905    })
1906}
1907
1908fn lower_access_seg(a: &ast_v1::AccessSeg) -> cst::ast::AccessSeg {
1909    cst::ast::AccessSeg {
1910        hash: a.hash.clone(),
1911        label: a.label.clone(),
1912    }
1913}
1914
1915fn lower_app_arg(a: &ast_v1::AppArg) -> Result<cst::ast::AppArg, LowerError> {
1916    match a {
1917        // `?(l = e, …) atom` — a SATySFi 0.1 labeled-optional bundle paired
1918        // with its following positional argument.
1919        ast_v1::AppArg::Bundled {
1920            opts,
1921            excl,
1922            atom,
1923            accesses,
1924        } => Ok(cst::ast::AppArg::Bundled {
1925            opts: lower_opt_args(opts)?,
1926            excl: excl.clone(),
1927            atom: lower_atomic(atom)?,
1928            accesses: accesses.iter().map(lower_access_seg).collect(),
1929        }),
1930        ast_v1::AppArg::BundledCtor { opts, ctor } => Ok(cst::ast::AppArg::BundledCtor {
1931            opts: lower_opt_args(opts)?,
1932            ctor: ctor.clone(),
1933        }),
1934        ast_v1::AppArg::Atom {
1935            stage,
1936            excl,
1937            atom,
1938            accesses,
1939        } => Ok(cst::ast::AppArg::Atom {
1940            stage: stage.as_ref().map(lower_stage_prefix),
1941            excl: excl.clone(),
1942            atom: lower_atomic(atom)?,
1943            accesses: accesses.iter().map(lower_access_seg).collect(),
1944        }),
1945        ast_v1::AppArg::Ctor(t) => Ok(cst::ast::AppArg::Ctor(t.clone())),
1946    }
1947}
1948
1949fn lower_atomic(a: &ast_v1::Atomic) -> Result<cst::ast::Atomic, LowerError> {
1950    match a {
1951        ast_v1::Atomic::Length(t) => Ok(cst::ast::Atomic::Length(t.clone())),
1952        ast_v1::Atomic::Float(t) => Ok(cst::ast::Atomic::Float(t.clone())),
1953        ast_v1::Atomic::Int(t) => Ok(cst::ast::Atomic::Int(t.clone())),
1954        ast_v1::Atomic::Literal(t) => Ok(cst::ast::Atomic::Literal(t.clone())),
1955        ast_v1::Atomic::True(t) => Ok(cst::ast::Atomic::True(t.clone())),
1956        ast_v1::Atomic::False(t) => Ok(cst::ast::Atomic::False(t.clone())),
1957        ast_v1::Atomic::Ctor(t) => Ok(cst::ast::Atomic::Ctor(t.clone())),
1958        ast_v1::Atomic::Var(t) => Ok(cst::ast::Atomic::Var(t.clone())),
1959        ast_v1::Atomic::VarWithMod(t) => Ok(cst::ast::Atomic::VarWithMod(t.clone())),
1960        ast_v1::Atomic::Command { kw, name } => Ok(cst::ast::Atomic::Command {
1961            kw: kw.clone(),
1962            name: name.clone(),
1963        }),
1964        ast_v1::Atomic::Unit { paren } => Ok(cst::ast::Atomic::Unit {
1965            paren: paren.clone(),
1966        }),
1967        ast_v1::Atomic::Paren { paren, inner } => Ok(cst::ast::Atomic::Paren {
1968            paren: paren.clone(),
1969            inner: Box::new(lower_paren_body(inner)?),
1970        }),
1971        ast_v1::Atomic::Record { rec, body } => Ok(cst::ast::Atomic::Record {
1972            rec: rec.clone(),
1973            body: lower_record_body(body)?,
1974        }),
1975        ast_v1::Atomic::List { list, items } => Ok(cst::ast::Atomic::List {
1976            list: list.clone(),
1977            items: items
1978                .iter()
1979                .map(lower_list_item)
1980                .collect::<Result<_, _>>()?,
1981        }),
1982        ast_v1::Atomic::InlineText { igrp, elems } => Ok(cst::ast::Atomic::InlineText {
1983            igrp: igrp.clone(),
1984            elems: elems
1985                .iter()
1986                .map(lower_inline_elem)
1987                .collect::<Result<_, _>>()?,
1988        }),
1989        ast_v1::Atomic::BlockText { bgrp, elems } => Ok(cst::ast::Atomic::BlockText {
1990            bgrp: bgrp.clone(),
1991            elems: elems
1992                .iter()
1993                .map(lower_block_elem)
1994                .collect::<Result<_, _>>()?,
1995        }),
1996        // The `${…}`/`math`-elaboration split lives version-independently
1997        // in `elaborate.rs`'s SHARED math path — only structural
1998        // transcription is needed here, like every other `Atomic` arm.
1999        ast_v1::Atomic::MathText { mgrp, elems } => Ok(cst::ast::Atomic::MathText {
2000            mgrp: mgrp.clone(),
2001            elems: lower_math_elems(elems)?,
2002        }),
2003    }
2004}
2005
2006fn lower_record_body(b: &ast_v1::RecordBody) -> Result<cst::ast::RecordBody, LowerError> {
2007    match b {
2008        ast_v1::RecordBody::Update {
2009            base,
2010            with_kw,
2011            fields,
2012        } => Ok(cst::ast::RecordBody::Update {
2013            base: erase_expr(lower_expr(base)?),
2014            with_kw: with_kw.clone(),
2015            fields: fields
2016                .iter()
2017                .map(lower_record_field)
2018                .collect::<Result<_, _>>()?,
2019        }),
2020        ast_v1::RecordBody::Fields(fields) => Ok(cst::ast::RecordBody::Fields(
2021            fields
2022                .iter()
2023                .map(lower_record_field)
2024                .collect::<Result<_, _>>()?,
2025        )),
2026    }
2027}
2028
2029fn lower_record_field(f: &ast_v1::RecordField) -> Result<cst::ast::RecordField, LowerError> {
2030    Ok(cst::ast::RecordField {
2031        name: f.name.clone(),
2032        eq: f.eq.clone(),
2033        value: erase_expr(lower_expr(&f.value)?),
2034        // The `,` separator is dropped (`semi: None`) — harmless: the
2035        // synthetic tree this module builds is never unparsed.
2036        semi: None,
2037    })
2038}
2039
2040fn lower_paren_body(b: &ast_v1::ParenBody) -> Result<cst::ast::ParenBody, LowerError> {
2041    Ok(cst::ast::ParenBody {
2042        first: erase_expr(lower_expr(&b.first)?),
2043        rest: b
2044            .rest
2045            .iter()
2046            .map(lower_comma_expr)
2047            .collect::<Result<_, _>>()?,
2048    })
2049}
2050
2051fn lower_comma_expr(c: &ast_v1::CommaExpr) -> Result<cst::ast::CommaExpr, LowerError> {
2052    Ok(cst::ast::CommaExpr {
2053        comma: c.comma.clone(),
2054        value: erase_expr(lower_expr(&c.value)?),
2055    })
2056}
2057
2058fn lower_list_item(i: &ast_v1::ListItem) -> Result<cst::ast::ListItem, LowerError> {
2059    Ok(cst::ast::ListItem {
2060        value: erase_expr(lower_expr(&i.value)?),
2061        semi: None,
2062    })
2063}
2064
2065fn lower_inline_elem(e: &ast_v1::InlineElem) -> Result<cst::ast::InlineElem, LowerError> {
2066    match e {
2067        ast_v1::InlineElem::Char(t) => Ok(cst::ast::InlineElem::Char(t.clone())),
2068        ast_v1::InlineElem::CodeText(t) => Ok(cst::ast::InlineElem::CodeText(t.clone())),
2069        ast_v1::InlineElem::Space(t) => Ok(cst::ast::InlineElem::Space(t.clone())),
2070        ast_v1::InlineElem::Break(t) => Ok(cst::ast::InlineElem::Break(t.clone())),
2071        ast_v1::InlineElem::Embed { var, semi } => Ok(cst::ast::InlineElem::Embed {
2072            var: var.clone(),
2073            semi: semi.clone(),
2074        }),
2075        // Mechanical transcription, mirror of `Atomic::MathText` above.
2076        ast_v1::InlineElem::EmbedMath { mgrp, elems } => Ok(cst::ast::InlineElem::EmbedMath {
2077            mgrp: mgrp.clone(),
2078            elems: lower_math_elems(elems)?,
2079        }),
2080        ast_v1::InlineElem::Cmd { name, tail } => Ok(cst::ast::InlineElem::Cmd {
2081            name: name.clone(),
2082            tail: lower_cmd_tail(tail)?,
2083        }),
2084        ast_v1::InlineElem::ItemBullet(t) => Ok(cst::ast::InlineElem::ItemBullet(t.clone())),
2085        ast_v1::InlineElem::Sep(t) => Ok(cst::ast::InlineElem::Sep(t.clone())),
2086    }
2087}
2088
2089fn lower_block_elem(e: &ast_v1::BlockElem) -> Result<cst::ast::BlockElem, LowerError> {
2090    match e {
2091        ast_v1::BlockElem::Embed { var, semi } => Ok(cst::ast::BlockElem::Embed {
2092            var: var.clone(),
2093            semi: semi.clone(),
2094        }),
2095        ast_v1::BlockElem::Cmd { name, tail } => Ok(cst::ast::BlockElem::Cmd {
2096            name: name.clone(),
2097            tail: lower_cmd_tail(tail)?,
2098        }),
2099    }
2100}
2101
2102/// The `CmdTail` bridge — the one non-1:1
2103/// transcription. `cst_v1` kept the OLD "one application-chain `Expr`"
2104/// argument encoding (`Args { args: ExprErasedV1, semi }`), while `cst.rs`
2105/// has since moved to the flat `AppArg` list (`Args { first, rest, semi }`).
2106/// Semantics-preserving by construction: `cst_v1` parses `\cmd{a}{b}` as
2107/// `AppExpr { head: {a}, args: [{b}] }`, and `cst` parses the same surface
2108/// as `first = {a}, rest = [{b}]` — this bridge maps one onto the other
2109/// exactly. The two error arms are unreachable from token streams the
2110/// `cst_v1` grammar can actually produce in command-tail position (a bare
2111/// application chain with no operator/negation) — `LowerError` (not
2112/// `unreachable!()`) keeps a future grammar-drift bug user-visible rather
2113/// than silently mis-nesting arguments.
2114fn lower_cmd_tail(t: &ast_v1::CmdTail) -> Result<cst::ast::CmdTail, LowerError> {
2115    match t {
2116        ast_v1::CmdTail::Semi(s) => Ok(cst::ast::CmdTail::Semi(s.clone())),
2117        ast_v1::CmdTail::Args {
2118            lead_opts,
2119            args,
2120            semi,
2121        } => {
2122            let ast_v1::Expr::Ops(chain) = &*args.0 else {
2123                return Err(unsupported(
2124                    "command arguments that are not a plain application chain",
2125                    "grammar-drift guard — the cst_v1 grammar cannot actually \
2126                     produce this shape in command-tail position",
2127                    Span::default(),
2128                ));
2129            };
2130            if !chain.tail.is_empty() || chain.head.minus.is_some() {
2131                return Err(unsupported(
2132                    "an operator or unary negation inside a command argument chain",
2133                    "grammar-drift guard — the cst_v1 grammar cannot actually \
2134                     produce this shape in command-tail position",
2135                    Span::default(),
2136                ));
2137            }
2138            let a = &chain.head;
2139            // A LEADING `?(l = e, …)` bundle
2140            // (peeled off in the grammar because the app-chain head can't be
2141            // `?`-headed — see `cst_v1::CmdTail`) re-attaches to the first
2142            // argument here, producing a `cst::ast::AppArg::Bundled` exactly
2143            // as an ordinary mid-chain bundle would. An empty `?()` is a
2144            // `LowerError` via `lower_opt_args`.
2145            let first = cst::AppArgErased(Box::new(match lead_opts {
2146                Some(opts) => {
2147                    // `cst::ast::AppArg::Bundled` has no `stage` slot (0.0.6
2148                    // has no labeled-optional bundles at all, so the two
2149                    // features never met there). Refuse rather than drop the
2150                    // prefix on the floor and silently run the argument at the
2151                    // wrong stage.
2152                    if a.stage.is_some() {
2153                        return Err(unsupported(
2154                            "a staging prefix on a command argument that also \
2155                             carries a `?(l = e, …)` bundle",
2156                            "the lowered 0.0.6 node has no stage slot on a \
2157                             bundled argument; write the `&`/`~` inside the \
2158                             parenthesized argument instead",
2159                            Span::default(),
2160                        ));
2161                    }
2162                    cst::ast::AppArg::Bundled {
2163                        opts: lower_opt_args(opts)?,
2164                        excl: a.excl.clone(),
2165                        atom: lower_atomic(&a.head)?,
2166                        accesses: a.head_accesses.iter().map(lower_access_seg).collect(),
2167                    }
2168                }
2169                None => cst::ast::AppArg::Atom {
2170                    stage: a.stage.as_ref().map(lower_stage_prefix),
2171                    excl: a.excl.clone(),
2172                    atom: lower_atomic(&a.head)?,
2173                    accesses: a.head_accesses.iter().map(lower_access_seg).collect(),
2174                },
2175            }));
2176            let rest = a
2177                .args
2178                .iter()
2179                .map(|arg| Ok(cst::AppArgErased(Box::new(lower_app_arg(arg)?))))
2180                .collect::<Result<Vec<_>, LowerError>>()?;
2181            Ok(cst::ast::CmdTail::Args {
2182                first,
2183                rest,
2184                semi: semi.clone(),
2185            })
2186        }
2187    }
2188}
2189
2190// ---- Math ---------------------------------------------------------------
2191//
2192// The cst_v1 math layer is declared shape-identical to `crate::cst::ast`'s
2193// (cst_v1.rs's own module doc comment: "no 0.1 delta"), so every function
2194// below is a field-wise transcription, exactly like the rest of this
2195// module — the ONE non-mechanical seam is `lower_math_arg`'s bridge from
2196// cst_v1's flat 6-variant `MathArg` (no `?:`/`?*` forms at all — 0.1's
2197// optional math-command arguments are `?(l = e)` labeled bundles, a
2198// grammar production this port's `MathArg` node doesn't parse yet)
2199// onto cst.rs's two-level `MathArg::Plain(MathArgBody::…)` shape.
2200
2201fn lower_math_elems(elems: &[cst_v1::MathErasedV1]) -> Result<Vec<cst::MathErased>, LowerError> {
2202    elems
2203        .iter()
2204        .map(|e| Ok(cst::MathErased(Box::new(lower_math_elem_cst(e)?))))
2205        .collect()
2206}
2207
2208fn lower_math_elem_cst(m: &ast_v1::MathElemCst) -> Result<cst::ast::MathElemCst, LowerError> {
2209    Ok(cst::ast::MathElemCst {
2210        base: lower_math_bot(&m.base)?,
2211        scripts: m
2212            .scripts
2213            .iter()
2214            .map(lower_math_script)
2215            .collect::<Result<_, _>>()?,
2216    })
2217}
2218
2219fn lower_math_bot(b: &ast_v1::MathBot) -> Result<cst::ast::MathBot, LowerError> {
2220    Ok(match b {
2221        ast_v1::MathBot::Cmd { name, args } => cst::ast::MathBot::Cmd {
2222            // `ast_v1::MathBot::Cmd.name` is ALREADY `AnyMathCmdTok` — the
2223            // exact same tag cst.rs's 0.0.6 `MathBot` carries — so no
2224            // `Plain` wrapping is needed here, just carry it through. It must
2225            // stay `AnyMathCmdTok`: a sigil-only `MathCmdTok` silently cannot
2226            // parse a `\Mod.cmd` qualified math command, even though the
2227            // shared lexer always emits `Token::MathCmdWithMod` for one.
2228            name: name.clone(),
2229            args: args.iter().map(lower_math_arg).collect::<Result<_, _>>()?,
2230        },
2231        ast_v1::MathBot::Chars(t) => cst::ast::MathBot::Chars(t.clone()),
2232        ast_v1::MathBot::Embed(t) => cst::ast::MathBot::Embed(t.clone()),
2233        ast_v1::MathBot::Sep(t) => cst::ast::MathBot::Sep(t.clone()),
2234        ast_v1::MathBot::Group { mgrp, elems } => cst::ast::MathBot::Group {
2235            mgrp: mgrp.clone(),
2236            elems: lower_math_elems(elems)?,
2237        },
2238    })
2239}
2240
2241fn lower_math_script(s: &ast_v1::MathScript) -> Result<cst::ast::MathScript, LowerError> {
2242    Ok(match s {
2243        ast_v1::MathScript::Super { hat, group } => cst::ast::MathScript::Super {
2244            hat: hat.clone(),
2245            group: lower_math_group_arg(group)?,
2246        },
2247        ast_v1::MathScript::Sub { under, group } => cst::ast::MathScript::Sub {
2248            under: under.clone(),
2249            group: lower_math_group_arg(group)?,
2250        },
2251        ast_v1::MathScript::Primes(t) => cst::ast::MathScript::Primes(t.clone()),
2252    })
2253}
2254
2255fn lower_math_group_arg(g: &ast_v1::MathGroupArg) -> Result<cst::ast::MathGroupArg, LowerError> {
2256    Ok(match g {
2257        ast_v1::MathGroupArg::Group { mgrp, elems } => cst::ast::MathGroupArg::Group {
2258            mgrp: mgrp.clone(),
2259            elems: lower_math_elems(elems)?,
2260        },
2261        ast_v1::MathGroupArg::Bot(b) => cst::ast::MathGroupArg::Bot(Box::new(lower_math_bot(b)?)),
2262    })
2263}
2264
2265fn lower_math_arg(a: &ast_v1::MathArg) -> Result<cst::ast::MathArg, LowerError> {
2266    Ok(cst::ast::MathArg::Plain(match a {
2267        ast_v1::MathArg::Math { mgrp, elems } => cst::ast::MathArgBody::Math {
2268            mgrp: mgrp.clone(),
2269            elems: lower_math_elems(elems)?,
2270        },
2271        ast_v1::MathArg::Inline { igrp, elems } => cst::ast::MathArgBody::Inline {
2272            igrp: igrp.clone(),
2273            elems: elems
2274                .iter()
2275                .map(lower_inline_elem)
2276                .collect::<Result<_, _>>()?,
2277        },
2278        ast_v1::MathArg::Block { bgrp, elems } => cst::ast::MathArgBody::Block {
2279            bgrp: bgrp.clone(),
2280            elems: elems
2281                .iter()
2282                .map(lower_block_elem)
2283                .collect::<Result<_, _>>()?,
2284        },
2285        ast_v1::MathArg::ParenEscape { paren, inner } => cst::ast::MathArgBody::ParenEscape {
2286            paren: paren.clone(),
2287            inner: Box::new(lower_paren_body(inner)?),
2288        },
2289        ast_v1::MathArg::ListEscape { list, items } => cst::ast::MathArgBody::ListEscape {
2290            list: list.clone(),
2291            items: items
2292                .iter()
2293                .map(lower_list_item)
2294                .collect::<Result<_, _>>()?,
2295        },
2296        ast_v1::MathArg::RecordEscape { rec, body } => cst::ast::MathArgBody::RecordEscape {
2297            rec: rec.clone(),
2298            body: lower_record_body(body)?,
2299        },
2300    }))
2301}
2302
2303// ---- Pattern layer --------------------------------------------------------
2304
2305fn lower_pattern(p: &ast_v1::Pattern) -> Result<cst::ast::Pattern, LowerError> {
2306    Ok(cst::ast::Pattern {
2307        head: lower_pat_cons(&p.head)?,
2308        as_clause: p.as_clause.as_ref().map(lower_as_clause),
2309    })
2310}
2311
2312fn lower_as_clause(a: &ast_v1::AsClause) -> cst::ast::AsClause {
2313    cst::ast::AsClause {
2314        as_kw: a.as_kw.clone(),
2315        name: a.name.clone(),
2316    }
2317}
2318
2319fn lower_pat_cons(c: &ast_v1::PatCons) -> Result<cst::ast::PatCons, LowerError> {
2320    Ok(cst::ast::PatCons {
2321        head: lower_pat_bot(&c.head)?,
2322        tail: c
2323            .tail
2324            .iter()
2325            .map(lower_cons_seg)
2326            .collect::<Result<_, _>>()?,
2327    })
2328}
2329
2330fn lower_cons_seg(s: &ast_v1::ConsSeg) -> Result<cst::ast::ConsSeg, LowerError> {
2331    Ok(cst::ast::ConsSeg {
2332        cons: s.cons.clone(),
2333        tail: lower_pat_bot(&s.tail)?,
2334    })
2335}
2336
2337fn lower_pat_bot(p: &ast_v1::PatBot) -> Result<cst::ast::PatBot, LowerError> {
2338    match p {
2339        ast_v1::PatBot::CtorApplied { ctor, arg } => Ok(cst::ast::PatBot::CtorApplied {
2340            ctor: ctor.clone(),
2341            arg: Box::new(lower_pat_bot(arg)?),
2342        }),
2343        ast_v1::PatBot::Ctor(t) => Ok(cst::ast::PatBot::Ctor(t.clone())),
2344        ast_v1::PatBot::Int(t) => Ok(cst::ast::PatBot::Int(t.clone())),
2345        ast_v1::PatBot::True(t) => Ok(cst::ast::PatBot::True(t.clone())),
2346        ast_v1::PatBot::False(t) => Ok(cst::ast::PatBot::False(t.clone())),
2347        ast_v1::PatBot::Str(t) => Ok(cst::ast::PatBot::Str(t.clone())),
2348        ast_v1::PatBot::Wild(t) => Ok(cst::ast::PatBot::Wild(t.clone())),
2349        ast_v1::PatBot::Var(t) => Ok(cst::ast::PatBot::Var(t.clone())),
2350        ast_v1::PatBot::Unit { paren } => Ok(cst::ast::PatBot::Unit {
2351            paren: paren.clone(),
2352        }),
2353        ast_v1::PatBot::Paren { paren, inner } => Ok(cst::ast::PatBot::Paren {
2354            paren: paren.clone(),
2355            inner: Box::new(lower_pattern_paren_body(inner)?),
2356        }),
2357        ast_v1::PatBot::List { plist, items } => Ok(cst::ast::PatBot::List {
2358            plist: plist.clone(),
2359            items: items
2360                .iter()
2361                .map(lower_pat_list_item)
2362                .collect::<Result<_, _>>()?,
2363        }),
2364    }
2365}
2366
2367/// A [`ast_v1::Param`]'s trailing shape:
2368/// either a plain `patbot` (unchanged path), or a `( pattern : typ )`
2369/// ascribed pattern. The ascription's `typ` is DROPPED — a documented
2370/// carve-out, precedent `cst::ast::RecBinding.ascription`'s own
2371/// parse-and-ignore (`cst.rs:729-737`; enforcing it needs an `Ast`-level
2372/// ascription node, a later follow-up).
2373/// Once dropped, `( pat : typ )` reduces exactly to a trivially-parenthesized
2374/// FULL pattern — precisely [`cst::ast::PatBot::Paren`]'s own shape (a single
2375/// `first` pattern, no `rest`), since the ascribed form's parens were already
2376/// there in the source.
2377fn lower_param_body(pb: &ast_v1::ParamBody) -> Result<cst::ast::PatBot, LowerError> {
2378    match pb {
2379        ast_v1::ParamBody::Pat(p) => lower_pat_bot(p),
2380        ast_v1::ParamBody::Ascribed { paren, inner } => Ok(cst::ast::PatBot::Paren {
2381            paren: paren.clone(),
2382            inner: Box::new(cst::ast::PatternParenBody {
2383                first: erase_pat(lower_pattern(&inner.pat)?),
2384                rest: Vec::new(),
2385            }),
2386        }),
2387    }
2388}
2389
2390fn lower_pattern_paren_body(
2391    b: &ast_v1::PatternParenBody,
2392) -> Result<cst::ast::PatternParenBody, LowerError> {
2393    Ok(cst::ast::PatternParenBody {
2394        first: erase_pat(lower_pattern(&b.first)?),
2395        rest: b
2396            .rest
2397            .iter()
2398            .map(lower_comma_pattern)
2399            .collect::<Result<_, _>>()?,
2400    })
2401}
2402
2403fn lower_comma_pattern(c: &ast_v1::CommaPattern) -> Result<cst::ast::CommaPattern, LowerError> {
2404    Ok(cst::ast::CommaPattern {
2405        comma: c.comma.clone(),
2406        value: erase_pat(lower_pattern(&c.value)?),
2407    })
2408}
2409
2410fn lower_pat_list_item(i: &ast_v1::PatListItem) -> Result<cst::ast::PatListItem, LowerError> {
2411    Ok(cst::ast::PatListItem {
2412        value: erase_pat(lower_pattern(&i.value)?),
2413        semi: None,
2414    })
2415}
2416
2417// ---- erasure helpers --------------------------------------------------
2418
2419fn erase_expr(e: cst::ast::Expr) -> cst::ExprErased {
2420    cst::ExprErased(Box::new(e))
2421}
2422
2423fn erase_pat(p: cst::ast::Pattern) -> cst::PatErased {
2424    cst::PatErased(Box::new(p))
2425}
2426
2427/// [`erase_pat`] into the destructuring-`let` target, whose parser refuses a
2428/// bare variable (see [`cst::PatNonVarErased`]).
2429///
2430/// Only reachable from a 0.1 `Expr::LetPatternIn`, whose own target is a
2431/// [`cst_v1::PatNonVarErasedV1`] — so the source pattern is already
2432/// non-variable, and `lower_pattern` maps `PatBot::Var` to `PatBot::Var` and
2433/// every other shape to its counterpart, preserving that. The assertion says
2434/// so rather than trusting it: a future lowering that flattened, say, a
2435/// one-element tuple pattern to its element would otherwise synthesize a node
2436/// this grammar cannot re-parse.
2437fn erase_pat_non_var(p: cst::ast::Pattern) -> cst::PatNonVarErased {
2438    debug_assert!(
2439        !p.is_bare_var(),
2440        "a destructuring `let`'s target lowered to a bare variable"
2441    );
2442    cst::PatNonVarErased(Box::new(p))
2443}
2444
2445#[cfg(test)]
2446mod tests {
2447    use super::*;
2448
2449    fn parse_v1(src: &str) -> cst_v1::FileV1 {
2450        rustyfi_syntax::parse_file_v1(src).unwrap_or_else(|e| panic!("v1 parse failed: {e}"))
2451    }
2452
2453    /// Expression-level `let rec … and … in` lowers to a full
2454    /// `Expr::LetRecIn` with the `RecBinding` reshape's 0.1-only fields all
2455    /// empty/`None` (no ascription/leading-bar/multi-clause sugar exists in
2456    /// 0.1's `bind_value_nonrec`).
2457    #[test]
2458    fn let_rec_document_lowers_with_and_chain() {
2459        let file = parse_v1(
2460            "let rec even n = if n <= 0 then true else odd (n - 1)\n\
2461             and odd n = if n <= 0 then false else even (n - 1) in even 4",
2462        );
2463        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2464        let cst::ast::Expr::LetRecIn { first, ands, .. } = ast else {
2465            panic!("expected Expr::LetRecIn");
2466        };
2467        assert_eq!(ands.len(), 1, "one `and` continuation");
2468        assert!(first.ascription.is_none());
2469        assert!(first.leading_bar.is_none());
2470        assert!(first.extra.is_empty());
2471        assert_eq!(first.name.name, "even");
2472        assert_eq!(ands[0].binding.name.name, "odd");
2473    }
2474
2475    /// `val rec … and …` lowers to `TopBinding::LetRec` inside the
2476    /// module's `decls`.
2477    #[test]
2478    fn val_rec_library_lowers_to_top_binding_letrec() {
2479        let file = parse_v1(
2480            "module M = struct\n\
2481             val rec even n = odd n\n\
2482             and odd n = even n\n\
2483             end",
2484        );
2485        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2486        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2487            panic!("expected a TopBinding::Module");
2488        };
2489        assert_eq!(decls.len(), 1);
2490        let cst::TopBinding::LetRec { first, ands, .. } = &*decls[0].0 else {
2491            panic!("expected TopBinding::LetRec, got {:?}", decls[0].0);
2492        };
2493        assert_eq!(first.name.name, "even");
2494        assert_eq!(ands.len(), 1);
2495        assert_eq!(ands[0].binding.name.name, "odd");
2496    }
2497
2498    /// `val mutable` → `TopBinding::LetMutable`.
2499    #[test]
2500    fn val_mutable_lowers_to_top_binding_letmutable() {
2501        let file = parse_v1("module M = struct\nval mutable c <- 0\nend");
2502        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2503        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2504            panic!("expected a TopBinding::Module");
2505        };
2506        assert_eq!(decls.len(), 1);
2507        assert!(
2508            matches!(&*decls[0].0, cst::TopBinding::LetMutable { name, .. } if name.name == "c"),
2509            "{:?}",
2510            decls[0].0
2511        );
2512    }
2513
2514    /// `let mutable … in` → `Expr::LetMutableIn`.
2515    #[test]
2516    fn let_mutable_in_document_lowers_to_expr_letmutablein() {
2517        let file = parse_v1("let mutable c <- 0 in c <- !c + 1");
2518        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2519        assert!(
2520            matches!(&ast, cst::ast::Expr::LetMutableIn { name, .. } if name.name == "c"),
2521            "{ast:?}"
2522        );
2523    }
2524
2525    /// `type t = int and u = t` inside `module M` lowers to TWO
2526    /// consecutive `TopBinding::Type` decls, both names qualified
2527    /// (`"M.t"`/`"M.u"`), and `u`'s synonym body references the ALREADY-
2528    /// qualified `"M.t"` — the pre-qualification pin.
2529    #[test]
2530    fn type_and_chain_inside_module_qualifies_names_and_synonym_reference() {
2531        let file = parse_v1(
2532            "module M = struct\n\
2533             type t = int\n\
2534             and u = t\n\
2535             end",
2536        );
2537        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2538        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2539            panic!("expected a TopBinding::Module");
2540        };
2541        assert_eq!(
2542            decls.len(),
2543            2,
2544            "an `and`-chain lowers to N consecutive Type decls"
2545        );
2546        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2547            panic!("expected decls[0] to be a Type decl");
2548        };
2549        assert_eq!(t_decl.name.name, "M.t");
2550        let cst::TopBinding::Type(u_decl) = &*decls[1].0 else {
2551            panic!("expected decls[1] to be a Type decl");
2552        };
2553        assert_eq!(u_decl.name.name, "M.u");
2554        let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
2555            panic!("expected a synonym body");
2556        };
2557        let cst::ast::TypeExpr::Atom(prod) = ty else {
2558            panic!("expected a bare TypeProd (no arrow)");
2559        };
2560        let cst::ast::TypeApp {
2561            head: cst::ast::TypeAtom::Name(n),
2562            ..
2563        } = &prod.first
2564        else {
2565            panic!("expected a bare type name atom");
2566        };
2567        assert_eq!(
2568            n.name, "M.t",
2569            "u's synonym body must reference the QUALIFIED t"
2570        );
2571    }
2572
2573    /// Nested-module pre-qualification — `module M = struct type
2574    /// t = int module N = struct type u = t end end` → `"M.N.u"`'s body
2575    /// references `"M.t"` (outer types stay visible inside a nested
2576    /// module, per ordinary module scoping).
2577    #[test]
2578    fn nested_module_type_reference_qualifies_to_outer_path() {
2579        let file = parse_v1(
2580            "module M = struct\n\
2581             type t = int\n\
2582             module N = struct\n\
2583             type u = t\n\
2584             end\n\
2585             end",
2586        );
2587        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2588        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2589            panic!("expected a TopBinding::Module");
2590        };
2591        assert_eq!(decls.len(), 2);
2592        let cst::TopBinding::Module {
2593            name: inner_name,
2594            decls: inner_decls,
2595            ..
2596        } = &*decls[1].0
2597        else {
2598            panic!("expected decls[1] to be a nested TopBinding::Module");
2599        };
2600        assert_eq!(inner_name.name, "N");
2601        assert_eq!(inner_decls.len(), 1);
2602        let cst::TopBinding::Type(u_decl) = &*inner_decls[0].0 else {
2603            panic!("expected a Type decl");
2604        };
2605        assert_eq!(u_decl.name.name, "M.N.u");
2606        let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
2607            panic!("expected a synonym body");
2608        };
2609        let cst::ast::TypeExpr::Atom(prod) = ty else {
2610            panic!("expected a bare TypeProd");
2611        };
2612        let cst::ast::TypeApp {
2613            head: cst::ast::TypeAtom::Name(n),
2614            ..
2615        } = &prod.first
2616        else {
2617            panic!("expected a bare type name atom");
2618        };
2619        assert_eq!(
2620            n.name, "M.t",
2621            "the outer M.t must stay visible/qualified inside N"
2622        );
2623    }
2624
2625    /// The prefix→postfix `TypeApp` bridge — `type t = option
2626    /// int` (0.1 prefix, arity 1) lowers to the cst target's postfix
2627    /// `Applied { arg: int, ctor: option }` shape.
2628    #[test]
2629    fn type_app_prefix_to_postfix_bridge() {
2630        let file = parse_v1("module M = struct\ntype t = option int\nend");
2631        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2632        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2633            panic!("expected a TopBinding::Module");
2634        };
2635        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2636            panic!("expected a Type decl");
2637        };
2638        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2639            panic!("expected a synonym body");
2640        };
2641        let cst::ast::TypeExpr::Atom(prod) = ty else {
2642            panic!("expected a bare TypeProd");
2643        };
2644        // Postfix form: `head = int`, `rest = [option]` (the ctor is last).
2645        assert!(prod.first.rest.len() == 1, "{:?}", prod.first);
2646        assert!(
2647            matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "option"),
2648            "{:?}",
2649            prod.first.rest[0]
2650        );
2651        assert!(
2652            matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
2653            "{:?}",
2654            prod.first.head
2655        );
2656    }
2657
2658    /// An applied type constructor with arity ≥ 2 (`pair int
2659    /// int`) lowers to the 0.0.6 cst target's N-ary atom run — `head =
2660    /// int`, `rest = [int, pair]` (the two arguments then the constructor).
2661    #[test]
2662    fn type_app_arity_2_lowers_to_nary_atom_run() {
2663        let file = parse_v1("module M = struct\ntype t = pair int int\nend");
2664        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2665        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2666            panic!("expected a TopBinding::Module");
2667        };
2668        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2669            panic!("expected a Type decl");
2670        };
2671        let cst::TypeDeclBody::Synonym(cst::ast::TypeExpr::Atom(prod)) = &t_decl.body else {
2672            panic!("expected a bare synonym TypeProd");
2673        };
2674        // args = [int, int], ctor = pair → head = int, rest = [int, pair].
2675        assert!(
2676            matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
2677            "{:?}",
2678            prod.first.head
2679        );
2680        assert_eq!(prod.first.rest.len(), 2, "{:?}", prod.first.rest);
2681        assert!(matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "int"));
2682        assert!(matches!(&prod.first.rest[1], cst::ast::TypeAtom::Name(n) if n.name == "pair"));
2683    }
2684
2685    /// A variant↔variant mutual pair (`type a = A of b and b = B
2686    /// of a`) lowers to two consecutive `TopBinding::Type` decls — the
2687    /// forward-reference tolerance `typecheck.rs` provides is exercised at
2688    /// the typecheck/elaborate layer, not here; this only pins the
2689    /// lowering shape.
2690    #[test]
2691    fn mutual_variant_pair_lowers_to_two_type_decls() {
2692        let file = parse_v1(
2693            "module M = struct\n\
2694             type a = A of b\n\
2695             and b = B of a\n\
2696             end",
2697        );
2698        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2699        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2700            panic!("expected a TopBinding::Module");
2701        };
2702        assert_eq!(decls.len(), 2);
2703        assert!(matches!(&*decls[0].0, cst::TopBinding::Type(d) if d.name.name == "M.a"));
2704        assert!(matches!(&*decls[1].0, cst::TopBinding::Type(d) if d.name.name == "M.b"));
2705    }
2706
2707    /// `type t = (| x : int, y : bool |)` lowers to `cst::ast::
2708    /// TypeAtom::Record` with the field list transcribed field-by-field
2709    /// (labels, colons, and lowered field types) — the pure-transcription
2710    /// arm in `lower_type_atom`.
2711    #[test]
2712    fn type_record_lowers_to_cst_record_atom() {
2713        let file = parse_v1("module M = struct\ntype t = (| x : int, y : bool |)\nend");
2714        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2715        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2716            panic!("expected a TopBinding::Module");
2717        };
2718        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2719            panic!("expected a Type decl");
2720        };
2721        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2722            panic!("expected a synonym body");
2723        };
2724        let cst::ast::TypeExpr::Atom(prod) = ty else {
2725            panic!("expected a bare TypeProd");
2726        };
2727        let cst::ast::TypeApp {
2728            head: cst::ast::TypeAtom::Record { fields, .. },
2729            ..
2730        } = &prod.first
2731        else {
2732            panic!("expected TypeAtom::Record, got {:?}", prod.first);
2733        };
2734        assert_eq!(fields.len(), 2);
2735        assert_eq!(fields[0].name.name, "x");
2736        assert!(
2737            matches!(&*fields[0].ty.0, cst::ast::TypeExpr::Atom(p)
2738                if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "int")),
2739            "{:?}",
2740            fields[0].ty.0
2741        );
2742        assert_eq!(fields[1].name.name, "y");
2743        assert!(
2744            matches!(&*fields[1].ty.0, cst::ast::TypeExpr::Atom(p)
2745                if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "bool")),
2746            "{:?}",
2747            fields[1].ty.0
2748        );
2749    }
2750
2751    /// A bare local type name used INSIDE a record type field gets the
2752    /// same `tyenv.qualify` every other type position gets (mirrors
2753    /// `type_and_chain_inside_module_qualifies_names_and_synonym_reference`
2754    /// above, but for a field type rather than a synonym body).
2755    #[test]
2756    fn type_record_field_type_is_qualified_like_any_other_type_position() {
2757        let file = parse_v1(
2758            "module M = struct\n\
2759             type config = int\n\
2760             type t = (| c : config |)\n\
2761             end",
2762        );
2763        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2764        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2765            panic!("expected a TopBinding::Module");
2766        };
2767        assert_eq!(decls.len(), 2);
2768        let cst::TopBinding::Type(t_decl) = &*decls[1].0 else {
2769            panic!("expected decls[1] to be a Type decl");
2770        };
2771        assert_eq!(t_decl.name.name, "M.t");
2772        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2773            panic!("expected a synonym body");
2774        };
2775        let cst::ast::TypeExpr::Atom(prod) = ty else {
2776            panic!("expected a bare TypeProd");
2777        };
2778        let cst::ast::TypeApp {
2779            head: cst::ast::TypeAtom::Record { fields, .. },
2780            ..
2781        } = &prod.first
2782        else {
2783            panic!("expected TypeAtom::Record, got {:?}", prod.first);
2784        };
2785        assert_eq!(fields.len(), 1);
2786        assert_eq!(fields[0].name.name, "c");
2787        let cst::ast::TypeExpr::Atom(field_prod) = &*fields[0].ty.0 else {
2788            panic!("expected a bare TypeProd for the field type");
2789        };
2790        let cst::ast::TypeApp {
2791            head: cst::ast::TypeAtom::Name(n),
2792            ..
2793        } = &field_prod.first
2794        else {
2795            panic!("expected a bare type name atom, got {:?}", field_prod.first);
2796        };
2797        assert_eq!(
2798            n.name, "M.config",
2799            "the field's bare `config` must qualify to M.config"
2800        );
2801    }
2802
2803    /// SATySFi 0.1 dropped the fused `?:`/`?*` optional sigils entirely:
2804    /// under V0_1 the lexer emits only `?` =
2805    /// `OptionalType`, so `?:1`/`?*` now lex as `?` + `:`/`*` — a downstream
2806    /// PARSE error, no longer reaching the lowerer. Replaced by the labeled
2807    /// `?(l = e)` bundle.
2808    #[test]
2809    fn old_optional_sigils_no_longer_parse() {
2810        assert!(rustyfi_syntax::parse_file_v1("f ?:1").is_err());
2811        assert!(rustyfi_syntax::parse_file_v1("f ?*").is_err());
2812    }
2813
2814    /// A `?(l = e, …)` labeled-optional application bundle lowers to
2815    /// `AppArg::Bundled`; an empty `?()` bundle is a `LowerError`.
2816    #[test]
2817    fn empty_opt_arg_bundle_is_a_lower_error() {
2818        let file = parse_v1("f ?() x");
2819        let err = lower_document_v1(&file).unwrap_err();
2820        assert!(
2821            err.to_string().contains("optional-argument bundle"),
2822            "{err}"
2823        );
2824    }
2825
2826    /// `${…}` lowers STRUCTURALLY: its *value* is version-independent (only
2827    /// the surrounding type/prim tables differ, per `typecheck.rs`'s
2828    /// `name_to_mono`). Round-trips `${x}` through `Atomic::MathText` down
2829    /// to its one `MathBot::Chars("x")` element.
2830    #[test]
2831    fn math_text_lowers_structurally() {
2832        let file = parse_v1("${x}");
2833        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2834        let cst::ast::Expr::Ops(chain) = &ast else {
2835            panic!("expected Expr::Ops, got {ast:?}");
2836        };
2837        let cst::ast::Atomic::MathText { elems, .. } = &chain.head.head else {
2838            panic!("expected Atomic::MathText, got {:?}", chain.head.head);
2839        };
2840        assert_eq!(elems.len(), 1, "one math element (`x`)");
2841        let cst::ast::MathBot::Chars(t) = &elems[0].base else {
2842            panic!("expected MathBot::Chars, got {:?}", elems[0].base);
2843        };
2844        assert_eq!(t.text, "x");
2845    }
2846
2847    /// The shared lexer never actually emits a module-qualified command
2848    /// token (`HorzCmdWithMod`/`VertCmdWithMod`) in program-mode binding
2849    /// position — only inline/block-text and math areas dotted-scan a
2850    /// backslash/plus command name (`lexer.rs`'s `lex_program` vs.
2851    /// `lex_horz`/`lex_vert`/`lex_math`) — so `AnyHorzCmdTok::Mod`/
2852    /// `AnyVertCmdTok::Mod` are unreachable from any real `parse_file_v1`
2853    /// input at a `val inline`/`val block` binding's command-name position.
2854    /// This exercises `plain_horz`/`plain_vert` directly (a hand-built
2855    /// token, not a parse) so the `LowerError` arm itself is still proven,
2856    /// same rationale as the `CmdTail` bridge's own unreachable-in-practice
2857    /// guards.
2858    #[test]
2859    fn mod_qualified_command_name_in_bind_is_a_lower_error() {
2860        let tok = HorzCmdWithModTok {
2861            mods: vec!["Mod".to_string()],
2862            name: "\\emph".to_string(),
2863            span: Span::default(),
2864        };
2865        let err = plain_horz(&AnyHorzCmdTok::Mod(tok)).unwrap_err();
2866        assert!(err.to_string().contains("module-qualified"), "{err}");
2867
2868        let tok = VertCmdWithModTok {
2869            mods: vec!["Mod".to_string()],
2870            name: "+p".to_string(),
2871            span: Span::default(),
2872        };
2873        let err = plain_vert(&AnyVertCmdTok::Mod(tok)).unwrap_err();
2874        assert!(err.to_string().contains("module-qualified"), "{err}");
2875    }
2876
2877    #[test]
2878    fn lower_file_v1_on_a_document_is_an_error_not_a_panic() {
2879        let file = parse_v1("3");
2880        assert!(lower_file_v1(&file).is_err());
2881    }
2882
2883    #[test]
2884    fn lower_document_v1_on_a_library_is_an_error_not_a_panic() {
2885        let file = parse_v1("module M = struct\nval x = 1\nend");
2886        assert!(lower_document_v1(&file).is_err());
2887    }
2888
2889    /// `lower_file_v1` on a `Library` yields exactly ONE
2890    /// `TopBinding::Module` (not a spliced-flat `Vec` of the inner binds),
2891    /// with `sig: None` and the right name/decl count.
2892    #[test]
2893    fn lower_file_v1_yields_one_real_module_binding() {
2894        let file = parse_v1(
2895            "module V01Mini = struct\n\
2896             val x = 1\n\
2897             val y = 2\n\
2898             end",
2899        );
2900        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2901        assert_eq!(
2902            lowered.len(),
2903            1,
2904            "one TopBinding::Module, not spliced binds"
2905        );
2906        let cst::TopBinding::Module {
2907            name, sig, decls, ..
2908        } = &lowered[0]
2909        else {
2910            panic!("expected a TopBinding::Module, got {:?}", lowered[0]);
2911        };
2912        assert_eq!(name.name, "V01Mini");
2913        assert!(sig.is_none(), "no signature annotation in Sub-slice 2a");
2914        assert_eq!(decls.len(), 2);
2915    }
2916
2917    /// A nested `module N = struct … end` bind lowers to a
2918    /// nested `TopBinding::Module` inside the outer module's `decls`.
2919    #[test]
2920    fn lower_file_v1_nested_module_bind_lowers_to_nested_module() {
2921        let file = parse_v1(
2922            "module M = struct\n\
2923             val x = 1\n\
2924             module N = struct\n\
2925             val y = 2\n\
2926             end\n\
2927             end",
2928        );
2929        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2930        assert_eq!(lowered.len(), 1);
2931        let cst::TopBinding::Module { name, decls, .. } = &lowered[0] else {
2932            panic!("expected a TopBinding::Module");
2933        };
2934        assert_eq!(name.name, "M");
2935        assert_eq!(decls.len(), 2);
2936        assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
2937        let cst::TopBinding::Module {
2938            name: inner_name,
2939            sig: inner_sig,
2940            decls: inner_decls,
2941            ..
2942        } = &*decls[1].0
2943        else {
2944            panic!("expected decls[1] to be a nested TopBinding::Module");
2945        };
2946        assert_eq!(inner_name.name, "N");
2947        assert!(inner_sig.is_none());
2948        assert_eq!(inner_decls.len(), 1);
2949    }
2950
2951    // ---- Sealing lowers to NOTHING (zero runtime residue) -----------------
2952
2953    /// Sealing-erasure twin 1: a library-level `:>` ascription lowers to the
2954    /// byte-identical `TopBinding::Module { sig: None, .. }` shape its
2955    /// unsealed twin does — enforcement is entirely `v1/module_check.rs`'s,
2956    /// off the original `cst_v1` tree.
2957    #[test]
2958    fn sig_annot_on_library_lowers_like_its_unsealed_twin() {
2959        let sealed = parse_v1("module M :> sig val x : int end = struct\nval x = 1\nend");
2960        let unsealed = parse_v1("module M = struct\nval x = 1\nend");
2961        let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
2962        let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
2963        assert_eq!(sealed_lowered.len(), 1);
2964        assert_eq!(unsealed_lowered.len(), 1);
2965        let cst::TopBinding::Module {
2966            sig: sealed_sig,
2967            decls: sealed_decls,
2968            ..
2969        } = &sealed_lowered[0]
2970        else {
2971            panic!("expected a TopBinding::Module");
2972        };
2973        let cst::TopBinding::Module {
2974            sig: unsealed_sig,
2975            decls: unsealed_decls,
2976            ..
2977        } = &unsealed_lowered[0]
2978        else {
2979            panic!("expected a TopBinding::Module");
2980        };
2981        assert!(
2982            sealed_sig.is_none(),
2983            "the seal must lower to NO cst::SigAnnot at all"
2984        );
2985        assert!(unsealed_sig.is_none());
2986        assert_eq!(sealed_decls.len(), unsealed_decls.len());
2987        assert!(matches!(&*sealed_decls[0].0, cst::TopBinding::Let(_)));
2988        assert!(matches!(&*unsealed_decls[0].0, cst::TopBinding::Let(_)));
2989    }
2990
2991    /// Sealing-erasure twin 2: the struct body still lowers (surfacing its own precise
2992    /// error) whether or not the module carries a `:>` annotation — a body
2993    /// error is identical either way, since lowering never consults the
2994    /// annotation. Body error: an `include` of an inline `struct … end`
2995    /// literal, unsupported.
2996    #[test]
2997    fn sig_annot_body_error_is_identical_with_or_without_a_seal() {
2998        let sealed = parse_v1("module M :> sig end = struct\ninclude struct val x = 1 end\nend");
2999        let unsealed = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
3000        let sealed_err = lower_file_v1(&sealed).unwrap_err();
3001        let unsealed_err = lower_file_v1(&unsealed).unwrap_err();
3002        assert!(
3003            sealed_err.to_string().contains("inline `struct"),
3004            "{sealed_err}"
3005        );
3006        assert_eq!(sealed_err.construct, unsealed_err.construct);
3007        assert_eq!(sealed_err.hint, unsealed_err.hint);
3008    }
3009
3010    /// Sealing-erasure twin 3: the bind-level twin of
3011    /// `sig_annot_on_library_lowers_like_its_unsealed_twin` — a nested
3012    /// `module N :> sig .. end = struct .. end` lowers to the same nested
3013    /// `TopBinding::Module { sig: None, .. }` shape as its unsealed twin.
3014    #[test]
3015    fn nested_module_sig_annot_lowers_like_its_unsealed_twin() {
3016        let sealed = parse_v1(
3017            "module M = struct\n\
3018             module N :> sig val y : int end = struct\n\
3019             val y = 2\n\
3020             end\n\
3021             end",
3022        );
3023        let unsealed = parse_v1(
3024            "module M = struct\n\
3025             module N = struct\n\
3026             val y = 2\n\
3027             end\n\
3028             end",
3029        );
3030        let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
3031        let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
3032        let cst::TopBinding::Module {
3033            decls: sealed_decls,
3034            ..
3035        } = &sealed_lowered[0]
3036        else {
3037            panic!("expected a TopBinding::Module");
3038        };
3039        let cst::TopBinding::Module {
3040            decls: unsealed_decls,
3041            ..
3042        } = &unsealed_lowered[0]
3043        else {
3044            panic!("expected a TopBinding::Module");
3045        };
3046        assert_eq!(sealed_decls.len(), 1);
3047        assert_eq!(unsealed_decls.len(), 1);
3048        let cst::TopBinding::Module {
3049            name: sealed_name,
3050            sig: sealed_sig,
3051            decls: sealed_inner,
3052            ..
3053        } = &*sealed_decls[0].0
3054        else {
3055            panic!("expected decls[0] to be a nested TopBinding::Module");
3056        };
3057        let cst::TopBinding::Module {
3058            name: unsealed_name,
3059            sig: unsealed_sig,
3060            decls: unsealed_inner,
3061            ..
3062        } = &*unsealed_decls[0].0
3063        else {
3064            panic!("expected decls[0] to be a nested TopBinding::Module");
3065        };
3066        assert_eq!(sealed_name.name, "N");
3067        assert_eq!(unsealed_name.name, "N");
3068        assert!(
3069            sealed_sig.is_none(),
3070            "the seal must lower to NO cst::SigAnnot at all"
3071        );
3072        assert!(unsealed_sig.is_none());
3073        assert_eq!(sealed_inner.len(), unsealed_inner.len());
3074    }
3075
3076    /// A module alias/path binding naming an UNKNOWN target
3077    /// is a precise `LowerError` — `N` is never defined anywhere in this
3078    /// file, so `lower_module_alias` can't resolve it.
3079    #[test]
3080    fn module_alias_with_unknown_target_is_a_lower_error() {
3081        let file = parse_v1("module M = struct\nmodule P = N\nend");
3082        let err = lower_file_v1(&file).unwrap_err();
3083        assert!(err.to_string().contains("module alias"), "{err}");
3084    }
3085
3086    /// A module alias to an EARLIER, real sibling
3087    /// module lowers for real — one `Let` per exported value member,
3088    /// referencing the target's absolute qualified name.
3089    #[test]
3090    fn module_alias_to_a_real_target_lowers_member_copies() {
3091        let file = parse_v1(
3092            "module M = struct\n\
3093             module Base = struct val x = 1 val f y = y end\n\
3094             module Alias = Base\n\
3095             end",
3096        );
3097        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3098        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3099            panic!("expected a TopBinding::Module");
3100        };
3101        let cst::TopBinding::Module {
3102            name,
3103            decls: alias_decls,
3104            ..
3105        } = &*decls[1].0
3106        else {
3107            panic!("expected decls[1] to be the Alias module");
3108        };
3109        assert_eq!(name.name, "Alias");
3110        assert_eq!(alias_decls.len(), 2, "one copy per exported value member");
3111        for (decl, expected_name) in alias_decls.iter().zip(["x", "f"]) {
3112            let cst::TopBinding::Let(top_let) = &*decl.0 else {
3113                panic!("expected a TopBinding::Let copy");
3114            };
3115            assert_eq!(top_let.name.name, expected_name);
3116            let cst::ast::Expr::Ops(chain) = &top_let.value else {
3117                panic!("expected an application-chain expression");
3118            };
3119            let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3120                panic!("expected a VarWithMod reference to the target");
3121            };
3122            assert_eq!(tok.mods, vec!["M.Base".to_string()]);
3123            assert_eq!(tok.name, expected_name);
3124        }
3125    }
3126
3127    /// A FORWARD reference (`module M = Later` before
3128    /// `Later` is itself defined) is the same "unknown module" error as a
3129    /// genuinely nonexistent name — an alias may only target an EARLIER
3130    /// module.
3131    #[test]
3132    fn module_alias_forward_reference_is_a_lower_error() {
3133        let file = parse_v1(
3134            "module M = struct\n\
3135             module Early = Later\n\
3136             module Later = struct val x = 1 end\n\
3137             end",
3138        );
3139        let err = lower_file_v1(&file).unwrap_err();
3140        assert!(err.to_string().contains("module alias"), "{err}");
3141    }
3142
3143    /// A functor application.
3144    #[test]
3145    fn functor_application_is_a_lower_error() {
3146        let file = parse_v1("module M = struct\nmodule P = F X\nend");
3147        let err = lower_file_v1(&file).unwrap_err();
3148        assert!(err.to_string().contains("functor application"), "{err}");
3149    }
3150
3151    /// A functor DEFINITION (`module F = fun (X:S) -> ...`)
3152    /// emits ZERO runtime bindings — a functor is not a value, exactly
3153    /// `Bind::Signature`'s posture. `M`'s own `TopBinding::Module` still
3154    /// lowers, just with no `F` member inside it.
3155    #[test]
3156    fn functor_literal_emits_zero_runtime_bindings() {
3157        let file = parse_v1(
3158            "module M = struct\n\
3159             module F = fun (X : sig val x : int end) -> struct val y = X.x end\n\
3160             end",
3161        );
3162        let bindings =
3163            lower_file_v1(&file).expect("a functor definition now lowers, emitting no member");
3164        let cst::TopBinding::Module { decls, .. } = &bindings[0] else {
3165            panic!("expected M's TopBinding::Module")
3166        };
3167        assert!(
3168            decls.is_empty(),
3169            "a functor literal contributes no decls: {decls:?}"
3170        );
3171    }
3172
3173    /// A bare module coercion `N :> S` naming an UNKNOWN
3174    /// `N` is a precise `LowerError` — the same "unknown module" wording a
3175    /// bare `Var` alias produces (`lower_module_alias` is shared).
3176    #[test]
3177    fn module_coercion_with_unknown_target_is_a_lower_error() {
3178        let file = parse_v1("module M = struct\nmodule P = N :> S\nend");
3179        let err = lower_file_v1(&file).unwrap_err();
3180        assert!(err.to_string().contains("module alias"), "{err}");
3181    }
3182
3183    /// `module M = N :> S` lowers the SAME member-copy
3184    /// shape as `module M = N` (the seal-rule pin, alias flavor) — the
3185    /// `:> S` annotation is dropped by lowering and enforced entirely by
3186    /// `v1/module_check.rs`. (Compared structurally, not by full-AST
3187    /// equality: the longer `:> sig …` source text legitimately shifts
3188    /// every byte span, so spans differ even though the shape is identical
3189    /// — the same span-insensitivity every other alias-shape test here
3190    /// relies on by asserting on fields rather than whole nodes.)
3191    #[test]
3192    fn module_coercion_lowers_the_same_copies_as_its_uncoerced_twin() {
3193        fn alias_copy_names(src: &str) -> (Vec<String>, Vec<Vec<String>>) {
3194            let lowered = lower_file_v1(&parse_v1(src)).unwrap_or_else(|e| panic!("{e}"));
3195            let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3196                panic!("expected a TopBinding::Module");
3197            };
3198            let cst::TopBinding::Module {
3199                name,
3200                decls: alias_decls,
3201                ..
3202            } = &*decls[1].0
3203            else {
3204                panic!("expected decls[1] to be the Alias module");
3205            };
3206            assert_eq!(name.name, "Alias");
3207            let mut names = Vec::new();
3208            let mut refs = Vec::new();
3209            for d in alias_decls {
3210                let cst::TopBinding::Let(top_let) = &*d.0 else {
3211                    panic!("expected a Let copy");
3212                };
3213                names.push(top_let.name.name.clone());
3214                let cst::ast::Expr::Ops(chain) = &top_let.value else {
3215                    panic!("expected an application chain");
3216                };
3217                let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3218                    panic!("expected a VarWithMod reference");
3219                };
3220                refs.push(tok.mods.clone());
3221            }
3222            (names, refs)
3223        }
3224        let bare = alias_copy_names(
3225            "module M = struct\n\
3226             module Base = struct val x = 1 end\n\
3227             module Alias = Base\n\
3228             end",
3229        );
3230        let coerced = alias_copy_names(
3231            "module M = struct\n\
3232             module Base = struct val x = 1 end\n\
3233             module Alias = Base :> sig val x : int end\n\
3234             end",
3235        );
3236        assert_eq!(bare, coerced);
3237        assert_eq!(bare.0, vec!["x".to_string()]);
3238        assert_eq!(bare.1, vec![vec!["M.Base".to_string()]]);
3239    }
3240
3241    /// A `signature S = ...` bind lowers to NOTHING — zero
3242    /// runtime/type-spine residue.
3243    #[test]
3244    fn signature_bind_lowers_to_nothing() {
3245        let file = parse_v1("module M = struct\nsignature S = sig end\nval x = 1\nend");
3246        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3247        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3248            panic!("expected a TopBinding::Module");
3249        };
3250        assert_eq!(decls.len(), 1, "the signature bind contributes zero decls");
3251        assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
3252    }
3253
3254    /// `include N` naming an UNKNOWN module is a precise
3255    /// `LowerError` — `N` is never defined anywhere in this file.
3256    #[test]
3257    fn include_of_an_unknown_module_is_a_lower_error() {
3258        let file = parse_v1("module M = struct\ninclude N\nend");
3259        let err = lower_file_v1(&file).unwrap_err();
3260        let msg = err.to_string();
3261        assert!(msg.contains("include"), "{msg}");
3262        assert!(msg.contains("unknown module"), "{msg}");
3263    }
3264
3265    /// `include M` naming an EARLIER real sibling
3266    /// module splices copies of ALL its exported members DIRECTLY into the
3267    /// includer's own decls — no synthetic wrapper module (contrast the
3268    /// alias arm's `module_alias_to_a_real_target_lowers_member_copies`).
3269    #[test]
3270    fn include_of_a_real_target_splices_member_copies_unwrapped() {
3271        let file = parse_v1(
3272            "module M = struct\n\
3273             module Base = struct val x = 1 val f y = y end\n\
3274             include Base\n\
3275             end",
3276        );
3277        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3278        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3279            panic!("expected a TopBinding::Module");
3280        };
3281        // decls[0] = the `Base` module; decls[1..] = the SPLICED copies
3282        // (unwrapped — no synthetic `TopBinding::Module` wrapping them).
3283        assert_eq!(decls.len(), 3, "Base + 2 spliced copies, unwrapped");
3284        for (decl, expected_name) in decls[1..].iter().zip(["x", "f"]) {
3285            let cst::TopBinding::Let(top_let) = &*decl.0 else {
3286                panic!("expected a TopBinding::Let copy, got {:?}", decl.0);
3287            };
3288            assert_eq!(top_let.name.name, expected_name);
3289            let cst::ast::Expr::Ops(chain) = &top_let.value else {
3290                panic!("expected an application-chain expression");
3291            };
3292            let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3293                panic!("expected a VarWithMod reference to the target");
3294            };
3295            assert_eq!(tok.mods, vec!["M.Base".to_string()]);
3296            assert_eq!(tok.name, expected_name);
3297        }
3298    }
3299
3300    /// A FORWARD reference (`include Later`
3301    /// before `Later` is itself defined) is the same "unknown module" error
3302    /// as a genuinely nonexistent name (the `alias_targets`-style frozen
3303    /// resolution twin, `include_targets`).
3304    #[test]
3305    fn include_forward_reference_is_a_lower_error() {
3306        let file = parse_v1(
3307            "module M = struct\n\
3308             include Later\n\
3309             module Later = struct val x = 1 end\n\
3310             end",
3311        );
3312        let err = lower_file_v1(&file).unwrap_err();
3313        assert!(err.to_string().contains("unknown module"), "{err}");
3314    }
3315
3316    /// `include F X` (a functor application) reuses the
3317    /// EXISTING functor `LowerError` wording verbatim — the `map.satyg
3318    /// :344` `include Make Int` shape.
3319    #[test]
3320    fn include_of_a_functor_application_is_the_2f_functor_error() {
3321        // `F` here is a plain STRUCT, not a functor, so `include F X`
3322        // freezes an unresolved (`None`) app target — hence the "unknown"
3323        // wording rather than an out-of-scope one (a
3324        // parameter-argument application does resolve, once its enclosing
3325        // functor is applied).
3326        let file = parse_v1("module M = struct\nmodule F = struct end\ninclude F X\nend");
3327        let err = lower_file_v1(&file).unwrap_err();
3328        let msg = err.to_string();
3329        assert!(msg.contains("functor application"), "{msg}");
3330        assert!(msg.contains("unknown"), "{msg}");
3331    }
3332
3333    /// `include struct … end` (an inline struct literal)
3334    /// is its own precise out-of-scope error — zero upstream-stdlib demand.
3335    #[test]
3336    fn include_of_an_inline_struct_literal_is_a_lower_error() {
3337        let file = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
3338        let err = lower_file_v1(&file).unwrap_err();
3339        let msg = err.to_string();
3340        assert!(msg.contains("struct"), "{msg}");
3341        assert!(msg.contains("name the module first"), "{msg}");
3342    }
3343
3344    /// `include M :> S` (a coerced module body) is its
3345    /// own precise out-of-scope error.
3346    #[test]
3347    fn include_of_a_coerced_module_is_a_lower_error() {
3348        let file = parse_v1(
3349            "module M = struct\n\
3350             module Base = struct val x = 1 end\n\
3351             include Base :> sig val x : int end\n\
3352             end",
3353        );
3354        let err = lower_file_v1(&file).unwrap_err();
3355        let msg = err.to_string();
3356        assert!(msg.contains("coerced module"), "{msg}");
3357    }
3358
3359    /// Self-include (`module P = struct include
3360    /// P end`) is the same "unknown module" error — `P` only registers in
3361    /// `env.modules` AFTER its own body walk finishes, so it is never its
3362    /// own earlier module.
3363    #[test]
3364    fn self_include_is_a_lower_error() {
3365        let file = parse_v1("module P = struct\ninclude P\nend");
3366        let err = lower_file_v1(&file).unwrap_err();
3367        assert!(err.to_string().contains("unknown module"), "{err}");
3368    }
3369
3370    /// The CmdTail bridge: `\cmd{a}{b}` parses under `cst_v1` as one
3371    /// application chain (`AppExpr { head: {a}, args: [{b}] }`) and must
3372    /// lower to the same shape `cst.rs`'s own `\cmd{a}{b}` parse produces
3373    /// (`CmdTail::Args { first: {a}, rest: [{b}] }`).
3374    #[test]
3375    fn cmd_tail_bridge_matches_flat_app_arg_shape() {
3376        let file = parse_v1(r"{\cmd{a}{b}}");
3377        let cst_v1::FileV1::Document { body, .. } = file else {
3378            panic!("expected a document file");
3379        };
3380        let ast_v1::Expr::Ops(chain) = body else {
3381            panic!("expected an operator-chain expression");
3382        };
3383        let ast_v1::Atomic::InlineText { elems, .. } = chain.head.head else {
3384            panic!("expected inline text");
3385        };
3386        let ast_v1::InlineElem::Cmd { tail, .. } = &elems[0] else {
3387            panic!("expected the first element to be a command");
3388        };
3389        let lowered = lower_cmd_tail(tail).unwrap();
3390        let cst::ast::CmdTail::Args { first, rest, .. } = lowered else {
3391            panic!("expected CmdTail::Args");
3392        };
3393        assert_eq!(
3394            rest.len(),
3395            1,
3396            "\\cmd{{a}}{{b}} has exactly one trailing arg"
3397        );
3398        assert!(matches!(
3399            &*first.0,
3400            cst::ast::AppArg::Atom {
3401                stage: None,
3402                atom: cst::ast::Atomic::InlineText { .. },
3403                ..
3404            }
3405        ));
3406        assert!(matches!(
3407            &*rest[0].0,
3408            cst::ast::AppArg::Atom {
3409                stage: None,
3410                atom: cst::ast::Atomic::InlineText { .. },
3411                ..
3412            }
3413        ));
3414    }
3415}