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
1 change: 1 addition & 0 deletions changelog.d/13163-integration-witnesses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix(test): align integration witnesses with injective symbols, current array layout, and validated per-site ShapeId reads.
28 changes: 17 additions & 11 deletions crates/perry-codegen/tests/export_function_alias_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,27 @@ fn dollar_export_aliases_resolve_function_identity_not_sanitized_spelling() {
}
// An exported function alias can carry a local name different from the
// original body. The recorded function ID is authoritative in that case.
module.exports.push(Export::Named {
local: "alias".into(),
exported: "$renamedWide".into(),
});
module.exported_functions.push(("$renamedWide".into(), 3));
for (exported, id) in [("$renamedN", 1), ("$renamedWide", 3)] {
module.exports.push(Export::Named {
local: "alias".into(),
exported: exported.into(),
});
module.exported_functions.push((exported.into(), id));
}
assert_ne!(
perry_codegen::user_function_symbol(&module.name, "$n"),
perry_codegen::user_function_symbol(&module.name, "_n"),
"dollar and underscore bindings must remain distinct",
);
let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap();
for (exported, body, arity) in [
("$n", "$n", 0),
("$wide", "$wide", 6),
("$renamedWide", "$wide", 6),
] {
for (exported, body, arity) in [("$renamedN", "$n", 0), ("$renamedWide", "$wide", 6)] {
let target = perry_codegen::user_function_symbol(&module.name, body);
for wrapper in [false, true] {
let prefix = if wrapper { "__perry_wrap_" } else { "" };
let symbol = format!("{prefix}perry_fn_export_collision_js__{exported}");
let symbol = format!(
"{prefix}{}",
perry_codegen::user_function_symbol(&module.name, exported)
);
let definition = ir
.split(&format!("define double @{symbol}("))
.nth(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn namespace_variable_getters_preserve_raw_local_binding_names() {
.and_then(|rest| rest.split("\n}").next())
.expect("the namespace live getter must be emitted");
assert!(
getter.contains("call double @perry_fn_provider_js__$item()"),
getter.contains("call double @perry_fn_provider_js__.u24item()"),
"namespace getter must read the raw dollar binding:\n{getter}"
);
assert!(!getter.contains("call double @perry_fn_provider_js___item()"));
Expand Down
95 changes: 56 additions & 39 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1367,12 +1367,21 @@ fn guards_array_read_inline(ir: &str) -> bool {
&& ir.contains("call double @perry_array_read_cold_shared(")
}

/// A property read that holds no site state: the receiver layout's probe
/// (`perry_probe_get*`, a GC-leaf over the live shape) and, when it declines,
/// the complete `[[Get]]` (`js_object_get_field_slow`).
#[path = "native_proof_support/read_memo_contract.rs"]
mod read_memo_contract;

/// #12986 selects validated shape/slot memos where getters are reachable.
/// Both variants retain a complete [[Get]] miss; selected memo entrances must
/// pass the same site storage to that miss.
fn is_layout_probe_read(body: &str) -> bool {
body.contains("call double @perry_probe_get")
&& body.contains("call double @js_object_get_field_slow(")
if body.contains("call double @perry_probe_get_memo(")
|| body.contains("call double @perry_probe_get_value_memo(")
{
read_memo_contract::paired_memo_reads(body).is_some()
} else {
body.contains("call double @perry_probe_get")
&& body.contains("call double @js_object_get_field_slow(")
}
}

/// Whether `ir` births a HEAP receiver object. A class instance is born by the
Expand Down Expand Up @@ -1593,14 +1602,8 @@ fn pod_layout_constants_specialize_generic_layout_type_params() {
module.functions.retain(|func| func.type_params.is_empty());
module.init.clear();
let ir = compile_ir_for_module_with_opts(module, pod_layout_specialization_opts()).unwrap();
let tiny_ir = function_ir_section(
&ir,
"perry_fn_pod_layout_specialization_ts__u_layout_24_Tiny",
);
let wide_ir = function_ir_section(
&ir,
"perry_fn_pod_layout_specialization_ts__u_layout_24_Wide",
);
let tiny_ir = function_ir_section(&ir, "perry_fn_pod_layout_specialization_ts__layout.u24Tiny");
let wide_ir = function_ir_section(&ir, "perry_fn_pod_layout_specialization_ts__layout.u24Wide");

assert!(
tiny_ir.contains("8.0") && tiny_ir.contains("4.0") && !tiny_ir.contains("16.0"),
Expand Down Expand Up @@ -1641,11 +1644,11 @@ fn native_pod_view_specializes_generic_layout_type_params() {
let ir = compile_ir_for_module_with_opts(module, pod_layout_specialization_opts()).unwrap();
let tiny_ir = function_ir_section(
&ir,
"perry_fn_native_pod_view_specialization_ts__u_view_24_Tiny",
"perry_fn_native_pod_view_specialization_ts__view.u24Tiny",
);
let wide_ir = function_ir_section(
&ir,
"perry_fn_native_pod_view_specialization_ts__u_view_24_Wide",
"perry_fn_native_pod_view_specialization_ts__view.u24Wide",
);

assert!(
Expand Down Expand Up @@ -13740,7 +13743,7 @@ fn typed_f64_closure_clone_emits_internal_clone_and_guarded_direct_call() {
"numeric-guard failure should target the internal generic closure body:\n{ir}"
);
assert!(
!ir.contains(&format!("call double @{public}(i64 ")),
!wrapper_ir.contains(&format!("call double @{public}(i64 ")),
"typed guard failure must not recursively call the public closure wrapper:\n{ir}"
);
assert!(
Expand Down Expand Up @@ -13899,7 +13902,7 @@ fn typed_i32_closure_clone_emits_internal_clone_and_guarded_direct_call() {
"Int32-guard failure should target the internal generic closure body:\n{ir}"
);
assert!(
!ir.contains(&format!("call double @{public}(i64 ")),
!wrapper_ir.contains(&format!("call double @{public}(i64 ")),
"typed guard failure must not recursively call the public closure wrapper:\n{ir}"
);
assert!(
Expand Down Expand Up @@ -14098,7 +14101,7 @@ fn typed_i1_closure_clone_emits_internal_clone_and_guarded_direct_call() {
"boolean-guard failure should target the internal generic closure body:\n{ir}"
);
assert!(
!ir.contains(&format!("call double @{public}(i64 ")),
!wrapper_ir.contains(&format!("call double @{public}(i64 ")),
"typed guard failure must not recursively call the public closure wrapper:\n{ir}"
);
assert!(
Expand Down Expand Up @@ -14180,7 +14183,7 @@ fn typed_i1_numeric_predicate_closure_uses_f64_params_and_guarded_direct_call()
"numeric-guard failure should target the internal generic closure body:\n{ir}"
);
assert!(
!ir.contains(&format!("call double @{public}(i64 ")),
!wrapper_ir.contains(&format!("call double @{public}(i64 ")),
"typed guard failure must not recursively call the public closure wrapper:\n{ir}"
);

Expand Down Expand Up @@ -14345,7 +14348,7 @@ fn typed_string_closure_clone_emits_internal_clone_and_guarded_direct_call() {
"string-guard failure should target the internal generic closure body:\n{ir}"
);
assert!(
!ir.contains(&format!("call double @{public}(i64 ")),
!wrapper_ir.contains(&format!("call double @{public}(i64 ")),
"typed guard failure must not recursively call the public closure wrapper:\n{ir}"
);
assert!(
Expand Down Expand Up @@ -16652,7 +16655,7 @@ fn packed_clone_reads_a_foreign_counter_without_the_feedback_call() {
);
}
#[test]
fn static_method_certificate_retains_canonical_layout_clone() {
fn static_method_shape_guard_retains_canonical_layout_clone() {
let mut fixture = typed_i32_method_clone_module("eligible");
fixture.name = "static_certificate.ts".to_string();
// Receiver-reading body: its exact layout clone must remain reachable.
Expand All @@ -16679,6 +16682,7 @@ fn static_method_certificate_retains_canonical_layout_clone() {
let mut derived = fixture.classes[0].clone();
derived.id = 206;
derived.name = "DerivedBits".to_string();
derived.extends = Some(fixture.classes[0].id);
derived.extends_name = Some("Bits".to_string());
derived.fields.clear();
derived.methods[0].id = 231;
Expand All @@ -16687,39 +16691,52 @@ fn static_method_certificate_retains_canonical_layout_clone() {
let incomplete = String::from_utf8(compile_module(&fixture, opts.clone()).unwrap()).unwrap();
let incomplete_caller = body_ir_section(&incomplete, "perry_fn_static_certificate_ts__probe");
assert!(
!incomplete_caller.contains("pconst."),
"incomplete prototypes acquired fast arms: {incomplete_caller}"
!incomplete_caller.contains("pconst.")
&& incomplete_caller.contains("icmp ult i32")
&& incomplete_caller.contains("method_direct.fallback."),
"an unassigned layout must be guarded by the runtime ShapeId domain: {incomplete_caller}"
);
// Production compiles assign whole-program shapes before lowering sites.
let births = perry_codegen::module_birth_shapes(&fixture, opts.clone()).unwrap();
opts.static_shape_ids = perry_codegen::assign_static_shape_ids(births.iter().map(|b| &b.shape))
.into_iter()
.collect();
let ir = String::from_utf8(compile_module(&fixture, opts).unwrap()).unwrap();
let caller = body_ir_section(&ir, "perry_fn_static_certificate_ts__probe");
assert!(caller.contains("pconst.selected"), "{caller}");
assert!(
caller.contains(") noinline"),
"cold selected bodies must stay shared: {caller}"
);
// The single-class arm uses the exact live (class, ShapeId) word guard,
// not the polymorphic pconst switch. Both retain the canonical-layout body.
let deref: String = caller
.lines()
.skip_while(|line| !line.starts_with("method_direct.inline_deref."))
.skip(1)
.take_while(|line| line.is_empty() || line.starts_with(' '))
.collect::<Vec<_>>()
.join("\n");
assert!(
!caller.contains("@PERRY_PROTO_VALIDITY") && !caller.contains("certify"),
"{caller}"
deref.contains("load i64")
&& deref.contains("icmp eq i64")
&& deref.contains("icmp ult i32")
&& deref.contains("label %method_direct.fast.")
&& deref.contains("label %method_direct.fallback."),
"the canonical receiver word and ShapeId domain must guard the fast arm: {deref}"
);
assert!(
caller.contains("method_direct.layout") && caller.contains("$pshape"),
"{caller}"
caller.contains("call double @perry_method_static_certificate_ts__Bits__mix_i32$pshape(")
&& caller.contains("call double @js_native_call_method_by_id("),
"the guarded layout clone and complete miss must both remain live: {caller}"
);
let public = function_ir_section(&ir, "perry_fn_static_certificate_ts__probe");
assert!(
caller.contains("icmp eq i64"),
"canonical receiver-word compare missing: {caller}"
public.contains(") noinline"),
"the public wrapper stays shared: {public}"
);
assert!(
!caller.contains("holder.") && !caller.contains("perry_class_value_dir_cell"),
"live holder walk returned: {caller}"
caller.contains("call ptr @perry_class_value_dir_cell("),
"the prototype holder must be checked from its live descriptor: {caller}"
);
assert!(
!caller.contains("js_native_call_method_by_id_learn"),
"second learned memo returned: {caller}"
!caller.contains("@PERRY_PROTO_VALIDITY")
&& !caller.contains("certify")
&& !caller.contains("js_native_call_method_by_id_learn"),
"a detached validity/learned proof must not replace live shape authority: {caller}"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! #12986: selected read sites pair a validated memo leaf with its complete miss.

pub fn paired_memo_reads(ir: &str) -> Option<usize> {
let mut count = 0;
for line in ir.lines().filter(|line| {
line.contains("call double @perry_probe_get_memo(")
|| line.contains("call double @perry_probe_get_value_memo(")
}) {
let (_, site) = line.rsplit_once(", ptr @")?;
if !ir.lines().any(|miss| {
(miss.contains("call double @js_object_get_field_value_site(")
|| miss.contains("call double @js_object_get_field_site_slow("))
&& miss
.rsplit_once(", ptr @")
.is_some_and(|(_, other)| other == site)
}) {
return None;
}
count += 1;
}
(count > 0).then_some(count)
}

#[test]
fn memo_pair_gate_rejects_missing_or_wrong_site_misses() {
let leaf = "%v = call double @perry_probe_get_memo(ptr %d, i64 %o, i64 %k, ptr @site)";
let miss = "%v = call double @js_object_get_field_value_site(i64 %o, i64 %k, i64 0, ptr @site)";
assert_eq!(paired_memo_reads(&format!("{leaf}\n{miss}")), Some(1));
assert_eq!(paired_memo_reads(leaf), None);
assert_eq!(
paired_memo_reads(&format!("{leaf}\n{}", miss.replace("@site)", "@other)"))),
None
);
assert_eq!(paired_memo_reads(miss), None);
}
52 changes: 38 additions & 14 deletions crates/perry-codegen/tests/typed_feedback.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use perry_codegen::{compile_module, AppMetadata, CompileOptions};

#[path = "native_proof_support/read_memo_contract.rs"]
mod read_memo_contract;
use perry_hir::types::{FunctionType, Type};
use perry_hir::{
ArgumentsObjectMeta, BinaryOp, Class, ClassField, Expr, Function, Module, ModuleInitKind,
Expand Down Expand Up @@ -615,7 +618,11 @@ fn a_default_build_emits_no_typed_feedback_recording_calls() {
// this test proves the RECORDING is gone, not the program.
assert!(
ir.contains("call double @perry_probe_get(")
&& ir.contains("call double @js_object_get_field_slow("),
&& read_memo_contract::paired_memo_reads(&ir) == Some(1)
&& ir
.matches(" = private global [5 x i64] zeroinitializer")
.count()
== 1,
"the property reads themselves must still be lowered; emitted:\n{ir}"
);
// And the helpers that DECIDE something, rather than merely counting, are
Expand All @@ -626,7 +633,7 @@ fn a_default_build_emits_no_typed_feedback_recording_calls() {
// all emitted in a default build, as CALLS (every runtime entry is
// `declare`d, so a bare symbol match would be satisfied by the declaration).
assert!(
ir.contains("call double @js_object_get_field_nonptr("),
ir.contains("call double @js_object_get_field_value_site("),
"dispatching wrappers must still be emitted in a default build \
(property get):\n{ir}"
);
Expand Down Expand Up @@ -809,24 +816,41 @@ fn full_outline_ic_collapses_class_field_get_to_single_call() {

let _lock = env_lock();

// The read site is ALREADY the collapsed form in both modes: one GC-leaf
// `perry_probe_get_value` (the hit) and a cold collecting
// `js_object_get_field_value` (the miss), with no tower, per-site word or
// class guard to outline. Forcing the full outline changes nothing.
// Both modes keep one leaf and a complete collecting miss. #12986 selects
// a validated memo in the outlined mode; the non-outlined own field keeps
// the stateless entrance. No memo storage belongs to the latter site.
for mode in ["1", "0"] {
let _g = EnvVarGuard::set("PERRY_FULL_OUTLINE_IC", Some(mode));
let ir = ir_for(build());
// One probe per lowering of the body (the guarded clone and the
// generic body each hold exactly one site).
let probes = ir.matches("call double @perry_probe_get_value(").count();
let entrance = if mode == "1" {
"call double @perry_probe_get_value_memo("
} else {
"call double @perry_probe_get_value("
};
let probes = ir.matches(entrance).count();
assert!(
(1..=2).contains(&probes),
"full-outline={mode}: one value probe per body, saw {probes}:\n{ir}"
);
assert!(
ir.contains("call double @js_object_get_field_value("),
"full-outline={mode}: the cold miss is the complete [[Get]]:\n{ir}"
);
if mode == "1" {
assert_eq!(
read_memo_contract::paired_memo_reads(&ir),
Some(probes),
"{ir}"
);
} else {
assert!(
ir.contains("call double @js_object_get_field_value("),
"{ir}"
);
assert_eq!(read_memo_contract::paired_memo_reads(&ir), None, "{ir}");
assert!(
!ir.contains(" = private global [5 x i64] zeroinitializer"),
"{ir}"
);
}
assert!(!ir.contains("_packed_get"), "{mode}");
assert!(
!ir.contains("call double @js_object_get_field_ic_fast("),
Expand Down Expand Up @@ -1343,10 +1367,10 @@ fn typed_feedback_marks_numeric_array_literals() {
)))],
));

// GcHeader: size=48 (8-byte GcHeader, 16-byte ArrayHeader with its
// GcHeader: size=56 (8-byte GcHeader, 24-byte ArrayHeader with its
// `named` word, three slots), pointer-free/raw-f64 reserved bits=0x4080,
// flags=ARENA, type=ARRAY. Live birth flags are ORed into this word.
let numeric_header = (48u64 << 32) | (0x4080u64 << 16) | 0x0201;
let numeric_header = (56u64 << 32) | (0x4080u64 << 16) | 0x0201;
assert!(numeric_ir.contains(&numeric_header.to_string()));
assert!(!numeric_ir.contains("call i32 @js_array_mark_numeric_f64_layout"));

Expand All @@ -1361,7 +1385,7 @@ fn typed_feedback_marks_numeric_array_literals() {
));

assert!(!mixed_ir.contains("call i32 @js_array_mark_numeric_f64_layout"));
let mixed_raw_f64_header = (32u64 << 32) | (0x4080u64 << 16) | 0x0201;
let mixed_raw_f64_header = (48u64 << 32) | (0x4080u64 << 16) | 0x0201;
assert!(!mixed_ir.contains(&mixed_raw_f64_header.to_string()));
}

Expand Down
Loading