Skip to content

Commit 283897b

Browse files
author
Ralph Küpper
committed
perf(repsel): resolve object-literal element types in the element-shape loop clone (#7480)
#7480's own kernel (`keep: {v, w}[]`, 200k x 50 sweeps) went from 408 ms to 12 ms on the pinned quiet mini — parity with node (12 ms) and bun (12 ms), down from 34x node. The named-class arm #7612 already covered is unchanged at 13 ms. Checksums identical across every arm and runtime. `element_class_name` resolved `Array(Named(C))` only, so an object-literal element type never reached the clone. It now also resolves the declared object type to the `__AnonShape_<hash>` class its literals allocate, by matching the declared property order against the module's anon shapes (ambiguity declines rather than guessing, so the answer does not depend on `ctx.classes` iteration order). `receiver_class_name` is deliberately NOT widened — that is the #6377 blast radius #7612 refused. The clone is made self-contained instead: its `ElementShapeLoopFact` already carried the class name and packed slot index, and the three sites that would otherwise re-derive the class from the receiver now consult that fact through one predicate. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
1 parent db4a2d0 commit 283897b

8 files changed

Lines changed: 889 additions & 163 deletions

File tree

crates/perry-codegen/src/expr/binary.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,19 @@ fn lower_arithmetic_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<(String,
5858
return Ok((value, true));
5959
}
6060
}
61-
if expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr) {
61+
// repsel #7480 step 3: a tracked `arr[i].field` read inside an
62+
// element-shape fast clone routes to the raw-f64 lowering WITHOUT the
63+
// boxed-fallback test below. That test asks `receiver_class_name`, which
64+
// by design does not resolve an object-literal element type, so the read
65+
// would otherwise fall through to `lower_expr` — a generic diamond, whose
66+
// calls then fail the clone's call-free admission and cost the clone
67+
// entirely. The predicate is left alone rather than widened: this read has
68+
// no boxed fallback at all (the residual per-element check proves the slot
69+
// is a raw double before the load), so claiming one here would be a lie
70+
// that other consumers of that predicate would read.
71+
let in_element_shape_clone = matches!(expr, Expr::PropertyGet { object, property, .. }
72+
if crate::expr::element_shape_loop_fact_for_property_get(ctx, object, property).is_some());
73+
if in_element_shape_clone || expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr) {
6274
if let Some(value) =
6375
super::property_get::lower_raw_f64_class_field_get_for_number_context(ctx, expr)?
6476
{

crates/perry-codegen/src/expr/mod.rs

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,21 +1605,56 @@ pub(crate) struct ElementShapeLoopFact {
16051605
pub max_field_index: u32,
16061606
}
16071607

1608-
/// Find the innermost active element-shape loop fact covering
1609-
/// `(array_local_id, index_local_id, class_name, property)`. Returns the fact
1610-
/// and the packed slot index of the field.
1611-
pub(crate) fn element_shape_loop_fact_lookup<'f>(
1612-
facts: &'f [ElementShapeLoopFact],
1613-
array_local_id: u32,
1614-
index_local_id: u32,
1615-
class_name: &str,
1608+
/// Find the innermost active element-shape loop fact covering a
1609+
/// `PropertyGet`'s receiver: answers `Some((fact, packed_slot_index))` exactly
1610+
/// when `object.property` is a tracked `arr[counter].field` read inside an
1611+
/// element-shape fast clone.
1612+
///
1613+
/// The single entry point for the three sites that must agree about that read
1614+
/// — the field lowering itself
1615+
/// (`expr::property_get::lower_raw_f64_class_field_get_for_number_context`),
1616+
/// `type_analysis::is_numeric_expr`, and `expr::binary`'s arithmetic-operand
1617+
/// router. #7480 step 3 made the clone self-contained by routing all three
1618+
/// through the fact instead of through `receiver_class_name`, which by design
1619+
/// does not resolve an object-literal element type; the fact's own
1620+
/// `class_name` is therefore the authoritative answer rather than a filter on
1621+
/// one the caller supplies.
1622+
///
1623+
/// `(array, counter)` already identifies one loop — a counter local is minted
1624+
/// per `for`, and the matcher admits exactly one array per loop. Cheap
1625+
/// early-out first: outside a fast clone the fact vector is empty.
1626+
///
1627+
/// The **canonical-i32 counter slot is part of the predicate**, not a
1628+
/// precondition the caller re-checks. Answering `Some` is a promise that the
1629+
/// read really does take the bare-load lowering, and `is_numeric_expr` bets a
1630+
/// raw `double` on that promise: if the field lowering declined for want of an
1631+
/// i32 slot while the numeric predicate still said yes, the operand would be
1632+
/// consumed as a real double while the generic lowering handed back a NaN-boxed
1633+
/// value. The matcher declines the whole loop without that slot
1634+
/// (`lower_element_shape_versioned_for`), so today the two can't disagree —
1635+
/// asking here keeps them unable to disagree if the matcher is ever widened.
1636+
pub(crate) fn element_shape_loop_fact_for_property_get<'f>(
1637+
ctx: &'f FnCtx<'_>,
1638+
object: &perry_hir::Expr,
16161639
property: &str,
16171640
) -> Option<(&'f ElementShapeLoopFact, u32)> {
1618-
facts.iter().rev().find_map(|fact| {
1619-
if fact.array_local_id != array_local_id
1620-
|| fact.index_local_id != index_local_id
1621-
|| fact.class_name != class_name
1622-
{
1641+
use perry_hir::Expr;
1642+
if ctx.element_shape_loop_facts.is_empty() {
1643+
return None;
1644+
}
1645+
let Expr::IndexGet { object, index } = object else {
1646+
return None;
1647+
};
1648+
let (Expr::LocalGet(array_local_id), Expr::LocalGet(index_local_id)) =
1649+
(object.as_ref(), index.as_ref())
1650+
else {
1651+
return None;
1652+
};
1653+
if !ctx.i32_counter_slots.contains_key(index_local_id) {
1654+
return None;
1655+
}
1656+
ctx.element_shape_loop_facts.iter().rev().find_map(|fact| {
1657+
if fact.array_local_id != *array_local_id || fact.index_local_id != *index_local_id {
16231658
return None;
16241659
}
16251660
fact.fields.get(property).map(|idx| (fact, *idx))

crates/perry-codegen/src/expr/property_get/helpers.rs

Lines changed: 75 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,81 @@ pub(crate) fn lower_raw_f64_class_field_get_for_number_context(
331331
}
332332
}
333333

334+
// repsel #7480 / #5093: inside the fast clone of an ELEMENT-shape
335+
// versioned loop, `arr[i].field` in number context lowers to a bare
336+
// element load plus the residual per-element check, with no element-read
337+
// tier and no guard call (see stmt/element_shape_loop.rs).
338+
//
339+
// #7480 step 3: this sits ABOVE the `receiver_class_name` gate on purpose.
340+
// The clone's element class can be one that resolver does not answer for —
341+
// an object-literal element type (`keep: {v: number}[]`) resolves to its
342+
// `__AnonShape_<hash>` only inside the matcher, which is where that
343+
// resolution is kept so it cannot un-gate anything else (#6377). Every
344+
// fact this consults was validated by the matcher when the fact was built:
345+
// the class has no computed members and no base, the property is not an
346+
// accessor and is not denylisted, and its declared type is a raw-f64
347+
// candidate at the packed slot index carried here. So the lowering needs
348+
// nothing from the receiver's static type, and asking for it would have
349+
// made the whole clone dead IR.
350+
if let Some((fact, field_index)) =
351+
crate::expr::element_shape_loop_fact_for_property_get(ctx, object, property)
352+
.map(|(fact, idx)| (fact.clone(), idx))
353+
{
354+
if let Expr::IndexGet { object: array, .. } = object.as_ref() {
355+
if let Expr::LocalGet(arr_id) = array.as_ref() {
356+
// The counter's canonical i32 slot is what the matcher
357+
// required; without it there is nothing to index with.
358+
if let Some(slot) = ctx.i32_counter_slots.get(&fact.index_local_id).cloned() {
359+
let idx_i32 = ctx.block().load(I32, &slot);
360+
let value = crate::expr::element_shape_guard::emit_element_shape_field_load(
361+
ctx,
362+
&fact,
363+
&idx_i32,
364+
field_index,
365+
);
366+
let lowered = LoweredValue {
367+
semantic: SemanticKind::JsNumber,
368+
rep: NativeRep::F64,
369+
llvm_ty: DOUBLE,
370+
value: value.clone(),
371+
};
372+
ctx.record_lowered_value_with_access_mode_and_facts(
373+
"ElementShapeFieldGet",
374+
Some(*arr_id),
375+
"element_shape_loop.raw_f64_load",
376+
&lowered,
377+
Some(BoundsState::Guarded {
378+
guard_id: "element_shape_loop_preheader_check".to_string(),
379+
}),
380+
None,
381+
Some(BufferAccessMode::CheckedNative),
382+
None,
383+
None,
384+
None,
385+
vec![raw_f64_layout_fact(
386+
Some(*arr_id),
387+
"consumed",
388+
"element_shape_loop_preheader_check",
389+
None,
390+
)],
391+
Vec::new(),
392+
false,
393+
false,
394+
vec![
395+
format!("field={property}"),
396+
format!("class={}", fact.class_name),
397+
"loop_versioning=element_shape".to_string(),
398+
"index_range=nonnegative_i32".to_string(),
399+
"length_range=guarded_i32".to_string(),
400+
"element_shape=homogeneous_class".to_string(),
401+
],
402+
);
403+
return Ok(Some(value));
404+
}
405+
}
406+
}
407+
}
408+
334409
let Some(class_name) = receiver_class_name(ctx, object) else {
335410
return Ok(None);
336411
};
@@ -368,80 +443,6 @@ pub(crate) fn lower_raw_f64_class_field_get_for_number_context(
368443
return Ok(None);
369444
};
370445

371-
// repsel #7480 / #5093: inside the fast clone of an ELEMENT-shape
372-
// versioned loop, `arr[i].field` in number context lowers to a bare
373-
// element load plus the residual per-element check, with no element-read
374-
// tier and no guard call (see stmt/element_shape_loop.rs). Checked before
375-
// the class-field fact because the receiver shapes are disjoint
376-
// (`IndexGet` vs `LocalGet`) and this one is the cheaper lowering.
377-
if !ctx.element_shape_loop_facts.is_empty() {
378-
if let Expr::IndexGet { object, index } = object.as_ref() {
379-
if let (Expr::LocalGet(arr_id), Expr::LocalGet(idx_id)) =
380-
(object.as_ref(), index.as_ref())
381-
{
382-
let hit = crate::expr::element_shape_loop_fact_lookup(
383-
&ctx.element_shape_loop_facts,
384-
*arr_id,
385-
*idx_id,
386-
&class_name,
387-
property,
388-
)
389-
.filter(|(_, loop_idx)| *loop_idx == field_index)
390-
.map(|(fact, _)| fact.clone());
391-
if let Some(fact) = hit {
392-
// The counter's canonical i32 slot is what the matcher
393-
// required; without it there is nothing to index with.
394-
if let Some(slot) = ctx.i32_counter_slots.get(idx_id).cloned() {
395-
let idx_i32 = ctx.block().load(I32, &slot);
396-
let value = crate::expr::element_shape_guard::emit_element_shape_field_load(
397-
ctx,
398-
&fact,
399-
&idx_i32,
400-
field_index,
401-
);
402-
let lowered = LoweredValue {
403-
semantic: SemanticKind::JsNumber,
404-
rep: NativeRep::F64,
405-
llvm_ty: DOUBLE,
406-
value: value.clone(),
407-
};
408-
ctx.record_lowered_value_with_access_mode_and_facts(
409-
"ElementShapeFieldGet",
410-
Some(*arr_id),
411-
"element_shape_loop.raw_f64_load",
412-
&lowered,
413-
Some(BoundsState::Guarded {
414-
guard_id: "element_shape_loop_preheader_check".to_string(),
415-
}),
416-
None,
417-
Some(BufferAccessMode::CheckedNative),
418-
None,
419-
None,
420-
None,
421-
vec![raw_f64_layout_fact(
422-
Some(*arr_id),
423-
"consumed",
424-
"element_shape_loop_preheader_check",
425-
None,
426-
)],
427-
Vec::new(),
428-
false,
429-
false,
430-
vec![
431-
format!("field={property}"),
432-
"loop_versioning=element_shape".to_string(),
433-
"index_range=nonnegative_i32".to_string(),
434-
"length_range=guarded_i32".to_string(),
435-
"element_shape=homogeneous_class".to_string(),
436-
],
437-
);
438-
return Ok(Some(value));
439-
}
440-
}
441-
}
442-
}
443-
}
444-
445446
// #5093 loop versioning: inside the fast clone of a class-field versioned
446447
// loop, a tracked number-context field read on the proven receiver lowers
447448
// to a bare slot load on the preheader-cached object pointer — no shape

0 commit comments

Comments
 (0)