iter 23.4: mono-pass unification — class methods + polymorphic free fns in one fixpoint

Restructure ailang-check/src/mono.rs::monomorphise_workspace into a
single specialiser over every Type::Forall-quantified Def::Fn, covering
both class-method residuals AND polymorphic free-fn call sites in one
fixpoint pass. Remove the codegen-time specialiser (lower_polymorphic_call,
module_polymorphic_fns, mono_queue, mono_emitted, emit_specialised_fn,
plus the now-dead helpers apply_subst_to_term, descriptor_for_subst,
type_descriptor) entirely. Codegen sees only monomorphic Def::Fns after
this iter — no polymorphic call sites, no codegen-time queue.

Architecture changes:
- MonoTarget: struct → enum with ClassMethod / FreeFn variants; dedup
  keys are guaranteed-disjoint via 'class'/'free' first component.
- collect_mono_targets: new FreeFnCall channel through synth (cleaner
  than the plan's separate-walker proposal — one less data flow,
  substitution tracking comes 'for free' via fresh metavars +
  post-synth subst.apply). Plan Step 4.4 (polymorphic_free_fns env
  field) consequently obsolete.
- synthesise_mono_fn_for_free_fn: new free-fn entry point; substitutes
  rigid vars in BOTH the type AND the body (body substitution required
  because free-fn bodies carry inner Lams whose param_tys reference
  outer Forall vars — adapted from plan's 'body unchanged' sketch).
- mono_symbol_n: N-ary extension of mono_symbol; bit-stable for the
  single-type-var case (hash-stability pin Task 1 fires zero times
  through all eight refactor tasks).
- rewrite_class_method_calls → rewrite_mono_calls; interleaved-slot
  collector preserves the positional-cursor invariant across both
  channels.
- workspace_has_typeclasses → workspace_has_specialisable_targets
  (principled correctness; old predicate happened to already be true
  for every user workspace because the prelude is auto-injected).

Codegen cleanup:
- Two call sites of lower_polymorphic_call deleted (codegen/src/lib.rs
  lines ~1939-1945, ~1965-1973).
- lower_polymorphic_call body, module_polymorphic_fns field +
  population + Emitter wiring, mono_queue / mono_emitted, drain loop,
  emit_specialised_fn — all removed.
- is_static_callee collapsed to module_user_fns-only.
- subst.rs apply_subst_to_term + descriptor_for_subst removed; synth.rs
  type_descriptor removed (all dead post-removal).
- ail emit-ir command pipeline gains lift_letrecs + monomorphise_workspace
  pre-passes (pre-iter-23.4 the codegen-time poly path was masking
  this dependency — emit-ir is now consistent with build).
- Net codegen reduction: -285 lines lib.rs, -93 lines subst.rs.

Tests:
- mono_hash_stability.rs (new): regression pin for six primitive Eq/Ord
  mono-symbol body hashes; stayed bit-stable through all eight refactor
  tasks.
- mono_unification.rs (new): six tests covering variant-key disjointness,
  free-fn target emission, free-fn synthesis with rigid substitution,
  rewrite walker on free fns, identity for poly-free-fn-only workspaces,
  end-to-end cmp_max_smoke runtime correctness.
- typeclass_22b3.rs: three test sites updated to MonoTarget::ClassMethod
  enum form.
- 73 e2e + 18 typeclass + 81 codegen tests all green workspace-wide.

DESIGN.md §'Monomorphisation (post-typecheck, pre-codegen)' amended:
two-arm fixpoint described, disjoint-keyed dedup, cursor-aligned
walker, removed codegen-time path.

Bench check (all exit 0, 112 metrics stable): bench/check.py +
compile_check.py + cross_lang.py.

Plan parent: docs/plans/2026-05-11-iter-23.4.md (fab1685).
Spec parent: docs/specs/2026-05-11-23-eq-ord-prelude.md (841d65d).
Per-iter journal documents two adapted deviations from the plan
(synth-channel vs separate walker; body substitution; emit-ir fix);
no spec defects.
This commit is contained in:
2026-05-11 23:59:45 +02:00
parent fab1685336
commit a1692a4859
17 changed files with 1524 additions and 618 deletions
+35 -250
View File
@@ -49,7 +49,7 @@ mod synth;
use escape::NonEscapeSet;
use subst::{
apply_subst_to_term, apply_subst_to_type, derive_substitution, descriptor_for_subst,
apply_subst_to_type, derive_substitution,
qualify_local_types_codegen, unify_for_subst,
};
use synth::{
@@ -285,16 +285,16 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
// Pass 1: per-module top-level symbol tables.
// - `module_user_fns`: LLVM-typed FnSig for monomorphic fns. Used by
// the call resolver. Polymorphic fns are deliberately excluded —
// they don't have a single LLVM signature; specialised entries
// appear here on demand during monomorphisation (Iter 12b).
// - `module_def_ail_types`: AILang `Type` for every fn-typed def
// (poly or mono). Used by the codegen-side type tracker to derive
// substitutions at polymorphic call sites and to look up the
// original `Forall` body when specialising.
// the call resolver. Post-iter-23.4 the workspace contains ONLY
// monomorphic `Def::Fn`s (the typecheck-time mono pass synthesises
// one per concrete instantiation); any residual `Type::Forall` ty
// is a stale source def kept for round-trip / `ail diff` purposes
// and is intentionally not lowered.
// - `module_def_ail_types`: AILang `Type` for every fn-typed def.
// Retained for codegen-side type-tracking utilities (e.g.
// `synth_arg_type` for ctor-arg type inference).
let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new();
let mut module_def_ail_types: BTreeMap<String, BTreeMap<String, Type>> = BTreeMap::new();
let mut module_polymorphic_fns: BTreeMap<String, BTreeMap<String, FnDef>> = BTreeMap::new();
// Iter 15a: cross-module ctor table. Maps module name → ctor name →
// CtorRef (with `type_name` *unqualified*, since the ctor is defined
// in that module). Cross-module ctor lookups resolve through this
@@ -309,26 +309,20 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
for (mname, m) in &ws.modules {
let mut user_fns = BTreeMap::new();
let mut ail_types = BTreeMap::new();
let mut poly_fns = BTreeMap::new();
let mut ctors = BTreeMap::new();
for def in &m.defs {
if let Def::Fn(f) = def {
ail_types.insert(f.name.clone(), f.ty.clone());
match &f.ty {
Type::Fn { params, ret, .. } => {
let psig: Result<Vec<String>> = params.iter().map(llvm_type).collect();
let rsig = llvm_type(ret);
if let (Ok(params), Ok(ret)) = (psig, rsig) {
user_fns.insert(f.name.clone(), FnSig { params, ret });
}
if let Type::Fn { params, ret, .. } = &f.ty {
let psig: Result<Vec<String>> = params.iter().map(llvm_type).collect();
let rsig = llvm_type(ret);
if let (Ok(params), Ok(ret)) = (psig, rsig) {
user_fns.insert(f.name.clone(), FnSig { params, ret });
}
Type::Forall { .. } => {
// Polymorphic def — no LLVM sig now; specialised
// versions get queued as call sites are lowered.
poly_fns.insert(f.name.clone(), f.clone());
}
_ => {}
}
// iter 23.4: `Type::Forall`-quantified defs are
// intentionally skipped — the mono pass has already
// produced their monomorphic counterparts.
}
if let Def::Type(td) = def {
for (i, c) in td.ctors.iter().enumerate() {
@@ -360,7 +354,6 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
}
module_user_fns.insert(mname.clone(), user_fns);
module_def_ail_types.insert(mname.clone(), ail_types);
module_polymorphic_fns.insert(mname.clone(), poly_fns);
module_ctor_index.insert(mname.clone(), ctors);
module_consts.insert(mname.clone(), consts);
}
@@ -396,7 +389,6 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
mname,
&module_user_fns,
&module_def_ail_types,
&module_polymorphic_fns,
&module_ctor_index,
&module_consts,
import_map,
@@ -555,15 +547,6 @@ struct Emitter<'a> {
/// `Forall` for polymorphic defs (used to derive substitutions at
/// monomorphic call sites). Populated in pass 1 of `lower_workspace`.
module_def_ail_types: &'a BTreeMap<String, BTreeMap<String, Type>>,
/// Polymorphic defs per module — full FnDef so we can specialise
/// the body when monomorphising. Mono fns aren't included.
module_polymorphic_fns: &'a BTreeMap<String, BTreeMap<String, FnDef>>,
/// Iter 12b: queue of (module, def, substitution) tuples that need
/// to be emitted as specialised versions of polymorphic defs.
/// `mono_emitted` tracks the same keys to deduplicate. The descriptor
/// string is the deterministic name suffix (`Int`, `Int_Bool`, ...).
mono_queue: Vec<(String, String, BTreeMap<String, Type>, String)>,
mono_emitted: BTreeSet<(String, String, String)>, // (module, def, descriptor)
/// Import map of the current module (alias/module name → actual module name).
import_map: BTreeMap<String, String>,
/// ADT table: type_name -> list of ctors in definition order.
@@ -723,7 +706,6 @@ impl<'a> Emitter<'a> {
module_name: &'a str,
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
module_def_ail_types: &'a BTreeMap<String, BTreeMap<String, Type>>,
module_polymorphic_fns: &'a BTreeMap<String, BTreeMap<String, FnDef>>,
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>,
import_map: BTreeMap<String, String>,
@@ -774,9 +756,6 @@ impl<'a> Emitter<'a> {
str_counter: 0,
module_user_fns,
module_def_ail_types,
module_polymorphic_fns,
mono_queue: Vec::new(),
mono_emitted: BTreeSet::new(),
import_map,
types,
module_ctor_index,
@@ -836,18 +815,13 @@ impl<'a> Emitter<'a> {
Def::Class(_) | Def::Instance(_) => {}
}
}
// Drain the monomorphisation queue. Specialised fns may
// themselves invoke polymorphic defs and queue further entries,
// so iterate until empty.
while let Some((owner_module, def_name, subst, descriptor)) = self.mono_queue.pop() {
self.emit_specialised_fn(&owner_module, &def_name, &subst, &descriptor)
.map_err(|e| {
CodegenError::Def(
format!("{def_name}__{descriptor}"),
Box::new(e),
)
})?;
}
// iter 23.4: the monomorphisation queue is gone — the
// typecheck-time mono pass synthesises every specialised
// `Def::Fn` before codegen runs (see
// `ailang_check::mono::monomorphise_workspace`). Codegen
// sees only monomorphic defs; the drain loop and
// `emit_specialised_fn` have been removed.
// Iter 18c.4: per-ADT drop functions. Emitted only under
// `--alloc=rc`. One `void @drop_<module>_<TypeName>(ptr)` per
// `Def::Type` in the current module — the call site for
@@ -890,65 +864,6 @@ impl<'a> Emitter<'a> {
/// calls `emit_fn` against a synthetic FnDef whose `name` already
/// contains the descriptor — the existing mangling concatenates
/// `ail_<module>_<name>` and produces the desired symbol.
fn emit_specialised_fn(
&mut self,
owner_module: &str,
def_name: &str,
subst: &BTreeMap<String, Type>,
descriptor: &str,
) -> Result<()> {
let fdef = self
.module_polymorphic_fns
.get(owner_module)
.and_then(|m| m.get(def_name))
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"emit_specialised_fn: `{owner_module}.{def_name}` not registered"
))
})?;
let inner_ty = match &fdef.ty {
Type::Forall { body, .. } => (**body).clone(),
other => other.clone(),
};
let mono_ty = apply_subst_to_type(&inner_ty, subst);
let mono_body = apply_subst_to_term(&fdef.body, subst);
let synthetic = FnDef {
name: format!("{def_name}__{descriptor}"),
ty: mono_ty,
params: fdef.params.clone(),
body: mono_body,
suppress: vec![],
doc: fdef.doc.clone(),
};
// Specialised def belongs to the polymorphic def's owner
// module, not necessarily self.module_name. Swap module_name
// briefly so mangling stays correct.
let saved_module = self.module_name;
// SAFETY: we rebind module_name through a raw pointer cast
// because the field is `&'a str`. Equivalent: hand
// emit_fn the mangling target via a parameter. Simpler to
// restore.
// Instead of unsafe, we just call emit_fn directly — the
// mangling uses self.module_name which is &'a str but the
// owner_module string lives inside self.module_polymorphic_fns,
// also borrowed for 'a, so we can re-borrow it.
let owner_ref: &'a str = self
.module_polymorphic_fns
.keys()
.find(|k| k.as_str() == owner_module)
.map(|s| s.as_str())
.ok_or_else(|| {
CodegenError::Internal(format!(
"owner module `{owner_module}` not in module_polymorphic_fns"
))
})?;
self.module_name = owner_ref;
let r = self.emit_fn(&synthetic);
self.module_name = saved_module;
r
}
fn emit_const(&mut self, c: &ConstDef) -> Result<()> {
// Iter 15b: non-literal const values (e.g. ctor expressions) are
// not emitted as globals. They are inlined at every `Term::Var`
@@ -1935,14 +1850,12 @@ impl<'a> Emitter<'a> {
"cross-module call `{name}`: prefix `{prefix}` not in import map"
))
})?;
// Polymorphic def in target module? Monomorphise on demand.
if self
.module_polymorphic_fns
.get(&target_module)
.is_some_and(|m| m.contains_key(suffix))
{
return self.lower_polymorphic_call(&target_module, suffix, args, tail);
}
// iter 23.4: codegen-time poly-call dispatch removed. Post-mono
// every poly call site has been rewritten by `rewrite_mono_calls`
// to a monomorphic symbol, so the lookup-ladder below sees only
// user fns / consts / builtins. The pre-iter-23.4 path checked
// `module_polymorphic_fns` and dispatched to `lower_polymorphic_call`;
// both are gone (and the supporting Emitter fields with them).
let target_fns = self
.module_user_fns
.get(&target_module)
@@ -1962,15 +1875,7 @@ impl<'a> Emitter<'a> {
return self.emit_call(&target_module, suffix, &sig, args, tail);
}
// Polymorphic def in the current module?
if self
.module_polymorphic_fns
.get(self.module_name)
.is_some_and(|m| m.contains_key(name))
{
let owner = self.module_name.to_string();
return self.lower_polymorphic_call(&owner, name, args, tail);
}
// iter 23.4: codegen-time poly-call dispatch removed (see above).
// User function in the current module?
if let Some(sig) = self
@@ -1987,122 +1892,6 @@ impl<'a> Emitter<'a> {
)))
}
/// Iter 12b: lower a direct call to a polymorphic def. Derives the
/// substitution from the actual arg types, queues the (def,
/// substitution) pair for specialisation if not yet emitted, and
/// emits a direct call to the mangled name `@ail_<m>_<def>__<descr>`.
fn lower_polymorphic_call(
&mut self,
owner_module: &str,
def_name: &str,
args: &[Term],
tail: bool,
) -> Result<(String, String)> {
let fdef = self
.module_polymorphic_fns
.get(owner_module)
.and_then(|m| m.get(def_name))
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"lower_polymorphic_call: `{owner_module}.{def_name}` not registered"
))
})?;
let (forall_vars, params, ret) = match &fdef.ty {
Type::Forall { vars, constraints: _, body } => match body.as_ref() {
Type::Fn { params, ret, .. } => {
(vars.clone(), params.clone(), (**ret).clone())
}
_ => {
return Err(CodegenError::Internal(format!(
"lower_polymorphic_call: `{def_name}` Forall body is not Fn"
)));
}
},
_ => {
return Err(CodegenError::Internal(format!(
"lower_polymorphic_call: `{def_name}` is not polymorphic"
)));
}
};
// Iter 15a: when we're calling into another module, the fn's
// params and ret reference local type names that — from this
// call site's perspective — are qualified `module.T`. Qualify
// before deriving the substitution so the unification mirrors
// what the typechecker has already validated.
let (params, ret) = if owner_module != self.module_name {
let owner_types = self.collect_owner_local_types(owner_module);
(
params
.iter()
.map(|p| qualify_local_types_codegen(p, owner_module, &owner_types))
.collect::<Vec<_>>(),
qualify_local_types_codegen(&ret, owner_module, &owner_types),
)
} else {
(params, ret)
};
// Derive the substitution by comparing the declared param
// types against the actual arg types.
let arg_ail_tys: Vec<Type> = args
.iter()
.map(|a| self.synth_arg_type(a))
.collect::<Result<_>>()?;
let subst = derive_substitution(&forall_vars, &params, &arg_ail_tys)?;
// Specialised types and signature.
let mono_params: Vec<Type> = params
.iter()
.map(|p| apply_subst_to_type(p, &subst))
.collect();
let mono_ret = apply_subst_to_type(&ret, &subst);
let llvm_params: Vec<String> = mono_params.iter().map(llvm_type).collect::<Result<_>>()?;
let llvm_ret = llvm_type(&mono_ret)?;
let descriptor = descriptor_for_subst(&forall_vars, &subst);
let mangled = format!("ail_{owner_module}_{def_name}__{descriptor}");
// Queue specialisation (deduplicated).
let key = (owner_module.to_string(), def_name.to_string(), descriptor.clone());
if self.mono_emitted.insert(key) {
self.mono_queue.push((
owner_module.to_string(),
def_name.to_string(),
subst.clone(),
descriptor,
));
}
// Lower args and emit a direct call to the mangled name.
let mut compiled_args = Vec::new();
for (a, exp_ty) in args.iter().zip(llvm_params.iter()) {
let (v, vty) = self.lower_term(a)?;
if &vty != exp_ty {
return Err(CodegenError::Internal(format!(
"poly call `{owner_module}.{def_name}` arg type mismatch: expected {exp_ty}, got {vty}"
)));
}
compiled_args.push((v, vty));
}
let arglist = compiled_args
.iter()
.map(|(v, t)| format!("{t} {v}"))
.collect::<Vec<_>>()
.join(", ");
let dst = self.fresh_ssa();
let call_kw = if tail { "musttail call" } else { "call" };
self.body.push_str(&format!(
" {dst} = {call_kw} {ret} @{mangled}({arglist})\n",
ret = llvm_ret,
));
if tail {
self.body
.push_str(&format!(" ret {ret} {dst}\n", ret = llvm_ret));
self.block_terminated = true;
}
Ok((dst, llvm_ret))
}
fn emit_call(
&mut self,
target_module: &str,
@@ -2250,14 +2039,10 @@ impl<'a> Emitter<'a> {
if name.matches('.').count() == 1 {
return true;
}
if self
.module_user_fns
.get(self.module_name)
.is_some_and(|m| m.contains_key(name))
{
return true;
}
self.module_polymorphic_fns
// iter 23.4: poly-def arm dropped — post-mono there are no
// `Type::Forall` defs in `module_user_fns` to begin with, and
// `module_polymorphic_fns` no longer exists.
self.module_user_fns
.get(self.module_name)
.is_some_and(|m| m.contains_key(name))
}
+6 -87
View File
@@ -14,7 +14,6 @@ use ailang_core::ast::*;
use std::collections::{BTreeMap, BTreeSet};
use super::{CodegenError, Result};
use crate::synth::type_descriptor;
/// Iter 12b: derive a name → concrete-type substitution from the
/// declared params of a `Forall` body and the actual arg types at a
@@ -233,89 +232,9 @@ pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> T
}
}
/// Iter 12b: substitute rigid type vars throughout a Term. Only
/// `Term::Lam` carries types in the AST (params/ret), so most arms
/// just recurse. `Term::Var` contains a name string only and is
/// left untouched.
pub(crate) fn apply_subst_to_term(t: &Term, subst: &BTreeMap<String, Type>) -> Term {
match t {
Term::Lit { .. } | Term::Var { .. } => t.clone(),
Term::App { callee, args, tail } => Term::App {
callee: Box::new(apply_subst_to_term(callee, subst)),
args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
tail: *tail,
},
Term::Let { name, value, body } => Term::Let {
name: name.clone(),
value: Box::new(apply_subst_to_term(value, subst)),
body: Box::new(apply_subst_to_term(body, subst)),
},
Term::If { cond, then, else_ } => Term::If {
cond: Box::new(apply_subst_to_term(cond, subst)),
then: Box::new(apply_subst_to_term(then, subst)),
else_: Box::new(apply_subst_to_term(else_, subst)),
},
Term::Do { op, args, tail } => Term::Do {
op: op.clone(),
args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
tail: *tail,
},
Term::Ctor { type_name, ctor, args } => Term::Ctor {
type_name: type_name.clone(),
ctor: ctor.clone(),
args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
},
Term::Match { scrutinee, arms } => Term::Match {
scrutinee: Box::new(apply_subst_to_term(scrutinee, subst)),
arms: arms
.iter()
.map(|a| Arm {
pat: a.pat.clone(),
body: apply_subst_to_term(&a.body, subst),
})
.collect(),
},
Term::Lam { params, param_tys, ret_ty, effects, body } => Term::Lam {
params: params.clone(),
param_tys: param_tys
.iter()
.map(|t| apply_subst_to_type(t, subst))
.collect(),
ret_ty: Box::new(apply_subst_to_type(ret_ty, subst)),
effects: effects.clone(),
body: Box::new(apply_subst_to_term(body, subst)),
},
Term::Seq { lhs, rhs } => Term::Seq {
lhs: Box::new(apply_subst_to_term(lhs, subst)),
rhs: Box::new(apply_subst_to_term(rhs, subst)),
},
Term::LetRec { .. } => {
// Iter 16b.1: eliminated by desugar before any
// monomorphisation pass runs.
unreachable!("Term::LetRec eliminated by desugar")
}
Term::Clone { value } => Term::Clone {
// Iter 18c.1: structural recursion through the wrapper.
value: Box::new(apply_subst_to_term(value, subst)),
},
Term::ReuseAs { source, body } => Term::ReuseAs {
// Iter 18d.1: structural recursion through both children.
source: Box::new(apply_subst_to_term(source, subst)),
body: Box::new(apply_subst_to_term(body, subst)),
},
}
}
/// Iter 12b: deterministic descriptor string for a substitution. Used
/// as the suffix in the mangled name `@ail_<m>_<def>__<descriptor>`.
/// Vars are emitted in the order given by the FnDef's forall vars
/// (so two call sites with the same instantiation map to the same
/// descriptor regardless of internal BTreeMap ordering).
pub(crate) fn descriptor_for_subst(vars: &[String], subst: &BTreeMap<String, Type>) -> String {
let mut parts: Vec<String> = Vec::with_capacity(vars.len());
for v in vars {
let ty = subst.get(v).cloned().unwrap_or_else(|| Type::unit());
parts.push(type_descriptor(&ty));
}
parts.join("_")
}
// iter 23.4: `apply_subst_to_term` and `descriptor_for_subst` were
// the codegen-side body-substitution and symbol-mangling helpers used
// by `lower_polymorphic_call` / `emit_specialised_fn`. Both are gone:
// the typecheck-time mono pass synthesises every monomorphic body
// (via `ailang_check::substitute_rigids_in_term`) and produces
// surface-named mono symbols (via `ailang_check::mono::mono_symbol_n`).
+5 -42
View File
@@ -196,48 +196,11 @@ pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
})
}
/// Iter 12b: a stable, identifier-safe descriptor for a `Type`.
/// Maps `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT name `Foo →
/// FFoo`, fn → `F<params...>R<ret>` (no recursion guard since types in
/// the MVP are non-recursive at the type level).
pub(crate) fn type_descriptor(t: &Type) -> String {
match t {
Type::Con { name, args } => {
let head = match name.as_str() {
"Int" => "I".into(),
"Bool" => "B".into(),
"Unit" => "U".into(),
"Str" => "S".into(),
"Float" => "Fl".into(),
other => format!("F{other}"),
};
if args.is_empty() {
head
} else {
// Iter 13a: parameterised ADTs get their type-arg
// descriptors appended, e.g. `FBox` of `Int` → `FBox_I`.
let mut s = head;
for a in args {
s.push('_');
s.push_str(&type_descriptor(a));
}
s
}
}
Type::Fn { params, ret, .. } => {
let mut s = String::from("Fn");
for p in params {
s.push('_');
s.push_str(&type_descriptor(p));
}
s.push_str("__r_");
s.push_str(&type_descriptor(ret));
s
}
Type::Var { name } => format!("V{name}"),
Type::Forall { .. } => "FORALL".into(),
}
}
// iter 23.4: `type_descriptor` was the helper that mangled a `Type`
// into an identifier-safe suffix for the codegen-side mono mangling
// (`Int → I`, `Bool → B`, etc.). The unified mono pass produces
// surface-named mono symbols instead (`Int → "Int"`, parameterised
// via 8-hex hash) — see `ailang_check::mono::mono_symbol_n`.
/// Floats iter 4.2: arithmetic / comparison ops are type-dispatched
/// over `{Int, Float}`. Caller (`lower_app`) resolves the arg type