Add tuple type recomputation after inlining

Introduce helper functions to recompute the `StaticType` of a tuple node
based on its children's types. This ensures that after inlining, tuples
within the AST have concrete types, rather than unresolved type
variables, even when derived from polymorphic functions.

Also adds tests to verify this behavior for both homogeneous and
heterogeneous tuples.
This commit is contained in:
2026-03-29 18:44:48 +02:00
parent 0e806b9e5d
commit 03862d50c6
2 changed files with 84 additions and 3 deletions
+51 -3
View File
@@ -1,8 +1,8 @@
use crate::ast::nodes::{
Address, AnalyzedNode, AssignBinding, GlobalAnalyzedRegistry,
IdentifierBinding, LambdaBinding, Node, NodeKind, UpvalueIdx, VirtualId,
IdentifierBinding, LambdaBinding, Node, NodeKind, NodeMetrics, UpvalueIdx, VirtualId,
};
use crate::ast::types::{Purity, Value};
use crate::ast::types::{Purity, StaticType, Value};
use crate::ast::vm::GlobalStore;
use std::cell::RefCell;
@@ -13,6 +13,48 @@ use super::inliner::Inliner;
use super::substitution_map::SubstitutionMap;
use super::utils::{collect_pattern_addrs, PathTracker, UsageInfo};
/// Recompute the `StaticType` for a Tuple node from its children's concrete types.
/// Mirrors the type-computation logic in `TypeChecker::check_node` for `NodeKind::Tuple`.
fn compute_tuple_type(elements: &[Rc<AnalyzedNode>]) -> StaticType {
if elements.is_empty() {
return StaticType::Vector(Box::new(StaticType::Any), 0);
}
let first_ty = &elements[0].ty.original.ty;
let all_same = elements.iter().all(|e| e.ty.original.ty == *first_ty);
if all_same {
match first_ty {
StaticType::Vector(inner, len) => {
StaticType::Matrix(inner.clone(), vec![elements.len(), *len])
}
StaticType::Matrix(inner, shape) => {
let mut new_shape = vec![elements.len()];
new_shape.extend(shape);
StaticType::Matrix(inner.clone(), new_shape)
}
_ => StaticType::Vector(Box::new(first_ty.clone()), elements.len()),
}
} else {
StaticType::Tuple(elements.iter().map(|e| e.ty.original.ty.clone()).collect())
}
}
/// Build a new `NodeMetrics` with an updated `StaticType`, reusing the original
/// node's identity, kind, and comments. Only call when the type actually changed.
fn updated_metrics(old: &NodeMetrics, new_ty: StaticType) -> NodeMetrics {
let orig = &old.original;
NodeMetrics {
original: Rc::new(Node {
identity: orig.identity.clone(),
kind: orig.kind.clone(),
ty: new_ty,
comments: orig.comments.clone(),
}),
purity: old.purity,
is_recursive: old.is_recursive,
}
}
pub struct Optimizer {
pub enabled: bool,
max_passes: usize,
@@ -663,7 +705,13 @@ impl Optimizer {
if !changed {
return node_rc;
}
(NodeKind::Tuple { elements: new_elements }, node.ty.clone())
let recomputed = compute_tuple_type(&new_elements);
let metrics = if recomputed != node.ty.original.ty {
updated_metrics(&node.ty, recomputed)
} else {
node.ty.clone()
};
(NodeKind::Tuple { elements: new_elements }, metrics)
}
NodeKind::Record { fields, layout } => {
let mut mapped_fields = Vec::with_capacity(fields.len());
+33
View File
@@ -216,3 +216,36 @@ fn test_slot_reuse_non_overlapping_scopes() {
dump
);
}
// ── Tuple type recomputation after inlining ─────────────────────────────────
#[test]
fn test_inlined_tuple_has_concrete_type_homogeneous() {
// A polymorphic function returning a tuple, called with all-Int args.
// After inlining, the tuple type must be concrete (no unresolved TypeVars).
let env = Environment::new();
let source = r#"(do
(def wrap (fn [x y z] [x y z]))
(wrap 1 2 3)
)"#;
let dump = env.dump_ast_compact(source).unwrap();
assert!(
!dump.contains('?'),
"Inlined tuple must not contain unresolved TypeVars. Dump:\n{dump}"
);
}
#[test]
fn test_inlined_tuple_has_concrete_type_heterogeneous() {
// Heterogeneous case: Int + String should produce Tuple([int, str]), not ?0/?1.
let env = Environment::new();
let source = r#"(do
(def wrap (fn [x y] [x y]))
(wrap 1 "hello")
)"#;
let dump = env.dump_ast_compact(source).unwrap();
assert!(
!dump.contains('?'),
"Inlined tuple must not contain unresolved TypeVars. Dump:\n{dump}"
);
}