Files
AILang/crates/ailang-check/src/mono.rs
T
Brummel 84dcc46645 fix: mono.rs per-module env.ctor_index overlay (same family as 13b36cc / 5c5180f)
The mono pass re-runs synth on every fn body to recover residual class
constraints. build_workspace_env (delegating to build_check_env) leaves
env.types and env.ctor_index workspace-flat. The synth path for cross-
module Pattern::Ctor / Term::Ctor resolution depends on a per-module
shape — local-first lookup, then imports-fallback that produces a
qualified type name. Without the overlay, the local-flat hit short-
circuits the fallback and yields a bare type name, mismatching the
qualified scrutinee/pattern from the sibling path with
CheckError::PatternTypeMismatch.

Add a per-module overlay helper that clears and rebuilds both
env.types and env.ctor_index from the current module's Def::Type list
— mirroring check_in_workspace at lib.rs:1257-1287 — and apply it at
the two mono entry points that re-walk bodies: the Phase 3 rewrite
loop in monomorphise_workspace and collect_targets_workspace_wide.

Pinned by tests/mono_xmod_ctor_pattern.rs (e580f75); also unblocks
the latent E2E regressions nested_ctor_pattern_first_two_sum,
std_either_list_demo, and ordering_match_via_prelude_prints_1 that
surfaced once the typeclass gate in workspace_has_typeclasses flipped
to true.
2026-05-10 22:23:53 +02:00

887 lines
36 KiB
Rust

