Iter 16a: nested constructor patterns via AST desugaring
Lifts the gate that rejected `(pat-ctor Cons a (pat-ctor Cons b _))`.
Approach: a pure AST → AST pass `ailang_core::desugar::desugar_module`
that flattens nested-Ctor sub-patterns into chains of single-level
matches with let-bound fresh vars and duplicate fall-through. Both
ailang-check and ailang-codegen call the pass at pipeline entry.
Hash-relevant canonical bytes are untouched: the pass runs after
load_module, in memory only. `check`'s returned CheckedModule.symbols
still carries hashes of the original defs so `ail diff` / `ail manifest`
see on-disk identities.
Lit sub-patterns inside a Ctor still rejected — that's the narrower
scope of `nested-ctor-pattern-not-allowed` post-16a; the variant doc
reflects the new meaning.
Fresh-name safety: `$` is a valid ident character in form (A), so
the Desugarer pre-walks every Term::Var / Pattern::Var name into a
BTreeSet and bumps its counter past any user-spelled `$mp_N`.
Already-flat matches go through `is_flat()` and emit identical AST
shapes; every existing fixture produces the same output as before.
New: examples/nested_pat.{ailx,ail.json} prints 30 from
first_two_sum on [10, 20, 30]. e2e test
`nested_ctor_pattern_first_two_sum` guards the path.
Tests: 92/92 (e2e 32→33, ailang-core unit 10→12).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -374,8 +374,12 @@ pub enum CheckError {
|
||||
#[error("duplicate definition: `{0}`")]
|
||||
DuplicateDef(String),
|
||||
|
||||
/// A `Pattern::Ctor` has a non-`Var`/non-`Wild` sub-pattern. The
|
||||
/// MVP forbids decision-tree lowering of nested patterns. Code:
|
||||
/// A `Pattern::Ctor` has a [`Pattern::Lit`] sub-pattern. Iter 16a
|
||||
/// added an AST-level desugar that flattens nested **Ctor**
|
||||
/// sub-patterns before the checker sees the module, so this
|
||||
/// diagnostic now fires only for literal sub-patterns inside a
|
||||
/// Ctor (still out of scope; a future iter would lower them as a
|
||||
/// nested Match on the field). Code:
|
||||
/// `nested-ctor-pattern-not-allowed`.
|
||||
#[error("nested constructor pattern not allowed in MVP: `{0}`")]
|
||||
NestedCtorPatternNotAllowed(String),
|
||||
@@ -546,6 +550,11 @@ impl CheckError {
|
||||
/// workspace will inevitably produce `unknown-module` errors on qualified
|
||||
/// references — which is correct.
|
||||
pub fn check_module(m: &Module) -> Vec<Diagnostic> {
|
||||
// Iter 16a: flatten nested constructor patterns before any check
|
||||
// logic touches the AST. The rewrite is pure and runs in memory;
|
||||
// canonical-JSON hashes (computed by `ailang_core::load_module` /
|
||||
// `def_hash`) are unaffected because they use the on-disk form.
|
||||
let m = ailang_core::desugar::desugar_module(m);
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
let ws = Workspace {
|
||||
@@ -570,6 +579,20 @@ pub fn check_module(m: &Module) -> Vec<Diagnostic> {
|
||||
/// Module iteration order is deterministic: entry first, then the rest in
|
||||
/// BTreeMap order, so output ordering is stable.
|
||||
pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
|
||||
// Iter 16a: desugar every module of the workspace before any check
|
||||
// logic runs. The rewrite is pure and per-module; we rebuild a
|
||||
// workspace shell around the desugared modules (paths and entry
|
||||
// are unchanged).
|
||||
let ws_owned = Workspace {
|
||||
entry: ws.entry.clone(),
|
||||
modules: ws
|
||||
.modules
|
||||
.iter()
|
||||
.map(|(k, m)| (k.clone(), ailang_core::desugar::desugar_module(m)))
|
||||
.collect(),
|
||||
root_dir: ws.root_dir.clone(),
|
||||
};
|
||||
let ws = &ws_owned;
|
||||
// Pass 1: build per-module top-level symbol table — without checking
|
||||
// bodies. This lets module A access defs from module B even when B
|
||||
// comes later in the BTreeMap. Duplicate def names and dot-in-def
|
||||
@@ -622,6 +645,14 @@ pub struct CheckedModule {
|
||||
/// kept for legacy callers (snapshot tests, the `manifest` codepath
|
||||
/// that needs the hash-to-type mapping).
|
||||
pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
// Iter 16a: desugar nested ctor patterns before constructing the
|
||||
// trivial workspace. See `check_module` for the rationale. The
|
||||
// returned `CheckedModule.symbols` content hashes are derived from
|
||||
// the *original* on-disk module so they keep the canonical-bytes
|
||||
// identity that `ail diff` / `ail manifest` rely on.
|
||||
let original = m;
|
||||
let desugared = ailang_core::desugar::desugar_module(m);
|
||||
let m = &desugared;
|
||||
// Trivial workspace: the module alone, without cross-module resolution.
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
@@ -638,8 +669,10 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
return Err(first);
|
||||
}
|
||||
// Collect symbols for the return value (existing semantics).
|
||||
// Hashes are computed over the *original* defs, so callers like
|
||||
// `ail diff` see the on-disk identity, not a post-desugar one.
|
||||
let mut symbols = IndexMap::new();
|
||||
for def in &m.defs {
|
||||
for def in &original.defs {
|
||||
let h = ailang_core::hash::def_hash(def);
|
||||
let ty = match def {
|
||||
Def::Fn(f) => f.ty.clone(),
|
||||
@@ -1531,12 +1564,25 @@ fn type_check_pattern(
|
||||
Ok(vec![])
|
||||
}
|
||||
Pattern::Ctor { ctor, fields } => {
|
||||
// MVP: sub-patterns of ctor patterns may only be `Var` or `Wild`.
|
||||
// Nested ctor or lit patterns need decision-tree lowering,
|
||||
// which we don't have in codegen yet.
|
||||
// Iter 16a: nested **Ctor** sub-patterns are removed by
|
||||
// `ailang_core::desugar::desugar_module` before this checker
|
||||
// runs, so we only have to defend against the still-rejected
|
||||
// **Lit** sub-pattern case. The `nested-ctor-pattern-not-allowed`
|
||||
// error code is reused for that — its meaning is now narrower
|
||||
// ("non-flat: a Lit sub-pattern was found"), and the docstring
|
||||
// on the variant reflects that.
|
||||
for sub in fields {
|
||||
if !matches!(sub, Pattern::Var { .. } | Pattern::Wild) {
|
||||
return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone()));
|
||||
match sub {
|
||||
Pattern::Var { .. } | Pattern::Wild => {}
|
||||
Pattern::Lit { .. } => {
|
||||
return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone()));
|
||||
}
|
||||
Pattern::Ctor { .. } => {
|
||||
unreachable!(
|
||||
"nested Ctor sub-patterns are removed by \
|
||||
ailang_core::desugar before check"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Iter 15a: try local ctor_index first; if the bare name
|
||||
|
||||
Reference in New Issue
Block a user