bugfix: mono cursor misalignment at poly-free-fn Var with class-constrained Forall

When `synth` visits a `Term::Var v` where v is a polymorphic free fn
whose `Type::Forall` carries class constraints (e.g. `print : forall a.
Show a => ...`, `lt`/`le`/`gt`/`ge : forall a. Ord a => ...`), it
pushes BOTH a class `ResidualConstraint` for each constraint AND a
`FreeFnCall` observation at the same Var site. The mono walkers
(`interleave_slots` collection-side and `rewrite_mono_calls`
rewrite-side) consumed only ONE slot per such Var, leaving the
class-residual cursor misaligned for every subsequent class-method
call in the same body. The next `eq`/`compare` Var in the body then
consumed the leftover `Show T` / `Ord T` slot and was mis-rewritten —
codegen reported `call prelude.show__Bool arg type mismatch: expected
i1, got i64` (or the Int dual).

Fix: new `poly_free_fn_constraint_counts_for_module` registry in
mono.rs (sibling to `poly_free_fn_names_for_module`) maps each
synth-visible poly-free-fn name to its declared constraint count.
Threaded into `collect_residuals_ordered`, `interleave_slots`, and
`rewrite_mono_calls`. Both walkers now advance the cursor by 1 + N
(FreeFn slot + N class-residual fillers) at every poly-free-fn Var.

RED test `print_with_class_method_arg_does_not_misalign_mono_cursor`
(crates/ail/tests/show_print_e2e.rs) + fixture
examples/print_eq_arg_repro.ail pin the bug — body `(app print (app
eq 1 2))` previously crashed at codegen, now prints "false".

