Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions benchmarks/object-write-6812/followup-polymorphic-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Bounded polymorphic write PIC follow-up

The write PIC now has four bounded shape entries. Later entries are consulted
only after earlier entries have been primed; all existing mutable receiver and
slot guards remain on every hit path. Measurements below are three alternating
Node/Perry samples with matching checksums from the two-entry implementation;
the four-entry extension has an additional correctness-only parity run below.

| Cell | Node median | Perry median | Writes | Checksum |
| --- | ---: | ---: | ---: | ---: |
| `shape_monomorphic` | 133 ms | 123 ms | 120,000,000 | 122,876,400 |
| `shape_two` | 133 ms | 667 ms | 96,000,000 | 98,876,400 |
| `shape_four` | 110 ms | 461 ms | 60,000,000 | 62,876,400 |

The two-shape case improves substantially over the prior monomorphic-cache
fallback (~5.3 s). The four-entry extension beats the prior four-shape
fallback (~3.0 s) while preserving exact parity.

The final 15-pair raw samples were Node
`[108, 110, 110, 109, 113, 109, 109, 110, 108, 110, 113, 108, 109, 111, 111]`
and Perry
`[461, 464, 461, 461, 463, 472, 460, 492, 459, 461, 459, 460, 460, 460, 463]`.
6 changes: 6 additions & 0 deletions changelog.d/6812-polymorphic-write-pic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Bounded polymorphic object-write PIC

