diff --git a/ndc_analyser/src/analyser.rs b/ndc_analyser/src/analyser.rs index e77d0394..626e8d94 100644 --- a/ndc_analyser/src/analyser.rs +++ b/ndc_analyser/src/analyser.rs @@ -510,20 +510,18 @@ impl Analyser { } Binding::Resolved(res) => self.scope_tree.get_type(*res).clone(), - Binding::Dynamic(candidates) => { - let return_type = candidates - .iter() - .map(|c| self.scope_tree.get_type(*c).clone()) - .filter_map(|t| match t { - StaticType::Function { return_type, .. } => Some(*return_type), - _ => None, - }) - .reduce(|a, b| a.lub(&b)) - .unwrap_or(StaticType::Any); - + Binding::Dynamic(_) => { + // Dispatch is decided at runtime, so we have no sound static bound + // on the result. The runtime may pick a declared overload or fall + // through to elementwise (vectorized) dispatch, which can produce + // a value no declared overload returns — treating the LUB of + // declared returns as the result type is unsound and led to issue + // #139, where `let diff = a - b` over tuples was inferred as + // `Number` and a follow-up `diff * diff` then matched the numeric + // overload directly and bypassed dynamic dispatch entirely. StaticType::Function { parameters: None, - return_type: Box::new(return_type), + return_type: Box::new(StaticType::Any), } } }; diff --git a/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc b/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc new file mode 100644 index 00000000..dc116e84 --- /dev/null +++ b/tests/functional/programs/900_bugs/bug0021_chained_vectorized_tuple_arith.ndc @@ -0,0 +1,29 @@ +// BUG#0021 (issue #139): vectorized tuple arithmetic broke when the result +// of one tuple op was fed into another. The analyser inferred the result of +// `a - b` as `Number` (the LUB of the dynamic candidates' return types), so +// `diff * diff` resolved statically to a `(Number, Number) -> Number` +// overload via `Binding::Resolved` and bypassed OverloadSet dispatch — and +// therefore the VM's vectorized fallback. The native then bailed with +// "expected number, got Tuple". +let a = (1, 2, 3); +let b = (4, 5, 6); + +assert_eq(a * b, (4, 10, 18)); + +let diff = a - b; +assert_eq(diff, (-3, -3, -3)); +assert_eq(diff * diff, (9, 9, 9)); + +assert_eq((a - b) * (a - b), (9, 9, 9)); + +// Same hazard but the tuple elements are statically `Any`. `id` here erases +// type information so the tuples below are `Tuple`; this +// mirrors what happens with stdlib natives whose `Value` return type infers +// to `Any`. The vectorization detection must treat `Any` as a potentially- +// numeric element so chained ops still go through dynamic dispatch. +fn id(x) -> Any => x; +let av = (id(1), id(2), id(3)); +let bv = (id(4), id(5), id(6)); +let cv = av - bv; +assert_eq(cv, (-3, -3, -3)); +assert_eq(cv * cv, (9, 9, 9));