From 1fb225ee25566d3f7f49463d65282fd76df2c447 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 14 May 2026 01:38:41 +0200 Subject: [PATCH] bugfix: mono cursor misalignment at poly-free-fn Var with class-constrained Forall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...no-cursor-print-with-class-method-arg.json | 12 ++ crates/ail/tests/show_print_e2e.rs | 33 +++ crates/ailang-check/src/mono.rs | 190 ++++++++++++++---- ...mono-cursor-print-with-class-method-arg.md | 95 +++++++++ examples/print_eq_arg_repro.ail | 6 + 5 files changed, 295 insertions(+), 41 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json create mode 100644 docs/journals/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.md create mode 100644 examples/print_eq_arg_repro.ail diff --git a/bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json b/bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json new file mode 100644 index 0000000..6d4dd50 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.json @@ -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 +} diff --git a/crates/ail/tests/show_print_e2e.rs b/crates/ail/tests/show_print_e2e.rs index df740bd..7f631a8 100644 --- a/crates/ail/tests/show_print_e2e.rs +++ b/crates/ail/tests/show_print_e2e.rs @@ -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 )` where `` +/// 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:?}"); +} diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 7cb38b3..3bdee49 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -178,16 +178,34 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { // 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> = { 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 { &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 { &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 { + let mut out: BTreeMap = 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>, poly_free_fns: &BTreeSet, + poly_free_fn_ccounts: &BTreeMap, caller_module: &str, ordered_targets: &[Option], 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 ¶ms_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 = 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, + poly_free_fn_ccounts: &BTreeMap, ) -> Result>> { let (rigids, inner_ty): (Vec, 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>, poly_free_fns: &BTreeSet, + poly_free_fn_ccounts: &BTreeMap, class_slots: &[Option], free_fn_slots: &[Option], 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 ¶ms_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 = 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 { .. } => {} } diff --git a/docs/journals/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.md b/docs/journals/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.md new file mode 100644 index 0000000..20c3e2b --- /dev/null +++ b/docs/journals/2026-05-14-iter-bugfix-mono-cursor-print-with-class-method-arg.md @@ -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` 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` 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 diff --git a/examples/print_eq_arg_repro.ail b/examples/print_eq_arg_repro.ail new file mode 100644 index 0000000..74acfad --- /dev/null +++ b/examples/print_eq_arg_repro.ail @@ -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 )` where `` 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)))))