//! Iter 22b.3: workspace monomorphisation pass.
//!
//! Slots into the build pipeline after [`crate::lift_letrecs`] and
//! before `ailang_codegen::lower_workspace_with_alloc`.
//!
//! ## What Task 1 delivers
//!
//! Skeleton only. [`monomorphise_workspace`] is a no-op pass with
//! two branches:
//!
//! - Class-free workspaces (no [`Def::Class`] / [`Def::Instance`]
//! anywhere) take the early-out and the input workspace is
//! cloned unchanged.
//! - Class-present workspaces fall through to a second
//! `Ok(ws.clone())`, dead-equivalent today but the placeholder
//! for the real fixpoint body that later tasks will fill in.
//!
//! Both branches return a byte-identical workspace clone, so
//! `module_hash` is preserved end-to-end through the pass at this
//! stage of the iteration.
//!
//! ## Planned (later tasks in iter 22b.3)
//!
//! See `docs/plans/2026-05-09-22b3-monomorphisation.md` for the
//! full task list. The eventual contract is for the pass to
//! consume the workspace's typeclass [`Registry`] and the per-
//! module class-method tables (already populated by
//! [`crate::check_in_workspace`]) and produce a workspace with:
//!
//! 1. Synthesised [`Def::Fn`] entries — one per unique
//! `(class, method, type-hash)` triple observed at any
//! class-method call site — appended to the [`Registry`]'s
//! `defining_module` for that instance.
//! 2. Rewritten call sites — every `Term::Var { name }` whose
//! `name` resolves through [`Env::class_methods`] is replaced
//! by the corresponding mono-symbol name (qualified with the
//! instance's `defining_module` iff that module differs from
//! the calling module).
//!
//! Pre-existing `Def::Class` and `Def::Instance` entries are
//! preserved verbatim — codegen ignores them, but downstream
//! tooling (e.g. `ail describe`) may still consult them.
//!
//! ## Symbol-hashing invariant (eventual-state)
//!
//! Once the synthesis body lands, synthesised FnDefs will be
//! post-typecheck artefacts. They will NOT enter
//! `CheckedModule.symbols` (built from the original module at
//! typecheck time and used by `ail diff` / `ail manifest`). Same
//! convention as [`crate::lift_letrecs`]. Documented here ahead of
//! the implementation so the constraint is visible to anyone
//! extending the skeleton.
use ailang_core::ast::{Arm, ClassDef, ConstDef, Def, FnDef as AstFnDef, InstanceDef, Pattern, Term, Type};
use ailang_core::workspace::Workspace;
use crate::{CtorRef, Result};
use indexmap::IndexMap;
use std::collections::{BTreeMap, BTreeSet};
/// Iter 22b.3: workspace-wide monomorphisation pass entry. See the
/// module-level doc for the architecture and contract.
///
/// Pre-condition: `ws` has been typechecked (`check_workspace(ws)`
/// returned no errors) and lifted (`lift_letrecs` per module). The
/// pass does not perform new type checking — it queries types via
/// `synth` on already-typechecked bodies.
pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
// Fast path — no class / instance defs anywhere → nothing to do.
// The pass also has no targets when there are class defs but no
// instance defs (no callable methods at concrete types), but
// the body walks would still be a wasted traversal; the
// class-free check is the cheap way to opt out.
if !workspace_has_typeclasses(ws) {
return Ok(ws.clone());
}
let mut ws_owned: Workspace = ws.clone();
let env = build_workspace_env(&ws_owned);
let class_index = build_class_index(&ws_owned);
let mut synthesised: BTreeSet<(String, String, String)> = BTreeSet::new();
// Fixpoint: keep collecting until a round adds nothing new.
// Each round walks every fn body in every module — including
// bodies appended by the previous round, which is what makes
// the loop close on chained class-method calls (e.g.
// `Eq.ne x y = not (eq x y)`).
loop {
let new_targets = collect_targets_workspace_wide(&ws_owned, &env)?;
// Dedup within a round (multiple call sites of `show` at
// the same type produce N copies of the same MonoTarget)
// AND across rounds (already-synthesised keys filtered out).
// Stable iteration order — preserve first-seen target so
// any later debugging round-trips to a deterministic output.
let mut seen_this_round: BTreeSet<(String, String, String)> = BTreeSet::new();
let mut new: Vec<MonoTarget> = Vec::new();
for t in new_targets {
let k = mono_target_key(&t);
if synthesised.contains(&k) || !seen_this_round.insert(k) {
continue;
}
new.push(t);
}
if new.is_empty() {
break;
}
// Synthesise each new target; append its FnDef to the
// target's `defining_module`. Mark the key as synthesised
// so the next round won't re-collect.
//
// Iter 22b.3 uses ws_owned.registry as the source of
// truth for ClassDef + InstanceDef lookups. Both come
// from the un-modified workspace registry — synthesis
// never mutates the registry.
for t in &new {
let key = mono_target_key(t);
let registry_key = (t.class.clone(), ailang_core::canonical::type_hash(&t.type_));
let entry = ws_owned
.registry
.entries
.get(&registry_key)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: target `{} {}` has no registry entry",
t.class,
ailang_core::pretty::type_to_string(&t.type_),
))
})?;
let class_def = class_index.get(&t.class).ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: class `{}` not found",
t.class
))
})?;
let f = synthesise_mono_fn(t, class_def, &entry.instance)?;
let target_module = ws_owned
.modules
.get_mut(&t.defining_module)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: defining module `{}` missing",
t.defining_module
))
})?;
target_module.defs.push(Def::Fn(f));
synthesised.insert(key);
}
}
// Phase 3: rewrite call sites in every fn / const body. Walk in
// the same pre-order as collect_residuals_ordered so the cursor
// and the per-callsite target list align position-by-position.
let env = build_workspace_env(&ws_owned); // re-build: ws_owned has new defs.
let module_names: Vec<String> = ws_owned.modules.keys().cloned().collect();
for mname in &module_names {
// Per-module ctor_index overlay — see
// [`apply_per_module_ctor_index_overlay`] for rationale.
// Mirrors the overlay applied in
// `collect_targets_workspace_wide` so the rewrite-phase
// residual replay produces the same residual list as the
// collection-phase walk (cursor alignment depends on it).
let mut env_mod = env.clone();
apply_per_module_ctor_index_overlay(&mut env_mod, &ws_owned, mname);
let n_defs = ws_owned.modules[mname].defs.len();
for i in 0..n_defs {
let ordered: Vec<Option<MonoTarget>> = {
let m = &ws_owned.modules[mname];
let d = &m.defs[i];
match d {
Def::Fn(f) => collect_residuals_ordered(f, mname, &env_mod)?,
Def::Const(c) => {
let pseudo = const_as_pseudo_fn(c);
collect_residuals_ordered(&pseudo, mname, &env_mod)?
}
_ => continue,
}
};
let m = ws_owned.modules.get_mut(mname).unwrap();
let d = &mut m.defs[i];
let mut cursor = 0usize;
let mut locals: BTreeSet<String> = BTreeSet::new();
match d {
Def::Fn(f) => {
// Top-level fn params shadow class-method names too.
for p in &f.params {
locals.insert(p.clone());
}
rewrite_class_method_calls(
&mut f.body,
&env.class_methods,
mname,
&ordered,
&mut cursor,
&mut locals,
);
}
Def::Const(c) => {
rewrite_class_method_calls(
&mut c.value,
&env.class_methods,
mname,
&ordered,
&mut cursor,
&mut locals,
);
}
_ => {}
}
}
}
Ok(ws_owned)
}
/// Iter 22b.3: walk every fn / const body in the workspace,
/// returning the union of [`collect_mono_targets`] outputs. Const
/// bodies are wrapped in a synthetic zero-arg `FnDef` for the
/// collection call — the residual gathering only depends on body
/// shape, so the wrapping is harmless.
fn collect_targets_workspace_wide(
ws: &Workspace,
env: &crate::Env,
) -> Result<Vec<MonoTarget>> {
let mut out: Vec<MonoTarget> = Vec::new();
for (mname, m) in &ws.modules {
// Per-module ctor_index overlay — see
// [`apply_per_module_ctor_index_overlay`] for rationale. The
// env arriving here is workspace-flat (via `build_check_env`);
// synth's `Pattern::Ctor` resolution requires per-module
// shape to keep the qualified-type-name comparison intact
// for cross-module ctor patterns.
let mut env_mod = env.clone();
apply_per_module_ctor_index_overlay(&mut env_mod, ws, mname);
for d in &m.defs {
match d {
Def::Fn(f) => {
out.extend(collect_mono_targets(f, mname, &env_mod)?);
}
Def::Const(c) => {
let pseudo = const_as_pseudo_fn(c);
out.extend(collect_mono_targets(&pseudo, mname, &env_mod)?);
}
_ => {}
}
}
}
Ok(out)
}
/// Iter 22b.3.6: wrap a [`ConstDef`] in a synthetic zero-arg
/// [`AstFnDef`] for residual-collection / rewrite purposes. Const
/// bodies have no parameter list, so the residual gathering only
/// depends on body shape — the wrapping is a structural adapter,
/// not a semantic conversion. Two callers (Phase 1 collection and
/// Phase 3 rewrite) need the same shape; sharing the constructor
/// keeps them in lockstep.
fn const_as_pseudo_fn(c: &ConstDef) -> AstFnDef {
AstFnDef {
name: c.name.clone(),
ty: c.ty.clone(),
params: Vec::new(),
body: c.value.clone(),
doc: None,
suppress: Vec::new(),
}
}
/// Iter 22b.3: workspace-wide `class-name -> ClassDef` index.
/// Used by the fixpoint to look up the matching class definition
/// when synthesising a fn.
fn build_class_index(ws: &Workspace) -> BTreeMap<String, ClassDef> {
let mut idx = BTreeMap::new();
for m in ws.modules.values() {
for d in &m.defs {
if let Def::Class(c) = d {
idx.insert(c.name.clone(), c.clone());
}
}
}
idx
}
/// Iter 22b.3: returns `true` iff any module in `ws` declares at
/// least one [`Def::Class`] or [`Def::Instance`]. Cheap workspace
/// scan; the early-out keeps class-free workspaces byte-identical
/// through the pass.
fn workspace_has_typeclasses(ws: &Workspace) -> bool {
ws.modules.values().any(|m| {
m.defs.iter().any(|d| matches!(d, Def::Class(_) | Def::Instance(_)))
})
}
/// Iter 22b.3: deterministic mono-symbol name for a `(method,
/// type)` pair. Primitive types (`Int`, `Bool`, `Str`, `Unit`)
/// produce `<method>__<surface-name>` for diagnostic and
/// ABI legibility. All other types — parameterised cons,
/// user-defined ADTs, function types — fall to
/// `<method>__<8-hex-prefix-of-canonical-type-hash>`. The hash
/// route ensures uniqueness without requiring a flattened
/// surface form for arbitrarily nested types.
///
/// Separator choice: `__` (double underscore) rather than the spec's
/// recommended `#` — `#` terminates LLVM IR global identifiers (and
/// breaks C-ABI symbol names on most targets), so the produced name
/// has to survive codegen unaltered. The double underscore is
/// already in use elsewhere in the codegen pipeline as a descriptor
/// separator (see `emit_specialised_fn`), keeping the convention
/// uniform. Iter 22b.3.7 documents the substitution at the e2e
/// gate; Tasks 2/4 unit-tests are updated in lockstep.
///
/// Determinism: `ailang_core::canonical::type_hash` is the same
/// function `workspace::build_registry` uses to key
/// [`Registry::entries`], so a registry-key match implies a
/// `mono_symbol` match.
pub fn mono_symbol(method: &str, ty: &Type) -> String {
if let Some(prim) = primitive_surface_name(ty) {
return format!("{method}__{prim}");
}
let full_hash = ailang_core::canonical::type_hash(ty);
// 8-hex prefix is enough for low-collision keying across the
// workspace; the full hash remains in the registry for
// disambiguation should one ever be needed.
format!("{method}__{}", &full_hash[..8])
}
/// Returns the surface name iff `ty` is a zero-arity primitive
/// `Type::Con`. Used by [`mono_symbol`] to gate the human-readable
/// form. The match is intentionally narrow: `Int<args>` (malformed
/// but parser-accepting) is treated as compound, so it falls to
/// the hash form. The primitive-set itself lives in
/// [`ailang_core::primitives::primitive_surface_name`].
fn primitive_surface_name(ty: &Type) -> Option<&'static str> {
match ty {
Type::Con { name, args } if args.is_empty() => {
ailang_core::primitives::primitive_surface_name(name)
}
_ => None,
}
}
/// Iter 22b.3: one synthesisation request — a fully-concrete
/// `(class, method, type)` triple observed at a class-method call
/// site, plus the registry's `defining_module` for the matching
/// `InstanceDef` (the synthesised fn lives there). Equality of
/// targets is compared via [`mono_target_key`] — `(class,
/// method, type-hash)`.
#[derive(Debug, Clone)]
pub struct MonoTarget {
pub class: String,
pub method: String,
pub type_: Type,
pub defining_module: String,
}
/// Iter 22b.3: dedup key for a [`MonoTarget`] — the same triple
/// `Registry::entries` uses for instance lookup, plus the method
/// name. Two targets with the same key produce the same
/// synthesised symbol and the same body. Consumed by the workspace
/// fixpoint (Task 5) and the rewrite walker (Task 6); declared
/// here in Task 3 so the dedup contract lives next to `MonoTarget`.
#[allow(dead_code)]
pub(crate) fn mono_target_key(t: &MonoTarget) -> (String, String, String) {
(
t.class.clone(),
t.method.clone(),
ailang_core::canonical::type_hash(&t.type_),
)
}
/// Thin wrapper over [`crate::build_check_env`]. The mono pass needs
/// the same workspace-flat `Env` shape as `check_in_workspace`, so both
/// share one source of truth. Per-fn entry points
/// (`collect_mono_targets`, `collect_residuals_ordered`) clone the env
/// and apply per-fn overlay (current_module, globals from
/// module_globals, imports from module_imports, rigid_vars) plus the
/// per-module ctor/type overlay
/// (see [`apply_per_module_ctor_index_overlay`]).
pub fn build_workspace_env(ws: &Workspace) -> crate::Env {
crate::build_check_env(ws)
}
/// Iter 22c (sibling of commit 5c5180f for `env.ctor_index`):
/// rebuild `env.types` AND `env.ctor_index` to contain ONLY the
/// types and ctors declared in `module_name`'s `Def::Type`s,
/// mirroring `check_in_workspace`'s per-module overlay at
/// `crates/ailang-check/src/lib.rs:1257-1287`.
///
/// `build_workspace_env` (via `build_check_env`) leaves both fields
/// workspace-flat. Two synth paths need the per-module shape:
///
/// * `Pattern::Ctor` resolution (lib.rs:2486-2526) does a
/// local-first `ctor_index` lookup followed by an imports-fallback
/// that produces a *qualified* `resolved_type_name` (`Mod.Type`).
/// With a flat `ctor_index`, a cross-module ctor pattern resolves
/// locally and yields a bare type name, mismatching the qualified
/// scrutinee type and firing `CheckError::PatternTypeMismatch`.
/// * `Term::Ctor` synth (lib.rs:1962-2026) does a local-first
/// `env.types` lookup followed by an imports-fallback that
/// produces a qualified `result_type_name`. With a flat `env.types`,
/// a `Term::Ctor` against a prelude (or imported) type resolves
/// locally and yields a bare scrutinee type — which then mismatches
/// the qualified `Pattern::Ctor` `resolved_type_name` produced by
/// the per-module-cleared `ctor_index`.
///
/// Both fields must be cleared together so the two synth paths agree
/// on bare-vs-qualified naming. Mirrors the pattern in
/// `check_in_workspace`, which clears and rebuilds both for the
/// same reason.
///
/// Caller-contract assumption: the workspace has already typechecked,
/// so duplicate type or ctor names within a single module cannot
/// occur (the fail-fast `DuplicateType` / `DuplicateCtor` diagnostics
/// in `check_in_workspace` would have surfaced).
fn apply_per_module_ctor_index_overlay(env: &mut crate::Env, ws: &Workspace, module_name: &str) {
env.types.clear();
env.ctor_index.clear();
if let Some(m) = ws.modules.get(module_name) {
for d in &m.defs {
if let Def::Type(td) = d {
for c in &td.ctors {
env.ctor_index.insert(
c.name.clone(),
CtorRef { type_name: td.name.clone() },
);
}
env.types.insert(td.name.clone(), td.clone());
}
}
}
}
/// Iter 22b.3: re-run [`crate::synth`] on `f`'s body to recover
/// the per-fn residual class constraints. Filter to fully-
/// concrete residuals (the only ones eligible for monomorphisation),
/// look up each `(class, type-hash)` in the registry to recover
/// the `defining_module`, and return the list. Var-shaped or
/// metavar-shaped residuals are silently skipped — the
/// 22b.2 typecheck pass has already fired
/// `MissingConstraint`/`NoInstance` for any that should not exist
/// at this point.
pub fn collect_mono_targets(
f: &AstFnDef,
module_name: &str,
env: &crate::Env,
) -> Result<Vec<MonoTarget>> {
// Build the per-def env exactly as `check_fn` does — install
// rigid vars, set the current module, etc. This mirrors
// `crate::check_fn` minus the diagnostic emission.
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
};
let (param_tys, _ret_ty, _eff): (Vec<Type>, Type, Vec<String>) = match &inner_ty {
Type::Fn { params, ret, effects, .. } => {
(params.clone(), (**ret).clone(), effects.clone())
}
_ => return Ok(Vec::new()),
};
let mut env = env.clone();
for v in &rigids {
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// Iter 22c (sibling of commit 5c5180f): seed `env.globals` from
// the current module's fns so `synth`'s `Term::Var` lookup
// (lib.rs:1678) resolves bare same-module references — most
// importantly self-recursive top-level fns. Mirrors
// `check_in_workspace` (lib.rs:1135-1139). Top-level fn names
// are only per-module-unique (workspace-wide collisions are
// legal), so seeding must be scoped to the current module —
// unlike the workspace-wide flat `env.types` / `env.ctor_index`
// tables that 5c5180f populated in `build_workspace_env`.
if let Some(g) = env.module_globals.get(module_name).cloned() {
for (n, t) in g {
env.globals.insert(n, t);
}
}
// Iter 22c (sibling of commit 13b36cc): seed `env.imports` from
// the current module's import list so `synth`'s qualified-var
// path (lib.rs:1697) resolves `Mod.fn` references. Mirrors
// `check_in_workspace` (lib.rs:1147-1152). Per-module because
// aliases collide across modules.
if let Some(im) = env.module_imports.get(module_name).cloned() {
env.imports = im;
}
let mut locals: IndexMap<String, Type> = IndexMap::new();
for (n, t) in f.params.iter().zip(param_tys.iter()) {
locals.insert(n.clone(), t.clone());
}
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = crate::Subst::default();
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut effects,
&f.name,
&mut subst,
&mut counter,
&mut residuals,
)?;
// Filter residuals to fully-concrete ones; look up
// defining_module via the registry.
let mut out: Vec<MonoTarget> = Vec::new();
for r in residuals {
let r_ty = subst.apply(&r.type_);
if !crate::is_fully_concrete(&r_ty) {
continue;
}
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty));
let entry = match env.workspace_registry.entries.get(&key) {
Some(e) => e,
None => continue, // no-instance — Task 10 of 22b.2 fires; skip silently here.
};
out.push(MonoTarget {
class: r.class.clone(),
method: r.method.clone(),
type_: r_ty,
defining_module: entry.defining_module.clone(),
});
}
Ok(out)
}
/// Iter 22b.3: produce a `FnDef` for a single (target, class,
/// instance) triple. The synthesised fn:
///
/// 1. has name [`mono_symbol`]`(target.method, target.type_)`,
/// 2. has type = the class's method type with the class param
/// substituted to `target.type_` (rigid-var substitution via
/// [`crate::substitute_rigids`]); the result is a plain
/// `Type::Fn` with no `Forall`,
/// 3. has params + body taken from the instance's matching
/// `InstanceMethod.body` if present and Lam-shaped; from the
/// class's `default` body if the instance omits the method;
/// or directly from the body if the method has no params
/// (zero-arg method types).
///
/// Errors (all [`crate::CheckError::Internal`] — caller-contract
/// violations that cannot occur after typecheck has succeeded):
///
/// - The instance omits the method AND the class has no `default`
/// — unreachable in practice because
/// `workspace::build_registry`'s `MissingMethod` check fires at
/// load.
/// - The class itself has no method by that name — also caught at
/// load.
/// - The method type is `(args) -> ret` with `args` non-empty but
/// the resolved body is not [`Term::Lam`] — schema-shape
/// mismatch enforced by 22b.2's instance-method check.
pub fn synthesise_mono_fn(
target: &MonoTarget,
class_def: &ClassDef,
instance: &InstanceDef,
) -> Result<AstFnDef> {
// Locate the class method declaration.
let class_method = class_def
.methods
.iter()
.find(|m| m.name == target.method)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn: class `{}` has no method `{}`",
class_def.name, target.method
))
})?;
// Build the substitution `param := target.type_` and apply it
// to the method type. The result is a concrete `Type::Fn` (no
// `Forall`).
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
mapping.insert(class_def.param.clone(), target.type_.clone());
let concrete_method_ty = crate::substitute_rigids(&class_method.ty, &mapping);
// Resolve the body — instance override first, then class default.
let body_term: Term = match instance.methods.iter().find(|im| im.name == target.method) {
Some(im) => im.body.clone(),
None => class_method.default.clone().ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn: instance `{} {}` omits method `{}` and class has no \
default (registry-build should have rejected this)",
target.class,
ailang_core::pretty::type_to_string(&target.type_),
target.method,
))
})?,
};
// Decide the (params, body) shape based on whether the method
// type takes positional args.
let method_has_params = matches!(
&class_method.ty,
Type::Fn { params, .. } if !params.is_empty()
);
let (params, body): (Vec<String>, Term) = if method_has_params {
match body_term {
Term::Lam { params, body, .. } => (params, *body),
other => {
return Err(crate::CheckError::Internal(format!(
"synthesise_mono_fn: method `{}` has positional params but body is not \
a Lam: {:?}",
target.method, other
)));
}
}
} else {
(Vec::new(), body_term)
};
Ok(AstFnDef {
name: mono_symbol(&target.method, &target.type_),
ty: concrete_method_ty,
params,
body,
doc: None,
suppress: Vec::new(),
})
}
/// Iter 22b.3: rewrite every class-method call site in `body` to
/// the corresponding mono symbol, using `ordered_targets` —
/// the per-call-site resolved targets collected by a parallel
/// synth-replay run on the same body.
///
/// The walker increments a positional counter at each
/// class-method-named `Term::Var` it encounters that is NOT
/// shadowed by a local binding. The counter must match the order
/// in which `synth` pushes residuals (pre-order AST walk; child
/// evaluation order matches the `match` arms in [`crate::synth`]).
/// When this invariant holds, `ordered_targets[idx]` is the target
/// resolved at the i-th class-method call site.
///
/// `caller_module`: name of the module enclosing this body. If
/// the target's `defining_module` differs, the rewritten name is
/// qualified `<defining_module>.<mono_symbol>` (cross-module
/// resolution path); otherwise unqualified.
///
/// `locals` mirrors `synth`'s lookup-precedence rule: a `Term::Var`
/// whose `name` is in `locals` resolves to the local binding and
/// `synth` pushes NO residual for it, so the walker must NOT
/// advance the cursor either. Each binding-form pushes its binders
/// before recursing into the relevant scope and pops them after.
///
/// `ordered_targets[idx]`:
/// - `Some(t)` — concrete instance found; rewrite the name.
/// - `None` — residual was non-concrete or had no instance; cursor
/// still advances (to keep alignment) but the name is left
/// unchanged.
fn rewrite_class_method_calls(
body: &mut Term,
class_methods: &BTreeMap<String, crate::ClassMethodEntry>,
caller_module: &str,
ordered_targets: &[Option<MonoTarget>],
cursor: &mut usize,
locals: &mut BTreeSet<String>,
) {
match body {
Term::Var { name } => {
if class_methods.contains_key(name) {
// Mirror synth's lookup-precedence rule: if `name` is
// shadowed by a local, synth resolves to the local
// and pushes no residual — we must not advance.
if locals.contains(name) {
return;
}
if let Some(slot) = ordered_targets.get(*cursor) {
if let Some(t) = slot {
let sym = mono_symbol(&t.method, &t.type_);
let new_name = if t.defining_module == caller_module {
sym
} else {
format!("{}.{}", t.defining_module, sym)
};
*name = new_name;
}
// else: residual was non-concrete → leave name unchanged.
}
*cursor += 1;
}
}
Term::App { callee, args, .. } => {
rewrite_class_method_calls(callee, class_methods, caller_module, ordered_targets, cursor, locals);
for a in args {
rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor, locals);
}
}
Term::Let { name, value, body } => {
// synth: `value` is in the outer scope; `body` sees `name`.
rewrite_class_method_calls(value, class_methods, caller_module, ordered_targets, cursor, locals);
let inserted = locals.insert(name.clone());
rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor, locals);
if inserted {
locals.remove(name);
}
}
Term::LetRec { name, params, body, in_term, .. } => {
// synth: `name` is in scope in BOTH `body` (with `params`
// also in scope) AND `in_term` (without `params`).
let name_inserted = locals.insert(name.clone());
// Body scope: name + params.
let mut params_inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
params_inserted.push(p.clone());
}
}
rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor, locals);
for p in &params_inserted {
locals.remove(p);
}
// In-clause scope: name only.
rewrite_class_method_calls(in_term, class_methods, caller_module, ordered_targets, cursor, locals);
if name_inserted {
locals.remove(name);
}
}
Term::If { cond, then, else_ } => {
rewrite_class_method_calls(cond, class_methods, caller_module, ordered_targets, cursor, locals);
rewrite_class_method_calls(then, class_methods, caller_module, ordered_targets, cursor, locals);
rewrite_class_method_calls(else_, class_methods, caller_module, ordered_targets, cursor, locals);
}
Term::Do { args, .. } => {
for a in args {
rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor, locals);
}
}
Term::Ctor { args, .. } => {
for a in args {
rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor, locals);
}
}
Term::Match { scrutinee, arms } => {
// synth: scrutinee is in outer scope; each arm's body sees
// its pattern's binders.
rewrite_class_method_calls(scrutinee, class_methods, caller_module, ordered_targets, cursor, locals);
for Arm { pat, body } in arms {
let binders = pattern_binders(pat);
let mut inserted: Vec<String> = Vec::new();
for b in &binders {
if locals.insert(b.clone()) {
inserted.push(b.clone());
}
}
rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor, locals);
for b in &inserted {
locals.remove(b);
}
}
}
Term::Lam { params, body, .. } => {
// synth: params are in scope in `body`.
let mut inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
inserted.push(p.clone());
}
}
rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor, locals);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
rewrite_class_method_calls(lhs, class_methods, caller_module, ordered_targets, cursor, locals);
rewrite_class_method_calls(rhs, class_methods, caller_module, ordered_targets, cursor, locals);
}
Term::Clone { value } => {
rewrite_class_method_calls(value, class_methods, caller_module, ordered_targets, cursor, locals);
}
Term::ReuseAs { source, body } => {
rewrite_class_method_calls(source, class_methods, caller_module, ordered_targets, cursor, locals);
rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor, locals);
}
Term::Lit { .. } => {}
}
}
/// Iter 22b.3.6: collect the variable binders introduced by a
/// match pattern. Used by the walker's `Term::Match` arm to extend
/// `locals` for each arm body. Mirrors `Pattern`'s shape:
/// - [`Pattern::Wild`] / [`Pattern::Lit`] bind nothing,
/// - [`Pattern::Var`] binds its `name`,
/// - [`Pattern::Ctor`] recurses through its `fields`.
fn pattern_binders(pat: &Pattern) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
fn rec(pat: &Pattern, out: &mut Vec<String>) {
match pat {
Pattern::Wild | Pattern::Lit { .. } => {}
Pattern::Var { name } => out.push(name.clone()),
Pattern::Ctor { fields, .. } => {
for f in fields {
rec(f, out);
}
}
}
}
rec(pat, &mut out);
out
}
/// Iter 22b.3: traversal-ordered residual collection — used by
/// the rewrite walker to align cursor positions. Unlike
/// [`collect_mono_targets`], this includes non-concrete residuals
/// as `None`, preserving one-entry-per-callsite alignment.
pub(crate) fn collect_residuals_ordered(
f: &AstFnDef,
module_name: &str,
env: &crate::Env,
) -> Result<Vec<Option<MonoTarget>>> {
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
};
let param_tys: Vec<Type> = match &inner_ty {
Type::Fn { params, .. } => params.clone(),
_ => return Ok(Vec::new()),
};
let mut env = env.clone();
for v in &rigids {
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// Iter 22c: same `env.globals` seeding as `collect_mono_targets`
// — see the comment there for rationale. Both fns re-run `synth`
// and must agree with the main check path's per-module env shape.
if let Some(g) = env.module_globals.get(module_name).cloned() {
for (n, t) in g {
env.globals.insert(n, t);
}
}
// Iter 22c: same `env.imports` seeding as `collect_mono_targets`
// — see the comment there for rationale.
if let Some(im) = env.module_imports.get(module_name).cloned() {
env.imports = im;
}
let mut locals: IndexMap<String, Type> = IndexMap::new();
for (n, t) in f.params.iter().zip(param_tys.iter()) {
locals.insert(n.clone(), t.clone());
}
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = crate::Subst::default();
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut effects,
&f.name,
&mut subst,
&mut counter,
&mut residuals,
)?;
let mut out: Vec<Option<MonoTarget>> = Vec::new();
for r in residuals {
let r_ty = subst.apply(&r.type_);
if !crate::is_fully_concrete(&r_ty) {
out.push(None);
continue;
}
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty));
let entry = match env.workspace_registry.entries.get(&key) {
Some(e) => e,
None => {
out.push(None);
continue;
}
};
out.push(Some(MonoTarget {
class: r.class.clone(),
method: r.method.clone(),
type_: r_ty,
defining_module: entry.defining_module.clone(),
}));
}
Ok(out)
}