Files
AILang/crates/ailang-check/src/mono.rs
T
Brummel 895ba846e8 feat(codegen): switch the lowering walk from &Term to typed &MTerm (mir.1b)
Atomic completion of spec iteration mir.1 (docs/specs/0060-typed-mir.md):
codegen now consumes the typed MIR produced by `lower_to_mir` instead of
re-deriving types from the bare `ast::Term`. Every codegen helper that
took `&Term` (lower_term, lower_app, drop.rs, match_lower.rs) takes
`&MTerm` and reads each node's checker-proved type off `MTerm::ty()`.
The build path is `Workspace -> elaborate_workspace -> MirWorkspace ->
lower_workspace`; the public `lower_workspace*` entry points and their 18
call sites thread `&MirWorkspace`. The codegen-side type re-derivers
`synth_with_extras` + `synth_arg_type` (and the `builtin_ail_type` /
`builtin_effect_op_ret` mirror tables) are deleted — grep-clean. The
three re-derivers the spec keeps until mir.2/mir.3 (`type_home_module`,
`is_static_callee`, the second `infer_module_with_cross`) stay.

The mechanical Term->MTerm match-arm conversion was straightforward and
compiler-enforced. The substance was a set of producer-side correctness
gaps that only surface once codegen reads `MTerm::ty()` and once the
build path re-synthesises the post-mono AST through the canonical
`synth` (which, unlike the old codegen, fully re-unifies). Each was
root-caused against a failing e2e fixture:

1. `qualify_local_types` stripped fn-type modes (rebuilt `Type::Fn` with
   empty `param_modes` / `Implicit` `ret_mode`), so a monomorphised
   polymorphic intrinsic (`RawBuf.set`) lost its `Own` ret-mode and the
   owned temporary leaked at the call site. Made mode-preserving, like
   its sister `qualify_workspace_types` and `Subst::apply` (449df13).

2. `lower_to_mir::synth_pure` synthesises each node in isolation, so a
   nullary polymorphic ctor (`Nil : List<a>`) left its element type an
   unbound `$m` metavar that the canonical synth never pins. Codegen's
   mono unifier (`unify_for_subst`) already has a wildcard for exactly
   this — `$u`, the spelling the now-deleted codegen synth used — so the
   typed-MIR boundary normalises every residual `$m` to `$u` once
   (`wildcard_residual_metavars`), rather than teaching each consumer to
   tolerate a raw metavar.

3. The class-method mono arm (`synthesise_mono_fn`) substituted the
   registry-canonical *qualified* instance type into a method appended to
   the instance's own module, minting a `show_user_adt.IntBox` param
   against a bare-`IntBox` body. Localised to bare before substitution,
   symmetric to the free-fn arm (600565d).

4. Monomorphisation synthesises *downward* class-dispatch references —
   prelude's `print__<IntBox>` names the instance module `show_user_adt`
   that prelude never imports. The post-mono re-synth in `lower_module`
   seeds every workspace module name as an identity import (excluding the
   current module, to keep own types bare) so the qualified-var path
   resolves these; the canonical `synth` used by `check_workspace` stays
   strict.

5. Post-mono, a cross-module callee can name the consumer's *own* ADT
   qualified (`show_user_adt.IntBox`) where the consumer synthesises it
   bare — a spelling split that cannot exist pre-mono (the param is
   polymorphic there). synth's App arm strips the current module's own
   qualifier from both sides before unifying (`strip_own_module_qual`),
   a no-op pre-mono.

Acceptance: whole workspace suite green (698 tests); `e2e` 98/98 and
`show_print_e2e` 3/3; `synth_with_extras`/`synth_arg_type` grep-clean in
codegen; #51/#53 fixtures build and run; lower_to_mir_ty pins green. The
#49 heap-Str loop-binder leak remains ignored (lifts at mir.4).

Builds on the standalone producer fix (600565d, free-fn own-ADT
localisation) and the two standalone mode-preservation fixes
(449df13 Subst::apply); those landed separately as they are
independently correct and inert on the old codegen.
2026-05-31 18:29:36 +02:00

1989 lines
89 KiB
Rust

