Skip to content

Commit e367145

Browse files
timfennisclaude
andcommitted
fix(analyser): pin vec call to exact-subtype scalar overload when one exists 🎯
The vec path only used find_function_candidates (loose compatibility), so calls like `Tuple<Int, Int> - Tuple<Int, Int>` collected both `-(Int, Int)` and `-(Number, Number)` as candidates and the analyser LUB'd them into `Tuple<Number, Number>`. Scalar dispatch avoids this by trying find_function (exact subtype match) first. Mirror that step for vec: track `vec_exact` across the scope walk, and when no scalar competes, prefer the exact-subtype match over loose compatibles. Restores the precision the user expected. The Any-args case (no scalar accepts Any) still falls to Dynamic with multiple vec candidates and LUBs across their returns — that's the "interesting" Tuple<Number, Number> behaviour the user signed off on. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent df7b11b commit e367145

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

ndc_analyser/src/scope.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ impl ScopeTree {
417417
let mut env_scopes: Vec<usize> = Vec::default();
418418
let mut loose_candidates: Option<Vec<ResolvedVar>> = None;
419419
let mut loose_vec_candidates: Option<Vec<ResolvedVar>> = None;
420+
let mut vec_exact: Option<ResolvedVar> = None;
420421
let mut all_by_name: Vec<ResolvedVar> = Vec::new();
421422

422423
loop {
@@ -429,6 +430,16 @@ impl ScopeTree {
429430
)));
430431
}
431432

433+
// 1b. Vec exact subtype match — record but keep walking, because a
434+
// scalar exact match in an outer scope still wins. First scope
435+
// to produce one takes priority (same shadowing rule as scalar).
436+
if vec_exact.is_none()
437+
&& let Some(synthetic) = &vec_sig
438+
&& let Some(slot) = self.scopes[scope_ptr].find_function(ident, synthetic)
439+
{
440+
vec_exact = Some(self.resolve_found_local(ident, slot, &env_scopes));
441+
}
442+
432443
// 2. Upvalues with matching name — collect as candidates but continue
433444
// walking, because the upvalue may be a different overload (e.g.
434445
// different arity) and the exact match could be in a parent scope.
@@ -496,6 +507,13 @@ impl ScopeTree {
496507
}
497508
}
498509

510+
if vec_exact.is_none()
511+
&& let Some(synthetic) = &vec_sig
512+
&& let Some(slot) = self.global_scope.find_function(ident, synthetic)
513+
{
514+
vec_exact = Some(ResolvedVar::Global { slot });
515+
}
516+
499517
if loose_vec_candidates.is_none()
500518
&& let Some(synthetic) = &vec_sig
501519
{
@@ -524,9 +542,16 @@ impl ScopeTree {
524542
let scalars = loose_candidates.unwrap_or_default();
525543
let vecs = loose_vec_candidates.unwrap_or_default();
526544

527-
// Statically pinnable vec call: no scalar competes and a single vec match.
528-
if scalars.is_empty() && vecs.len() == 1 {
529-
return Binding::Resolved(Candidate::vec(vecs[0]));
545+
// Statically pinnable vec call: no scalar competes, and either the
546+
// synthetic sig has an exact subtype match (mirroring scalar's
547+
// find_function precedence) or only one loose vec candidate remains.
548+
if scalars.is_empty() {
549+
if let Some(exact) = vec_exact {
550+
return Binding::Resolved(Candidate::vec(exact));
551+
}
552+
if vecs.len() == 1 {
553+
return Binding::Resolved(Candidate::vec(vecs[0]));
554+
}
530555
}
531556

532557
if !scalars.is_empty() || !vecs.is_empty() {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Vec dispatch should pick the most specific scalar overload by subtype
2+
// (mirroring scalar dispatch's `find_function` precedence), not collapse
3+
// to the LUB of every compatible overload. `Tuple<Int, Int> - Tuple<Int,
4+
// Int>` must infer as `Tuple<Int, Int>`, not `Tuple<Number, Number>`.
5+
let a: Tuple<Int, Int> = (1, 2);
6+
let b: Tuple<Int, Int> = (3, 4);
7+
let c: Tuple<Int, Int> = a - b;
8+
let d: Tuple<Int, Int> = c * c;
9+
assert_eq(d, (4, 4));
10+
11+
// Chained: `+` keeps the precise element type through several operators.
12+
let e: Tuple<Int, Int> = a + b;
13+
let f: Tuple<Int, Int> = e + (10, 20);
14+
assert_eq(f, (14, 26));
15+
16+
// Any args fall to LUB (no scalar `-(Any, Any)` exists, so vec dispatch
17+
// can't pin a single overload at compile time): `Tuple<Any, Any> -
18+
// Tuple<Any, Any>` infers as `Tuple<Number, Number>`, the LUB across
19+
// every numeric overload's return type.
20+
let l: List<Int> = [1, 2];
21+
let p: Tuple<Any, Any> = (l.first, l.first);
22+
let q: Tuple<Any, Any> = (l.last, l.last);
23+
let r: Tuple<Number, Number> = p - q;
24+
let s: Tuple<Number, Number> = r * r;
25+
assert_eq(s, (1, 1));

0 commit comments

Comments
 (0)