Fix: Unify TypeVars when indexing generic parameters twice

When a generic function indexes the same TypeVar parameter multiple
times,
all resulting TypeVars must share a single element type. This change
ensures
that subsequent indexing operations unify their fresh TypeVar with an
existing
one if the parameter has already been indexed, preventing incorrect type
inference and false negatives.

A new test, `test_let_poly_multi_index_all_results_typed`, is added to
verify this behavior.
This commit is contained in:
2026-03-28 23:03:38 +01:00
parent 84abbd9721
commit 9c434fb49d
2 changed files with 51 additions and 4 deletions
+14 -4
View File
@@ -1438,10 +1438,20 @@ impl TypeChecker {
&& matches!(&elems[0], StaticType::Int) && matches!(&elems[0], StaticType::Int)
); );
if is_index_call && matches!(ret_ty, StaticType::Any) { if is_index_call && matches!(ret_ty, StaticType::Any) {
let elem_var = self.fresh_var(); let existing = self.index_constraints.borrow().get(n).copied();
let StaticType::TypeVar(elem_id) = &elem_var else { unreachable!() }; if let Some(existing_ret_id) = existing {
self.index_constraints.borrow_mut().insert(*n, *elem_id); // Same TypeVar indexed again — all index results on the same
ret_ty = elem_var; // parameter must share one element TypeVar. Unify the fresh
// var with the existing one so they resolve together.
let elem_var = self.fresh_var();
self.unify(elem_var.clone(), StaticType::TypeVar(existing_ret_id), diag);
ret_ty = Self::apply_subst(elem_var, &self.subst.borrow());
} else {
let elem_var = self.fresh_var();
let StaticType::TypeVar(elem_id) = &elem_var else { unreachable!() };
self.index_constraints.borrow_mut().insert(*n, *elem_id);
ret_ty = elem_var;
}
} }
} }
+37
View File
@@ -190,6 +190,43 @@ fn test_let_poly_generic_return_used_in_field_series() {
} }
} }
/// Regression: when a generic function indexes the same TypeVar parameter twice —
/// `(s 0)` and `(s 1)` — both result TypeVars must share the same element type.
///
/// Before the fix, Step 9 overwrote `index_constraints[n]` on the second call,
/// leaving the first result TypeVar unbound (`Any`). This caused false negatives
/// when the first-call result was pushed into a typed series.
///
/// After the fix, the second call unifies its fresh TypeVar with the existing one
/// so all index results on the same parameter resolve together.
#[test]
fn test_let_poly_multi_index_all_results_typed() {
let env = Environment::new();
let source = r#"
(do
(def tricky (fn [s]
(do
(def a (s 0))
(def b (s 1))
a
)
))
(def texts (series 5))
(push texts "hello")
(def check (series 5))
(push check (tricky texts))
(push check 1.0)
)
"#;
let result = env.compile(source);
assert!(
result.diagnostics.has_errors(),
"Multi-index: (s 0) result must be Text, not Any; \
pushing Float into a Text-derived series must produce a type error: {}",
result.diagnostics.format_errors()
);
}
/// Regression test: passing a non-series callable to a generic `last` function /// Regression test: passing a non-series callable to a generic `last` function
/// must NOT produce a type error. Before the index-constraint fix, Step 9 would /// must NOT produce a type error. Before the index-constraint fix, Step 9 would
/// eagerly unify `TypeVar = Series(elem)`, making `last` Series-only and causing /// eagerly unify `TypeVar = Series(elem)`, making `last` Series-only and causing