//! Workspace monomorphisation pass — class-method entry plus
//! free-fn entry.
//!
//! Slots into the build pipeline after [`crate::lift_letrecs`] and
//! before `ailang_codegen::lower_workspace_with_alloc`. It is only
//! on the `build` / `run` paths — `ail check` stops at typecheck and
//! never runs this pass.
//!
//! ## What the pass does
//!
//! [`monomorphise_workspace`] turns a polymorphic, typechecked
//! workspace into a fully monomorphic one that codegen can lower
//! without any instance dispatch or `Type::Forall` instantiation:
//!
//! - **Early-out.** Workspaces with no specialisable targets
//! (`workspace_has_specialisable_targets` — no resolvable
//! class-method call sites and no concretely-instantiated
//! polymorphic free fns) are returned cloned unchanged, so their
//! `module_hash` is preserved bit-for-bit.
//! - **Synthesis fixpoint.** Otherwise the pass repeatedly walks
//! every fn / const body in every module, collecting
//! [`MonoTarget`]s, and synthesises one top-level [`Def::Fn`] per
//! unique target, appended to that target's `defining_module`.
//! Two target kinds share one fixpoint:
//! 1. `MonoTarget::ClassMethod` — the resolved instance body is
//! looked up via [`ailang_core::workspace::Registry`], the
//! class parameter substituted to the concrete type, and a
//! `<method>__<type>` fn synthesised.
//! 2. `MonoTarget::FreeFn` — a `Type::Forall` free fn called at
//! a fully-concrete substitution; the source body is taken
//! directly and rigid-var-substituted into a
//! `<name>__<type>…` fn.
//! The loop re-walks bodies added by the previous round, which is
//! what closes it over chained class-method calls (e.g.
//! `Eq.ne x y = not (eq x y)`). Targets are deduplicated within a
//! round and across rounds via [`mono_target_key`].
//! - **Call-site rewrite.** A final pass rewrites every
//! class-method / poly-free-fn `Term::Var` to its synthesised
//! mono-symbol name (qualified with the instance's
//! `defining_module` when it differs from the calling module),
//! using a cursor aligned position-by-position with
//! `collect_residuals_ordered`.
//!
//! 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
//!
//! Synthesised FnDefs are post-typecheck artefacts. They do NOT
//! enter `CheckedModule.symbols` (built from the original on-disk
//! module at typecheck time and used by `ail diff` / `ail manifest`),
//! same convention as [`crate::lift_letrecs`]. The early-out path
//! additionally guarantees byte-identical workspace output —
//! `module_hash` is unchanged end-to-end for any workspace with no
//! monomorphisation targets.
use ailang_core::ast::{Arm, ClassDef, ConstDef, Def, FnDef as AstFnDef, InstanceDef, NewArg, Pattern, Term, Type};
use ailang_core::workspace::Workspace;
use crate::Result;
use indexmap::IndexMap;
use std::collections::{BTreeMap, BTreeSet};
/// 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> {
// prep.1 (kernel-extension-mechanics): mirror `check_workspace`'s
// pre-pass — desugar + qualify bare cross-module `Type::Con`
// references to qualified form. Both passes must operate on the
// identically-shaped workspace; otherwise the mono's residual /
// free-fn-call observations would key on the original bare-form
// Type values and fail unification against the qualified forms
// the typechecker emitted into the post-prep.1 `Forall` body.
let ws_prepared = crate::prepare_workspace_for_check(ws);
let ws = &ws_prepared;
// 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_specialisable_targets(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.
//
// The mono pass 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 (f, defining_module) = match t {
MonoTarget::ClassMethod { class, type_, defining_module, .. } => {
// normalize to match Registry::normalize_type_for_lookup contract
let t_ty_norm = ws_owned
.registry
.normalize_type_for_lookup(defining_module.as_str(), type_);
let registry_key =
(class.clone(), ailang_core::canonical::type_hash(&t_ty_norm));
let entry = ws_owned
.registry
.entries
.get(&registry_key)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: target `{} {}` has no registry entry",
class,
ailang_core::pretty::type_to_string(type_),
))
})?;
let class_def = class_index.get(class).ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: class `{}` not found",
class
))
})?;
// own ADT names of the module the synthesised
// method is appended to — so its instance type, if
// that module's own ADT, is localised back to bare
// before substitution (see `synthesise_mono_fn`).
let own_type_names: BTreeSet<String> = ws_owned
.modules
.get(defining_module)
.map(|m| {
m.defs
.iter()
.filter_map(|d| match d {
Def::Type(td) => Some(td.name.clone()),
_ => None,
})
.collect()
})
.unwrap_or_default();
(
synthesise_mono_fn(t, class_def, &entry.instance, &own_type_names)?,
defining_module.clone(),
)
}
MonoTarget::FreeFn { defining_module, .. } => (
synthesise_mono_fn_for_free_fn(t, &ws_owned)?,
defining_module.clone(),
),
};
let target_module = ws_owned
.modules
.get_mut(&defining_module)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"monomorphise_workspace: defining module `{}` missing",
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 env.types overlay — see
// [`apply_per_module_types_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_types_overlay(&mut env_mod, &ws_owned, mname);
// precompute the per-module poly-free-fn name
// set used as the rewrite walker's second predicate.
let poly_free_fns = poly_free_fn_names_for_module(&ws_owned, &env_mod, mname);
// 2026-05-14 bugfix: per-poly-free-fn-name constraint count,
// used by both walkers to advance their cursors past the
// synth-Var-arm's class-residual pushes (see
// [`poly_free_fn_constraint_counts_for_module`]).
let poly_free_fn_ccounts =
poly_free_fn_constraint_counts_for_module(&ws_owned, &env_mod, 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,
&poly_free_fns,
&poly_free_fn_ccounts,
)?,
Def::Const(c) => {
let pseudo = const_as_pseudo_fn(c);
collect_residuals_ordered(
&pseudo,
mname,
&env_mod,
&poly_free_fns,
&poly_free_fn_ccounts,
)?
}
_ => 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 / poly-free-fn names too.
for p in &f.params {
locals.insert(p.clone());
}
rewrite_mono_calls(
&mut f.body,
&env.method_to_candidate_classes,
&poly_free_fns,
&poly_free_fn_ccounts,
mname,
&ordered,
&mut cursor,
&mut locals,
);
}
Def::Const(c) => {
rewrite_mono_calls(
&mut c.value,
&env.method_to_candidate_classes,
&poly_free_fns,
&poly_free_fn_ccounts,
mname,
&ordered,
&mut cursor,
&mut locals,
);
}
_ => {}
}
}
}
Ok(ws_owned)
}
/// 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 env.types overlay — see
// [`apply_per_module_types_overlay`] for rationale. The env
// arriving here is workspace-flat (via `build_check_env`);
// synth's `Term::Ctor` arm needs the per-module shape so a
// bare canonical type-name resolves to the current module's
// TypeDef rather than a same-named entry from another module.
let mut env_mod = env.clone();
apply_per_module_types_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)
}
/// 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(),
export: None,
}
}
/// compute the set of names (bare + dot-qualified) that
/// `synth`'s Var arm would resolve to a polymorphic free fn when
/// called from module `mname`. The rewrite walker and the slot
/// collector both consult this set to decide whether a `Term::Var`
/// site contributes a slot (i.e. needs cursor advancement and a
/// potential mono-symbol rewrite).
///
/// The predicate must agree EXACTLY with `synth`'s Var arm's
/// `free_fn_owner` derivation. Three sources contribute:
///
/// 1. Same-module poly `Def::Fn`s (bare names).
/// 2. Implicitly-imported modules' poly `Def::Fn`s (bare names,
/// via the iter-23.4-prep fall-through).
/// 3. Dot-qualified `alias.name` forms for every import alias.
///
/// Builtins (`==`, `+`, etc.) are explicitly excluded — they live
/// only in `env.globals`, never in `env.module_globals[<owner>]`,
/// so they don't pass synth's "in module_globals" gate.
fn poly_free_fn_names_for_module(
ws: &Workspace,
env: &crate::Env,
mname: &str,
) -> BTreeSet<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
// 1. Same-module poly fns.
if let Some(m) = ws.modules.get(mname) {
for d in &m.defs {
if let Def::Fn(f) = d {
if matches!(&f.ty, Type::Forall { .. }) {
out.insert(f.name.clone());
}
}
}
}
// 2 + 3. Imported modules' poly fns: bare (via implicit-import
// fall-through) AND dot-qualified (alias.name).
// prep.1: also enumerate type-scoped spellings
// `<TypeName>.<fn>` for every TypeDef declared in the imported
// module — `synth`'s Var-arm resolves these via the TypeDef-first
// ladder and `rewrite_mono_calls` consumes the same spelling.
//
// raw-buf.3: the bare implicit-import name is suppressed when the
// current module declares a same-name global. `synth`'s Var-arm
// resolves a same-module global (lib.rs ~3317) BEFORE the
// implicit-import fall-through (~3329), so a bare call there is the
// local def, not the imported poly fn. Adding the bare name here
// anyway would make `rewrite_mono_calls` treat the local call as a
// poly-free-fn site and over-advance the slot cursor (surfaced when
// a kernel-tier type-scoped op collided with a fixture-local
// monomorphic fn of the same bare name). The dot-qualified /
// type-scoped spellings are unambiguous and stay unconditional.
let local_global_names: BTreeSet<String> = env
.module_globals
.get(mname)
.map(|g| g.keys().cloned().collect())
.unwrap_or_default();
if let Some(imports) = env.module_imports.get(mname) {
for (alias_or_name, target_mod) in imports {
let target_types: Vec<String> = env
.module_types
.get(target_mod)
.map(|tys| tys.keys().cloned().collect())
.unwrap_or_default();
if let Some(target_globals) = env.module_globals.get(target_mod) {
for (n, t) in target_globals {
if matches!(t, Type::Forall { .. }) {
// Implicit-import bare name (today only the
// prelude is implicit; the loop is generic).
// Suppressed if shadowed by a same-module global.
if !local_global_names.contains(n) {
out.insert(n.clone());
}
// Dot-qualified form.
out.insert(format!("{alias_or_name}.{n}"));
// prep.1: type-scoped form per TypeDef.
for tn in &target_types {
out.insert(format!("{tn}.{n}"));
}
}
}
}
}
}
out
}
/// Bugfix 2026-05-14 (`bugfix-mono-cursor-print-with-class-method-arg`):
/// per-module map from poly-free-fn name (in the exact spellings
/// produced by [`poly_free_fn_names_for_module`] — bare same-module,
/// implicit-import bare, and dot-qualified `alias.name`) to the number
/// of class constraints carried by that fn's `Type::Forall`.
///
/// Both mono walkers ([`interleave_slots`] and [`rewrite_mono_calls`])
/// must advance their cursors by `1 + N` at a poly-free-fn `Var` site
/// where `N` is the constraint count: the synth Var-arm (see
/// `crates/ailang-check/src/lib.rs` iter 24.3 block) pushes one
/// `ResidualConstraint` per declared constraint at the same site where
/// it pushes the `FreeFnCall` observation, so the slot-channel cursors
/// would otherwise drift by `N` on every poly-free-fn-with-constraints
/// `Var`.
fn poly_free_fn_constraint_counts_for_module(
ws: &Workspace,
env: &crate::Env,
mname: &str,
) -> BTreeMap<String, usize> {
let mut out: BTreeMap<String, usize> = BTreeMap::new();
// 1. Same-module poly fns.
if let Some(m) = ws.modules.get(mname) {
for d in &m.defs {
if let Def::Fn(f) = d {
if let Type::Forall { constraints, .. } = &f.ty {
out.insert(f.name.clone(), constraints.len());
}
}
}
}
// 2 + 3. Imported modules' poly fns: bare AND dot-qualified.
// prep.1: also enumerate type-scoped spellings `<TypeName>.<fn>`
// per TypeDef in the imported module — mirror of
// `poly_free_fn_names_for_module`.
//
// raw-buf.3: same bare-name shadow suppression as
// `poly_free_fn_names_for_module` — kept in lockstep so the two
// maps agree on which spellings count as poly-free-fn sites.
let local_global_names: BTreeSet<String> = env
.module_globals
.get(mname)
.map(|g| g.keys().cloned().collect())
.unwrap_or_default();
if let Some(imports) = env.module_imports.get(mname) {
for (alias_or_name, target_mod) in imports {
let target_types: Vec<String> = env
.module_types
.get(target_mod)
.map(|tys| tys.keys().cloned().collect())
.unwrap_or_default();
if let Some(target_globals) = env.module_globals.get(target_mod) {
for (n, t) in target_globals {
if let Type::Forall { constraints, .. } = t {
if !local_global_names.contains(n) {
out.insert(n.clone(), constraints.len());
}
out.insert(format!("{alias_or_name}.{n}"), constraints.len());
for tn in &target_types {
out.insert(format!("{tn}.{n}"), constraints.len());
}
}
}
}
}
}
out
}
/// workspace-wide `class-name -> ClassDef` index.
/// Used by the fixpoint to look up the matching class definition
/// when synthesising a fn.
///
/// keys carry the qualified class name
/// (`<defining_module>.<Class>`) to match the post-canonical-class-form residual /
/// `MonoTarget::ClassMethod.class` field and the registry's
/// qualified entries key.
fn build_class_index(ws: &Workspace) -> BTreeMap<String, ClassDef> {
let mut idx = BTreeMap::new();
for (mod_name, m) in &ws.modules {
for d in &m.defs {
if let Def::Class(c) = d {
let qualified = format!("{mod_name}.{}", c.name);
idx.insert(qualified, c.clone());
}
}
}
idx
}
/// returns `true` iff any module in `ws`
/// declares at least one specialisable target — a [`Def::Class`]
/// or [`Def::Instance`] (class-method residuals) OR a
/// `Type::Forall`-quantified [`Def::Fn`] (free-fn case). Cheap
/// workspace scan; the early-out keeps target-free workspaces
/// byte-identical through the pass.
///
/// Today the prelude is auto-injected into every workspace and
/// brings its Eq/Ord classes along, so the predicate is effectively
/// always true; the generalisation matters for principled
/// correctness should a future workspace skip the prelude.
fn workspace_has_specialisable_targets(ws: &Workspace) -> bool {
ws.modules.values().any(|m| {
m.defs.iter().any(|d| match d {
Def::Class(_) | Def::Instance(_) => true,
Def::Fn(f) => matches!(f.ty, Type::Forall { .. }),
_ => false,
})
})
}
/// deterministic mono-symbol name for a
/// `(base-name, types...)` tuple. The N-ary form supports both
/// single-type-var class methods (today's six primitive Eq/Ord
/// symbols pass a one-element slice and produce identical output
/// to the pre-iter-23.4 implementation — verified by the
/// hash-stability pin in `crates/ail/tests/mono_hash_stability.rs`)
/// AND N-ary free-fn instantiations (e.g. `Type::Forall.vars = ["a", "b"]`
/// produces `<name>__<surface-a>__<surface-b>` in declaration order).
///
/// Per type, primitive `Type::Con` (`Int`, `Bool`, `Str`, `Unit`,
/// `Float`) produces the surface name for diagnostic and ABI
/// legibility. All other types — parameterised cons, user-defined
/// ADTs, function types — fall to the 8-hex-prefix of
/// `ailang_core::canonical::type_hash`. The hash route ensures
/// uniqueness without requiring a flattened surface form for
/// arbitrarily nested types.
///
/// Separator choice: `__` (double underscore) — `#` terminates LLVM
/// IR global identifiers and breaks C-ABI symbol names on most
/// targets, so the produced name has to survive codegen unaltered.
///
/// Determinism: `ailang_core::canonical::type_hash` is the same
/// function `workspace::build_registry` uses to key
/// [`ailang_core::workspace::Registry::entries`], so a registry-key match implies a
/// `mono_symbol` match.
pub fn mono_symbol(base: &str, ty: &Type) -> String {
mono_symbol_n(base, std::slice::from_ref(ty))
}
/// N-ary variant of [`mono_symbol`]. The single-type
/// public-API stays as [`mono_symbol`] (used by class-method
/// emission) for surface compatibility; N-ary callers (free-fn
/// emission with multi-arg type instantiations) use this form
/// directly.
pub fn mono_symbol_n(base: &str, types: &[Type]) -> String {
let mut parts: Vec<String> = Vec::with_capacity(1 + types.len());
parts.push(base.to_string());
for ty in types {
parts.push(type_to_mono_suffix(ty));
}
parts.join("__")
}
/// raw-buf.3: the symbol base for a free-fn mono target. A
/// type-scoped op (`scope = Some("RawBuf")`, name `"get"`) bases on
/// `"RawBuf_get"`, so `mono_symbol_n` mints `RawBuf_get__Int`; a
/// bare free fn (`scope = None`) keeps its bare base.
pub fn scoped_base(scope: &Option<String>, name: &str) -> String {
match scope {
Some(t) => format!("{t}_{name}"),
None => name.to_string(),
}
}
fn type_to_mono_suffix(ty: &Type) -> String {
match primitive_surface_name(ty) {
Some(p) => p.to_string(),
None => {
let h = ailang_core::canonical::type_hash(ty);
h[..8].to_string()
}
}
}
/// 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,
}
}
/// a specialisation request. One synthesised
/// monomorphic `Def::Fn` will be produced per unique
/// [`mono_target_key`]. Two source-body kinds share one fixpoint:
///
/// - [`MonoTarget::ClassMethod`]: a class-constraint residual
/// resolved to a concrete `(class, method, type)` triple. The
/// body comes from `Registry::entries[(class, type-hash)]` (the
/// instance body, or the class default if the instance omits the
/// method).
/// - [`MonoTarget::FreeFn`]: a call site to a polymorphic free
/// `Def::Fn` with a fully-concrete substitution. The body comes
/// directly from the polymorphic `Def::Fn`'s `body` after
/// rigid-var substitution. Added in iter 23.4 to unify the
/// typecheck-time and codegen-time specialisers.
#[derive(Debug, Clone)]
pub enum MonoTarget {
ClassMethod {
class: String,
method: String,
type_: Type,
defining_module: String,
},
FreeFn {
name: String,
/// Concrete type arguments in `Type::Forall.vars` declaration order.
type_args: Vec<Type>,
/// Module in which the polymorphic source `Def::Fn` is declared;
/// the synthesised mono `Def::Fn` is appended there.
defining_module: String,
/// The TypeDef scope (`Some("RawBuf")`) when the call resolved
/// type-scoped; `None` for a bare free fn. Part of the dedup
/// key and the minted symbol base.
scope: Option<String>,
},
}
/// dedup key for a [`MonoTarget`]. Class-method
/// targets key on `("class", "<class>.<method>", <type-hash>)`; free-fn
/// targets key on `("free", "<name>", <joined-type-hashes>)`. The two
/// kinds are guaranteed-disjoint by the first component, so a
/// class-method and a free-fn with the same base name never collide.
/// Consumed by the workspace fixpoint and the rewrite walker.
pub fn mono_target_key(t: &MonoTarget) -> (String, String, String) {
match t {
MonoTarget::ClassMethod { class, method, type_, .. } => (
"class".into(),
format!("{class}.{method}"),
ailang_core::canonical::type_hash(type_),
),
MonoTarget::FreeFn { name, type_args, scope, .. } => {
let joined = type_args
.iter()
.map(ailang_core::canonical::type_hash)
.collect::<Vec<_>>()
.join("|");
("free".into(), scoped_base(scope, name), joined)
}
}
}
/// 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 env.types overlay
/// (see `apply_per_module_types_overlay`).
pub fn build_workspace_env(ws: &Workspace) -> crate::Env {
crate::build_check_env(ws)
}
/// Rebuild `env.types` to contain only the TypeDefs declared in
/// `module_name`'s `Def::Type`s, mirroring `check_in_workspace`'s
/// per-module overlay at `crates/ailang-check/src/lib.rs:1234`.
///
/// The mono pass re-runs `crate::synth` on every fn body to
/// recover residual class constraints. `synth`'s `Term::Ctor` arm
/// looks up bare canonical `type_name`s via `env.types.get(name)`
/// (lib.rs:1967); without this overlay, `env.types` would be the
/// workspace-flat map built by `build_workspace_env`, and a bare
/// `type_name` in module A could resolve to a same-named TypeDef
/// from module B. The overlay restores per-module bare-name
/// scoping.
///
/// Caller-contract assumption: the workspace has already
/// typechecked, so duplicate type names within a single module
/// cannot occur.
fn apply_per_module_types_overlay(env: &mut crate::Env, ws: &Workspace, module_name: &str) {
env.types.clear();
if let Some(m) = ws.modules.get(module_name) {
for d in &m.defs {
if let Def::Type(td) = d {
env.types.insert(td.name.clone(), td.clone());
}
}
}
}
/// 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.
/// Apply the current substitution to a meta and, if the result is
/// fully concrete, normalise it to canonical-form for registry lookup.
///
/// Returns `Some(normalised)` if the meta resolves to a concrete type;
/// `None` if it does not (rigid var or unbound meta — the caller
/// decides the policy: break with `has_rigid = true` at the FreeFn
/// target-collection site, or `Type::unit()`-default at the residual-
/// ordering site).
///
/// extracted from two byte-identical call sites at
/// `collect_mono_targets` and `collect_residuals_ordered` per
/// audit-24's [medium-2] drift item. The byte-identity invariant
/// (Phase 2 synthesis name must match Phase 3 rewrite cursor's
/// lookup name) is now enforced by construction.
fn apply_subst_and_normalize(
env: &crate::Env,
module_name: &str,
m: &Type,
subst: &crate::Subst,
) -> Option<Type> {
let resolved = subst.apply(m);
if crate::is_fully_concrete(&resolved) {
Some(
env.workspace_registry
.normalize_type_for_lookup(module_name, &resolved),
)
} else {
None
}
}
pub fn collect_mono_targets(
f: &AstFnDef,
module_name: &str,
env: &crate::Env,
) -> Result<Vec<MonoTarget>> {
// An `(intrinsic)` body is signature-only — codegen supplies it via
// the intercept registry, so it carries no mono targets. Skip the
// synth re-entry (which would hit synth's `Term::Intrinsic`
// unreachable guard), matching `check_fn`'s body-check skip.
if crate::is_intrinsic_body(f) {
return Ok(Vec::new());
}
// 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()),
};
// Non-fn signature carries no mono slots — keep the early-return
// guard; the param-type extraction itself goes through the shared
// `crate::fn_param_types` helper so this site and
// `lower_to_mir::lower_module` cannot drift. (`ret` / `effects`
// were extracted here only to be discarded.)
if !matches!(&inner_ty, Type::Fn { .. }) {
return Ok(Vec::new());
}
let param_tys: Vec<Type> = crate::fn_param_types(&f.ty);
let mut env = env.clone();
for v in &rigids {
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// 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 `build_workspace_env` populates.
if let Some(g) = env.module_globals.get(module_name).cloned() {
for (n, t) in g {
env.globals.insert(n, t);
}
}
// 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();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
// loop-recur iter 2: mono re-synth begins from top-of-body —
// empty loop-stack (any `Term::Loop` pushes its own frame as the
// walk descends).
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut loop_stack,
&mut effects,
&f.name,
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings_discarded,
)?;
// 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;
}
// normalize to match Registry::normalize_type_for_lookup contract
let r_ty_norm = env
.workspace_registry
.normalize_type_for_lookup(module_name, &r_ty);
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
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::ClassMethod {
class: r.class.clone(),
method: r.method.clone(),
type_: r_ty,
defining_module: entry.defining_module.clone(),
});
}
// Free-fn arm: each `FreeFnCall` observation recorded by
// `synth`'s Var arm carries the bare name, the
// owning module (resolved through synth's lookup ladder), the
// forall vars (declaration order), and the per-var fresh
// metavars. Post-`synth`, `subst.apply` each meta to recover
// the concrete type-arg at this call site. Unbound (metavar)
// vars default to `Type::unit()` — mirrors the pre-iter-23.4
// codegen-side `derive_substitution` behaviour at
// `crates/ailang-codegen/src/subst.rs:57-61`, where a forall
// var the args couldn't pin (e.g. `is_empty Nil` for
// `is_empty : forall a. (List<a>) -> Bool`) collapses to a
// single Unit-defaulted specialisation. The specialised body
// must not actually consume an `a`-typed value, or typecheck
// would have rejected; the Unit default is sound and
// deterministic.
for fc in free_fn_calls {
// if any resolved type-arg is a rigid Type::Var
// (i.e. a forall var of the *enclosing* polymorphic fn,
// not a free metavar), skip this target. The enclosing fn
// will be monomorphised in its own right, and that mono
// synthesis re-walks the body with rigid → concrete
// substitution, at which point this same call site is
// re-observed with concrete type-args. Without the skip, a
// body like `at_most x y = not (gt x y)` (a polymorphic
// free fn calling another polymorphic free fn) would
// produce a spurious `gt__Unit` target whose body
// references `compare` at Unit — which has no Ord
// instance, so the synthesised body fails to typecheck.
//
// Unit-default for unbound metavars (e.g. `is_empty(Nil)`
// where the elem type is unobservable from the args alone)
// is preserved: a metavar resolves to a `$m`-prefixed
// Var via `subst.apply`, distinct from a rigid Var.
let mut type_args: Vec<Type> = Vec::with_capacity(fc.metas.len());
let mut has_rigid = false;
for m in &fc.metas {
match apply_subst_and_normalize(&env, module_name, m, &subst) {
Some(normalised) => type_args.push(normalised),
None => {
// Helper returned None: either rigid var or unbound
// metavar. Site-1 policy diverges: rigid → break with
// `has_rigid = true` (the enclosing poly fn will be
// monomorphised in its own right and this site will
// be re-observed with concrete substitution); unbound
// metavar → default to Unit (iter 23.4 behaviour,
// matching `derive_substitution`'s unobservable-var
// policy in codegen).
let resolved = subst.apply(m);
if contains_rigid_var(&resolved) {
has_rigid = true;
break;
} else {
type_args.push(Type::unit());
}
}
}
}
if has_rigid {
continue;
}
out.push(MonoTarget::FreeFn {
name: fc.name.clone(),
type_args,
defining_module: fc.owner_module.clone(),
scope: fc.scope.clone(),
});
}
Ok(out)
}
/// true iff `t` contains a non-metavar `Type::Var` anywhere.
/// Used by free-fn target collection to distinguish rigid forall vars
/// (skip — wait for the enclosing fn's mono pass) from unbound metavars
/// (Unit-default — no later round will pin them). The naming
/// convention is: metavars are `Type::Var { name: "$m<id>" }`; rigid
/// forall vars use their source name (`a`, `b`, …) which cannot start
/// with `$` per the identifier rules.
fn contains_rigid_var(t: &Type) -> bool {
match t {
Type::Var { name } => !name.starts_with("$m"),
Type::Con { args, .. } => args.iter().any(contains_rigid_var),
Type::Fn { params, ret, .. } => {
params.iter().any(contains_rigid_var) || contains_rigid_var(ret)
}
Type::Forall { body, .. } => contains_rigid_var(body),
}
}
/// 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,
own_type_names: &BTreeSet<String>,
) -> Result<AstFnDef> {
let (class_name, method, type_, defining_module) = match target {
MonoTarget::ClassMethod { class, method, type_, defining_module } => {
(class, method, type_, defining_module)
}
MonoTarget::FreeFn { .. } => {
return Err(crate::CheckError::Internal(
"synthesise_mono_fn: called with FreeFn variant; use \
synthesise_mono_fn_for_free_fn instead"
.into(),
));
}
};
// Locate the class method declaration.
let class_method = class_def
.methods
.iter()
.find(|m| &m.name == method)
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn: class `{}` has no method `{}`",
class_def.name, method
))
})?;
// Build the substitution `param := type_` and apply it to the
// method type. The result is a concrete `Type::Fn` (no
// `Forall`).
//
// `type_` is registry-canonical: an instance whose type is the
// *defining module's own* ADT carries the qualified
// `<defining_module>.<T>` spelling (mono normalises every observed
// instance type through `Registry::normalize_type_for_lookup` for
// dedup + symbol naming). The synthesised method is appended to that
// same `defining_module`, whose own type-cons stay bare under the
// own-module-types-stay-bare invariant — so substituting the
// qualified `type_` into the method signature mints a param typed
// `<defining_module>.<T>` against a body that pattern-matches the
// bare ctor, and the post-mono `synth` re-entry in `lower_to_mir`
// rejects the clash (`expected show_user_adt.IntBox, got IntBox`).
// Localise `type_` back to the defining module's bare convention
// before substitution, symmetric to `synthesise_mono_fn_for_free_fn`.
// (Primitives and cross-module instance types are untouched —
// `localize_own_types` only strips `<defining_module>.<T>` for `T` in
// that module's own ADTs.)
let local_type = localize_own_types(type_, defining_module, own_type_names);
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
mapping.insert(class_def.param.clone(), local_type);
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 == 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)",
class_name,
ailang_core::pretty::type_to_string(type_),
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: {:?}",
method, other
)));
}
}
} else {
(Vec::new(), body_term)
};
Ok(AstFnDef {
name: mono_symbol(method, type_),
ty: concrete_method_ty,
params,
body,
doc: None,
suppress: Vec::new(),
export: None,
})
}
/// Rewrite every `<defining_module>.<T>` `Type::Con` whose `<T>` is one
/// of `defining_module`'s own ADT names back to the bare `<T>` form,
/// recursing structurally. This is the inverse of
/// [`Registry::normalize_type_for_lookup`](ailang_core::workspace::Registry::normalize_type_for_lookup)
/// restricted to the owning module: cross-module qualified names,
/// primitives, and type variables are left intact. Used to localise a
/// registry-canonical monomorphisation type-arg into the defining
/// module's own bare convention before it is substituted into a
/// polymorphic free-fn signature/body (which carry own-module type-cons
/// bare). See the call site in [`synthesise_mono_fn_for_free_fn`].
fn localize_own_types(t: &Type, defining_module: &str, own_type_names: &BTreeSet<String>) -> Type {
match t {
Type::Con { name, args } => {
let local = name
.strip_prefix(defining_module)
.and_then(|rest| rest.strip_prefix('.'))
.filter(|bare| own_type_names.contains(*bare))
.map(|bare| bare.to_string())
.unwrap_or_else(|| name.clone());
Type::Con {
name: local,
args: args
.iter()
.map(|a| localize_own_types(a, defining_module, own_type_names))
.collect(),
}
}
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
params: params
.iter()
.map(|p| localize_own_types(p, defining_module, own_type_names))
.collect(),
ret: Box::new(localize_own_types(ret, defining_module, own_type_names)),
effects: effects.clone(),
param_modes: param_modes.clone(),
ret_mode: ret_mode.clone(),
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints.clone(),
body: Box::new(localize_own_types(body, defining_module, own_type_names)),
},
Type::Var { .. } => t.clone(),
}
}
/// synthesise a monomorphic `FnDef` for a polymorphic
/// free-fn target. The source body comes directly from the
/// polymorphic `Def::Fn`'s `body` (NOT from `Registry::entries`);
/// rigid-var substitution applies the `target.type_args` to the
/// source `Type::Forall { body }`.
///
/// The body is passed through unchanged — the rewrite walker
/// (Task 6) handles inner class-method / poly-call rewrites in a
/// subsequent fixpoint round, mirroring the class-method arm's
/// body-passthrough pattern.
///
/// Errors are all [`crate::CheckError::Internal`] — caller-contract
/// violations that cannot occur after typecheck has succeeded.
pub fn synthesise_mono_fn_for_free_fn(
target: &MonoTarget,
ws: &Workspace,
) -> Result<AstFnDef> {
let (name, type_args, defining_module, scope) = match target {
MonoTarget::FreeFn { name, type_args, defining_module, scope } => {
(name, type_args, defining_module, scope)
}
MonoTarget::ClassMethod { .. } => {
return Err(crate::CheckError::Internal(
"synthesise_mono_fn_for_free_fn: called with ClassMethod variant; use \
synthesise_mono_fn instead"
.into(),
));
}
};
let source_module = ws.modules.get(defining_module).ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn_for_free_fn: defining module `{}` not in workspace",
defining_module
))
})?;
let source_fn = source_module
.defs
.iter()
.find_map(|d| match d {
Def::Fn(f) if &f.name == name => Some(f),
_ => None,
})
.ok_or_else(|| {
crate::CheckError::Internal(format!(
"synthesise_mono_fn_for_free_fn: no source Def::Fn `{}` in module `{}`",
name, defining_module
))
})?;
let (forall_vars, inner_ty) = match &source_fn.ty {
Type::Forall { vars, body, .. } => (vars.clone(), (**body).clone()),
other => {
return Err(crate::CheckError::Internal(format!(
"synthesise_mono_fn_for_free_fn: source fn `{}` is not Type::Forall; got {:?}",
name, other
)));
}
};
if forall_vars.len() != type_args.len() {
return Err(crate::CheckError::Internal(format!(
"synthesise_mono_fn_for_free_fn: arity mismatch for `{}` — {} forall vars vs {} \
type args",
name,
forall_vars.len(),
type_args.len()
)));
}
// The observed `type_args` are registry-canonical: a forall var
// bound to the *defining module's own* ADT carries the qualified
// `<defining_module>.<T>` spelling, because mono normalises every
// observed type-arg through `Registry::normalize_type_for_lookup`
// (see `apply_subst_and_normalize`) so that cross-module call sites
// dedup to one specialisation and mint a stable symbol name. The
// polymorphic source signature and body, by contrast, are in the
// module's own *bare* convention — the qualify pre-pass deliberately
// leaves own-module type-cons bare (lib.rs:4358). Substituting a
// qualified type-arg into that bare signature mints a
// self-inconsistent specialisation: a qualified `std_list.List`
// accumulator parameter against a bare `List` list argument and Lam
// body. The post-mono `synth` re-entry (`lower_to_mir`) re-derives
// own-module type-cons in bare form and rejects the clash
// (`expected std_list.List<Int>, got List<Int>`). Localise each
// type-arg back to the defining module's bare convention before
// substitution so the synthesised signature + body stay uniformly
// bare-own. The stored `type_args` are left normalised — the symbol
// name (line below) and the Phase-3 rewrite cursor both key on the
// canonical form, preserving their byte-identity invariant. (The
// ClassMethod arm needs no analogue: it already substitutes the
// un-normalised `target.type_`.)
let own_type_names: BTreeSet<String> = source_module
.defs
.iter()
.filter_map(|d| match d {
Def::Type(td) => Some(td.name.clone()),
_ => None,
})
.collect();
let mapping: BTreeMap<String, Type> = forall_vars
.iter()
.cloned()
.zip(
type_args
.iter()
.map(|t| localize_own_types(t, defining_module, &own_type_names)),
)
.collect();
// Apply rigid-var substitution on the function type AND on the
// body (the latter required because a free-fn body may contain
// inner `Term::Lam` nodes whose `param_tys` / `ret_ty` reference
// the outer Forall vars; those references must be substituted
// so the post-mono body's re-synth in Phase 3 sees concrete
// types throughout).
//
// The class-method arm in `synthesise_mono_fn` doesn't need
// this because instance bodies are Lam-unwrapped during synth
// (the outer Lam's `param_tys` are dropped); a free-fn body
// can carry arbitrary nested Lams with `Type::Var`-bearing
// `param_tys`, which is the case for e.g. `List.length`
// (type-scoped, an inner accumulating lambda over `(b, a)`).
let new_type = crate::substitute_rigids(&inner_ty, &mapping);
let new_body = crate::substitute_rigids_in_term(&source_fn.body, &mapping);
Ok(AstFnDef {
name: mono_symbol_n(&scoped_base(scope, name), type_args.as_slice()),
ty: new_type,
params: source_fn.params.clone(),
body: new_body,
doc: source_fn.doc.clone(),
suppress: Vec::new(),
export: None,
})
}
/// rewrite every polymorphic 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 via [`collect_residuals_ordered`].
///
/// Two kinds of call site trigger cursor advancement:
///
/// - **Class-method** sites: `Term::Var { name }` where `name` is in
/// `class_methods`. Synth pushes a residual at such sites.
/// - **Polymorphic free-fn** sites (iter 23.4): `Term::Var { name }`
/// where `name` is in `poly_free_fns` (the set of bare + qualified
/// names that resolve to a `Type::Forall`-quantified `Def::Fn` in
/// `caller_module`'s scope per `synth`'s Var arm). Synth pushes a
/// `FreeFnCall` observation at such sites.
///
/// Local-shadowed names are skipped in both kinds — synth's
/// lookup-precedence rule resolves them to the local binding and
/// pushes nothing, so the walker must not advance either.
///
/// At a matching cursor position, the slot's `MonoTarget` variant
/// drives the rewrite:
///
/// - `ClassMethod`: `<method>__<typesurfacename>`, possibly
/// `<defining_module>.<...>` if cross-module.
/// - `FreeFn`: `<name>__<typesurfacename1>__<typesurfacename2>__…`,
/// possibly `<defining_module>.<...>` if cross-module.
/// - `None` (slot is None): residual was non-concrete or instance
/// not registered; cursor still advances (to keep alignment) but
/// the name is left unchanged.
#[allow(clippy::too_many_arguments)]
fn rewrite_mono_calls(
body: &mut Term,
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
poly_free_fns: &BTreeSet<String>,
poly_free_fn_ccounts: &BTreeMap<String, usize>,
caller_module: &str,
ordered_targets: &[Option<MonoTarget>],
cursor: &mut usize,
locals: &mut BTreeSet<String>,
) {
match body {
Term::Var { name } => {
// Two-way predicate: cursor advances at any Term::Var
// whose name synth would resolve to either a class-method
// residual OR a poly-free-fn observation. Locally
// shadowed names match neither — synth pushes nothing.
//
// the class-method side is method-keyed natively via
// `method_to_candidate_classes`. Class disambiguation
// (which class declared the resolved method) lives in the
// residual slot the cursor consumes, not here.
//
// 2026-05-14 bugfix: a poly-free-fn `Var` whose source
// `Type::Forall` carries `N` class constraints consumes
// `1 + N` slots, not 1: the synth Var-arm pushes one
// `ResidualConstraint` per constraint (driving an entry
// into the `class_slots` channel) AT THE SAME SITE as the
// single `FreeFnCall` observation. The slot at `*cursor`
// is the FreeFn slot used for the actual rename; the
// following `N` slots are filler class slots — they exist
// only to keep the post-Var cursor positions aligned for
// later class-method `Var`s in the body. Class-method
// `Var`s themselves stay at advance-by-1 (synth pushes
// exactly one residual and no FreeFnCall).
let is_class_method = method_to_candidate_classes.contains_key(name);
let is_poly_free_fn = !is_class_method && poly_free_fns.contains(name);
let should_advance = (is_class_method || is_poly_free_fn) && !locals.contains(name);
if should_advance {
// Capture the constraint count BEFORE the rename
// below mutates `name`; lookup keys are the synth-
// visible spelling.
let n_filler = if is_poly_free_fn {
poly_free_fn_ccounts.get(name).copied().unwrap_or(0)
} else {
0
};
// The outer `Some` means the cursor slot exists; the inner
// `Some` means the observation at that slot is concrete.
// A `Some(None)` slot is a residual / non-concrete
// observation and leaves the name unchanged.
if let Some(Some(t)) = ordered_targets.get(*cursor) {
let (sym, defining_module) = match t {
MonoTarget::ClassMethod { method, type_, defining_module, .. } => {
(mono_symbol(method, type_), defining_module.as_str())
}
MonoTarget::FreeFn { name: fn_name, type_args, defining_module, scope } => {
(mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice()), defining_module.as_str())
}
};
let new_name = if defining_module == caller_module {
sym
} else {
format!("{}.{}", defining_module, sym)
};
*name = new_name;
}
*cursor += 1 + n_filler;
}
}
Term::App { callee, args, .. } => {
rewrite_mono_calls(callee, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
for a in args {
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
Term::Let { name, value, body } => {
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
let inserted = locals.insert(name.clone());
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
if inserted {
locals.remove(name);
}
}
Term::LetRec { name, params, body, in_term, .. } => {
let name_inserted = locals.insert(name.clone());
let mut params_inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
params_inserted.push(p.clone());
}
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
for p in &params_inserted {
locals.remove(p);
}
rewrite_mono_calls(in_term, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
if name_inserted {
locals.remove(name);
}
}
Term::If { cond, then, else_ } => {
rewrite_mono_calls(cond, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(then, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(else_, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Do { args, .. } => {
for a in args {
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
Term::Ctor { args, .. } => {
for a in args {
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
Term::Match { scrutinee, arms } => {
rewrite_mono_calls(scrutinee, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, 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_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
for b in &inserted {
locals.remove(b);
}
}
}
Term::Lam { params, body, .. } => {
let mut inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
inserted.push(p.clone());
}
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
rewrite_mono_calls(lhs, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(rhs, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Clone { value } => {
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::ReuseAs { source, body } => {
rewrite_mono_calls(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Loop { binders, body } => {
for b in binders.iter_mut() {
rewrite_mono_calls(&mut b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
Term::Recur { args } => {
for a in args.iter_mut() {
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
// #51: a `Term::New` carrying a written `NewArg::Type` survives
// desugar (polymorphic `new`, e.g. `(new RawBuf (con Int) 3)`)
// and is lowered to a monomorphised global-fn call HERE. synth
// pushed exactly one `FreeFnCall` observation for this site
// (BEFORE its value-args), so the cursor slot at `*cursor` is the
// mono target for `RawBuf_new__<elem>`. We rename the call to the
// mangled symbol, then descend into the value-args (which become
// the App args) so their own cursor advances stay aligned.
//
// A type-arg-free `Term::New` (monomorphic `new`, e.g.
// `(new Counter 42)`) was already lowered to `(app T.new …)` at
// desugar and never reaches this arm — synth pushed no
// observation, so no slot is consumed.
Term::New { type_name, args, .. } => {
let has_type_arg = args.iter().any(|a| matches!(a, NewArg::Type(_)));
if has_type_arg {
let callee_name = if let Some(Some(MonoTarget::FreeFn {
name: fn_name,
type_args,
defining_module,
scope,
})) = ordered_targets.get(*cursor)
{
let sym = mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice());
if defining_module == caller_module {
sym
} else {
format!("{}.{}", defining_module, sym)
}
} else {
// Slot is None / non-concrete: leave an unmangled
// type-scoped spelling. The written type-arg is
// concrete (it is the author's annotation), so this
// branch is not expected; fall back to the
// type-scoped name (`RawBuf` from `raw_buf.RawBuf`)
// rather than panic.
let bare_type =
type_name.rsplit('.').next().unwrap_or(type_name.as_str());
format!("{bare_type}.new")
};
*cursor += 1;
// Rewrite value-args in place (advances the cursor for
// any nested mono calls), then replace the node with the
// equivalent app to the monomorphised `new`.
let mut value_args: Vec<Term> = Vec::new();
for arg in args.iter_mut() {
if let NewArg::Value(v) = arg {
rewrite_mono_calls(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
value_args.push(v.clone());
}
}
*body = Term::App {
callee: Box::new(Term::Var { name: callee_name }),
args: value_args,
tail: false,
};
} else {
for arg in args.iter_mut() {
if let NewArg::Value(v) = arg {
rewrite_mono_calls(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
}
}
}
}
Term::Lit { .. } => {}
Term::Intrinsic => {}
}
}
/// 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
}
/// resolve a residual's class for mono target construction.
/// Returns `Some(class)` for a discharge-ready residual (single-class
/// or refined-multi-candidate); `None` if the multi-candidate residual
/// cannot be refined — in which case typecheck-side already raised a
/// `CheckError`, so reaching this branch means the residual slipped
/// past discharge and the mono cursor should emit a None-slot
/// defensively.
pub fn resolve_residual_class_for_mono(
r: &crate::ResidualConstraint,
registry_unit: &BTreeMap<(String, String), ()>,
) -> Option<String> {
if r.candidates.is_some() {
match crate::refine_multi_candidate_residual(r, &[], registry_unit) {
crate::RefineOutcome::Resolved(c) => Some(c),
_ => None,
}
} else {
Some(r.class.clone())
}
}
/// 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,
poly_free_fns: &BTreeSet<String>,
poly_free_fn_ccounts: &BTreeMap<String, usize>,
) -> Result<Vec<Option<MonoTarget>>> {
// Intrinsic bodies carry no mono slots — see `collect_mono_targets`.
if crate::is_intrinsic_body(f) {
return Ok(Vec::new());
}
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
};
// Non-fn signature carries no mono slots — keep the early-return
// guard; the param-type extraction itself goes through the shared
// `crate::fn_param_types` helper so this site and
// `lower_to_mir::lower_module` cannot drift.
if !matches!(&inner_ty, Type::Fn { .. }) {
return Ok(Vec::new());
}
let param_tys: Vec<Type> = crate::fn_param_types(&f.ty);
let mut env = env.clone();
for v in &rigids {
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// 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);
}
}
// 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();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
// loop-recur iter 2: mono re-synth begins from top-of-body —
// empty loop-stack (any `Term::Loop` pushes its own frame as the
// walk descends).
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = Vec::new();
crate::synth(
&f.body,
&env,
&mut locals,
&mut loop_stack,
&mut effects,
&f.name,
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings_discarded,
)?;
// Build two per-channel slot lists — one for class-method
// residuals, one for poly-free-fn observations —
// each in synth's push order. Then walk the AST in synth's
// traversal order, classifying each `Term::Var` site as
// class-method, poly-free-fn, or neither (and consuming from
// the appropriate channel). The output slot list interleaves
// the two channels in source-AST order; the rewrite walker
// consumes it with a single cursor.
let class_slots: Vec<Option<MonoTarget>> = residuals
.into_iter()
.map(|r| {
let r_ty = subst.apply(&r.type_);
if !crate::is_fully_concrete(&r_ty) {
return None;
}
let r_ty_norm = env
.workspace_registry
.normalize_type_for_lookup(module_name, &r_ty);
// resolve the residual's class via the multi-
// candidate refinement helper. Single-class residuals
// (`candidates: None`) flow through `r.class.clone()`
// unchanged; multi-candidate residuals filter against
// the registry, with the discharge path's CheckError
// already raised at typecheck time (so reaching here
// with a non-Resolved outcome means the residual was
// somehow not gated upstream — emit `None` defensively).
let registry_unit: BTreeMap<(String, String), ()> = env
.workspace_registry
.entries
.keys()
.map(|k| (k.clone(), ()))
.collect();
let normalized_residual = crate::ResidualConstraint {
class: r.class.clone(),
type_: r_ty_norm.clone(),
method: r.method.clone(),
candidates: r.candidates.clone(),
};
let resolved_class =
resolve_residual_class_for_mono(&normalized_residual, &registry_unit)?;
let key = (resolved_class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
let entry = env.workspace_registry.entries.get(&key)?;
Some(MonoTarget::ClassMethod {
class: resolved_class,
method: r.method.clone(),
type_: r_ty,
defining_module: entry.defining_module.clone(),
})
})
.collect();
let free_fn_slots: Vec<Option<MonoTarget>> = free_fn_calls
.iter()
.map(|fc| {
// Mirrors `collect_mono_targets`'s Unit-default for unpinned
// forall vars. Per-meta resolution: pinned → keep; unpinned
// → `Type::unit()`. This ensures the cursor-walker emits a
// Some(target) at every poly-free-fn call site, so the
// rewrite walker's cursor advancement maps to a real
// mono-symbol rewrite (no `None`-slots from this channel).
let type_args: Vec<Type> = fc
.metas
.iter()
.map(|m| {
apply_subst_and_normalize(&env, module_name, m, &subst)
.unwrap_or_else(Type::unit)
})
.collect();
Some(MonoTarget::FreeFn {
name: fc.name.clone(),
type_args,
defining_module: fc.owner_module.clone(),
scope: fc.scope.clone(),
})
})
.collect();
// Walk the AST in the same pre-order the rewrite walker uses
// (which mirrors synth's traversal order). At each Term::Var
// matching `class_methods` or `poly_free_fns` (and not locally
// shadowed), emit one slot consuming from the appropriate
// channel. The resulting Vec is in interleaved walker-order,
// ready for the rewrite walker's single-cursor consumption.
let mut out: Vec<Option<MonoTarget>> = Vec::new();
let mut class_cur = 0usize;
let mut free_cur = 0usize;
let mut walker_locals: BTreeSet<String> = f.params.iter().cloned().collect();
interleave_slots(
&f.body,
&env.method_to_candidate_classes,
poly_free_fns,
poly_free_fn_ccounts,
&class_slots,
&free_fn_slots,
&mut class_cur,
&mut free_cur,
&mut walker_locals,
&mut out,
);
Ok(out)
}
/// walk a body
/// in synth's traversal order, interleaving class-method and
/// poly-free-fn slots in source-AST order. The walker MUST stay
/// in lockstep with `rewrite_mono_calls` — same predicates, same
/// shadowing handling.
///
/// class-method presence is method-keyed natively via
/// `method_to_candidate_classes` (post-`MethodNameCollision`-retirement,
/// `class_methods` is tuple-keyed by `(class, method)` and not
/// directly probable by method name alone).
#[allow(clippy::too_many_arguments)]
fn interleave_slots(
term: &Term,
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
poly_free_fns: &BTreeSet<String>,
poly_free_fn_ccounts: &BTreeMap<String, usize>,
class_slots: &[Option<MonoTarget>],
free_fn_slots: &[Option<MonoTarget>],
class_cur: &mut usize,
free_cur: &mut usize,
locals: &mut BTreeSet<String>,
out: &mut Vec<Option<MonoTarget>>,
) {
match term {
Term::Var { name } => {
let is_class_method = method_to_candidate_classes.contains_key(name);
let is_poly_free_fn = !is_class_method && poly_free_fns.contains(name);
let advances = (is_class_method || is_poly_free_fn) && !locals.contains(name);
if advances {
if is_class_method {
let slot = class_slots.get(*class_cur).cloned().unwrap_or(None);
out.push(slot);
*class_cur += 1;
} else {
// 2026-05-14 bugfix: a poly-free-fn Var consumes
// `1 + N` slots — the FreeFn slot (drives the
// rewrite walker's rename) followed by `N` filler
// class slots, where `N` is the constraint count
// of this fn's source `Type::Forall`. Synth's
// Var-arm pushes one `ResidualConstraint` per
// declared constraint AT THIS SITE before pushing
// the `FreeFnCall` observation; without consuming
// the corresponding class slots here, the class-
// method cursor drifts by `N` for every following
// class-method Var in the body.
let slot = free_fn_slots.get(*free_cur).cloned().unwrap_or(None);
out.push(slot);
*free_cur += 1;
let n_filler = poly_free_fn_ccounts.get(name).copied().unwrap_or(0);
for _ in 0..n_filler {
let filler = class_slots.get(*class_cur).cloned().unwrap_or(None);
out.push(filler);
*class_cur += 1;
}
}
}
}
Term::App { callee, args, .. } => {
interleave_slots(callee, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for a in args {
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Let { name, value, body } => {
interleave_slots(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
let inserted = locals.insert(name.clone());
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
if inserted {
locals.remove(name);
}
}
Term::LetRec { name, params, body, in_term, .. } => {
let name_inserted = locals.insert(name.clone());
let mut params_inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
params_inserted.push(p.clone());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for p in &params_inserted {
locals.remove(p);
}
interleave_slots(in_term, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
if name_inserted {
locals.remove(name);
}
}
Term::If { cond, then, else_ } => {
interleave_slots(cond, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(then, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(else_, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Do { args, .. } => {
for a in args {
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Ctor { args, .. } => {
for a in args {
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Match { scrutinee, arms } => {
interleave_slots(scrutinee, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for b in &inserted {
locals.remove(b);
}
}
}
Term::Lam { params, body, .. } => {
let mut inserted: Vec<String> = Vec::new();
for p in params {
if locals.insert(p.clone()) {
inserted.push(p.clone());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
interleave_slots(lhs, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(rhs, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Clone { value } => {
interleave_slots(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::ReuseAs { source, body } => {
interleave_slots(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Loop { binders, body } => {
for b in binders {
interleave_slots(&b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Recur { args } => {
for a in args {
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
// #51: a `Term::New` carrying a written `NewArg::Type` survives
// desugar and synth pushed exactly one `FreeFnCall` observation
// for it (BEFORE its value-args), so it consumes one free-fn
// slot HERE — mirroring `rewrite_mono_calls`'s `Term::New` arm
// so the two walkers stay in lockstep. A type-arg-free `Term::New`
// was lowered to `(app T.new …)` at desugar and never reaches
// this arm, pushing no slot.
Term::New { args, .. } => {
let has_type_arg = args.iter().any(|a| matches!(a, NewArg::Type(_)));
if has_type_arg {
let slot = free_fn_slots.get(*free_cur).cloned().unwrap_or(None);
out.push(slot);
*free_cur += 1;
}
for arg in args {
if let NewArg::Value(v) = arg {
interleave_slots(v, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
}
Term::Lit { .. } => {}
Term::Intrinsic => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ailang_core::ast::Type;
/// mono's residual-class resolver refines multi-candidate
/// residuals via the same logic as discharge. A multi-candidate
/// residual with a concrete `type_` and a single registry-survivor
/// resolves to that class.
#[test]
fn mq2_mono_multi_candidate_resolves_to_single_class() {
let mut candidates = std::collections::BTreeSet::new();
candidates.insert("prelude.Show".to_string());
candidates.insert("userlib.Show".to_string());
let residual = crate::ResidualConstraint {
class: "prelude.Show".to_string(), // tentative
type_: Type::Con { name: "Int".to_string(), args: vec![] },
method: "show".to_string(),
candidates: Some(candidates),
};
let mut registry_unit: BTreeMap<(String, String), ()> = Default::default();
let int_h = ailang_core::canonical::type_hash(
&Type::Con { name: "Int".to_string(), args: vec![] }
);
registry_unit.insert(("prelude.Show".to_string(), int_h), ());
let resolved = resolve_residual_class_for_mono(&residual, &registry_unit);
assert_eq!(resolved, Some("prelude.Show".to_string()));
}
/// single-class residual (candidates: None) flows through
/// unchanged.
#[test]
fn mq2_mono_single_class_residual_unchanged() {
let residual = crate::ResidualConstraint {
class: "prelude.Eq".to_string(),
type_: Type::Con { name: "Int".to_string(), args: vec![] },
method: "eq".to_string(),
candidates: None,
};
let registry_unit: BTreeMap<(String, String), ()> = Default::default();
let resolved = resolve_residual_class_for_mono(&residual, &registry_unit);
assert_eq!(resolved, Some("prelude.Eq".to_string()));
}
/// multi-candidate residual that cannot refine (zero
/// registry survivors) returns None.
#[test]
fn mq2_mono_multi_candidate_no_survivors_returns_none() {
let mut candidates = std::collections::BTreeSet::new();
candidates.insert("prelude.Show".to_string());
candidates.insert("userlib.Show".to_string());
let residual = crate::ResidualConstraint {
class: "prelude.Show".to_string(),
type_: Type::Con { name: "MyType".to_string(), args: vec![] },
method: "show".to_string(),
candidates: Some(candidates),
};
let registry_unit: BTreeMap<(String, String), ()> = Default::default();
let resolved = resolve_residual_class_for_mono(&residual, &registry_unit);
assert_eq!(resolved, None);
}
}