Iter 15g-aux — symmetric $u early-return in unify_for_subst
Fix the codegen asymmetry that 15g surfaced. unify_for_subst had an arg-side-only early-return for $u-prefixed synth wildcards. The function's prev-binding recursion can swap a $u from arg into param position when re-unifying a previously-bound type against a fresh arg. Reduced repro: `length [Left 1, Right 10]` — `a` first binds to `Either<Int, $u>`, then the recursive unification against `Either<$u, Int>` lands $u in param-pos[1] and falls through to the catch-all error. Fix is three lines: add a symmetric $u early-return for param side. $u is a synth-only wildcard regardless of which side it ends up on after the prev-binding swap. Doc comment expanded to record the origin and justification. std_either_list_demo refactored: mkleft/mkright workaround helpers removed; the list is now constructed inline by mixing (term-ctor std_either.Either Left 1) and (... Right 10) directly. Same expected output (2, 3, 2, 3); the demo doubles as the 15g-aux regression fixture. Tests: 94/94, unchanged. The fix expanded what compiles without changing observable behaviour for any prior fixture.
This commit is contained in:
@@ -2687,18 +2687,36 @@ fn unify_for_subst(
|
||||
vars: &BTreeSet<&str>,
|
||||
subst: &mut BTreeMap<String, Type>,
|
||||
) -> Result<()> {
|
||||
// Iter 14a fix: an arg-side `$u`-prefixed var is a synth-only
|
||||
// wildcard produced by `synth_arg_type` for nullary ctors of a
|
||||
// parameterised ADT (e.g. `Nil : List<$u>`). It carries no real
|
||||
// constraint — accept without binding so a sibling arg can pin
|
||||
// the type var instead. Without this, `Cons(Int, Nil)` synth
|
||||
// would unify `a = Int` (from head) and then `a = $u` (from
|
||||
// tail's recursive `List<a>` slot) and falsely error.
|
||||
// Iter 14a fix, extended in 15g-aux: a `$u`-prefixed var is a
|
||||
// synth-only wildcard produced by `synth_arg_type` for nullary
|
||||
// ctors of a parameterised ADT (e.g. `Nil : List<$u>`). It
|
||||
// carries no real constraint — accept without binding so a
|
||||
// sibling arg can pin the type var instead. Without this,
|
||||
// `Cons(Int, Nil)` synth would unify `a = Int` (from head) and
|
||||
// then `a = $u` (from tail's recursive `List<a>` slot) and
|
||||
// falsely error.
|
||||
//
|
||||
// 15g-aux: the early-return must accept `$u` on **either** side.
|
||||
// `$u` enters in arg position from synth, but the prev-binding
|
||||
// recursion below (`unify_for_subst(&prev, arg, ...)`) can swap
|
||||
// a `$u` onto the param side when a previously-bound type is
|
||||
// unified against a fresher arg whose roles differ. Reduced
|
||||
// repro: `length [Left 1, Right 10]` — `a` first binds to
|
||||
// `Either<Int, $u>` from `Left 1`, then a recursive unification
|
||||
// against `Either<$u, Int>` from `Right 10` lands `$u` in the
|
||||
// param-pos[1] slot. Symmetric early-return is correct because
|
||||
// `$u` is a synth-only wildcard regardless of which side carries
|
||||
// it after the prev-binding swap.
|
||||
if let Type::Var { name } = arg {
|
||||
if name.starts_with("$u") {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Type::Var { name } = param {
|
||||
if name.starts_with("$u") {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
match (param, arg) {
|
||||
(Type::Var { name }, _) if vars.contains(name.as_str()) => {
|
||||
if let Some(prev) = subst.get(name).cloned() {
|
||||
|
||||
Reference in New Issue
Block a user