diff --git a/src/ast/compiler/optimizer/engine.rs b/src/ast/compiler/optimizer/engine.rs index a8320d6..145b4f4 100644 --- a/src/ast/compiler/optimizer/engine.rs +++ b/src/ast/compiler/optimizer/engine.rs @@ -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]) -> 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()); diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 48b18ab..c65b72e 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -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}" + ); +}