Static object writes now retain a second shape/slot cache entry. Stable
two-shape receiver sites can stay on the guarded direct-store path while
exotic, mutable, and higher-polymorphism cases continue through the complete
runtime miss path.
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ pub(super) fn compile_closure(
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ pub(super) fn compile_module_entry(
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down Expand Up @@ -1510,7 +1510,7 @@ pub(super) fn compile_module_entry(
// three symbols must be defined exactly once per shared library.
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ pub(super) fn compile_function(
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ pub(super) fn compile_method(
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down Expand Up @@ -1593,7 +1593,7 @@ pub(super) fn compile_static_method(
}
for ic_name in &ic_globals {
llmod.add_raw_global(format!(
"@{} = private global [2 x i64] zeroinitializer",
"@{} = private global [8 x i64] zeroinitializer",
ic_name
));
}
Expand Down
157 changes: 154 additions & 3 deletions crates/perry-codegen/src/expr/proxy_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,30 @@ fn lower_put_value_static_write_ic(
let above_handles = ctx.block().icmp_ugt(I64, &target_handle, "1048575"); // 0x100000
let heap_candidate = ctx.block().and(I1, &pointer_tag, &above_handles);
let guard_idx = ctx.new_block("put.pic.guard");
let guard2_idx = ctx.new_block("put.pic.guard2");
let guard3_idx = ctx.new_block("put.pic.guard3");
let guard4_idx = ctx.new_block("put.pic.guard4");
let fallback_idx = ctx.new_block("put.pic.fallback");
let dispatch3_idx = ctx.new_block("put.pic.dispatch3");
let dispatch4_idx = ctx.new_block("put.pic.dispatch4");
let hit_idx = ctx.new_block("put.pic.hit");
let miss_idx = ctx.new_block("put.pic.miss");
let miss2_idx = ctx.new_block("put.pic.miss2");
let miss3_idx = ctx.new_block("put.pic.miss3");
let miss4_idx = ctx.new_block("put.pic.miss4");
let merge_idx = ctx.new_block("put.pic.merge");
let guard_label = ctx.block_label(guard_idx);
let guard2_label = ctx.block_label(guard2_idx);
let guard3_label = ctx.block_label(guard3_idx);
let guard4_label = ctx.block_label(guard4_idx);
let fallback_label = ctx.block_label(fallback_idx);
let dispatch3_label = ctx.block_label(dispatch3_idx);
let dispatch4_label = ctx.block_label(dispatch4_idx);
let hit_label = ctx.block_label(hit_idx);
let miss_label = ctx.block_label(miss_idx);
let miss2_label = ctx.block_label(miss2_idx);
let miss3_label = ctx.block_label(miss3_idx);
let miss4_label = ctx.block_label(miss4_idx);
let merge_label = ctx.block_label(merge_idx);
ctx.block()
.cond_br(&heap_candidate, &guard_label, &miss_label);
Expand Down Expand Up @@ -374,21 +392,106 @@ fn lower_put_value_static_write_ic(
hit = ctx.block().and(I1, &hit, &token_nonzero);
hit = ctx.block().and(I1, &hit, &slot_in_bounds);

ctx.block().cond_br(&hit, &hit_label, &miss_label);
ctx.block().cond_br(&hit, &hit_label, &fallback_label);

// A second bounded cache entry handles stable polymorphism without
// changing the miss ABI. The first entry is filled initially; only after
// it contains a different shape do we consult/prime the second entry.
ctx.current_block = fallback_idx;
let first_empty = ctx.block().icmp_eq(I64, &cached_token, "0");
ctx.block()
.cond_br(&first_empty, &miss_label, &guard2_label);

ctx.current_block = guard2_idx;
let cached2_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "2")]);
let cached2_token = ctx.block().load(I64, &cached2_token_ptr);
let cached2_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "3")]);
let slot2 = ctx.block().load(I64, &cached2_slot_ptr);
let token2_match = ctx.block().icmp_eq(I64, &shape_token, &cached2_token);
let token2_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0");
let slot2_in_bounds = ctx.block().icmp_ult(I64, &slot2, &inline_limit);
let mut hit2 = ctx.block().and(I1, &heap_candidate, &gc_object);
hit2 = ctx.block().and(I1, &hit2, &not_forwarded);
hit2 = ctx.block().and(I1, &hit2, &flags_clear);
hit2 = ctx.block().and(I1, &hit2, &regular);
hit2 = ctx.block().and(I1, &hit2, &class_nonzero);
hit2 = ctx.block().and(I1, &hit2, &not_native_module);
hit2 = ctx.block().and(I1, &hit2, &token2_match);
hit2 = ctx.block().and(I1, &hit2, &token2_nonzero);
hit2 = ctx.block().and(I1, &hit2, &slot2_in_bounds);
ctx.block().cond_br(&hit2, &hit_label, &dispatch3_label);

ctx.current_block = dispatch3_idx;
let second_empty = ctx.block().icmp_eq(I64, &cached2_token, "0");
ctx.block()
.cond_br(&second_empty, &miss2_label, &guard3_label);

ctx.current_block = guard3_idx;
let cached3_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "4")]);
let cached3_token = ctx.block().load(I64, &cached3_token_ptr);
let cached3_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "5")]);
let slot3 = ctx.block().load(I64, &cached3_slot_ptr);
let token3_match = ctx.block().icmp_eq(I64, &shape_token, &cached3_token);
let token3_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0");
let slot3_in_bounds = ctx.block().icmp_ult(I64, &slot3, &inline_limit);
let mut hit3 = ctx.block().and(I1, &heap_candidate, &gc_object);
hit3 = ctx.block().and(I1, &hit3, &not_forwarded);
hit3 = ctx.block().and(I1, &hit3, &flags_clear);
hit3 = ctx.block().and(I1, &hit3, &regular);
hit3 = ctx.block().and(I1, &hit3, &class_nonzero);
hit3 = ctx.block().and(I1, &hit3, &not_native_module);
hit3 = ctx.block().and(I1, &hit3, &token3_match);
hit3 = ctx.block().and(I1, &hit3, &token3_nonzero);
hit3 = ctx.block().and(I1, &hit3, &slot3_in_bounds);
ctx.block().cond_br(&hit3, &hit_label, &dispatch4_label);

ctx.current_block = dispatch4_idx;
let third_empty = ctx.block().icmp_eq(I64, &cached3_token, "0");
ctx.block()
.cond_br(&third_empty, &miss3_label, &guard4_label);

ctx.current_block = guard4_idx;
let cached4_token_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "6")]);
let cached4_token = ctx.block().load(I64, &cached4_token_ptr);
let cached4_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "7")]);
let slot4 = ctx.block().load(I64, &cached4_slot_ptr);
let token4_match = ctx.block().icmp_eq(I64, &shape_token, &cached4_token);
let token4_nonzero = ctx.block().icmp_ne(I64, &shape_token, "0");
let slot4_in_bounds = ctx.block().icmp_ult(I64, &slot4, &inline_limit);
let mut hit4 = ctx.block().and(I1, &heap_candidate, &gc_object);
hit4 = ctx.block().and(I1, &hit4, &not_forwarded);
hit4 = ctx.block().and(I1, &hit4, &flags_clear);
hit4 = ctx.block().and(I1, &hit4, &regular);
hit4 = ctx.block().and(I1, &hit4, &class_nonzero);
hit4 = ctx.block().and(I1, &hit4, &not_native_module);
hit4 = ctx.block().and(I1, &hit4, &token4_match);
hit4 = ctx.block().and(I1, &hit4, &token4_nonzero);
hit4 = ctx.block().and(I1, &hit4, &slot4_in_bounds);
ctx.block().cond_br(&hit4, &hit_label, &miss4_label);

