895ba846e8
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 (449df13Subst::apply); those landed separately as they are independently correct and inert on the old codegen.
1989 lines
89 KiB
Rust
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(®istry_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 ¶ms_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, ®istry_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 ¶ms_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, ®istry_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, ®istry_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, ®istry_unit);
|
|
assert_eq!(resolved, None);
|
|
}
|
|
}
|