diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index d713c0c..4608842 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -1438,10 +1438,20 @@ impl TypeChecker { && matches!(&elems[0], StaticType::Int) ); if is_index_call && matches!(ret_ty, StaticType::Any) { - 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; + let existing = self.index_constraints.borrow().get(n).copied(); + if let Some(existing_ret_id) = existing { + // Same TypeVar indexed again — all index results on the same + // 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; + } } } diff --git a/tests/hm_poly.rs b/tests/hm_poly.rs index c2bffac..341c725 100644 --- a/tests/hm_poly.rs +++ b/tests/hm_poly.rs @@ -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 /// 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