Bug surfaced 2026-05-14 during the rpe.1 BLOCKED orchestrator run;
existed in latent form since iter 24.3 (the poly-free-fn-with-class-
constraint synth shape was new there). No schema / codegen / DESIGN.md
changes. cargo test --workspace: 564 / 0 / 3.
This commit is contained in:
2026-05-14 01:38:41 +02:00
parent 05e4c04e3b
commit 1fb225ee25
5 changed files with 295 additions and 41 deletions
@@ -0,0 +1,12 @@
{
"iter_id": "bugfix-mono-cursor-print-with-class-method-arg",
"date": "2026-05-14",
"mode": "mini",
"outcome": "DONE",
"tasks_total": 1,
"tasks_completed": 1,
"reloops_per_task": { "1": 0 },
"review_loops_spec": 0,
"review_loops_quality": 0,
"blocked_reason": null
}
+33
View File
@@ -72,3 +72,36 @@ fn print_user_adt_runs_end_to_end() {
// IntBox that unwraps via match + int_to_str).
assert_eq!(stdout, "7", "got: {stdout:?}");
}
/// RED pin (rpe-bug-1, 2026-05-14): `(app print <arg>)` where `<arg>`
/// contains a class-method call (here `eq 1 2 : Bool`) must lower
/// cleanly. The iter 24.3 rewire taught `synth` to push a `Show T`
/// residual for `print`'s declared constraint at the same `Var` site
/// where the poly-free-fn observation is pushed (see
/// `crates/ailang-check/src/lib.rs` lines 2908-2960). But the
/// mono-rewrite walker in `crates/ailang-check/src/mono.rs`
/// (`interleave_slots` and `rewrite_mono_calls`) treats every
/// poly-free-fn `Var` as consuming exactly one free-fn slot — it does
/// not advance the class-residual cursor for the constraint that was
/// pushed at the same site. The consequence is a one-position
/// misalignment: the *next* class-method call in the body consumes
/// the `Show T` slot, rewriting `eq` → `show__Bool`. Codegen then
/// reports `call prelude.show__Bool arg type mismatch: expected i1,
/// got i64`.
///
/// Property protected: a `print` call whose argument contains a
/// class-method invocation lowers to a valid IR call sequence — the
/// `eq` (or other class-method) call is rewritten to its correct
/// mono symbol, not to a stale `Show T` residual symbol.
///
/// Fixture `examples/print_eq_arg_repro.ail` exercises the shape
/// `(app print (app eq 1 2))`. Expected stdout: `"false"` (Show Bool
/// of `eq 1 2` → false). This test is GREEN once the cursor
/// alignment in `interleave_slots` / `rewrite_mono_calls` is fixed
/// to advance the class cursor for constraints carried by a
/// poly-free-fn Var.
#[test]
fn print_with_class_method_arg_does_not_misalign_mono_cursor() {
let stdout = build_and_run("print_eq_arg_repro.ail");
assert_eq!(stdout, "false", "got: {stdout:?}");
}
+149 -41
View File
@@ -178,16 +178,34 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
// Iter 23.4: 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)?,
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)?
collect_residuals_ordered(
&pseudo,
mname,
&env_mod,
&poly_free_fns,
&poly_free_fn_ccounts,
)?
}
_ => continue,
}
@@ -206,6 +224,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
&mut f.body,
&env.method_to_candidate_classes,
&poly_free_fns,
&poly_free_fn_ccounts,
mname,
&ordered,
&mut cursor,
@@ -217,6 +236,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
&mut c.value,
&env.method_to_candidate_classes,
&poly_free_fns,
&poly_free_fn_ccounts,
mname,
&ordered,
&mut cursor,
@@ -338,6 +358,52 @@ fn poly_free_fn_names_for_module(
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.
if let Some(imports) = env.module_imports.get(mname) {
for (alias_or_name, target_mod) in imports {
if let Some(target_globals) = env.module_globals.get(target_mod) {
for (n, t) in target_globals {
if let Type::Forall { constraints, .. } = t {
out.insert(n.clone(), constraints.len());
out.insert(format!("{alias_or_name}.{n}"), constraints.len());
}
}
}
}
}
out
}
/// Iter 22b.3: workspace-wide `class-name -> ClassDef` index.
/// Used by the fixpoint to look up the matching class definition
/// when synthesising a fn.
@@ -1005,6 +1071,7 @@ 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,
@@ -1021,10 +1088,31 @@ fn rewrite_mono_calls(
// `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
@@ -1045,19 +1133,19 @@ fn rewrite_mono_calls(
};
*name = new_name;
}
*cursor += 1;
*cursor += 1 + n_filler;
}
}
Term::App { callee, args, .. } => {
rewrite_mono_calls(callee, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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, 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);
if inserted {
locals.remove(name);
}
@@ -1070,32 +1158,32 @@ fn rewrite_mono_calls(
params_inserted.push(p.clone());
}
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, 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);
for p in &params_inserted {
locals.remove(p);
}
rewrite_mono_calls(in_term, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(then, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(else_, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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();
@@ -1104,7 +1192,7 @@ fn rewrite_mono_calls(
inserted.push(b.clone());
}
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, 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);
for b in &inserted {
locals.remove(b);
}
@@ -1117,21 +1205,21 @@ fn rewrite_mono_calls(
inserted.push(p.clone());
}
}
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, 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);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
rewrite_mono_calls(lhs, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(rhs, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
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, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
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::Lit { .. } => {}
}
@@ -1190,6 +1278,7 @@ pub(crate) fn collect_residuals_ordered(
module_name: &str,
env: &crate::Env,
poly_free_fns: &BTreeSet<String>,
poly_free_fn_ccounts: &BTreeMap<String, usize>,
) -> Result<Vec<Option<MonoTarget>>> {
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
@@ -1335,6 +1424,7 @@ pub(crate) fn collect_residuals_ordered(
&f.body,
&env.method_to_candidate_classes,
poly_free_fns,
poly_free_fn_ccounts,
&class_slots,
&free_fn_slots,
&mut class_cur,
@@ -1360,6 +1450,7 @@ 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,
@@ -1378,22 +1469,39 @@ fn interleave_slots(
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, 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);
if inserted {
locals.remove(name);
}
@@ -1406,32 +1514,32 @@ fn interleave_slots(
params_inserted.push(p.clone());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, 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);
for p in &params_inserted {
locals.remove(p);
}
interleave_slots(in_term, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(then, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(else_, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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();
@@ -1440,7 +1548,7 @@ fn interleave_slots(
inserted.push(b.clone());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, 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);
for b in &inserted {
locals.remove(b);
}
@@ -1453,21 +1561,21 @@ fn interleave_slots(
inserted.push(p.clone());
}
}
interleave_slots(body, method_to_candidate_classes, poly_free_fns, 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);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
interleave_slots(lhs, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(rhs, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
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::Lit { .. } => {}
}
@@ -0,0 +1,95 @@
# iter bugfix-mono-cursor-print-with-class-method-arg — `(app print (app eq …))` no longer rewrites the inner class-method to a stale `Show T` symbol
**Date:** 2026-05-14
**Started from:** 05e4c04e3ba74f3382216ff3ea285e3fb1e3a45d
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
The iter 24.3 `synth` Var-arm pushes BOTH a class `ResidualConstraint`
for each declared constraint AND a `FreeFnCall` observation at the
same poly-free-fn `Var` site (`crates/ailang-check/src/lib.rs` ≈ 2908
2966). The two mono walkers in `crates/ailang-check/src/mono.rs`,
however, were each consuming exactly one slot per `Var`: at a
poly-free-fn `Var` they popped one `free_fn_slot` and zero
`class_slot`s. Every constraint on a poly-free-fn-with-constraints
left a leftover class slot, so the next class-method `Var` in the
same body consumed the wrong slot — the residual that belonged to
`print`'s `Show T` constraint got cursor-aligned with the inner `eq`
`Var`, and codegen reported `call prelude.show__Bool arg type
mismatch: expected i1, got i64`.
The fix advances both walkers' cursors by `1 + N` at a poly-free-fn
`Var` whose source `Type::Forall` carries `N` class constraints. A
new sibling helper `poly_free_fn_constraint_counts_for_module`
mirrors `poly_free_fn_names_for_module`'s three-source spelling
contract (same-module / implicit-import bare / dot-qualified
`alias.name`) and produces a `BTreeMap<String, usize>` from synth-
visible name to declared-constraint count. The map is threaded into
the rewrite-phase loop alongside `poly_free_fns` and passed to both
`rewrite_mono_calls` and `interleave_slots`.
In `interleave_slots`, a poly-free-fn `Var` now pushes the `FreeFn`
slot first (this is what the rewrite walker reads for the rename),
then `N` filler entries drawn in order from `class_slots` (consumed
only to keep `class_cur` aligned). In `rewrite_mono_calls`, the
`Var`-arm captures `n_filler` from the count map BEFORE the rename
mutates `name` (lookup keys are the synth-visible spelling), reads
the slot at `*cursor` as before, then advances by `1 + n_filler`.
Class-method `Var`s stay at advance-by-one (synth pushes exactly one
residual and no `FreeFnCall`).
## Per-task notes
- task 1: write/verify the cursor-alignment fix.
- Reproduced RED at `print_with_class_method_arg_does_not_misalign_mono_cursor`
with the exact failure mode stated in the carrier:
`call prelude.show__Bool arg type mismatch: expected i1, got i64`.
- Added `poly_free_fn_constraint_counts_for_module` helper
immediately after `poly_free_fn_names_for_module`; identical
name-source contract, returning constraint counts instead of a
presence set.
- Threaded `poly_free_fn_ccounts: &BTreeMap<String, usize>` into
`collect_residuals_ordered`, `interleave_slots`, and
`rewrite_mono_calls` (signatures + every recursive call site).
- `interleave_slots::Term::Var` poly-free-fn branch: after pushing
the `FreeFn` slot, drain `N` entries from `class_slots` and push
them as fillers in `out`, advancing `class_cur` by `N`.
- `rewrite_mono_calls::Term::Var`: compute `n_filler` from the
count map before the rename mutation, then `*cursor += 1 +
n_filler`. Class-method `Var`s are guarded with `if
is_poly_free_fn { … } else { 0 }`.
- RED `print_with_class_method_arg_does_not_misalign_mono_cursor`
flips to GREEN with stdout `false` as predicted by the fixture
docstring.
- Full `cargo test --workspace` passes 564 / 564 (= 563 baseline
from `clippy-sweep` + the 1 new mini-mode RED). Zero
regressions across any of the existing show / mono / typeclass
test suites.
## Concerns
(none)
## Known debt
(none — the constraint set is a single new sibling helper plus a
new threaded argument; the two-walker symmetry remains the invariant
the existing comments already document.)
## Files touched
- `crates/ailang-check/src/mono.rs` — the fix (helper + two walker
signatures + Var-arm cursor advancement + recursive-call
threading).
- `crates/ail/tests/show_print_e2e.rs` — RED pin
`print_with_class_method_arg_does_not_misalign_mono_cursor`
(committed by `/debug` upstream of this iter; flips RED → GREEN
here).
- `examples/print_eq_arg_repro.ail` — RED fixture (committed by
`/debug` upstream; consumed by the pin).
## Stats
bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json
+6
View File
@@ -0,0 +1,6 @@
(module print_eq_arg_repro
(fn main
(doc "RED-pin fixture for the iter 24.3 cursor-misalignment bug. `(app print <arg>)` where `<arg>` itself contains a class-method call (here `eq`) causes the mono-rewrite cursor to consume the `Show T` residual pushed by `print`'s poly-free-fn observation as if it were the slot for the inner class method. Result: the inner `eq` Var is rewritten to `prelude.show__Bool` instead of `prelude.eq__Int`, and codegen reports `call prelude.show__Bool arg type mismatch: expected i1, got i64`. Bug surfaced 2026-05-14 during the rpe.1 BLOCKED orchestrator run; root cause is `crates/ailang-check/src/mono.rs::interleave_slots` not advancing the class-residual cursor when visiting a poly-free-fn Var whose source `Type::Forall` carries a class constraint. Fixture pinned by `print_with_class_method_arg_does_not_misalign_mono_cursor` in crates/ail/tests/show_print_e2e.rs.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (app print (app eq 1 2)))))