diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index 2936d8b4f..47cb71ea3 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -38,9 +38,10 @@ use super::{ emit_layout_note_slot_on_block, emit_shadow_slot_clear, emit_shadow_slot_update_for_expr, emit_string_literal_global, emit_typed_feedback_register_site, emit_v8_export_call, emit_v8_member_method_call, emit_write_barrier, emit_write_barrier_slot_on_block, - expr_is_known_non_pointer_shadow_value, extract_array_of_object_shape, i32_bool_to_nanbox, - import_origin_suffix, is_global_this_builtin_function_name, is_global_this_builtin_name, - is_known_finite, lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, + expr_is_known_non_pointer_shadow_value, expr_produces_non_pointer_bits_by_construction, + extract_array_of_object_shape, i32_bool_to_nanbox, import_origin_suffix, + is_global_this_builtin_function_name, is_global_this_builtin_name, is_known_finite, + lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_index_set_fast, lower_js_args_array, lower_object_literal, lower_stream_super_init, lower_url_string_getter, nanbox_bigint_inline, nanbox_pointer_inline, nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, raw_f64_layout_fact, @@ -370,6 +371,19 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .cond_br(&guard_pass, &fast_label, &fallback_label); ctx.current_block = fast_idx; + // #5334 lever D: a value that is a non-pointer by + // construction (number / bool / undefined / null / + // comparison / arithmetic) creates no parent→child heap + // reference, so the generational write barrier is a + // semantic no-op and can be skipped. Computed before the + // block builder is borrowed below. The LAYOUT NOTE is + // kept regardless: it records the slot's pointer-ness for + // minor-scan skipping, and a non-pointer write into a + // slot that previously held a pointer is a real + // transition the GC must observe. Same soundness standard + // as the array-store barrier elision. + let field_set_barrier_needed = + !expr_produces_non_pointer_bits_by_construction(ctx, value); let blk = ctx.block(); let obj_ptr = blk.inttoptr(I64, &obj_handle); let header_skip = "24".to_string(); @@ -393,7 +407,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { true, &obj_bits, &field_addr, - true, + field_set_barrier_needed, ); } blk.br(&merge_label); diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 9ad0fad33..70673f2d5 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -307,6 +307,45 @@ fn typed_feedback_guards_direct_class_field_specialization() { assert!(ir.contains("call double @js_object_get_field_by_name_f64")); } +#[test] +fn class_field_set_elides_write_barrier_for_nonpointer_value() { + // #5334 lever D: storing a value that is a non-pointer by construction + // into a BOXED class field (a String slot — only Number is raw-f64) skips + // the generational write barrier, since the store creates no parent→child + // heap reference. The layout note still fires (it tracks the slot's + // pointer-ness). A value that may be a heap pointer keeps the barrier. + let build = |val: Expr| { + let c = class(140, "Bx", vec![field("s", Type::String)]); + module_with_classes( + "lever_d_field_barrier.ts", + vec![c], + vec![ + param(1, "o", Type::Named("Bx".to_string())), + param(2, "p", Type::String), + ], + Type::Number, + vec![ + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::LocalGet(1)), + property: "s".to_string(), + value: Box::new(val), + }), + Stmt::Return(Some(Expr::Number(0.0))), + ], + ) + }; + + // A numeric-by-construction value takes the boxed class-field fast store + // but needs no write barrier. + let ir_num = ir_for(build(Expr::Number(7.0))); + assert!(ir_num.contains("class_field_set.fast")); + assert!(!ir_num.contains("call void @js_write_barrier_slot")); + + // A definite heap pointer (string literal) keeps the slot barrier. + let ir_ptr = ir_for(build(Expr::String("hi".to_string()))); + assert!(ir_ptr.contains("call void @js_write_barrier_slot")); +} + #[test] fn typed_feedback_guards_direct_class_method_specialization() { let mut point = class(103, "Point", vec![field("x", Type::Number)]);