diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 70ab169f7..ae0ad763f 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -1656,6 +1656,38 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .as_ref() .is_some_and(crate::typed_shape::type_is_raw_f64_candidate); let requires_raw_f64_str = if requires_raw_f64 { "1" } else { "0" }; + // #5391 path 2: oversized modules full-outline the entire + // class-field-GET diamond (guard + fast load + fallback + + // phi) to a single `js_class_field_get_ic(...)` call that + // returns the field value. This shrinks large minified + // user functions enough for clang -O0 to compile them + // (the per-function compile time is superlinear in size). + // Mirrors the field-SET full-outline (#5334 lever B). + if crate::codegen::full_outline_ic_enabled() { + let (key_raw, expected_keys) = { + let blk = ctx.block(); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + let expected_keys = + blk.load(I64, &format!("@{}", keys_global_name)); + (key_raw, expected_keys) + }; + let val = ctx.block().call( + DOUBLE, + "js_class_field_get_ic", + &[ + (I64, &site_id), + (DOUBLE, &recv_box), + (I32, &expected_class_id_str), + (I64, &expected_keys), + (I64, &key_raw), + (I32, &field_idx_str), + (I32, requires_raw_f64_str), + ], + ); + return Ok(val); + } // #5093: build the guard operands once, up front, so both // the inline shape pre-check and the guard-call fallback // can reference them. diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index d68a75475..67aa1fc9f 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -134,6 +134,15 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { I32, &[I64, DOUBLE, I32, I64, I64, I32, I32], ); + // #5391 path 2: class-field-GET inline cache, FULLY outlined. For oversized + // modules the whole get diamond collapses to one call returning the field + // value. Args: (site_id, recv, expected_class_id, expected_keys, key, + // field_index, require_raw_f64). Same signature as the get guard (+ f64 ret). + module.declare_function( + "js_class_field_get_ic", + DOUBLE, + &[I64, DOUBLE, I32, I64, I64, I32, I32], + ); module.declare_function( "js_typed_feedback_native_call_method", DOUBLE, diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index ea9100e91..b4acebd2e 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -366,6 +366,46 @@ fn full_outline_ic_collapses_class_field_set_to_single_call() { } } +#[test] +fn full_outline_ic_collapses_class_field_get_to_single_call() { + // #5391 path 2: full-outline collapses the class-field-GET diamond to a + // single `js_class_field_get_ic` call returning the field value. + let build = || { + let point = class(101, "Point", vec![field("x", Type::Number)]); + module_with_classes( + "full_outline_get.ts", + vec![point], + vec![param(1, "p", Type::Named("Point".to_string()))], + Type::Number, + vec![Stmt::Return(Some(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(1)), + property: "x".to_string(), + }))], + ) + }; + + let _lock = ENV_LOCK.lock().unwrap(); + + // Forced ON: one outlined call, no inline get diamond. + { + let _g = EnvVarGuard::set("PERRY_FULL_OUTLINE_IC", Some("1")); + let ir = ir_for(build()); + assert!(ir.contains("call double @js_class_field_get_ic")); + assert!(!ir.contains("class_field_get.fast")); + assert!(!ir.contains("class_field_get.fallback")); + assert!(!ir.contains("call i32 @js_typed_feedback_class_field_get_guard")); + } + + // Forced OFF: the inline diamond, no full-outline call. + { + let _g = EnvVarGuard::set("PERRY_FULL_OUTLINE_IC", Some("0")); + let ir = ir_for(build()); + assert!(!ir.contains("call double @js_class_field_get_ic")); + assert!(ir.contains("class_field_get.fast")); + assert!(ir.contains("js_typed_feedback_class_field_get_guard")); + } +} + #[test] fn full_outline_ic_auto_gate_counts_class_methods() { // #5334 lever B: the auto size-gate counts class CALLABLES (methods, diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index a6d56af3f..cadabcf7f 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -713,6 +713,59 @@ pub extern "C" fn js_class_field_set_ic( js_class_field_set_fallback(site_id, obj_bits, key_raw, value); } +/// Class-field-GET inline cache, FULLY OUTLINED (#5391 path 2 — extends the +/// #5334 lever-B full-outline from field-SET to field-GET). For oversized +/// modules the entire `class_field_get` diamond (inline precheck + guard call + +/// fast slot load + by-name fallback + phi) collapses to this one call, +/// shrinking the large minified user functions enough for `clang -O0` to +/// compile them in practical time. +/// +/// Reproduces the diamond's semantics: run the same +/// `js_typed_feedback_class_field_get_guard`; on a PASS read the field slot as +/// `f64` (a plain number is self-boxing in nan-boxing, so raw-f64 and boxed +/// slots read identically — matching the inline `class_field_get.fast` plain +/// `load double`); on a FAIL record the fallback and read by name. The full +/// outline drops the inline path's static raw-number type hint (the result is +/// treated as a general JS value), which is value-correct — acceptable on the +/// size-gated full-outline path. +#[no_mangle] +pub extern "C" fn js_class_field_get_ic( + site_id: u64, + receiver: f64, + expected_class_id: u32, + expected_keys: *const ArrayHeader, + key: *const crate::StringHeader, + expected_field_index: u32, + require_raw_f64: i32, +) -> f64 { + let guard_ok = js_typed_feedback_class_field_get_guard( + site_id, + receiver, + expected_class_id, + expected_keys, + key, + expected_field_index, + require_raw_f64, + ); + + if guard_ok != 0 { + let object_addr = normalize_raw_object_addr(receiver.to_bits()); + unsafe { + let fields_ptr = + (object_addr as *const u8).add(std::mem::size_of::()) as *const f64; + return std::ptr::read(fields_ptr.add(expected_field_index as usize)); + } + } + + crate::typed_feedback::js_typed_feedback_record_fallback_call(site_id); + let obj_bits = receiver.to_bits(); + let key_raw = key as u64 & crate::value::POINTER_MASK; + crate::object::js_object_get_field_by_name_f64( + obj_bits as *const ObjectHeader, + key_raw as *const crate::StringHeader, + ) +} + #[no_mangle] pub unsafe extern "C" fn js_typed_feedback_native_call_method( site_id: u64, @@ -951,6 +1004,7 @@ mod keep_guard_symbols { #[used] static G1: extern "C" fn(u64, f64, u32, *const ArrayHeader, *const crate::StringHeader, u32, f64, i32) -> i32 = js_typed_feedback_class_field_set_guard; #[used] static G1C: extern "C" fn(u64, u64, u64, f64) = js_class_field_set_fallback; #[used] static G1D: extern "C" fn(u64, f64, u32, *const ArrayHeader, *const crate::StringHeader, u32, f64, i32) = js_class_field_set_ic; + #[used] static G1E: extern "C" fn(u64, f64, u32, *const ArrayHeader, *const crate::StringHeader, u32, i32) -> f64 = js_class_field_get_ic; #[used] static G2: unsafe extern "C" fn(u64, f64, u32, *const ArrayHeader, *const i8, usize, *const u8) -> i32 = js_typed_feedback_method_direct_call_guard; #[used] static G3: extern "C" fn(u64, f64, *const u8, u32, u32) -> i32 = js_typed_feedback_closure_direct_call_guard; }