Summary
When a for-loop variable is reassigned to a value of a different type, the analyser computes the LUB of the loop's element type and the new type and widens the binding. This loses the precise type the runtime actually holds and can block valid runtime-dispatchable code under stricter static analysis (or produce confusingly inferred types for downstream code).
This is a specific instance of #122 (flow-sensitive typing) but worth filing on its own because it surfaces in real user programs and the fix may be narrower than the full flow-typing project.
Repro
let lines: Sequence<String> = ["a b c", "d e f"];
for line in lines {
line = line.split(" "); // line was String, now List<String>
let first = line.remove(0); // error
}
Analyser error:
error[resolver]: No function called 'remove' found that matches the arguments 'Sequence<String>, Int'
At runtime, line after the reassignment really is a List<String> — remove(List, Int) would dispatch correctly. But the analyser has widened line to Sequence<String> (LUB(String, List<String>) = Sequence<String>), and the stdlib's remove is declared as (List<Any>, Int), which fails the strict subtype check.
Why this matters
In PR #141 the analyser's compatibility check became stricter — that didn't introduce the widening but it does mean the widened type is now load-bearing in more places. The repro above was an Advent-of-Brian 2025 day 11 solution that used to run; it now fails analysis.
Expected behaviour
After a reassignment that fully replaces the binding's value (i.e. not a compound update), the subsequent uses of the variable should see the new type, not the LUB. The temporal union "`line` is either a String or a List" doesn't correspond to any actual program state — by the time line.remove(0) runs, the reassignment has definitely happened.
Workarounds today
- Use a different variable name for the split result instead of shadowing the loop var.
- Cast/widen the call site explicitly.
Related
Summary
When a for-loop variable is reassigned to a value of a different type, the analyser computes the LUB of the loop's element type and the new type and widens the binding. This loses the precise type the runtime actually holds and can block valid runtime-dispatchable code under stricter static analysis (or produce confusingly inferred types for downstream code).
This is a specific instance of #122 (flow-sensitive typing) but worth filing on its own because it surfaces in real user programs and the fix may be narrower than the full flow-typing project.
Repro
Analyser error:
At runtime,
lineafter the reassignment really is aList<String>—remove(List, Int)would dispatch correctly. But the analyser has widenedlinetoSequence<String>(LUB(String, List<String>) = Sequence<String>), and the stdlib'sremoveis declared as(List<Any>, Int), which fails the strict subtype check.Why this matters
In PR #141 the analyser's compatibility check became stricter — that didn't introduce the widening but it does mean the widened type is now load-bearing in more places. The repro above was an Advent-of-Brian 2025 day 11 solution that used to run; it now fails analysis.
Expected behaviour
After a reassignment that fully replaces the binding's value (i.e. not a compound update), the subsequent uses of the variable should see the new type, not the LUB. The temporal union "`line` is either a String or a List" doesn't correspond to any actual program state — by the time
line.remove(0)runs, the reassignment has definitely happened.Workarounds today
Related