Update record type computation

This commit refactors the way record types are computed within the
optimizer. Previously, the optimizer would often reuse the existing
`NodeMetrics` for records, which could lead to stale or polymorphic
types.

The changes introduce a new function, `compute_record_type`, which
explicitly recalculates the `StaticType` for a `Record` node based on
the concrete types of its child fields. This ensures that after
optimizations like inlining and folding, record types are as concrete as
possible, eliminating unresolved `TypeVars`.

Additionally, the `folder.rs` module is updated to recompute the
`RecordLayout` when creating constant record values, further improving
type accuracy. Two new tests are added to verify that inlined records,
including those with multiple fields, correctly result in concrete
types.
This commit is contained in:
2026-03-29 18:59:38 +02:00
parent 6c8e2df45c
commit 1f3fb40bcc
3 changed files with 64 additions and 3 deletions
+21 -2
View File
@@ -2,7 +2,8 @@ use crate::ast::nodes::{
Address, AnalyzedNode, AssignBinding, GlobalAnalyzedRegistry, Address, AnalyzedNode, AssignBinding, GlobalAnalyzedRegistry,
IdentifierBinding, LambdaBinding, Node, NodeKind, NodeMetrics, UpvalueIdx, VirtualId, IdentifierBinding, LambdaBinding, Node, NodeKind, NodeMetrics, UpvalueIdx, VirtualId,
}; };
use crate::ast::types::{Purity, Signature, StaticType, Value}; use crate::ast::types::{Purity, RecordLayout, Signature, StaticType, Value};
use std::sync::Arc;
use crate::ast::vm::GlobalStore; use crate::ast::vm::GlobalStore;
use std::cell::RefCell; use std::cell::RefCell;
@@ -39,6 +40,23 @@ fn compute_tuple_type(elements: &[Rc<AnalyzedNode>]) -> StaticType {
} }
} }
/// Recompute the `StaticType` for a Record node from its children's concrete types.
/// Mirrors the type-computation logic in `TypeChecker::check_node` for `NodeKind::Record`.
fn compute_record_type(
layout: &Arc<RecordLayout>,
fields: &[(Rc<AnalyzedNode>, Rc<AnalyzedNode>)],
) -> StaticType {
let new_fields: Vec<_> = layout
.fields
.iter()
.zip(fields.iter())
.map(|((keyword, _old_ty), (_key_node, val_node))| {
(*keyword, val_node.ty.original.ty.clone())
})
.collect();
StaticType::Record(RecordLayout::get_or_create(new_fields))
}
/// Return `NodeMetrics` with an updated `StaticType` if it differs from the /// Return `NodeMetrics` with an updated `StaticType` if it differs from the
/// original, otherwise clone cheaply. Avoids unnecessary `Rc` allocations. /// original, otherwise clone cheaply. Avoids unnecessary `Rc` allocations.
fn refreshed_metrics(old: &NodeMetrics, new_ty: StaticType) -> NodeMetrics { fn refreshed_metrics(old: &NodeMetrics, new_ty: StaticType) -> NodeMetrics {
@@ -745,12 +763,13 @@ impl Optimizer {
return node_rc; return node_rc;
} }
let metrics = refreshed_metrics(&node.ty, compute_record_type(layout, &mapped_fields));
( (
NodeKind::Record { NodeKind::Record {
fields: mapped_fields, fields: mapped_fields,
layout: layout.clone(), layout: layout.clone(),
}, },
node.ty.clone(), metrics,
) )
} }
NodeKind::Expansion { NodeKind::Expansion {
+10 -1
View File
@@ -69,7 +69,16 @@ impl<'a> Folder<'a> {
} }
} }
let record_val = Value::Record(layout.clone(), Rc::new(constant_values)); // Recompute layout from concrete value types to eliminate stale TypeVars.
let new_fields: Vec<_> = layout
.fields
.iter()
.zip(values.iter())
.map(|((keyword, _), v_node)| (*keyword, v_node.ty.original.ty.clone()))
.collect();
let new_layout = RecordLayout::get_or_create(new_fields);
let record_val = Value::Record(new_layout, Rc::new(constant_values));
Some(self.make_constant_node(record_val, template)) Some(self.make_constant_node(record_val, template))
} }
+33
View File
@@ -277,3 +277,36 @@ fn test_block_and_lambda_propagate_concrete_type_after_inlining() {
"Block type must be concrete. Line:\n{block_line}" "Block type must be concrete. Line:\n{block_line}"
); );
} }
// ── Record type recomputation after inlining ────────────────────────────────
#[test]
fn test_inlined_record_has_concrete_type() {
// A polymorphic function returning a record, called with a concrete arg.
// After inlining + folding, the record type must be concrete (no TypeVars).
let env = Environment::new();
let source = r#"(do
(def wrap (fn [x] {:val x}))
(wrap 42)
)"#;
let dump = env.dump_ast_compact(source).unwrap();
assert!(
!dump.contains('?'),
"Inlined record must not contain unresolved TypeVars. Dump:\n{dump}"
);
}
#[test]
fn test_inlined_record_has_concrete_type_multi_field() {
// Heterogeneous case: Int + String fields should produce concrete record type.
let env = Environment::new();
let source = r#"(do
(def mkr (fn [a b] {:x a :y b}))
(mkr 1 "hello")
)"#;
let dump = env.dump_ast_compact(source).unwrap();
assert!(
!dump.contains('?'),
"Inlined multi-field record must not contain unresolved TypeVars. Dump:\n{dump}"
);
}