@@ -10,7 +10,7 @@ use perry_hir::Expr;
1010use crate :: expr:: { lower_expr, nanbox_pointer_inline, unbox_to_i64, FnCtx } ;
1111use crate :: nanbox:: double_literal;
1212use crate :: type_analysis:: receiver_class_name;
13- use crate :: types:: { DOUBLE , I32 , I64 } ;
13+ use crate :: types:: { DOUBLE , I1 , I32 , I64 } ;
1414
1515// Reach the override-emit helpers (`pub(super)` of `lower_call`) by their
1616// canonical crate-relative path.
@@ -24,6 +24,49 @@ use crate::lower_call::method_override::{
2424/// call it replaces — so past this width the site keeps the single-arm guard.
2525const MAX_SUBCLASS_DISPATCH_ARMS : usize = 8 ;
2626
27+ /// Can an exact canonical shape prove that `property` is not an own field?
28+ ///
29+ /// A post-construction assignment such as `this.run = f` mints a successor
30+ /// ShapeId, so an exact canonical-shape match excludes that override. A
31+ /// declared field is different: it is already part of the canonical shape and
32+ /// may intentionally shadow a prototype method (#620). Computed fields and
33+ /// incomplete/dynamic parent chains are similarly unknowable here and retain
34+ /// the runtime own-property probe.
35+ fn canonical_shape_excludes_own_property (
36+ ctx : & FnCtx < ' _ > ,
37+ class_name : & str ,
38+ property : & str ,
39+ ) -> bool {
40+ let mut current = Some ( class_name. to_string ( ) ) ;
41+ let mut seen = std:: collections:: HashSet :: new ( ) ;
42+ while let Some ( name) = current {
43+ if !seen. insert ( name. clone ( ) ) {
44+ return false ;
45+ }
46+ let Some ( class) = ctx. classes . get ( & name) else {
47+ return false ;
48+ } ;
49+ if class
50+ . fields
51+ . iter ( )
52+ . any ( |field| field. key_expr . is_some ( ) || field. name == property)
53+ {
54+ return false ;
55+ }
56+ if class. extends_expr . is_some ( ) || class. native_extends . is_some ( ) {
57+ return false ;
58+ }
59+ current = class. extends_name . clone ( ) . or_else ( || {
60+ class. extends . and_then ( |parent_id| {
61+ ctx. classes
62+ . iter ( )
63+ . find_map ( |( name, candidate) | ( candidate. id == parent_id) . then ( || name. clone ( ) ) )
64+ } )
65+ } ) ;
66+ }
67+ true
68+ }
69+
2770/// A declared class may select the direct-method guard, but never prove the
2871/// direct call. The guard validates the live class id, keys token, own
2972/// override, and resolved method pointer; every miss uses dynamic dispatch.
@@ -338,6 +381,11 @@ pub(crate) fn try_lower_instance_method_call(
338381 // instance with a longer chain. Inherited dispatch gets `None` and
339382 // keeps today's lowering.
340383 let mut impl_owner: Vec < Option < String > > = Vec :: new ( ) ;
384+ // Concrete receiver class for each implementor entry. Unlike
385+ // `impl_owner`, this is present for inherited implementations too and
386+ // lets the override probe compare the receiver against that class's
387+ // canonical ShapeId.
388+ let mut impl_class: Vec < String > = Vec :: new ( ) ;
341389 let mut seen_pairs: std:: collections:: HashSet < ( u32 , String ) > =
342390 std:: collections:: HashSet :: new ( ) ;
343391 // Walk `class_ids` in a FIXED order, not `HashMap` order (#7622). Each
@@ -371,6 +419,7 @@ pub(crate) fn try_lower_instance_method_call(
371419 crate :: codegen:: arguments:: method_has_user_rest ( ctx, & c, property) ;
372420 let decl = ctx. method_param_counts . get ( & key) . copied ( ) . unwrap_or ( 0 ) ;
373421 impl_owner. push ( ( c == * start_cls) . then ( || start_cls. clone ( ) ) ) ;
422+ impl_class. push ( start_cls. clone ( ) ) ;
374423 implementors. push ( ( start_cid, fname) ) ;
375424 impl_meta. push ( ( has_rest, has_synthetic_arguments, has_user_rest, decl) ) ;
376425 }
@@ -449,6 +498,69 @@ pub(crate) fn try_lower_instance_method_call(
449498 let probe_entry = ctx. strings . entry ( key_idx_probe) ;
450499 let probe_bytes_global = format ! ( "@{}" , probe_entry. bytes_global) ;
451500 let probe_name_len_str = probe_entry. byte_len . to_string ( ) ;
501+ let probe_override_idx = ctx. new_block ( "idisp.override" ) ;
502+ let probe_dispatch_idx = ctx. new_block ( "idisp.dispatch" ) ;
503+ let probe_outer_merge_idx = ctx. new_block ( "idisp.outer_merge" ) ;
504+ let probe_override_label = ctx. block_label ( probe_override_idx) ;
505+ let probe_dispatch_label = ctx. block_label ( probe_dispatch_idx) ;
506+ let probe_outer_merge_label = ctx. block_label ( probe_outer_merge_idx) ;
507+
508+ // #8406: an exact compiler-published (class id, ShapeId) pair can
509+ // prove that no post-construction own-method override was added.
510+ // Probe the receiver once and bypass the keys-array scan for those
511+ // canonical shapes. Classes whose canonical layout itself may
512+ // contain `property` stay on the old probe, as do wide towers to
513+ // keep code-size growth bounded.
514+ let shape_probe_arms: Vec < ( u32 , String ) > = if implementors. len ( )
515+ <= MAX_SUBCLASS_DISPATCH_ARMS
516+ {
517+ implementors
518+ . iter ( )
519+ . zip ( impl_class. iter ( ) )
520+ . filter_map ( |( ( class_id, _) , class_name) | {
521+ if !canonical_shape_excludes_own_property ( ctx, class_name, property) {
522+ return None ;
523+ }
524+ let keys_global = ctx. class_keys_globals . get ( class_name) ?;
525+ let expected_shape =
526+ crate :: typed_shape:: load_class_shape_id ( ctx, class_name, keys_global) ;
527+ Some ( ( * class_id, expected_shape) )
528+ } )
529+ . collect ( )
530+ } else {
531+ Vec :: new ( )
532+ } ;
533+ let mut shape_probe_cid: Option < String > = None ;
534+ if !shape_probe_arms. is_empty ( ) {
535+ let shape_slot = ctx. func . alloca_entry ( I32 ) ;
536+ let cid = ctx. block ( ) . call (
537+ I32 ,
538+ "js_method_direct_shape_class" ,
539+ & [ ( DOUBLE , & recv_box) , ( crate :: types:: PTR , & shape_slot) ] ,
540+ ) ;
541+ let shape_id = ctx. block ( ) . load ( I32 , & shape_slot) ;
542+ shape_probe_cid = Some ( cid. clone ( ) ) ;
543+ let own_idx = ctx. new_block ( "idisp.own_probe" ) ;
544+ let test_idxs: Vec < usize > = ( 1 ..shape_probe_arms. len ( ) )
545+ . map ( |i| ctx. new_block ( & format ! ( "idisp.shape_test{i}" ) ) )
546+ . collect ( ) ;
547+ for ( i, ( class_id, expected_shape) ) in shape_probe_arms. iter ( ) . enumerate ( ) {
548+ if i > 0 {
549+ ctx. current_block = test_idxs[ i - 1 ] ;
550+ }
551+ let miss_label = test_idxs
552+ . get ( i)
553+ . map ( |& idx| ctx. block_label ( idx) )
554+ . unwrap_or_else ( || ctx. block_label ( own_idx) ) ;
555+ let blk = ctx. block ( ) ;
556+ let cid_ok = blk. icmp_eq ( I32 , & cid, & class_id. to_string ( ) ) ;
557+ let shape_ok = blk. icmp_eq ( I32 , & shape_id, expected_shape) ;
558+ let exact = blk. and ( I1 , & cid_ok, & shape_ok) ;
559+ blk. cond_br ( & exact, & probe_dispatch_label, & miss_label) ;
560+ }
561+ ctx. current_block = own_idx;
562+ }
563+
452564 let own_method_probe = ctx. block ( ) . call (
453565 DOUBLE ,
454566 "js_object_get_own_field_or_undef" ,
@@ -461,12 +573,6 @@ pub(crate) fn try_lower_instance_method_call(
461573 let own_bits_probe = ctx. block ( ) . bitcast_double_to_i64 ( & own_method_probe) ;
462574 let undef_bits_str = format ! ( "{}" , crate :: nanbox:: TAG_UNDEFINED as i64 ) ;
463575 let is_undef_probe = ctx. block ( ) . icmp_eq ( I64 , & own_bits_probe, & undef_bits_str) ;
464- let probe_override_idx = ctx. new_block ( "idisp.override" ) ;
465- let probe_dispatch_idx = ctx. new_block ( "idisp.dispatch" ) ;
466- let probe_outer_merge_idx = ctx. new_block ( "idisp.outer_merge" ) ;
467- let probe_override_label = ctx. block_label ( probe_override_idx) ;
468- let probe_dispatch_label = ctx. block_label ( probe_dispatch_idx) ;
469- let probe_outer_merge_label = ctx. block_label ( probe_outer_merge_idx) ;
470576 ctx. block ( ) . cond_br (
471577 & is_undef_probe,
472578 & probe_dispatch_label,
@@ -570,9 +676,17 @@ pub(crate) fn try_lower_instance_method_call(
570676 // closure-call fallback would also handle this but
571677 // returning a sentinel is cheaper).
572678 ctx. current_block = tower_idx;
573- let blk = ctx. block ( ) ;
574- let recv_handle = unbox_to_i64 ( blk, & recv_box) ;
575- let cid = blk. call ( I32 , "js_object_get_class_id" , & [ ( I64 , & recv_handle) ] ) ;
679+ let recv_handle = unbox_to_i64 ( ctx. block ( ) , & recv_box) ;
680+ let cid = if let Some ( probed_cid) = shape_probe_cid {
681+ // Reuse the class id that the shape probe already validated.
682+ // Zero is intentional: it sends descriptor/prototype
683+ // invalidation and every non-instance receiver to the runtime
684+ // fallback instead of re-entering this hard-coded tower.
685+ probed_cid
686+ } else {
687+ ctx. block ( )
688+ . call ( I32 , "js_object_get_class_id" , & [ ( I64 , & recv_handle) ] )
689+ } ;
576690
577691 for ( i, ( case_cid, _) ) in implementors. iter ( ) . enumerate ( ) {
578692 let case_label = ctx. block_label ( case_idxs[ i] ) ;
0 commit comments