From 794302b6759ff8efe641087bb690e6f5a3d84a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 14:44:17 +0200 Subject: [PATCH] fix(transform): keep shape barriers in source modules Cross-module helper localization made source-local delete and reflection barriers look module-wide in importers, disabling direct argument-shape routes for unrelated fresh locals. Reject barrier-bearing helper graphs at the relocation boundary while retaining ordinary containment checks on the outlined call. --- .../9284-cross-module-shape-barriers.md | 5 ++ .../src/collectors/ptr_shape_entry.rs | 31 +---------- crates/perry-hir/src/analysis.rs | 33 ++++++++++++ crates/perry-hir/src/lib.rs | 4 +- .../src/inline/cross_module.rs | 16 ++++++ crates/perry-transform/src/inline/mod.rs | 52 +++++++++++++++++++ 6 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 changelog.d/9284-cross-module-shape-barriers.md diff --git a/changelog.d/9284-cross-module-shape-barriers.md b/changelog.d/9284-cross-module-shape-barriers.md new file mode 100644 index 0000000000..60794e9e90 --- /dev/null +++ b/changelog.d/9284-cross-module-shape-barriers.md @@ -0,0 +1,5 @@ +### Fixed + +- Cross-module function localization now leaves object-shape barriers in their + source modules, preventing unrelated safe argument-shape clones in importers + from falling back to generic dispatch. diff --git a/crates/perry-codegen/src/collectors/ptr_shape_entry.rs b/crates/perry-codegen/src/collectors/ptr_shape_entry.rs index 188b2c844f..62aa9af318 100644 --- a/crates/perry-codegen/src/collectors/ptr_shape_entry.rs +++ b/crates/perry-codegen/src/collectors/ptr_shape_entry.rs @@ -2,36 +2,7 @@ use super::*; -/// Whether an expression node is a §5.2 shape barrier for the module-wide -/// first-increment kill rule. Targets are NOT inspected — any occurrence -/// disables all `Ptr` promotion in the module. -pub(crate) fn expr_is_shape_barrier(expr: &Expr) -> bool { - match expr { - Expr::ObjectDefineProperty(..) - | Expr::ObjectDefineProperties(..) - | Expr::ReflectDefineProperty { .. } - | Expr::ObjectSetPrototypeOf(..) - | Expr::ReflectSetPrototypeOf { .. } - | Expr::ReflectSet { .. } - | Expr::ReflectDelete { .. } - | Expr::ReflectPreventExtensions(..) - | Expr::Delete(..) - | Expr::ProxyNew { .. } => true, - // `__proto__` writes mutate the prototype chain of an arbitrary - // object. Reads and class-prototype naming are handled by the - // dispatch-stability facts; only writes are shape barriers. - Expr::PropertySet { property, .. } | Expr::PropertyUpdate { property, .. } => { - property == "__proto__" - } - Expr::PutValueSet { key, .. } => { - matches!(key.as_ref(), Expr::String(k) if k == "__proto__") - } - Expr::IndexSet { index, .. } => { - matches!(index.as_ref(), Expr::String(k) if k == "__proto__") - } - _ => false, - } -} +pub(crate) use perry_hir::expr_is_shape_barrier; /// Entry point: collect the shape-proven pointer locals of one lowered region. /// `not_bigint_locals` feeds the numeric-field proof. diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 457562406a..49a6ffa300 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -33,6 +33,39 @@ pub fn body_reads_dynamic_this(stmts: &[Stmt]) -> bool { stmts.iter().any(uses_this_stmt) } +/// Whether an expression node can invalidate module-wide object-shape proofs. +/// +/// Targets are deliberately not inspected. Representation selection treats any +/// occurrence as a conservative shape barrier, and transforms that relocate an +/// expression across modules must preserve that module attribution. +pub fn expr_is_shape_barrier(expr: &Expr) -> bool { + match expr { + Expr::ObjectDefineProperty(..) + | Expr::ObjectDefineProperties(..) + | Expr::ReflectDefineProperty { .. } + | Expr::ObjectSetPrototypeOf(..) + | Expr::ReflectSetPrototypeOf { .. } + | Expr::ReflectSet { .. } + | Expr::ReflectDelete { .. } + | Expr::ReflectPreventExtensions(..) + | Expr::Delete(..) + | Expr::ProxyNew { .. } => true, + // `__proto__` writes mutate the prototype chain of an arbitrary + // object. Reads and class-prototype naming are handled by the + // dispatch-stability facts; only writes are shape barriers. + Expr::PropertySet { property, .. } | Expr::PropertyUpdate { property, .. } => { + property == "__proto__" + } + Expr::PutValueSet { key, .. } => { + matches!(key.as_ref(), Expr::String(k) if k == "__proto__") + } + Expr::IndexSet { index, .. } => { + matches!(index.as_ref(), Expr::String(k) if k == "__proto__") + } + _ => false, + } +} + /// Collect every `LocalId` referenced by `expr` (and its sub-expressions). /// /// Per-variant work focuses on the LocalId-bearing variants (LocalGet, diff --git a/crates/perry-hir/src/lib.rs b/crates/perry-hir/src/lib.rs index b2279eb7dd..4473b46668 100644 --- a/crates/perry-hir/src/lib.rs +++ b/crates/perry-hir/src/lib.rs @@ -31,8 +31,8 @@ pub mod types; pub mod walker; pub use analysis::{ - collect_local_refs_expr, collect_local_refs_stmt, infer_expr_type, infer_refinable_expr_type, - HirTypeEnv, HirTypeFacts, + collect_local_refs_expr, collect_local_refs_stmt, expr_is_shape_barrier, infer_expr_type, + infer_refinable_expr_type, HirTypeEnv, HirTypeFacts, }; pub use audit::{audit_module, AuditManifest, ModuleAudit}; pub use capability::{audit_module_capabilities, CapabilityPolicy, CapabilityViolation}; diff --git a/crates/perry-transform/src/inline/cross_module.rs b/crates/perry-transform/src/inline/cross_module.rs index ad9361f045..2027d24b3f 100644 --- a/crates/perry-transform/src/inline/cross_module.rs +++ b/crates/perry-transform/src/inline/cross_module.rs @@ -7,6 +7,12 @@ use super::*; pub fn is_cross_module_safe(body: &[Stmt]) -> bool { fn check_expr(expr: &Expr) -> bool { + // Shape barriers are attributed module-wide during representation + // selection. Relocating one would conservatively poison unrelated + // shape proofs in the importer, so leave the source call outlined. + if perry_hir::expr_is_shape_barrier(expr) { + return false; + } match expr { // The disqualifying variants — anything tied to a particular // module's symbol table. @@ -345,6 +351,13 @@ fn cross_function_expr_is_safe( allowed_ids: &HashSet, extern_names: &mut Vec, ) -> bool { + // Keep module-wide shape barriers attributed to their source module. + // Argument containment already rejects values that escape through the + // remaining external call; moving the whole helper graph would instead + // disable shape proofs for unrelated locals in every importer. + if perry_hir::expr_is_shape_barrier(expr) { + return false; + } match expr { Expr::FuncRef(id) => allowed_ids.contains(id), Expr::ExternFuncRef { name, .. } => { @@ -1131,6 +1144,9 @@ pub fn gather_cross_module_methods_with_extern_imports( /// rules for FuncRef / GlobalGet / NativeModuleRef / Closure. pub fn is_cross_module_safe_with_externs(body: &[Stmt], extern_names: &mut Vec) -> bool { fn check_expr(expr: &Expr, extern_names: &mut Vec) -> bool { + if perry_hir::expr_is_shape_barrier(expr) { + return false; + } match expr { Expr::FuncRef(_) | Expr::GlobalGet(_) diff --git a/crates/perry-transform/src/inline/mod.rs b/crates/perry-transform/src/inline/mod.rs index b9c4bf9817..17b9be28f4 100644 --- a/crates/perry-transform/src/inline/mod.rs +++ b/crates/perry-transform/src/inline/mod.rs @@ -1243,6 +1243,58 @@ mod tests { assert!(gather_cross_module_functions(&source).is_empty()); } + #[test] + fn cross_module_free_function_graph_with_shape_barrier_is_rejected() { + let mut source = Module::new("/src/reshape.ts"); + let mut helper = function( + 1, + vec![Stmt::Return(Some(Expr::Delete(Box::new( + Expr::PropertyGet { + object: Box::new(Expr::LocalGet(10)), + property: "removed".to_string(), + byte_offset: 0, + }, + ))))], + ); + helper.params.push(Param { + id: 10, + name: "value".to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }); + let mut root = function( + 2, + vec![Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::FuncRef(1)), + args: vec![Expr::Undefined], + type_args: Vec::new(), + byte_offset: 0, + }))], + ); + root.name = "reshape".to_string(); + root.is_exported = true; + source.functions.extend([helper, root]); + source.exported_functions.push(("reshape".to_string(), 2)); + + let mut barrier_free = source.clone(); + barrier_free.functions[0].body = vec![Stmt::Return(Some(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(10)), + property: "removed".to_string(), + byte_offset: 0, + }))]; + assert!( + gather_cross_module_functions(&barrier_free).contains_key("reshape"), + "the helper graph must otherwise be eligible for localization" + ); + assert!( + gather_cross_module_functions(&source).is_empty(), + "a transitive shape barrier must keep the helper graph outlined" + ); + } + #[test] fn cross_module_free_function_with_module_local_is_rejected() { let mut source = Module::new("/src/constants.ts");