@@ -33,6 +33,24 @@ pub(crate) const PIC_WAYS: usize = 4;
3333/// both skip them. Mirrors the runtime's `PIC_WAY_STATE`.
3434pub ( crate ) const PIC_WAY_STATE : usize = 3 ;
3535
36+ /// `slot < max(field_count, INLINE_SLOT_FLOOR)` — the per-receiver
37+ /// inline-capacity bound both the MRU hit path and the polymorphic ways apply
38+ /// to a cached slot (#6804).
39+ ///
40+ /// Spelled as the equivalent disjunction `slot < FLOOR || slot < field_count`
41+ /// rather than as a `max` followed by one compare. The predicate is identical
42+ /// for every input (`x < max(a, b)` ⟺ `x < a ∨ x < b`), but the `max` had to be
43+ /// materialised — `mov w, #4` / `cmp` / `csel` — and that `csel` was the single
44+ /// hottest instruction in `interp.ts` (4.65% of `evalNode`, #7907), because it
45+ /// sits on the dependency chain out of the `field_count` load. The disjunction
46+ /// has no such node: LLVM folds the pair into `cmp` + `ccmp`, and the
47+ /// `slot < 4` half does not depend on the load at all.
48+ fn emit_slot_in_bounds ( ctx : & mut FnCtx < ' _ > , slot : & str , field_count : & str ) -> String {
49+ let below_floor = ctx. block ( ) . icmp_ult ( I64 , slot, "4" ) ; // INLINE_SLOT_FLOOR
50+ let below_count = ctx. block ( ) . icmp_ult ( I64 , slot, field_count) ;
51+ ctx. block ( ) . or ( I1 , & below_floor, & below_count)
52+ }
53+
3654/// The generic per-site monomorphic inline-cache dispatch for `obj.property`.
3755/// This is the fall-through tail of the general catch-all arm: all earlier
3856/// specializations have been ruled out.
@@ -329,9 +347,16 @@ pub(crate) fn lower_generic_property_get(
329347 // what the flat predicate computed there).
330348 let hit_idx = ctx. new_block ( "pic.hit" ) ;
331349 let miss_idx = ctx. new_block ( "pic.miss" ) ;
350+ // #7907: the two receiver-validation failures get their own landing block
351+ // so `pic.miss` is dominated by `pic.token`. See the comment on
352+ // `pic.miss.cold` below for why that is the whole point of this split.
353+ let cold_idx = ctx. new_block ( "pic.miss.cold" ) ;
354+ let call_idx = ctx. new_block ( "pic.miss.call" ) ;
332355 let merge_idx = ctx. new_block ( "pic.merge" ) ;
333356 let hit_label = ctx. block_label ( hit_idx) ;
334357 let miss_label = ctx. block_label ( miss_idx) ;
358+ let cold_label = ctx. block_label ( cold_idx) ;
359+ let call_label = ctx. block_label ( call_idx) ;
335360 let merge_label = ctx. block_label ( merge_idx) ;
336361 let hdr_idx = ctx. new_block ( "pic.recv_hdr" ) ;
337362 let hdr_label = ctx. block_label ( hdr_idx) ;
@@ -344,8 +369,10 @@ pub(crate) fn lower_generic_property_get(
344369 // materialisation) in front of every real object read. The miss path
345370 // still substitutes the sentinel, because the way compares below load
346371 // `field_count` unconditionally.
347- // (edge labels are no longer needed: the miss block recomputes.)
348- ctx. block ( ) . cond_br ( & is_real_ptr, & hdr_label, & miss_label) ;
372+ // A small-handle receiver can never resolve a way (`way_hit` requires a
373+ // real object), so it leaves for `pic.miss.cold` and never enters the
374+ // block the ways live in.
375+ ctx. block ( ) . cond_br ( & is_real_ptr, & hdr_label, & cold_label) ;
349376 ctx. current_block = hdr_idx;
350377
351378 // GcHeader sits 8 bytes before the user pointer; obj_type is the
@@ -421,7 +448,13 @@ pub(crate) fn lower_generic_property_get(
421448 // below: the keys load, the token select and the two epoch loads all
422449 // hang off the same predicate, so a non-object receiver used to execute
423450 // them before the flat `hit` could reject it.
424- ctx. block ( ) . cond_br ( & is_object, & tok_label, & miss_label) ;
451+ //
452+ // #7907: the false edge goes to `pic.miss.cold`, not `pic.miss` — a
453+ // receiver that is not a plain descriptor-free `ObjectHeader` fails
454+ // `way_hit` by construction, so consulting the ways for it was always dead
455+ // work, and keeping it out is what lets `pic.miss` reuse this block's
456+ // values instead of re-deriving them.
457+ ctx. block ( ) . cond_br ( & is_object, & tok_label, & cold_label) ;
425458 ctx. current_block = tok_idx;
426459
427460 // Load obj->keys_array at offset 16 of ObjectHeader.
@@ -510,9 +543,7 @@ pub(crate) fn lower_generic_property_get(
510543 let fc_ptr = ctx. block ( ) . inttoptr ( I64 , & fc_addr) ;
511544 let fc = ctx. block ( ) . load ( I32 , & fc_ptr) ;
512545 let fc64 = ctx. block ( ) . zext ( I32 , & fc, I64 ) ;
513- let fc_floor = ctx. block ( ) . icmp_ult ( I64 , & fc64, "4" ) ; // INLINE_SLOT_FLOOR
514- let limit = ctx. block ( ) . select ( I1 , & fc_floor, I64 , "4" , & fc64) ;
515- let slot_in_bounds = ctx. block ( ) . icmp_ult ( I64 , & slot, & limit) ;
546+ let slot_in_bounds = emit_slot_in_bounds ( ctx, & slot, & fc64) ;
516547 let bounds_hit = ctx. new_block ( "pic.hit.load" ) ;
517548 let bounds_hit_label = ctx. block_label ( bounds_hit) ;
518549 ctx. block ( )
@@ -557,62 +588,34 @@ pub(crate) fn lower_generic_property_get(
557588 // way hit still reports guard-fail + fallback-call exactly as it did when
558589 // it was a real miss — the feedback heuristics see an unchanged signal
559590 // (the site IS polymorphic; only the cost of that changed).
560- ctx. current_block = miss_idx;
561- // #7883: the guard chain now branches out at three points, so the values
562- // the polymorphic way compares consult are no longer live on every edge
563- // into this block — and phi-ing them would drag their materialisation
564- // (`cset`/`csinc` per value) back onto the hot path, which is the whole
565- // point of branching. They are recomputed here instead, from the SAME
566- // memory with no intervening store, so every one is bit-identical to
567- // what the pre-#7883 flat predicate computed. This block is cold — every
568- // path out of it either loads a way slot or calls the miss handler.
569591 //
570- // The small-handle sentinel substitution lives here for the same reason:
571- // the way compares load `field_count` unconditionally, and a native
572- // registry-id receiver reaches this block without ever being a pointer.
573- let cache_addr = ctx. block ( ) . ptrtoint ( & cache_ref, I64 ) ;
574- let safe_obj_handle = ctx
575- . block ( )
576- . select ( I1 , & is_real_ptr, I64 , & obj_handle, & cache_addr) ;
577- let m_gc_type_addr = ctx. block ( ) . sub ( I64 , & safe_obj_handle, "8" ) ;
578- let m_gc_type_ptr = ctx. block ( ) . inttoptr ( I64 , & m_gc_type_addr) ;
579- let m_gc_type = ctx. block ( ) . load ( I8 , & m_gc_type_ptr) ;
580- let m_gc_type_ok = ctx. block ( ) . icmp_eq ( I8 , & m_gc_type, "2" ) ;
581- let is_object = ctx. block ( ) . and ( I1 , & is_real_ptr, & m_gc_type_ok) ;
582- let m_magic_addr = ctx. block ( ) . add ( I64 , & safe_obj_handle, "12" ) ;
583- let m_magic_ptr = ctx. block ( ) . inttoptr ( I64 , & m_magic_addr) ;
584- let m_magic = ctx. block ( ) . load ( I32 , & m_magic_ptr) ;
585- let m_is_closure = ctx. block ( ) . icmp_eq ( I32 , & m_magic, "1129268819" ) ;
586- let m_not_closure = ctx. block ( ) . xor ( I1 , & m_is_closure, "true" ) ;
587- let is_object = ctx. block ( ) . and ( I1 , & is_object, & m_not_closure) ;
588- let m_ot_ptr = ctx. block ( ) . inttoptr ( I64 , & safe_obj_handle) ;
589- let m_ot = ctx. block ( ) . load ( I32 , & m_ot_ptr) ;
590- let m_ot_ok = ctx. block ( ) . icmp_eq ( I32 , & m_ot, "1" ) ;
591- let is_object = ctx. block ( ) . and ( I1 , & is_object, & m_ot_ok) ;
592- let m_res_addr = ctx. block ( ) . sub ( I64 , & safe_obj_handle, "6" ) ;
593- let m_res_ptr = ctx. block ( ) . inttoptr ( I64 , & m_res_addr) ;
594- let m_res = ctx. block ( ) . load ( crate :: types:: I16 , & m_res_ptr) ;
595- let m_has_desc = ctx. block ( ) . and ( crate :: types:: I16 , & m_res, "2048" ) ;
596- let m_no_desc = ctx. block ( ) . icmp_eq ( crate :: types:: I16 , & m_has_desc, "0" ) ;
597- let is_object = ctx. block ( ) . and ( I1 , & is_object, & m_no_desc) ;
598- let m_keys_addr = ctx. block ( ) . add ( I64 , & safe_obj_handle, "16" ) ;
599- let m_keys_ptr = ctx. block ( ) . inttoptr ( I64 , & m_keys_addr) ;
600- let m_keys = ctx. block ( ) . load ( I64 , & m_keys_ptr) ;
601- let m_pcid_addr = ctx. block ( ) . add ( I64 , & safe_obj_handle, "8" ) ;
602- let m_pcid_ptr = ctx. block ( ) . inttoptr ( I64 , & m_pcid_addr) ;
603- let m_pcid = ctx. block ( ) . load ( I32 , & m_pcid_ptr) ;
604- let m_pcid_rel = ctx. block ( ) . add ( I32 , & m_pcid, "-2147483648" ) ;
605- let m_is_stamp = ctx. block ( ) . icmp_ult ( I32 , & m_pcid_rel, "1073741824" ) ;
606- let m_pcid64 = ctx. block ( ) . zext ( I32 , & m_pcid, I64 ) ;
607- let m_id_token = ctx. block ( ) . or ( I64 , & m_pcid64, "4611686018427387904" ) ;
608- let token = ctx
609- . block ( )
610- . select ( I1 , & m_is_stamp, I64 , & m_id_token, & m_keys) ;
611- let token_nonnull = ctx. block ( ) . icmp_ne ( I64 , & token, "0" ) ;
612- let m_cache_epoch_ptr = ctx. block ( ) . gep ( I64 , & cache_ref, & [ ( I64 , "2" ) ] ) ;
613- let m_cache_epoch = ctx. block ( ) . load ( I64 , & m_cache_epoch_ptr) ;
614- let m_live_epoch = ctx. block ( ) . load ( I64 , "@PERRY_IC_EPOCH" ) ;
615- let epoch_eq = ctx. block ( ) . icmp_eq ( I64 , & m_cache_epoch, & m_live_epoch) ;
592+ // # Why this block is DOMINATED by `pic.token` (#7907)
593+ //
594+ // Its only predecessors are `pic.token` (the MRU token did not match) and
595+ // `pic.hit` (it matched but the cached slot is outside this receiver's
596+ // inline capacity), and `pic.hit` is itself dominated by `pic.token`. So
597+ // `token`, `token_nonnull` and `epoch_eq` — everything the way compares
598+ // need — are already in scope here, and `is_object` is statically TRUE.
599+ //
600+ // #7883 could not rely on that: it routed the two receiver-validation
601+ // failures here as well, which left the values live on only some edges, so
602+ // the block **re-derived them** — four header loads, the `keys_array` and
603+ // `parent_class_id` loads, the token select, a second pair of epoch loads,
604+ // and a `select` substituting a safe address for a small-handle receiver.
605+ // That was correct, and it was justified as cold. It is not cold: on a site
606+ // whose receiver rotates over more shapes than the MRU entry holds — the
607+ // shape #7753's ways exist for — this block runs on nearly every read, so
608+ // the duplicate ladder sat on the hot path. Measured on `interp.ts`'s
609+ // `evalNode`, the single hottest instruction in the whole program was the
610+ // `csel` materialising `max(field_count, INLINE_SLOT_FLOOR)` *inside this
611+ // recomputation*.
612+ //
613+ // Sending the two validation failures to `pic.miss.cold` instead is what
614+ // establishes the dominance. Nothing about the predicate changed: a
615+ // receiver that fails either check also fails `way_hit` (which ANDs
616+ // `is_object` in), so it could never have resolved a way — the compares
617+ // were dead work for it.
618+ ctx. current_block = miss_idx;
616619 crate :: expr:: emit_typed_feedback_record_call (
617620 ctx. block ( ) ,
618621 "js_typed_feedback_record_guard_fail" ,
@@ -655,16 +658,23 @@ pub(crate) fn lower_generic_property_get(
655658 let way_state = ctx. block ( ) . load ( I64 , & state_ptr) ;
656659 let ways_live = ctx. block ( ) . icmp_sgt ( I64 , & way_state, "0" ) ;
657660 let ways_idx = ctx. new_block ( "pic.ways" ) ;
658- let call_idx = ctx. new_block ( "pic.miss.call" ) ;
659661 let ways_label = ctx. block_label ( ways_idx) ;
660- let call_label = ctx. block_label ( call_idx) ;
661662 ctx. block ( ) . cond_br ( & ways_live, & ways_label, & call_label) ;
662663
663664 ctx. current_block = ways_idx;
664- let mut way_hit = ctx. block ( ) . and ( I1 , & is_object, & epoch_eq) ;
665- way_hit = ctx. block ( ) . and ( I1 , & way_hit, & token_nonnull) ;
666- let mut way_any = String :: from ( "false" ) ;
667- let mut way_slot = String :: from ( "0" ) ;
665+ // `is_object` is not ANDed in any more: it is statically true on every edge
666+ // that reaches here (#7907 — see the dominance note above). `epoch_eq` and
667+ // `token_nonnull` are the values `pic.token` computed, from the same memory
668+ // with no intervening store, so the predicate is unchanged.
669+ let mut way_hit = ctx. block ( ) . and ( I1 , & epoch_eq, & token_nonnull) ;
670+ // Reduced as a BALANCED TREE, not as a left fold. At most one way can hold
671+ // a given token (`pic_prime_get` evicts a duplicate before it writes one,
672+ // and a zero token is excluded by `token_nonnull`), so the association is
673+ // free to change — but the fold made `way_slot` a chain of `PIC_WAYS`
674+ // dependent `csel`s whose last node is the operand of the bounds compare
675+ // that gates the branch out of this block. On `interp.ts` that node was the
676+ // hottest instruction in `evalNode` (#7907). The tree halves the chain.
677+ let mut lanes: Vec < ( String , String ) > = Vec :: with_capacity ( PIC_WAYS ) ;
668678 for w in 0 ..PIC_WAYS {
669679 let tok_ptr = ctx. block ( ) . gep (
670680 I64 ,
@@ -679,20 +689,40 @@ pub(crate) fn lower_generic_property_get(
679689 & [ ( I64 , & ( PIC_WAY_BASE + w * 2 + 1 ) . to_string ( ) ) ] ,
680690 ) ;
681691 let way_slot_val = ctx. block ( ) . load ( I64 , & slot_ptr) ;
682- way_slot = ctx. block ( ) . select ( I1 , & eq, I64 , & way_slot_val, & way_slot) ;
683- way_any = ctx. block ( ) . or ( I1 , & way_any, & eq) ;
692+ let lane_slot = ctx. block ( ) . select ( I1 , & eq, I64 , & way_slot_val, "0" ) ;
693+ lanes. push ( ( eq, lane_slot) ) ;
694+ }
695+ while lanes. len ( ) > 1 {
696+ let mut merged: Vec < ( String , String ) > = Vec :: with_capacity ( lanes. len ( ) . div_ceil ( 2 ) ) ;
697+ for pair in lanes. chunks ( 2 ) {
698+ match pair {
699+ [ ( a_any, a_slot) , ( b_any, b_slot) ] => {
700+ let any = ctx. block ( ) . or ( I1 , a_any, b_any) ;
701+ let slot = ctx. block ( ) . select ( I1 , a_any, I64 , a_slot, b_slot) ;
702+ merged. push ( ( any, slot) ) ;
703+ }
704+ [ single] => merged. push ( single. clone ( ) ) ,
705+ _ => unreachable ! ( "chunks(2) yields one or two elements" ) ,
706+ }
707+ }
708+ lanes = merged;
684709 }
710+ let ( way_any, way_slot) = lanes
711+ . pop ( )
712+ . expect ( "PIC_WAYS is non-zero, so the reduction leaves exactly one lane" ) ;
685713 way_hit = ctx. block ( ) . and ( I1 , & way_hit, & way_any) ;
686714 // Same per-receiver inline-capacity bound the MRU hit path applies: a slot
687715 // primed from a larger-capacity sibling of the same shape must not drive a
688716 // raw load past this receiver's field region (#6804).
689- let way_fc_addr = ctx. block ( ) . add ( I64 , & safe_obj_handle, "12" ) ;
717+ //
718+ // The load is off `obj_handle` rather than the deleted small-handle
719+ // sentinel, so it is the SAME address `pic.recv_hdr` already read for the
720+ // closure-magic check and GVN folds the two together.
721+ let way_fc_addr = ctx. block ( ) . add ( I64 , & obj_handle, "12" ) ;
690722 let way_fc_ptr = ctx. block ( ) . inttoptr ( I64 , & way_fc_addr) ;
691723 let way_fc = ctx. block ( ) . load ( I32 , & way_fc_ptr) ;
692724 let way_fc64 = ctx. block ( ) . zext ( I32 , & way_fc, I64 ) ;
693- let way_fc_floor = ctx. block ( ) . icmp_ult ( I64 , & way_fc64, "4" ) ; // INLINE_SLOT_FLOOR
694- let way_limit = ctx. block ( ) . select ( I1 , & way_fc_floor, I64 , "4" , & way_fc64) ;
695- let way_in_bounds = ctx. block ( ) . icmp_ult ( I64 , & way_slot, & way_limit) ;
725+ let way_in_bounds = emit_slot_in_bounds ( ctx, & way_slot, & way_fc64) ;
696726 let way_ok = ctx. block ( ) . and ( I1 , & way_hit, & way_in_bounds) ;
697727 let way_load_idx = ctx. new_block ( "pic.way.load" ) ;
698728 let way_load_label = ctx. block_label ( way_load_idx) ;
@@ -707,6 +737,26 @@ pub(crate) fn lower_generic_property_get(
707737 let way_end_label = ctx. block ( ) . label . clone ( ) ;
708738 ctx. block ( ) . br ( & merge_label) ;
709739
740+ // #7907: receiver-validation failure. `way_hit` requires a real pointer to
741+ // a plain descriptor-free `ObjectHeader`, so a receiver that got here can
742+ // never match a way — it goes straight to the handler, which reproduces the
743+ // whole ladder anyway (proxy band, closure magic, buffer/typed-array
744+ // registries, small-handle dispatch). The typed-feedback counters are the
745+ // same two `pic.miss` records on the same edges, so the feedback signal is
746+ // byte-identical to what the merged block reported.
747+ ctx. current_block = cold_idx;
748+ crate :: expr:: emit_typed_feedback_record_call (
749+ ctx. block ( ) ,
750+ "js_typed_feedback_record_guard_fail" ,
751+ & [ ( I64 , & feedback_site_id) ] ,
752+ ) ;
753+ crate :: expr:: emit_typed_feedback_record_call (
754+ ctx. block ( ) ,
755+ "js_typed_feedback_record_fallback_call" ,
756+ & [ ( I64 , & feedback_site_id) ] ,
757+ ) ;
758+ ctx. block ( ) . br ( & call_label) ;
759+
710760 // PIC miss: slow path with cache population.
711761 ctx. current_block = call_idx;
712762 let val_miss = ctx. block ( ) . call (
0 commit comments