ac4d545570
Follow-up to bcd4181: the remaining ~530 inline `//` and `///`
comments still carrying opaque shorthand are now reformulated to
their content phrases. The only surviving `iter-<code>` reference
in source is the literal filename
`docs/journals/2026-05-13-iter-mq.3.md` (a real journal file).
Sweep covered:
- `// Iter X.Y: <text>` prefixes (Iter 13a / 14a / 14e / 15g-aux /
16b.x / 16d / 16e / 18b / 18c.x / 18d.x / 18e / 18g.x / 19a /
19a.1 / 19b / 20a / 20f / 22-floats.x / 22b.x / 22c / 23.x /
24.1 / cli-diag-human / hs.x / str-concat / etc.) — fully
removed; the descriptive text that followed each prefix stays.
- `// (Decision N)` and `per Decision N` and `Decision N axis 3` —
replaced with the content phrase plus the relevant contract
file (`design/contracts/tail-calls.md` for Decision 8,
`design/contracts/memory-model.md` for Decision 10,
`design/contracts/typeclasses.md` and `design/models/typeclasses.md`
for Decision 11, `design/contracts/authoring-surface.md` for
Decision 6, "the transitional dual-allocator" for Decision 9,
"Effect prose" for Decision 3).
- `// mq.X / mq.X (Task N) / mq.X journal / mq.X invariant` ->
"the canonical-class-form rule / Class-class repurpose /
method-dispatch-refactor journal / canonical-class-form
invariant".
- `// ct.X / ct.X (canonical-type-names) / ct.1.5a + ctt.2 /
ct.2 Task N` -> "the canonical-form rule for type references /
the canonical-form normalisation step / canonical-type-lookup
refactor".
- `// eob.X` -> "heap-Str-ABI" / "the Str carve-out".
- `// rpe.X` -> "the per-type-print-op retirement".
- `// post-mq.X / Pre-ct.X / pre-mq.X` -> "post-canonical-class-form" /
"Pre-canonical-type-form" etc.
- `/// Iter X regression: / /// Iter X.Y: / /// Iter A arm-close /
/// Iter ct.4 (...): / /// Iter rpe.1 ...` -> descriptive
phrases.
The journal filename in `crates/ailang-core/src/workspace.rs:573`
stays verbatim because it points at an actual file under
`docs/journals/`.
Tests: full `cargo test --workspace` green (80/80 test-result blocks
clean, no FAILED line). design_index_pin 5/5 + docs_honesty_pin 5/5
gating tests pass.
246 lines
10 KiB
Rust
246 lines
10 KiB
Rust
//! Type substitution + unification helpers for monomorphisation.
|
|
//!
|
|
//! Free functions extracted from `lib.rs` during the 18g tidy split.
|
|
//! The four-step pipeline is: `derive_substitution` walks declared
|
|
//! params against actual arg types, calling `unify_for_subst` to bind
|
|
//! `Type::Var`s; `apply_subst_to_type` / `apply_subst_to_term`
|
|
//! specialise a polymorphic def under that binding;
|
|
//! `qualify_local_types_codegen` rewrites bare ADT names into
|
|
//! `module.Type` form when a sig crosses an import boundary;
|
|
//! `descriptor_for_subst` produces the stable mangling suffix used in
|
|
//! the specialised symbol's name.
|
|
|
|
use ailang_core::ast::*;
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
use super::{CodegenError, Result};
|
|
|
|
/// derive a name → concrete-type substitution from the
|
|
/// declared params of a `Forall` body and the actual arg types at a
|
|
/// call site. Walks both sides in parallel; whenever a `Type::Var`
|
|
/// (rigid name) appears on the params side, binds it to the
|
|
/// corresponding concrete type. Conflicts (same var bound to two
|
|
/// different types) surface as an internal error — the typechecker
|
|
/// would already have rejected such a call.
|
|
pub(crate) fn derive_substitution(
|
|
vars: &[String],
|
|
params: &[Type],
|
|
arg_tys: &[Type],
|
|
) -> Result<BTreeMap<String, Type>> {
|
|
if params.len() != arg_tys.len() {
|
|
return Err(CodegenError::Internal(format!(
|
|
"derive_substitution: arity mismatch ({} params vs {} args)",
|
|
params.len(),
|
|
arg_tys.len(),
|
|
)));
|
|
}
|
|
let var_set: BTreeSet<&str> = vars.iter().map(|s| s.as_str()).collect();
|
|
let mut subst: BTreeMap<String, Type> = BTreeMap::new();
|
|
for (p, a) in params.iter().zip(arg_tys.iter()) {
|
|
unify_for_subst(p, a, &var_set, &mut subst)?;
|
|
}
|
|
// Any forall var not pinned by the args is left unbound. For the
|
|
// MVP this is an error — we can't specialise without a concrete
|
|
// type. The typechecker's body should have constrained it already
|
|
// through return-type unification, but at the call site we only
|
|
// see args; if needed, callers can extend this with expected-ret
|
|
// info.
|
|
// a forall var that the args couldn't pin (e.g.
|
|
// `is_none(Nothing) : forall a. (Maybe a) -> Bool` — `a` is
|
|
// genuinely unobservable from the args alone) defaults to `Unit`.
|
|
// The specialised body must not actually read an `a`-typed value,
|
|
// or it would have failed type-checking; a dummy concrete type is
|
|
// sound and lets monomorphisation proceed deterministically. The
|
|
// descriptor uses the same default, so all such call sites
|
|
// converge on a single specialisation.
|
|
for v in vars {
|
|
if !subst.contains_key(v) {
|
|
subst.insert(v.clone(), Type::unit());
|
|
}
|
|
}
|
|
Ok(subst)
|
|
}
|
|
|
|
/// Walks `param` and `arg` in parallel, treating any `Type::Var { name }`
|
|
/// on the param side whose name is in `vars` as an unknown to be bound
|
|
/// in `subst`. Identical concrete shapes pass through; structural
|
|
/// mismatches yield an internal error.
|
|
pub(crate) fn unify_for_subst(
|
|
param: &Type,
|
|
arg: &Type,
|
|
vars: &BTreeSet<&str>,
|
|
subst: &mut BTreeMap<String, Type>,
|
|
) -> Result<()> {
|
|
// A `$u`-prefixed var is a
|
|
// synth-only wildcard produced by `synth_arg_type` for nullary
|
|
// ctors of a parameterised ADT (e.g. `Nil : List<$u>`). It
|
|
// carries no real constraint — accept without binding so a
|
|
// sibling arg can pin the type var instead. Without this,
|
|
// `Cons(Int, Nil)` synth would unify `a = Int` (from head) and
|
|
// then `a = $u` (from tail's recursive `List<a>` slot) and
|
|
// falsely error.
|
|
//
|
|
// 15g-aux: the early-return must accept `$u` on **either** side.
|
|
// `$u` enters in arg position from synth, but the prev-binding
|
|
// recursion below (`unify_for_subst(&prev, arg, ...)`) can swap
|
|
// a `$u` onto the param side when a previously-bound type is
|
|
// unified against a fresher arg whose roles differ. Reduced
|
|
// repro: `length [Left 1, Right 10]` — `a` first binds to
|
|
// `Either<Int, $u>` from `Left 1`, then a recursive unification
|
|
// against `Either<$u, Int>` from `Right 10` lands `$u` in the
|
|
// param-pos[1] slot. Symmetric early-return is correct because
|
|
// `$u` is a synth-only wildcard regardless of which side carries
|
|
// it after the prev-binding swap.
|
|
if let Type::Var { name } = arg {
|
|
if name.starts_with("$u") {
|
|
return Ok(());
|
|
}
|
|
}
|
|
if let Type::Var { name } = param {
|
|
if name.starts_with("$u") {
|
|
return Ok(());
|
|
}
|
|
}
|
|
match (param, arg) {
|
|
(Type::Var { name }, _) if vars.contains(name.as_str()) => {
|
|
if let Some(prev) = subst.get(name).cloned() {
|
|
// the previously-bound type may be more
|
|
// concrete than `arg` (e.g. `prev = List<Int>` from a
|
|
// sibling binding, `arg = List<$u>` from a synth-
|
|
// wildcard nullary ctor). Use recursive unification
|
|
// instead of strict equality so the inner `$u`
|
|
// wildcard matches `Int`. The previous strict-
|
|
// equality check rejected such overlaps as bogus
|
|
// duplicate bindings.
|
|
return unify_for_subst(&prev, arg, vars, subst);
|
|
}
|
|
subst.insert(name.clone(), arg.clone());
|
|
Ok(())
|
|
}
|
|
(
|
|
Type::Con { name: pn, args: pa },
|
|
Type::Con { name: an, args: aa },
|
|
) if pn == an && pa.len() == aa.len() => {
|
|
for (p, a) in pa.iter().zip(aa.iter()) {
|
|
unify_for_subst(p, a, vars, subst)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
(
|
|
Type::Fn { params: pp, ret: pr, .. },
|
|
Type::Fn { params: ap, ret: ar, .. },
|
|
) => {
|
|
if pp.len() != ap.len() {
|
|
return Err(CodegenError::Internal(
|
|
"monomorphisation: fn arity mismatch in arg".into(),
|
|
));
|
|
}
|
|
for (p, a) in pp.iter().zip(ap.iter()) {
|
|
unify_for_subst(p, a, vars, subst)?;
|
|
}
|
|
unify_for_subst(pr, ar, vars, subst)
|
|
}
|
|
(Type::Var { name: pn }, Type::Var { name: an }) if pn == an => Ok(()),
|
|
_ => Err(CodegenError::Internal(format!(
|
|
"monomorphisation: cannot match param `{}` to arg `{}`",
|
|
ailang_core::pretty::type_to_string(param),
|
|
ailang_core::pretty::type_to_string(arg),
|
|
))),
|
|
}
|
|
}
|
|
|
|
/// rewrites bare `Type::Con` references that resolve against
|
|
/// `owner_local_types` into qualified `module.Type` form. Mirrors
|
|
/// `ailang_check::qualify_local_types`. Used when the codegen pulls a
|
|
/// polymorphic fn signature across the import boundary; without this
|
|
/// the substitution derived from the call site's qualified args
|
|
/// (`std_maybe.Maybe<Int>`) would fail to unify against the bare
|
|
/// signature (`Maybe<a>`).
|
|
pub(crate) fn qualify_local_types_codegen(
|
|
t: &Type,
|
|
owner_module: &str,
|
|
owner_local_types: &BTreeSet<String>,
|
|
) -> Type {
|
|
match t {
|
|
Type::Con { name, args } => {
|
|
// The first two branches share the body `name.clone()` but
|
|
// express semantically distinct reasons (already qualified;
|
|
// primitive needs no qualification). Combining them with
|
|
// `||` would obscure why each disqualifies the name.
|
|
#[allow(clippy::if_same_then_else)]
|
|
let qualified = if name.contains('.') {
|
|
name.clone()
|
|
} else if ailang_core::primitives::is_primitive_name(name) {
|
|
name.clone()
|
|
} else if owner_local_types.contains(name) {
|
|
format!("{owner_module}.{name}")
|
|
} else {
|
|
name.clone()
|
|
};
|
|
Type::Con {
|
|
name: qualified,
|
|
args: args
|
|
.iter()
|
|
.map(|a| qualify_local_types_codegen(a, owner_module, owner_local_types))
|
|
.collect(),
|
|
}
|
|
}
|
|
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
|
|
params: params
|
|
.iter()
|
|
.map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types))
|
|
.collect(),
|
|
ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)),
|
|
effects: effects.clone(),
|
|
param_modes: param_modes.clone(),
|
|
ret_mode: *ret_mode,
|
|
},
|
|
Type::Forall { vars, constraints, body } => Type::Forall {
|
|
vars: vars.clone(),
|
|
constraints: constraints.clone(),
|
|
body: Box::new(qualify_local_types_codegen(body, owner_module, owner_local_types)),
|
|
},
|
|
Type::Var { .. } => t.clone(),
|
|
}
|
|
}
|
|
|
|
/// substitute rigid type vars in `t` according to `subst`.
|
|
/// Used to specialise the type of a polymorphic def for a given
|
|
/// instantiation.
|
|
pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> Type {
|
|
match t {
|
|
Type::Var { name } => subst.get(name).cloned().unwrap_or_else(|| t.clone()),
|
|
Type::Con { name, args } => Type::Con {
|
|
name: name.clone(),
|
|
args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(),
|
|
},
|
|
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
|
|
params: params.iter().map(|p| apply_subst_to_type(p, subst)).collect(),
|
|
ret: Box::new(apply_subst_to_type(ret, subst)),
|
|
effects: effects.clone(),
|
|
param_modes: param_modes.clone(),
|
|
ret_mode: *ret_mode,
|
|
},
|
|
Type::Forall { vars, constraints, body } => {
|
|
// Inner forall shadows: don't substitute re-bound names.
|
|
let inner: BTreeMap<String, Type> = subst
|
|
.iter()
|
|
.filter(|(k, _)| !vars.contains(k))
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
Type::Forall {
|
|
vars: vars.clone(),
|
|
constraints: constraints.clone(),
|
|
body: Box::new(apply_subst_to_type(body, &inner)),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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`).
|