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:
+12
@@ -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
|
||||||
|
}
|
||||||
@@ -72,3 +72,36 @@ fn print_user_adt_runs_end_to_end() {
|
|||||||
// IntBox that unwraps via match + int_to_str).
|
// IntBox that unwraps via match + int_to_str).
|
||||||
assert_eq!(stdout, "7", "got: {stdout:?}");
|
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
@@ -178,16 +178,34 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
|
|||||||
// Iter 23.4: precompute the per-module poly-free-fn name
|
// Iter 23.4: precompute the per-module poly-free-fn name
|
||||||
// set used as the rewrite walker's second predicate.
|
// set used as the rewrite walker's second predicate.
|
||||||
let poly_free_fns = poly_free_fn_names_for_module(&ws_owned, &env_mod, mname);
|
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();
|
let n_defs = ws_owned.modules[mname].defs.len();
|
||||||
for i in 0..n_defs {
|
for i in 0..n_defs {
|
||||||
let ordered: Vec<Option<MonoTarget>> = {
|
let ordered: Vec<Option<MonoTarget>> = {
|
||||||
let m = &ws_owned.modules[mname];
|
let m = &ws_owned.modules[mname];
|
||||||
let d = &m.defs[i];
|
let d = &m.defs[i];
|
||||||
match d {
|
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) => {
|
Def::Const(c) => {
|
||||||
let pseudo = const_as_pseudo_fn(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,
|
_ => continue,
|
||||||
}
|
}
|
||||||
@@ -206,6 +224,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
|
|||||||
&mut f.body,
|
&mut f.body,
|
||||||
&env.method_to_candidate_classes,
|
&env.method_to_candidate_classes,
|
||||||
&poly_free_fns,
|
&poly_free_fns,
|
||||||
|
&poly_free_fn_ccounts,
|
||||||
mname,
|
mname,
|
||||||
&ordered,
|
&ordered,
|
||||||
&mut cursor,
|
&mut cursor,
|
||||||
@@ -217,6 +236,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
|
|||||||
&mut c.value,
|
&mut c.value,
|
||||||
&env.method_to_candidate_classes,
|
&env.method_to_candidate_classes,
|
||||||
&poly_free_fns,
|
&poly_free_fns,
|
||||||
|
&poly_free_fn_ccounts,
|
||||||
mname,
|
mname,
|
||||||
&ordered,
|
&ordered,
|
||||||
&mut cursor,
|
&mut cursor,
|
||||||
@@ -338,6 +358,52 @@ fn poly_free_fn_names_for_module(
|
|||||||
out
|
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.
|
/// Iter 22b.3: workspace-wide `class-name -> ClassDef` index.
|
||||||
/// Used by the fixpoint to look up the matching class definition
|
/// Used by the fixpoint to look up the matching class definition
|
||||||
/// when synthesising a fn.
|
/// when synthesising a fn.
|
||||||
@@ -1005,6 +1071,7 @@ fn rewrite_mono_calls(
|
|||||||
body: &mut Term,
|
body: &mut Term,
|
||||||
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
|
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
|
||||||
poly_free_fns: &BTreeSet<String>,
|
poly_free_fns: &BTreeSet<String>,
|
||||||
|
poly_free_fn_ccounts: &BTreeMap<String, usize>,
|
||||||
caller_module: &str,
|
caller_module: &str,
|
||||||
ordered_targets: &[Option<MonoTarget>],
|
ordered_targets: &[Option<MonoTarget>],
|
||||||
cursor: &mut usize,
|
cursor: &mut usize,
|
||||||
@@ -1021,10 +1088,31 @@ fn rewrite_mono_calls(
|
|||||||
// `method_to_candidate_classes`. Class disambiguation
|
// `method_to_candidate_classes`. Class disambiguation
|
||||||
// (which class declared the resolved method) lives in the
|
// (which class declared the resolved method) lives in the
|
||||||
// residual slot the cursor consumes, not here.
|
// 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_class_method = method_to_candidate_classes.contains_key(name);
|
||||||
let is_poly_free_fn = !is_class_method && poly_free_fns.contains(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);
|
let should_advance = (is_class_method || is_poly_free_fn) && !locals.contains(name);
|
||||||
if should_advance {
|
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
|
// The outer `Some` means the cursor slot exists; the inner
|
||||||
// `Some` means the observation at that slot is concrete.
|
// `Some` means the observation at that slot is concrete.
|
||||||
// A `Some(None)` slot is a residual / non-concrete
|
// A `Some(None)` slot is a residual / non-concrete
|
||||||
@@ -1045,19 +1133,19 @@ fn rewrite_mono_calls(
|
|||||||
};
|
};
|
||||||
*name = new_name;
|
*name = new_name;
|
||||||
}
|
}
|
||||||
*cursor += 1;
|
*cursor += 1 + n_filler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::App { callee, args, .. } => {
|
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 {
|
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 } => {
|
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());
|
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 {
|
if inserted {
|
||||||
locals.remove(name);
|
locals.remove(name);
|
||||||
}
|
}
|
||||||
@@ -1070,32 +1158,32 @@ fn rewrite_mono_calls(
|
|||||||
params_inserted.push(p.clone());
|
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 {
|
for p in ¶ms_inserted {
|
||||||
locals.remove(p);
|
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 {
|
if name_inserted {
|
||||||
locals.remove(name);
|
locals.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::If { cond, then, else_ } => {
|
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(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, 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, 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, .. } => {
|
Term::Do { args, .. } => {
|
||||||
for a in 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, .. } => {
|
Term::Ctor { args, .. } => {
|
||||||
for a in 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 } => {
|
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 {
|
for Arm { pat, body } in arms {
|
||||||
let binders = pattern_binders(pat);
|
let binders = pattern_binders(pat);
|
||||||
let mut inserted: Vec<String> = Vec::new();
|
let mut inserted: Vec<String> = Vec::new();
|
||||||
@@ -1104,7 +1192,7 @@ fn rewrite_mono_calls(
|
|||||||
inserted.push(b.clone());
|
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 {
|
for b in &inserted {
|
||||||
locals.remove(b);
|
locals.remove(b);
|
||||||
}
|
}
|
||||||
@@ -1117,21 +1205,21 @@ fn rewrite_mono_calls(
|
|||||||
inserted.push(p.clone());
|
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 {
|
for p in &inserted {
|
||||||
locals.remove(p);
|
locals.remove(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::Seq { lhs, rhs } => {
|
Term::Seq { lhs, rhs } => {
|
||||||
rewrite_mono_calls(lhs, 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, 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 } => {
|
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 } => {
|
Term::ReuseAs { source, body } => {
|
||||||
rewrite_mono_calls(source, 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, 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 { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
}
|
}
|
||||||
@@ -1190,6 +1278,7 @@ pub(crate) fn collect_residuals_ordered(
|
|||||||
module_name: &str,
|
module_name: &str,
|
||||||
env: &crate::Env,
|
env: &crate::Env,
|
||||||
poly_free_fns: &BTreeSet<String>,
|
poly_free_fns: &BTreeSet<String>,
|
||||||
|
poly_free_fn_ccounts: &BTreeMap<String, usize>,
|
||||||
) -> Result<Vec<Option<MonoTarget>>> {
|
) -> Result<Vec<Option<MonoTarget>>> {
|
||||||
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
|
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
|
||||||
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
|
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
|
||||||
@@ -1335,6 +1424,7 @@ pub(crate) fn collect_residuals_ordered(
|
|||||||
&f.body,
|
&f.body,
|
||||||
&env.method_to_candidate_classes,
|
&env.method_to_candidate_classes,
|
||||||
poly_free_fns,
|
poly_free_fns,
|
||||||
|
poly_free_fn_ccounts,
|
||||||
&class_slots,
|
&class_slots,
|
||||||
&free_fn_slots,
|
&free_fn_slots,
|
||||||
&mut class_cur,
|
&mut class_cur,
|
||||||
@@ -1360,6 +1450,7 @@ fn interleave_slots(
|
|||||||
term: &Term,
|
term: &Term,
|
||||||
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
|
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
|
||||||
poly_free_fns: &BTreeSet<String>,
|
poly_free_fns: &BTreeSet<String>,
|
||||||
|
poly_free_fn_ccounts: &BTreeMap<String, usize>,
|
||||||
class_slots: &[Option<MonoTarget>],
|
class_slots: &[Option<MonoTarget>],
|
||||||
free_fn_slots: &[Option<MonoTarget>],
|
free_fn_slots: &[Option<MonoTarget>],
|
||||||
class_cur: &mut usize,
|
class_cur: &mut usize,
|
||||||
@@ -1378,22 +1469,39 @@ fn interleave_slots(
|
|||||||
out.push(slot);
|
out.push(slot);
|
||||||
*class_cur += 1;
|
*class_cur += 1;
|
||||||
} else {
|
} 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);
|
let slot = free_fn_slots.get(*free_cur).cloned().unwrap_or(None);
|
||||||
out.push(slot);
|
out.push(slot);
|
||||||
*free_cur += 1;
|
*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, .. } => {
|
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 {
|
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 } => {
|
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());
|
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 {
|
if inserted {
|
||||||
locals.remove(name);
|
locals.remove(name);
|
||||||
}
|
}
|
||||||
@@ -1406,32 +1514,32 @@ fn interleave_slots(
|
|||||||
params_inserted.push(p.clone());
|
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 {
|
for p in ¶ms_inserted {
|
||||||
locals.remove(p);
|
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 {
|
if name_inserted {
|
||||||
locals.remove(name);
|
locals.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::If { cond, then, else_ } => {
|
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(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, 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, 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, .. } => {
|
Term::Do { args, .. } => {
|
||||||
for a in 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, .. } => {
|
Term::Ctor { args, .. } => {
|
||||||
for a in 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 } => {
|
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 {
|
for Arm { pat, body } in arms {
|
||||||
let binders = pattern_binders(pat);
|
let binders = pattern_binders(pat);
|
||||||
let mut inserted: Vec<String> = Vec::new();
|
let mut inserted: Vec<String> = Vec::new();
|
||||||
@@ -1440,7 +1548,7 @@ fn interleave_slots(
|
|||||||
inserted.push(b.clone());
|
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 {
|
for b in &inserted {
|
||||||
locals.remove(b);
|
locals.remove(b);
|
||||||
}
|
}
|
||||||
@@ -1453,21 +1561,21 @@ fn interleave_slots(
|
|||||||
inserted.push(p.clone());
|
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 {
|
for p in &inserted {
|
||||||
locals.remove(p);
|
locals.remove(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Term::Seq { lhs, rhs } => {
|
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(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, 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 } => {
|
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 } => {
|
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(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, 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 { .. } => {}
|
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
|
||||||
@@ -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)))))
|
||||||
Reference in New Issue
Block a user