11use std:: collections:: HashSet ;
22
3- use perry_hir:: Param ;
3+ use perry_hir:: { Expr , Param , Stmt } ;
44
55use crate :: block:: LlBlock ;
66use crate :: expr:: { nanbox_pointer_inline, FnCtx } ;
@@ -40,11 +40,21 @@ pub(crate) fn store_param_slot(
4040pub ( crate ) fn materialize_arguments_object (
4141 ctx : & mut FnCtx < ' _ > ,
4242 params : & [ Param ] ,
43+ body : Option < & [ Stmt ] > ,
4344 callee : ArgumentsCallee < ' _ > ,
4445) {
4546 let Some ( synth_param) = params. iter ( ) . find ( |p| p. arguments_object . is_some ( ) ) else {
4647 return ;
4748 } ;
49+ // Call lowering has already bundled every supplied argument into the
50+ // synthesized slot as a marked Array. When the only observable operation
51+ // is `arguments.length`, that bundle has exactly the required value and a
52+ // full ECMAScript Arguments object would only add allocation, mapped-index
53+ // setup, and GC pressure. Keep the existing conservative materialization
54+ // path for every other use (including callers that cannot provide a body).
55+ if body. is_some_and ( |body| arguments_used_only_for_length ( body, synth_param. id ) ) {
56+ return ;
57+ }
4858 let Some ( meta) = synth_param. arguments_object . as_ref ( ) else {
4959 return ;
5060 } ;
@@ -102,6 +112,37 @@ pub(crate) fn materialize_arguments_object(
102112 ctx. block ( ) . store ( DOUBLE , & boxed_args, & arguments_slot) ;
103113}
104114
115+ /// Prove that replacing the synthesized Arguments object with its raw argument
116+ /// bundle cannot be observed. The proof is deliberately fail-closed: HIR's
117+ /// canonical local-reference collector counts every use of the synthetic local,
118+ /// including specialized local-bearing expressions such as `ArrayPop(id)`.
119+ /// Every one of those uses must correspond to an exact `arguments.length` read
120+ /// found by the generic expression traversal.
121+ fn arguments_used_only_for_length ( body : & [ Stmt ] , arguments_id : u32 ) -> bool {
122+ let mut refs = Vec :: new ( ) ;
123+ let mut visited_closures = HashSet :: new ( ) ;
124+ for stmt in body {
125+ perry_hir:: collect_local_refs_stmt ( stmt, & mut refs, & mut visited_closures) ;
126+ }
127+
128+ let total_uses = refs. iter ( ) . filter ( |id| * * id == arguments_id) . count ( ) ;
129+ let mut length_reads = 0usize ;
130+ crate :: collectors:: for_each_expr_in_stmts ( body, & mut |expr| {
131+ if matches ! (
132+ expr,
133+ Expr :: PropertyGet {
134+ object,
135+ property,
136+ ..
137+ } if property == "length"
138+ && matches!( object. as_ref( ) , Expr :: LocalGet ( id) if * id == arguments_id)
139+ ) {
140+ length_reads += 1 ;
141+ }
142+ } ) ;
143+ length_reads > 0 && total_uses == length_reads
144+ }
145+
105146fn mapped_arguments_params ( params : & [ Param ] ) -> Vec < ( u32 , u32 ) > {
106147 params
107148 . iter ( )
@@ -110,6 +151,63 @@ fn mapped_arguments_params(params: &[Param]) -> Vec<(u32, u32)> {
110151 . collect ( )
111152}
112153
154+ #[ cfg( test) ]
155+ mod length_only_tests {
156+ use super :: arguments_used_only_for_length;
157+ use perry_hir:: { Expr , Stmt } ;
158+
159+ const ARGUMENTS : u32 = 17 ;
160+
161+ fn length ( ) -> Expr {
162+ Expr :: PropertyGet {
163+ object : Box :: new ( Expr :: LocalGet ( ARGUMENTS ) ) ,
164+ property : "length" . to_string ( ) ,
165+ byte_offset : 0 ,
166+ }
167+ }
168+
169+ #[ test]
170+ fn accepts_exact_length_reads_at_arbitrary_depth ( ) {
171+ let body = vec ! [ Stmt :: Return ( Some ( Expr :: Binary {
172+ op: perry_hir:: BinaryOp :: Add ,
173+ left: Box :: new( Expr :: Integer ( 1 ) ) ,
174+ right: Box :: new( length( ) ) ,
175+ } ) ) ] ;
176+ assert ! ( arguments_used_only_for_length( & body, ARGUMENTS ) ) ;
177+ }
178+
179+ #[ test]
180+ fn rejects_identity_index_and_mixed_uses ( ) {
181+ assert ! ( !arguments_used_only_for_length(
182+ & [ Stmt :: Return ( Some ( Expr :: LocalGet ( ARGUMENTS ) ) ) ] ,
183+ ARGUMENTS
184+ ) ) ;
185+ assert ! ( !arguments_used_only_for_length(
186+ & [ Stmt :: Return ( Some ( Expr :: IndexGet {
187+ object: Box :: new( Expr :: LocalGet ( ARGUMENTS ) ) ,
188+ index: Box :: new( Expr :: Integer ( 0 ) ) ,
189+ } ) ) ] ,
190+ ARGUMENTS
191+ ) ) ;
192+ assert ! ( !arguments_used_only_for_length(
193+ & [
194+ Stmt :: Expr ( length( ) ) ,
195+ Stmt :: Return ( Some ( Expr :: LocalGet ( ARGUMENTS ) ) ) ,
196+ ] ,
197+ ARGUMENTS
198+ ) ) ;
199+ }
200+
201+ #[ test]
202+ fn rejects_specialized_local_bearing_operations ( ) {
203+ let body = vec ! [
204+ Stmt :: Expr ( length( ) ) ,
205+ Stmt :: Return ( Some ( Expr :: ArrayPop ( ARGUMENTS ) ) ) ,
206+ ] ;
207+ assert ! ( !arguments_used_only_for_length( & body, ARGUMENTS ) ) ;
208+ }
209+ }
210+
113211/// Does `property`, resolved against `class_name`'s ancestry, declare a USER
114212/// `...rest` parameter — as opposed to (or in addition to) the trailing
115213/// `arguments` slot #677 synthesizes?
0 commit comments