From 8b019a712161a184ded8d13836566edfbd24cde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 13:13:36 +0200 Subject: [PATCH 1/2] test(codegen): exercise NBC order and numeric field intersection --- .../perry-codegen/src/collectors/hir_facts.rs | 5 +- .../src/collectors/number_by_construction.rs | 91 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 75723fbeae..1cb8f54deb 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -740,7 +740,10 @@ pub(crate) fn collect_type_facts( // `shape_proven_ptr_locals` yields empty inputs below and the fixpoint then // computes exactly what it computed before. let (nbc_shape_members, nbc_shape_numeric_fields) = - super::number_by_construction::shape_numeric_inputs(&shape_proven_ptr_locals); + super::number_by_construction::shape_numeric_inputs( + &shape_proven_ptr_locals, + super::number_by_construction::nbc_order_enabled(), + ); let number_by_construction_locals = super::collect_number_by_construction_locals( stmts, params, diff --git a/crates/perry-codegen/src/collectors/number_by_construction.rs b/crates/perry-codegen/src/collectors/number_by_construction.rs index ad5651d9c4..cefcb4bf0f 100644 --- a/crates/perry-codegen/src/collectors/number_by_construction.rs +++ b/crates/perry-codegen/src/collectors/number_by_construction.rs @@ -790,10 +790,13 @@ pub(crate) fn nbc_order_enabled() -> bool { /// /// A union would be a WRONG ANSWER, not a weaker one: `a` numeric on `C` and /// not on `D` would license a bare `fadd` on `D.a`. +/// The gate is passed in so both modes can be tested without changing the +/// process environment shared by parallel unit tests. pub(crate) fn shape_numeric_inputs( shape_proven: &HashMap, + enabled: bool, ) -> (HashSet, HashSet) { - if !nbc_order_enabled() || shape_proven.is_empty() { + if !enabled || shape_proven.is_empty() { return (HashSet::new(), HashSet::new()); } let mut members: HashSet = HashSet::new(); @@ -814,3 +817,89 @@ pub(crate) fn shape_numeric_inputs( } (members, fields) } + +#[cfg(test)] +mod shape_input_tests { + use super::*; + use crate::collectors::ptr_shape::PtrShapeLocal; + + fn property_local(id: u32, receiver: u32, property: &str) -> Stmt { + Stmt::Let { + id, + name: format!("value_{id}"), + ty: HirType::Any, + mutable: false, + init: Some(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(receiver)), + property: property.to_string(), + byte_offset: 0, + }), + } + } + + fn numeric_locals( + stmts: &[Stmt], + members: &HashSet, + fields: &HashSet, + ) -> HashSet { + super::super::ptr_shape::collect_numeric_by_construction_locals_for_type_analysis( + stmts, + &HashSet::new(), + &HashMap::new(), + &HashSet::new(), + &HashMap::new(), + &HashSet::new(), + members, + fields, + ) + } + + #[test] + fn nbc_order_intersects_numeric_fields_before_proving_property_locals() { + let shape_proven = HashMap::from([ + ( + 10, + PtrShapeLocal { + class_name: "First".to_string(), + numeric_fields: HashSet::from(["shared".to_string(), "first_only".to_string()]), + report_name: None, + }, + ), + ( + 11, + PtrShapeLocal { + class_name: "Second".to_string(), + numeric_fields: HashSet::from(["shared".to_string()]), + report_name: None, + }, + ), + ]); + let stmts = [ + property_local(20, 10, "shared"), + property_local(21, 11, "shared"), + property_local(22, 11, "first_only"), + property_local(23, 10, "first_only"), + property_local(24, 12, "shared"), + ]; + + let (members, fields) = shape_numeric_inputs(&shape_proven, true); + let numeric = numeric_locals(&stmts, &members, &fields); + assert!(numeric.contains(&20), "shared field on First is numeric"); + assert!(numeric.contains(&21), "shared field on Second is numeric"); + assert!( + !numeric.contains(&22), + "union would unsoundly admit Second.first_only" + ); + assert!( + !numeric.contains(&23), + "function-wide inputs must be safe for both receivers" + ); + assert!( + !numeric.contains(&24), + "an unproven receiver is not a numeric input" + ); + + let (off_members, off_fields) = shape_numeric_inputs(&shape_proven, false); + assert!(numeric_locals(&stmts, &off_members, &off_fields).is_empty()); + } +} From 2a7d7134bbb0c5eeccd8a7289d317e038d510da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 22 Sep 2026 13:14:01 +0200 Subject: [PATCH 2/2] docs(changelog): record NBC order test coverage --- changelog.d/10964-nbc-order-test-coverage.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10964-nbc-order-test-coverage.md diff --git a/changelog.d/10964-nbc-order-test-coverage.md b/changelog.d/10964-nbc-order-test-coverage.md new file mode 100644 index 0000000000..bdc67da575 --- /dev/null +++ b/changelog.d/10964-nbc-order-test-coverage.md @@ -0,0 +1 @@ +- Cover the enabled `PERRY_L14_NBC_ORDER` path with a two-receiver numeric-field intersection test, while keeping the disabled path independently verified.