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(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#[cfg(test)]
2428mod tests {
2429    use super::*;
2430
2431    fn parse_v1(src: &str) -> cst_v1::FileV1 {
2432        rustyfi_syntax::parse_file_v1(src).unwrap_or_else(|e| panic!("v1 parse failed: {e}"))
2433    }
2434
2435    /// Expression-level `let rec … and … in` lowers to a full
2436    /// `Expr::LetRecIn` with the `RecBinding` reshape's 0.1-only fields all
2437    /// empty/`None` (no ascription/leading-bar/multi-clause sugar exists in
2438    /// 0.1's `bind_value_nonrec`).
2439    #[test]
2440    fn let_rec_document_lowers_with_and_chain() {
2441        let file = parse_v1(
2442            "let rec even n = if n <= 0 then true else odd (n - 1)\n\
2443             and odd n = if n <= 0 then false else even (n - 1) in even 4",
2444        );
2445        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2446        let cst::ast::Expr::LetRecIn { first, ands, .. } = ast else {
2447            panic!("expected Expr::LetRecIn");
2448        };
2449        assert_eq!(ands.len(), 1, "one `and` continuation");
2450        assert!(first.ascription.is_none());
2451        assert!(first.leading_bar.is_none());
2452        assert!(first.extra.is_empty());
2453        assert_eq!(first.name.name, "even");
2454        assert_eq!(ands[0].binding.name.name, "odd");
2455    }
2456
2457    /// `val rec … and …` lowers to `TopBinding::LetRec` inside the
2458    /// module's `decls`.
2459    #[test]
2460    fn val_rec_library_lowers_to_top_binding_letrec() {
2461        let file = parse_v1(
2462            "module M = struct\n\
2463             val rec even n = odd n\n\
2464             and odd n = even n\n\
2465             end",
2466        );
2467        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2468        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2469            panic!("expected a TopBinding::Module");
2470        };
2471        assert_eq!(decls.len(), 1);
2472        let cst::TopBinding::LetRec { first, ands, .. } = &*decls[0].0 else {
2473            panic!("expected TopBinding::LetRec, got {:?}", decls[0].0);
2474        };
2475        assert_eq!(first.name.name, "even");
2476        assert_eq!(ands.len(), 1);
2477        assert_eq!(ands[0].binding.name.name, "odd");
2478    }
2479
2480    /// `val mutable` → `TopBinding::LetMutable`.
2481    #[test]
2482    fn val_mutable_lowers_to_top_binding_letmutable() {
2483        let file = parse_v1("module M = struct\nval mutable c <- 0\nend");
2484        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2485        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2486            panic!("expected a TopBinding::Module");
2487        };
2488        assert_eq!(decls.len(), 1);
2489        assert!(
2490            matches!(&*decls[0].0, cst::TopBinding::LetMutable { name, .. } if name.name == "c"),
2491            "{:?}",
2492            decls[0].0
2493        );
2494    }
2495
2496    /// `let mutable … in` → `Expr::LetMutableIn`.
2497    #[test]
2498    fn let_mutable_in_document_lowers_to_expr_letmutablein() {
2499        let file = parse_v1("let mutable c <- 0 in c <- !c + 1");
2500        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2501        assert!(
2502            matches!(&ast, cst::ast::Expr::LetMutableIn { name, .. } if name.name == "c"),
2503            "{ast:?}"
2504        );
2505    }
2506
2507    /// `type t = int and u = t` inside `module M` lowers to TWO
2508    /// consecutive `TopBinding::Type` decls, both names qualified
2509    /// (`"M.t"`/`"M.u"`), and `u`'s synonym body references the ALREADY-
2510    /// qualified `"M.t"` — the pre-qualification pin.
2511    #[test]
2512    fn type_and_chain_inside_module_qualifies_names_and_synonym_reference() {
2513        let file = parse_v1(
2514            "module M = struct\n\
2515             type t = int\n\
2516             and u = t\n\
2517             end",
2518        );
2519        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2520        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2521            panic!("expected a TopBinding::Module");
2522        };
2523        assert_eq!(
2524            decls.len(),
2525            2,
2526            "an `and`-chain lowers to N consecutive Type decls"
2527        );
2528        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2529            panic!("expected decls[0] to be a Type decl");
2530        };
2531        assert_eq!(t_decl.name.name, "M.t");
2532        let cst::TopBinding::Type(u_decl) = &*decls[1].0 else {
2533            panic!("expected decls[1] to be a Type decl");
2534        };
2535        assert_eq!(u_decl.name.name, "M.u");
2536        let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
2537            panic!("expected a synonym body");
2538        };
2539        let cst::ast::TypeExpr::Atom(prod) = ty else {
2540            panic!("expected a bare TypeProd (no arrow)");
2541        };
2542        let cst::ast::TypeApp {
2543            head: cst::ast::TypeAtom::Name(n),
2544            ..
2545        } = &prod.first
2546        else {
2547            panic!("expected a bare type name atom");
2548        };
2549        assert_eq!(
2550            n.name, "M.t",
2551            "u's synonym body must reference the QUALIFIED t"
2552        );
2553    }
2554
2555    /// Nested-module pre-qualification — `module M = struct type
2556    /// t = int module N = struct type u = t end end` → `"M.N.u"`'s body
2557    /// references `"M.t"` (outer types stay visible inside a nested
2558    /// module, per ordinary module scoping).
2559    #[test]
2560    fn nested_module_type_reference_qualifies_to_outer_path() {
2561        let file = parse_v1(
2562            "module M = struct\n\
2563             type t = int\n\
2564             module N = struct\n\
2565             type u = t\n\
2566             end\n\
2567             end",
2568        );
2569        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2570        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2571            panic!("expected a TopBinding::Module");
2572        };
2573        assert_eq!(decls.len(), 2);
2574        let cst::TopBinding::Module {
2575            name: inner_name,
2576            decls: inner_decls,
2577            ..
2578        } = &*decls[1].0
2579        else {
2580            panic!("expected decls[1] to be a nested TopBinding::Module");
2581        };
2582        assert_eq!(inner_name.name, "N");
2583        assert_eq!(inner_decls.len(), 1);
2584        let cst::TopBinding::Type(u_decl) = &*inner_decls[0].0 else {
2585            panic!("expected a Type decl");
2586        };
2587        assert_eq!(u_decl.name.name, "M.N.u");
2588        let cst::TypeDeclBody::Synonym(ty) = &u_decl.body else {
2589            panic!("expected a synonym body");
2590        };
2591        let cst::ast::TypeExpr::Atom(prod) = ty else {
2592            panic!("expected a bare TypeProd");
2593        };
2594        let cst::ast::TypeApp {
2595            head: cst::ast::TypeAtom::Name(n),
2596            ..
2597        } = &prod.first
2598        else {
2599            panic!("expected a bare type name atom");
2600        };
2601        assert_eq!(
2602            n.name, "M.t",
2603            "the outer M.t must stay visible/qualified inside N"
2604        );
2605    }
2606
2607    /// The prefix→postfix `TypeApp` bridge — `type t = option
2608    /// int` (0.1 prefix, arity 1) lowers to the cst target's postfix
2609    /// `Applied { arg: int, ctor: option }` shape.
2610    #[test]
2611    fn type_app_prefix_to_postfix_bridge() {
2612        let file = parse_v1("module M = struct\ntype t = option int\nend");
2613        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2614        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2615            panic!("expected a TopBinding::Module");
2616        };
2617        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2618            panic!("expected a Type decl");
2619        };
2620        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2621            panic!("expected a synonym body");
2622        };
2623        let cst::ast::TypeExpr::Atom(prod) = ty else {
2624            panic!("expected a bare TypeProd");
2625        };
2626        // Postfix form: `head = int`, `rest = [option]` (the ctor is last).
2627        assert!(prod.first.rest.len() == 1, "{:?}", prod.first);
2628        assert!(
2629            matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "option"),
2630            "{:?}",
2631            prod.first.rest[0]
2632        );
2633        assert!(
2634            matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
2635            "{:?}",
2636            prod.first.head
2637        );
2638    }
2639
2640    /// An applied type constructor with arity ≥ 2 (`pair int
2641    /// int`) lowers to the 0.0.6 cst target's N-ary atom run — `head =
2642    /// int`, `rest = [int, pair]` (the two arguments then the constructor).
2643    #[test]
2644    fn type_app_arity_2_lowers_to_nary_atom_run() {
2645        let file = parse_v1("module M = struct\ntype t = pair int int\nend");
2646        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2647        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2648            panic!("expected a TopBinding::Module");
2649        };
2650        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2651            panic!("expected a Type decl");
2652        };
2653        let cst::TypeDeclBody::Synonym(cst::ast::TypeExpr::Atom(prod)) = &t_decl.body else {
2654            panic!("expected a bare synonym TypeProd");
2655        };
2656        // args = [int, int], ctor = pair → head = int, rest = [int, pair].
2657        assert!(
2658            matches!(&prod.first.head, cst::ast::TypeAtom::Name(n) if n.name == "int"),
2659            "{:?}",
2660            prod.first.head
2661        );
2662        assert_eq!(prod.first.rest.len(), 2, "{:?}", prod.first.rest);
2663        assert!(matches!(&prod.first.rest[0], cst::ast::TypeAtom::Name(n) if n.name == "int"));
2664        assert!(matches!(&prod.first.rest[1], cst::ast::TypeAtom::Name(n) if n.name == "pair"));
2665    }
2666
2667    /// A variant↔variant mutual pair (`type a = A of b and b = B
2668    /// of a`) lowers to two consecutive `TopBinding::Type` decls — the
2669    /// forward-reference tolerance `typecheck.rs` provides is exercised at
2670    /// the typecheck/elaborate layer, not here; this only pins the
2671    /// lowering shape.
2672    #[test]
2673    fn mutual_variant_pair_lowers_to_two_type_decls() {
2674        let file = parse_v1(
2675            "module M = struct\n\
2676             type a = A of b\n\
2677             and b = B of a\n\
2678             end",
2679        );
2680        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2681        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2682            panic!("expected a TopBinding::Module");
2683        };
2684        assert_eq!(decls.len(), 2);
2685        assert!(matches!(&*decls[0].0, cst::TopBinding::Type(d) if d.name.name == "M.a"));
2686        assert!(matches!(&*decls[1].0, cst::TopBinding::Type(d) if d.name.name == "M.b"));
2687    }
2688
2689    /// `type t = (| x : int, y : bool |)` lowers to `cst::ast::
2690    /// TypeAtom::Record` with the field list transcribed field-by-field
2691    /// (labels, colons, and lowered field types) — the pure-transcription
2692    /// arm in `lower_type_atom`.
2693    #[test]
2694    fn type_record_lowers_to_cst_record_atom() {
2695        let file = parse_v1("module M = struct\ntype t = (| x : int, y : bool |)\nend");
2696        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2697        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2698            panic!("expected a TopBinding::Module");
2699        };
2700        let cst::TopBinding::Type(t_decl) = &*decls[0].0 else {
2701            panic!("expected a Type decl");
2702        };
2703        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2704            panic!("expected a synonym body");
2705        };
2706        let cst::ast::TypeExpr::Atom(prod) = ty else {
2707            panic!("expected a bare TypeProd");
2708        };
2709        let cst::ast::TypeApp {
2710            head: cst::ast::TypeAtom::Record { fields, .. },
2711            ..
2712        } = &prod.first
2713        else {
2714            panic!("expected TypeAtom::Record, got {:?}", prod.first);
2715        };
2716        assert_eq!(fields.len(), 2);
2717        assert_eq!(fields[0].name.name, "x");
2718        assert!(
2719            matches!(&*fields[0].ty.0, cst::ast::TypeExpr::Atom(p)
2720                if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "int")),
2721            "{:?}",
2722            fields[0].ty.0
2723        );
2724        assert_eq!(fields[1].name.name, "y");
2725        assert!(
2726            matches!(&*fields[1].ty.0, cst::ast::TypeExpr::Atom(p)
2727                if matches!(&p.first, cst::ast::TypeApp { head: cst::ast::TypeAtom::Name(n), .. } if n.name == "bool")),
2728            "{:?}",
2729            fields[1].ty.0
2730        );
2731    }
2732
2733    /// A bare local type name used INSIDE a record type field gets the
2734    /// same `tyenv.qualify` every other type position gets (mirrors
2735    /// `type_and_chain_inside_module_qualifies_names_and_synonym_reference`
2736    /// above, but for a field type rather than a synonym body).
2737    #[test]
2738    fn type_record_field_type_is_qualified_like_any_other_type_position() {
2739        let file = parse_v1(
2740            "module M = struct\n\
2741             type config = int\n\
2742             type t = (| c : config |)\n\
2743             end",
2744        );
2745        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2746        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
2747            panic!("expected a TopBinding::Module");
2748        };
2749        assert_eq!(decls.len(), 2);
2750        let cst::TopBinding::Type(t_decl) = &*decls[1].0 else {
2751            panic!("expected decls[1] to be a Type decl");
2752        };
2753        assert_eq!(t_decl.name.name, "M.t");
2754        let cst::TypeDeclBody::Synonym(ty) = &t_decl.body else {
2755            panic!("expected a synonym body");
2756        };
2757        let cst::ast::TypeExpr::Atom(prod) = ty else {
2758            panic!("expected a bare TypeProd");
2759        };
2760        let cst::ast::TypeApp {
2761            head: cst::ast::TypeAtom::Record { fields, .. },
2762            ..
2763        } = &prod.first
2764        else {
2765            panic!("expected TypeAtom::Record, got {:?}", prod.first);
2766        };
2767        assert_eq!(fields.len(), 1);
2768        assert_eq!(fields[0].name.name, "c");
2769        let cst::ast::TypeExpr::Atom(field_prod) = &*fields[0].ty.0 else {
2770            panic!("expected a bare TypeProd for the field type");
2771        };
2772        let cst::ast::TypeApp {
2773            head: cst::ast::TypeAtom::Name(n),
2774            ..
2775        } = &field_prod.first
2776        else {
2777            panic!("expected a bare type name atom, got {:?}", field_prod.first);
2778        };
2779        assert_eq!(
2780            n.name, "M.config",
2781            "the field's bare `config` must qualify to M.config"
2782        );
2783    }
2784
2785    /// SATySFi 0.1 dropped the fused `?:`/`?*` optional sigils entirely:
2786    /// under V0_1 the lexer emits only `?` =
2787    /// `OptionalType`, so `?:1`/`?*` now lex as `?` + `:`/`*` — a downstream
2788    /// PARSE error, no longer reaching the lowerer. Replaced by the labeled
2789    /// `?(l = e)` bundle.
2790    #[test]
2791    fn old_optional_sigils_no_longer_parse() {
2792        assert!(rustyfi_syntax::parse_file_v1("f ?:1").is_err());
2793        assert!(rustyfi_syntax::parse_file_v1("f ?*").is_err());
2794    }
2795
2796    /// A `?(l = e, …)` labeled-optional application bundle lowers to
2797    /// `AppArg::Bundled`; an empty `?()` bundle is a `LowerError`.
2798    #[test]
2799    fn empty_opt_arg_bundle_is_a_lower_error() {
2800        let file = parse_v1("f ?() x");
2801        let err = lower_document_v1(&file).unwrap_err();
2802        assert!(
2803            err.to_string().contains("optional-argument bundle"),
2804            "{err}"
2805        );
2806    }
2807
2808    /// `${…}` lowers STRUCTURALLY: its *value* is version-independent (only
2809    /// the surrounding type/prim tables differ, per `typecheck.rs`'s
2810    /// `name_to_mono`). Round-trips `${x}` through `Atomic::MathText` down
2811    /// to its one `MathBot::Chars("x")` element.
2812    #[test]
2813    fn math_text_lowers_structurally() {
2814        let file = parse_v1("${x}");
2815        let ast = lower_document_v1(&file).unwrap_or_else(|e| panic!("lower_document_v1: {e}"));
2816        let cst::ast::Expr::Ops(chain) = &ast else {
2817            panic!("expected Expr::Ops, got {ast:?}");
2818        };
2819        let cst::ast::Atomic::MathText { elems, .. } = &chain.head.head else {
2820            panic!("expected Atomic::MathText, got {:?}", chain.head.head);
2821        };
2822        assert_eq!(elems.len(), 1, "one math element (`x`)");
2823        let cst::ast::MathBot::Chars(t) = &elems[0].base else {
2824            panic!("expected MathBot::Chars, got {:?}", elems[0].base);
2825        };
2826        assert_eq!(t.text, "x");
2827    }
2828
2829    /// The shared lexer never actually emits a module-qualified command
2830    /// token (`HorzCmdWithMod`/`VertCmdWithMod`) in program-mode binding
2831    /// position — only inline/block-text and math areas dotted-scan a
2832    /// backslash/plus command name (`lexer.rs`'s `lex_program` vs.
2833    /// `lex_horz`/`lex_vert`/`lex_math`) — so `AnyHorzCmdTok::Mod`/
2834    /// `AnyVertCmdTok::Mod` are unreachable from any real `parse_file_v1`
2835    /// input at a `val inline`/`val block` binding's command-name position.
2836    /// This exercises `plain_horz`/`plain_vert` directly (a hand-built
2837    /// token, not a parse) so the `LowerError` arm itself is still proven,
2838    /// same rationale as the `CmdTail` bridge's own unreachable-in-practice
2839    /// guards.
2840    #[test]
2841    fn mod_qualified_command_name_in_bind_is_a_lower_error() {
2842        let tok = HorzCmdWithModTok {
2843            mods: vec!["Mod".to_string()],
2844            name: "\\emph".to_string(),
2845            span: Span::default(),
2846        };
2847        let err = plain_horz(&AnyHorzCmdTok::Mod(tok)).unwrap_err();
2848        assert!(err.to_string().contains("module-qualified"), "{err}");
2849
2850        let tok = VertCmdWithModTok {
2851            mods: vec!["Mod".to_string()],
2852            name: "+p".to_string(),
2853            span: Span::default(),
2854        };
2855        let err = plain_vert(&AnyVertCmdTok::Mod(tok)).unwrap_err();
2856        assert!(err.to_string().contains("module-qualified"), "{err}");
2857    }
2858
2859    #[test]
2860    fn lower_file_v1_on_a_document_is_an_error_not_a_panic() {
2861        let file = parse_v1("3");
2862        assert!(lower_file_v1(&file).is_err());
2863    }
2864
2865    #[test]
2866    fn lower_document_v1_on_a_library_is_an_error_not_a_panic() {
2867        let file = parse_v1("module M = struct\nval x = 1\nend");
2868        assert!(lower_document_v1(&file).is_err());
2869    }
2870
2871    /// `lower_file_v1` on a `Library` yields exactly ONE
2872    /// `TopBinding::Module` (not a spliced-flat `Vec` of the inner binds),
2873    /// with `sig: None` and the right name/decl count.
2874    #[test]
2875    fn lower_file_v1_yields_one_real_module_binding() {
2876        let file = parse_v1(
2877            "module V01Mini = struct\n\
2878             val x = 1\n\
2879             val y = 2\n\
2880             end",
2881        );
2882        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2883        assert_eq!(
2884            lowered.len(),
2885            1,
2886            "one TopBinding::Module, not spliced binds"
2887        );
2888        let cst::TopBinding::Module {
2889            name, sig, decls, ..
2890        } = &lowered[0]
2891        else {
2892            panic!("expected a TopBinding::Module, got {:?}", lowered[0]);
2893        };
2894        assert_eq!(name.name, "V01Mini");
2895        assert!(sig.is_none(), "no signature annotation in Sub-slice 2a");
2896        assert_eq!(decls.len(), 2);
2897    }
2898
2899    /// A nested `module N = struct … end` bind lowers to a
2900    /// nested `TopBinding::Module` inside the outer module's `decls`.
2901    #[test]
2902    fn lower_file_v1_nested_module_bind_lowers_to_nested_module() {
2903        let file = parse_v1(
2904            "module M = struct\n\
2905             val x = 1\n\
2906             module N = struct\n\
2907             val y = 2\n\
2908             end\n\
2909             end",
2910        );
2911        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
2912        assert_eq!(lowered.len(), 1);
2913        let cst::TopBinding::Module { name, decls, .. } = &lowered[0] else {
2914            panic!("expected a TopBinding::Module");
2915        };
2916        assert_eq!(name.name, "M");
2917        assert_eq!(decls.len(), 2);
2918        assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
2919        let cst::TopBinding::Module {
2920            name: inner_name,
2921            sig: inner_sig,
2922            decls: inner_decls,
2923            ..
2924        } = &*decls[1].0
2925        else {
2926            panic!("expected decls[1] to be a nested TopBinding::Module");
2927        };
2928        assert_eq!(inner_name.name, "N");
2929        assert!(inner_sig.is_none());
2930        assert_eq!(inner_decls.len(), 1);
2931    }
2932
2933    // ---- Sealing lowers to NOTHING (zero runtime residue) -----------------
2934
2935    /// Sealing-erasure twin 1: a library-level `:>` ascription lowers to the
2936    /// byte-identical `TopBinding::Module { sig: None, .. }` shape its
2937    /// unsealed twin does — enforcement is entirely `v1/module_check.rs`'s,
2938    /// off the original `cst_v1` tree.
2939    #[test]
2940    fn sig_annot_on_library_lowers_like_its_unsealed_twin() {
2941        let sealed = parse_v1("module M :> sig val x : int end = struct\nval x = 1\nend");
2942        let unsealed = parse_v1("module M = struct\nval x = 1\nend");
2943        let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
2944        let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
2945        assert_eq!(sealed_lowered.len(), 1);
2946        assert_eq!(unsealed_lowered.len(), 1);
2947        let cst::TopBinding::Module {
2948            sig: sealed_sig,
2949            decls: sealed_decls,
2950            ..
2951        } = &sealed_lowered[0]
2952        else {
2953            panic!("expected a TopBinding::Module");
2954        };
2955        let cst::TopBinding::Module {
2956            sig: unsealed_sig,
2957            decls: unsealed_decls,
2958            ..
2959        } = &unsealed_lowered[0]
2960        else {
2961            panic!("expected a TopBinding::Module");
2962        };
2963        assert!(
2964            sealed_sig.is_none(),
2965            "the seal must lower to NO cst::SigAnnot at all"
2966        );
2967        assert!(unsealed_sig.is_none());
2968        assert_eq!(sealed_decls.len(), unsealed_decls.len());
2969        assert!(matches!(&*sealed_decls[0].0, cst::TopBinding::Let(_)));
2970        assert!(matches!(&*unsealed_decls[0].0, cst::TopBinding::Let(_)));
2971    }
2972
2973    /// Sealing-erasure twin 2: the struct body still lowers (surfacing its own precise
2974    /// error) whether or not the module carries a `:>` annotation — a body
2975    /// error is identical either way, since lowering never consults the
2976    /// annotation. Body error: an `include` of an inline `struct … end`
2977    /// literal, unsupported.
2978    #[test]
2979    fn sig_annot_body_error_is_identical_with_or_without_a_seal() {
2980        let sealed = parse_v1("module M :> sig end = struct\ninclude struct val x = 1 end\nend");
2981        let unsealed = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
2982        let sealed_err = lower_file_v1(&sealed).unwrap_err();
2983        let unsealed_err = lower_file_v1(&unsealed).unwrap_err();
2984        assert!(
2985            sealed_err.to_string().contains("inline `struct"),
2986            "{sealed_err}"
2987        );
2988        assert_eq!(sealed_err.construct, unsealed_err.construct);
2989        assert_eq!(sealed_err.hint, unsealed_err.hint);
2990    }
2991
2992    /// Sealing-erasure twin 3: the bind-level twin of
2993    /// `sig_annot_on_library_lowers_like_its_unsealed_twin` — a nested
2994    /// `module N :> sig .. end = struct .. end` lowers to the same nested
2995    /// `TopBinding::Module { sig: None, .. }` shape as its unsealed twin.
2996    #[test]
2997    fn nested_module_sig_annot_lowers_like_its_unsealed_twin() {
2998        let sealed = parse_v1(
2999            "module M = struct\n\
3000             module N :> sig val y : int end = struct\n\
3001             val y = 2\n\
3002             end\n\
3003             end",
3004        );
3005        let unsealed = parse_v1(
3006            "module M = struct\n\
3007             module N = struct\n\
3008             val y = 2\n\
3009             end\n\
3010             end",
3011        );
3012        let sealed_lowered = lower_file_v1(&sealed).unwrap_or_else(|e| panic!("sealed: {e}"));
3013        let unsealed_lowered = lower_file_v1(&unsealed).unwrap_or_else(|e| panic!("unsealed: {e}"));
3014        let cst::TopBinding::Module {
3015            decls: sealed_decls,
3016            ..
3017        } = &sealed_lowered[0]
3018        else {
3019            panic!("expected a TopBinding::Module");
3020        };
3021        let cst::TopBinding::Module {
3022            decls: unsealed_decls,
3023            ..
3024        } = &unsealed_lowered[0]
3025        else {
3026            panic!("expected a TopBinding::Module");
3027        };
3028        assert_eq!(sealed_decls.len(), 1);
3029        assert_eq!(unsealed_decls.len(), 1);
3030        let cst::TopBinding::Module {
3031            name: sealed_name,
3032            sig: sealed_sig,
3033            decls: sealed_inner,
3034            ..
3035        } = &*sealed_decls[0].0
3036        else {
3037            panic!("expected decls[0] to be a nested TopBinding::Module");
3038        };
3039        let cst::TopBinding::Module {
3040            name: unsealed_name,
3041            sig: unsealed_sig,
3042            decls: unsealed_inner,
3043            ..
3044        } = &*unsealed_decls[0].0
3045        else {
3046            panic!("expected decls[0] to be a nested TopBinding::Module");
3047        };
3048        assert_eq!(sealed_name.name, "N");
3049        assert_eq!(unsealed_name.name, "N");
3050        assert!(
3051            sealed_sig.is_none(),
3052            "the seal must lower to NO cst::SigAnnot at all"
3053        );
3054        assert!(unsealed_sig.is_none());
3055        assert_eq!(sealed_inner.len(), unsealed_inner.len());
3056    }
3057
3058    /// A module alias/path binding naming an UNKNOWN target
3059    /// is a precise `LowerError` — `N` is never defined anywhere in this
3060    /// file, so `lower_module_alias` can't resolve it.
3061    #[test]
3062    fn module_alias_with_unknown_target_is_a_lower_error() {
3063        let file = parse_v1("module M = struct\nmodule P = N\nend");
3064        let err = lower_file_v1(&file).unwrap_err();
3065        assert!(err.to_string().contains("module alias"), "{err}");
3066    }
3067
3068    /// A module alias to an EARLIER, real sibling
3069    /// module lowers for real — one `Let` per exported value member,
3070    /// referencing the target's absolute qualified name.
3071    #[test]
3072    fn module_alias_to_a_real_target_lowers_member_copies() {
3073        let file = parse_v1(
3074            "module M = struct\n\
3075             module Base = struct val x = 1 val f y = y end\n\
3076             module Alias = Base\n\
3077             end",
3078        );
3079        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3080        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3081            panic!("expected a TopBinding::Module");
3082        };
3083        let cst::TopBinding::Module {
3084            name,
3085            decls: alias_decls,
3086            ..
3087        } = &*decls[1].0
3088        else {
3089            panic!("expected decls[1] to be the Alias module");
3090        };
3091        assert_eq!(name.name, "Alias");
3092        assert_eq!(alias_decls.len(), 2, "one copy per exported value member");
3093        for (decl, expected_name) in alias_decls.iter().zip(["x", "f"]) {
3094            let cst::TopBinding::Let(top_let) = &*decl.0 else {
3095                panic!("expected a TopBinding::Let copy");
3096            };
3097            assert_eq!(top_let.name.name, expected_name);
3098            let cst::ast::Expr::Ops(chain) = &top_let.value else {
3099                panic!("expected an application-chain expression");
3100            };
3101            let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3102                panic!("expected a VarWithMod reference to the target");
3103            };
3104            assert_eq!(tok.mods, vec!["M.Base".to_string()]);
3105            assert_eq!(tok.name, expected_name);
3106        }
3107    }
3108
3109    /// A FORWARD reference (`module M = Later` before
3110    /// `Later` is itself defined) is the same "unknown module" error as a
3111    /// genuinely nonexistent name — an alias may only target an EARLIER
3112    /// module.
3113    #[test]
3114    fn module_alias_forward_reference_is_a_lower_error() {
3115        let file = parse_v1(
3116            "module M = struct\n\
3117             module Early = Later\n\
3118             module Later = struct val x = 1 end\n\
3119             end",
3120        );
3121        let err = lower_file_v1(&file).unwrap_err();
3122        assert!(err.to_string().contains("module alias"), "{err}");
3123    }
3124
3125    /// A functor application.
3126    #[test]
3127    fn functor_application_is_a_lower_error() {
3128        let file = parse_v1("module M = struct\nmodule P = F X\nend");
3129        let err = lower_file_v1(&file).unwrap_err();
3130        assert!(err.to_string().contains("functor application"), "{err}");
3131    }
3132
3133    /// A functor DEFINITION (`module F = fun (X:S) -> ...`)
3134    /// emits ZERO runtime bindings — a functor is not a value, exactly
3135    /// `Bind::Signature`'s posture. `M`'s own `TopBinding::Module` still
3136    /// lowers, just with no `F` member inside it.
3137    #[test]
3138    fn functor_literal_emits_zero_runtime_bindings() {
3139        let file = parse_v1(
3140            "module M = struct\n\
3141             module F = fun (X : sig val x : int end) -> struct val y = X.x end\n\
3142             end",
3143        );
3144        let bindings =
3145            lower_file_v1(&file).expect("a functor definition now lowers, emitting no member");
3146        let cst::TopBinding::Module { decls, .. } = &bindings[0] else {
3147            panic!("expected M's TopBinding::Module")
3148        };
3149        assert!(
3150            decls.is_empty(),
3151            "a functor literal contributes no decls: {decls:?}"
3152        );
3153    }
3154
3155    /// A bare module coercion `N :> S` naming an UNKNOWN
3156    /// `N` is a precise `LowerError` — the same "unknown module" wording a
3157    /// bare `Var` alias produces (`lower_module_alias` is shared).
3158    #[test]
3159    fn module_coercion_with_unknown_target_is_a_lower_error() {
3160        let file = parse_v1("module M = struct\nmodule P = N :> S\nend");
3161        let err = lower_file_v1(&file).unwrap_err();
3162        assert!(err.to_string().contains("module alias"), "{err}");
3163    }
3164
3165    /// `module M = N :> S` lowers the SAME member-copy
3166    /// shape as `module M = N` (the seal-rule pin, alias flavor) — the
3167    /// `:> S` annotation is dropped by lowering and enforced entirely by
3168    /// `v1/module_check.rs`. (Compared structurally, not by full-AST
3169    /// equality: the longer `:> sig …` source text legitimately shifts
3170    /// every byte span, so spans differ even though the shape is identical
3171    /// — the same span-insensitivity every other alias-shape test here
3172    /// relies on by asserting on fields rather than whole nodes.)
3173    #[test]
3174    fn module_coercion_lowers_the_same_copies_as_its_uncoerced_twin() {
3175        fn alias_copy_names(src: &str) -> (Vec<String>, Vec<Vec<String>>) {
3176            let lowered = lower_file_v1(&parse_v1(src)).unwrap_or_else(|e| panic!("{e}"));
3177            let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3178                panic!("expected a TopBinding::Module");
3179            };
3180            let cst::TopBinding::Module {
3181                name,
3182                decls: alias_decls,
3183                ..
3184            } = &*decls[1].0
3185            else {
3186                panic!("expected decls[1] to be the Alias module");
3187            };
3188            assert_eq!(name.name, "Alias");
3189            let mut names = Vec::new();
3190            let mut refs = Vec::new();
3191            for d in alias_decls {
3192                let cst::TopBinding::Let(top_let) = &*d.0 else {
3193                    panic!("expected a Let copy");
3194                };
3195                names.push(top_let.name.name.clone());
3196                let cst::ast::Expr::Ops(chain) = &top_let.value else {
3197                    panic!("expected an application chain");
3198                };
3199                let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3200                    panic!("expected a VarWithMod reference");
3201                };
3202                refs.push(tok.mods.clone());
3203            }
3204            (names, refs)
3205        }
3206        let bare = alias_copy_names(
3207            "module M = struct\n\
3208             module Base = struct val x = 1 end\n\
3209             module Alias = Base\n\
3210             end",
3211        );
3212        let coerced = alias_copy_names(
3213            "module M = struct\n\
3214             module Base = struct val x = 1 end\n\
3215             module Alias = Base :> sig val x : int end\n\
3216             end",
3217        );
3218        assert_eq!(bare, coerced);
3219        assert_eq!(bare.0, vec!["x".to_string()]);
3220        assert_eq!(bare.1, vec![vec!["M.Base".to_string()]]);
3221    }
3222
3223    /// A `signature S = ...` bind lowers to NOTHING — zero
3224    /// runtime/type-spine residue.
3225    #[test]
3226    fn signature_bind_lowers_to_nothing() {
3227        let file = parse_v1("module M = struct\nsignature S = sig end\nval x = 1\nend");
3228        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3229        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3230            panic!("expected a TopBinding::Module");
3231        };
3232        assert_eq!(decls.len(), 1, "the signature bind contributes zero decls");
3233        assert!(matches!(&*decls[0].0, cst::TopBinding::Let(_)));
3234    }
3235
3236    /// `include N` naming an UNKNOWN module is a precise
3237    /// `LowerError` — `N` is never defined anywhere in this file.
3238    #[test]
3239    fn include_of_an_unknown_module_is_a_lower_error() {
3240        let file = parse_v1("module M = struct\ninclude N\nend");
3241        let err = lower_file_v1(&file).unwrap_err();
3242        let msg = err.to_string();
3243        assert!(msg.contains("include"), "{msg}");
3244        assert!(msg.contains("unknown module"), "{msg}");
3245    }
3246
3247    /// `include M` naming an EARLIER real sibling
3248    /// module splices copies of ALL its exported members DIRECTLY into the
3249    /// includer's own decls — no synthetic wrapper module (contrast the
3250    /// alias arm's `module_alias_to_a_real_target_lowers_member_copies`).
3251    #[test]
3252    fn include_of_a_real_target_splices_member_copies_unwrapped() {
3253        let file = parse_v1(
3254            "module M = struct\n\
3255             module Base = struct val x = 1 val f y = y end\n\
3256             include Base\n\
3257             end",
3258        );
3259        let lowered = lower_file_v1(&file).unwrap_or_else(|e| panic!("lower_file_v1: {e}"));
3260        let cst::TopBinding::Module { decls, .. } = &lowered[0] else {
3261            panic!("expected a TopBinding::Module");
3262        };
3263        // decls[0] = the `Base` module; decls[1..] = the SPLICED copies
3264        // (unwrapped — no synthetic `TopBinding::Module` wrapping them).
3265        assert_eq!(decls.len(), 3, "Base + 2 spliced copies, unwrapped");
3266        for (decl, expected_name) in decls[1..].iter().zip(["x", "f"]) {
3267            let cst::TopBinding::Let(top_let) = &*decl.0 else {
3268                panic!("expected a TopBinding::Let copy, got {:?}", decl.0);
3269            };
3270            assert_eq!(top_let.name.name, expected_name);
3271            let cst::ast::Expr::Ops(chain) = &top_let.value else {
3272                panic!("expected an application-chain expression");
3273            };
3274            let cst::ast::Atomic::VarWithMod(tok) = &chain.head.head else {
3275                panic!("expected a VarWithMod reference to the target");
3276            };
3277            assert_eq!(tok.mods, vec!["M.Base".to_string()]);
3278            assert_eq!(tok.name, expected_name);
3279        }
3280    }
3281
3282    /// A FORWARD reference (`include Later`
3283    /// before `Later` is itself defined) is the same "unknown module" error
3284    /// as a genuinely nonexistent name (the `alias_targets`-style frozen
3285    /// resolution twin, `include_targets`).
3286    #[test]
3287    fn include_forward_reference_is_a_lower_error() {
3288        let file = parse_v1(
3289            "module M = struct\n\
3290             include Later\n\
3291             module Later = struct val x = 1 end\n\
3292             end",
3293        );
3294        let err = lower_file_v1(&file).unwrap_err();
3295        assert!(err.to_string().contains("unknown module"), "{err}");
3296    }
3297
3298    /// `include F X` (a functor application) reuses the
3299    /// EXISTING functor `LowerError` wording verbatim — the `map.satyg
3300    /// :344` `include Make Int` shape.
3301    #[test]
3302    fn include_of_a_functor_application_is_the_2f_functor_error() {
3303        // `F` here is a plain STRUCT, not a functor, so `include F X`
3304        // freezes an unresolved (`None`) app target — hence the "unknown"
3305        // wording rather than an out-of-scope one (a
3306        // parameter-argument application does resolve, once its enclosing
3307        // functor is applied).
3308        let file = parse_v1("module M = struct\nmodule F = struct end\ninclude F X\nend");
3309        let err = lower_file_v1(&file).unwrap_err();
3310        let msg = err.to_string();
3311        assert!(msg.contains("functor application"), "{msg}");
3312        assert!(msg.contains("unknown"), "{msg}");
3313    }
3314
3315    /// `include struct … end` (an inline struct literal)
3316    /// is its own precise out-of-scope error — zero upstream-stdlib demand.
3317    #[test]
3318    fn include_of_an_inline_struct_literal_is_a_lower_error() {
3319        let file = parse_v1("module M = struct\ninclude struct val x = 1 end\nend");
3320        let err = lower_file_v1(&file).unwrap_err();
3321        let msg = err.to_string();
3322        assert!(msg.contains("struct"), "{msg}");
3323        assert!(msg.contains("name the module first"), "{msg}");
3324    }
3325
3326    /// `include M :> S` (a coerced module body) is its
3327    /// own precise out-of-scope error.
3328    #[test]
3329    fn include_of_a_coerced_module_is_a_lower_error() {
3330        let file = parse_v1(
3331            "module M = struct\n\
3332             module Base = struct val x = 1 end\n\
3333             include Base :> sig val x : int end\n\
3334             end",
3335        );
3336        let err = lower_file_v1(&file).unwrap_err();
3337        let msg = err.to_string();
3338        assert!(msg.contains("coerced module"), "{msg}");
3339    }
3340
3341    /// Self-include (`module P = struct include
3342    /// P end`) is the same "unknown module" error — `P` only registers in
3343    /// `env.modules` AFTER its own body walk finishes, so it is never its
3344    /// own earlier module.
3345    #[test]
3346    fn self_include_is_a_lower_error() {
3347        let file = parse_v1("module P = struct\ninclude P\nend");
3348        let err = lower_file_v1(&file).unwrap_err();
3349        assert!(err.to_string().contains("unknown module"), "{err}");
3350    }
3351
3352    /// The CmdTail bridge: `\cmd{a}{b}` parses under `cst_v1` as one
3353    /// application chain (`AppExpr { head: {a}, args: [{b}] }`) and must
3354    /// lower to the same shape `cst.rs`'s own `\cmd{a}{b}` parse produces
3355    /// (`CmdTail::Args { first: {a}, rest: [{b}] }`).
3356    #[test]
3357    fn cmd_tail_bridge_matches_flat_app_arg_shape() {
3358        let file = parse_v1(r"{\cmd{a}{b}}");
3359        let cst_v1::FileV1::Document { body, .. } = file else {
3360            panic!("expected a document file");
3361        };
3362        let ast_v1::Expr::Ops(chain) = body else {
3363            panic!("expected an operator-chain expression");
3364        };
3365        let ast_v1::Atomic::InlineText { elems, .. } = chain.head.head else {
3366            panic!("expected inline text");
3367        };
3368        let ast_v1::InlineElem::Cmd { tail, .. } = &elems[0] else {
3369            panic!("expected the first element to be a command");
3370        };
3371        let lowered = lower_cmd_tail(tail).unwrap();
3372        let cst::ast::CmdTail::Args { first, rest, .. } = lowered else {
3373            panic!("expected CmdTail::Args");
3374        };
3375        assert_eq!(
3376            rest.len(),
3377            1,
3378            "\\cmd{{a}}{{b}} has exactly one trailing arg"
3379        );
3380        assert!(matches!(
3381            &*first.0,
3382            cst::ast::AppArg::Atom {
3383                stage: None,
3384                atom: cst::ast::Atomic::InlineText { .. },
3385                ..
3386            }
3387        ));
3388        assert!(matches!(
3389            &*rest[0].0,
3390            cst::ast::AppArg::Atom {
3391                stage: None,
3392                atom: cst::ast::Atomic::InlineText { .. },
3393                ..
3394            }
3395        ));
3396    }
3397}