diff --git a/changelog.d/13163-integration-witnesses.md b/changelog.d/13163-integration-witnesses.md new file mode 100644 index 00000000000..3a03a41a971 --- /dev/null +++ b/changelog.d/13163-integration-witnesses.md @@ -0,0 +1 @@ +fix(test): align integration witnesses with injective symbols, current array layout, and validated per-site ShapeId reads. diff --git a/crates/perry-codegen/tests/export_function_alias_identity.rs b/crates/perry-codegen/tests/export_function_alias_identity.rs index 44178b7c3f1..9497fdea5f7 100644 --- a/crates/perry-codegen/tests/export_function_alias_identity.rs +++ b/crates/perry-codegen/tests/export_function_alias_identity.rs @@ -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) diff --git a/crates/perry-codegen/tests/namespace_getter_binding_identity.rs b/crates/perry-codegen/tests/namespace_getter_binding_identity.rs index 5c2e13d3a60..9e42013b848 100644 --- a/crates/perry-codegen/tests/namespace_getter_binding_identity.rs +++ b/crates/perry-codegen/tests/namespace_getter_binding_identity.rs @@ -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()")); diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 3abc71aff9b..f829037ea92 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -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 @@ -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"), @@ -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!( @@ -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!( @@ -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!( @@ -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!( @@ -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}" ); @@ -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!( @@ -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. @@ -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; @@ -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::>() + .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}" ); } diff --git a/crates/perry-codegen/tests/native_proof_support/read_memo_contract.rs b/crates/perry-codegen/tests/native_proof_support/read_memo_contract.rs new file mode 100644 index 00000000000..e200f94ed1d --- /dev/null +++ b/crates/perry-codegen/tests/native_proof_support/read_memo_contract.rs @@ -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 { + 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); +} diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index a1bd7c2202e..f84b62cad3e 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -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, @@ -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 @@ -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}" ); @@ -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("), @@ -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")); @@ -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())); }