ctx.current_block = hit_idx;
let selected_slot = ctx.block().phi(
I64,
&[
(&slot, &guard_label),
(&slot2, &guard2_label),
(&slot3, &guard3_label),
(&slot4, &guard4_label),
],
);

let pointer_possible = !(is_numeric_expr(ctx, value)
|| expr_produces_non_pointer_bits_by_construction(ctx, value));
{
let header_size =
crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();
let blk = ctx.block();
let slot_offset = blk.shl(I64, &slot, "3");
let slot_offset = blk.shl(I64, &selected_slot, "3");
let fields_base = blk.add(I64, &target_handle, &header_size);
let field_addr = blk.add(I64, &fields_base, &slot_offset);
let field_ptr = blk.inttoptr(I64, &field_addr);
if pointer_possible {
let slot_i32 = blk.trunc(I64, &slot, I32);
let slot_i32 = blk.trunc(I64, &selected_slot, I32);
emit_jsvalue_slot_store_scalar_aware_on_block(
blk,
&field_ptr,
Expand Down Expand Up @@ -428,12 +531,60 @@ fn lower_put_value_static_write_ic(
let miss_end_label = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = miss2_idx;
let miss2_value = ctx.block().call(
DOUBLE,
"js_put_value_set_ic_miss",
&[
(DOUBLE, &target_value),
(I64, &key_handle),
(DOUBLE, &stored_value),
(I32, strict_i32),
(PTR, &cached2_token_ptr),
],
);
let miss2_end_label = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = miss3_idx;
let miss3_value = ctx.block().call(
DOUBLE,
"js_put_value_set_ic_miss",
&[
(DOUBLE, &target_value),
(I64, &key_handle),
(DOUBLE, &stored_value),
(I32, strict_i32),
(PTR, &cached3_token_ptr),
],
);
let miss3_end_label = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = miss4_idx;
let miss4_value = ctx.block().call(
DOUBLE,
"js_put_value_set_ic_miss",
&[
(DOUBLE, &target_value),
(I64, &key_handle),
(DOUBLE, &stored_value),
(I32, strict_i32),
(PTR, &cached4_token_ptr),
],
);
let miss4_end_label = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = merge_idx;
let result = ctx.block().phi(
DOUBLE,
&[
(&stored_value, &hit_end_label),
(&miss_value, &miss_end_label),
(&miss2_value, &miss2_end_label),
(&miss3_value, &miss3_end_label),
(&miss4_value, &miss4_end_label),
],
);
Ok(Some(result))
Expand Down
11 changes: 9 additions & 2 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13481,6 +13481,13 @@ fn static_put_value_uses_write_pic_for_call_free_rhs() {
ir.contains("4611686018427387904") && ir.contains("1073741824"),
"the write PIC must mirror the read PIC's discriminated, never-reused ShapeId token"
);
assert!(
ir.contains("put.pic.guard2")
&& ir.contains("put.pic.guard3")
&& ir.contains("put.pic.guard4")
&& ir.contains("put.pic.miss4"),
"the write PIC should retain four bounded shape entries"
Comment on lines +13485 to +13489

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the second and third miss paths too.

The existing generic put.pic.miss check only covers the first entry, while this new assertion checks only put.pic.miss4 explicitly. Add put.pic.miss2 and put.pic.miss3; otherwise either path could regress while the “four bounded shape entries” assertion still passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/native_proof_regressions.rs` around lines 13485 -
13489, Extend the write PIC assertions in the regression test to require both
put.pic.miss2 and put.pic.miss3 in addition to the existing miss checks. Keep
the guard2, guard3, guard4, and miss4 assertions unchanged so all four bounded
shape miss paths are validated.

);
}

#[test]
Expand Down Expand Up @@ -13746,8 +13753,8 @@ fn nested_same_shape_object_writes_version_one_through_four_fields() {
rejected
.matches("call double @js_put_value_set_ic_miss")
.count(),
5,
"the bounded rejection must preserve all five semantic write sites:\n{rejected}"
20,
"the bounded rejection must preserve all four cache miss entries for all five semantic write sites:\n{rejected}"
);

let mut nonfinite_body = loop_body(1);
Expand Down
Loading