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/7485-repsel-4b-field-store-elision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Repsel Phase 4b cleanup: dead `PERRY_UNBOXED_OBJECT_FIELDS` prototype deleted.** The Phase 4b (narrow) mandatory items — class-field store note/addref elision on `Ptr<Shape>`-proven receivers (4b.1) and the INT32 layout-poison canonicalization (4b.2) — landed in #6919/#6930; this ships the remaining sanctioned item. The prototype's write path was bit-identical to the default typed-shape path and its read side was never implemented, so the env flag, the hard-coded `{x,y}` object-literal matcher, `js_gc_init_unboxed_object_layout`, `js_object_{get,set}_unboxed_f64_field`, the codegen declarations/`gc_call_effects` entry, and the flag's object-cache/build-cache key entries are gone. The `js_typed_feedback_object_set_unboxed_f64_field` sentinel symbol stays (`check_runtime_symbols.sh` + keepalive-anchor surface, #854 foundation); its guarded fast path now uses the plain indexed setter, which routes through the identical `runtime_store_jsvalue_slot`. GC layout tests that used the prototype installer as a store convenience moved onto the load-bearing `js_gc_init_typed_shape_layout` (`layout_trace/unboxed_object.rs` → `layout_trace/object_layout_invalidation.rs`), preserving the shape-change invalidation coverage (dynamic add / delete / `defineProperty` / accessors) on the default path; prototype-only twins of existing typed-shape tests were dropped. Full gap suite: zero changed outcomes vs a same-session pristine-main baseline (measured twice, before and after rebasing onto #7474's main). The `addr_class` ratchet baseline tightened by the deleted setter's bare-address site.
139 changes: 1 addition & 138 deletions crates/perry-codegen/src/expr/object_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::temp_root::{
};
use super::{lower_expr, nanbox_pointer_inline, FnCtx};
use crate::nanbox::POINTER_MASK_I64;
use crate::type_analysis::{compute_auto_captures, is_numeric_expr};
use crate::type_analysis::compute_auto_captures;
use crate::types::{DOUBLE, I32, I64, PTR};

fn expected_interface_property_type(
Expand Down Expand Up @@ -120,87 +120,6 @@ fn typed_object_literal_layout(
})
}

fn unboxed_object_fields_enabled() -> bool {
matches!(
std::env::var("PERRY_UNBOXED_OBJECT_FIELDS").as_deref(),
Ok("1")
)
}

fn is_number_type(ty: &HirType) -> bool {
matches!(ty, HirType::Number)
}

fn object_type_is_exact_xy_number(ty: &perry_hir::types::ObjectType) -> bool {
ty.index_signature.is_none()
&& ty.properties.len() == 2
&& ty
.properties
.get("x")
.map(|prop| is_number_type(&prop.ty))
.unwrap_or(false)
&& ty
.properties
.get("y")
.map(|prop| is_number_type(&prop.ty))
.unwrap_or(false)
}

fn interface_is_exact_xy_number(iface: &perry_hir::Interface) -> bool {
iface.extends.is_empty()
&& iface.methods.is_empty()
&& iface.properties.len() == 2
&& iface
.properties
.iter()
.any(|prop| prop.name == "x" && is_number_type(&prop.ty))
&& iface
.properties
.iter()
.any(|prop| prop.name == "y" && is_number_type(&prop.ty))
}

fn expected_type_is_exact_xy_number(ctx: &FnCtx<'_>, expected_ty: &HirType, depth: usize) -> bool {
if depth > 32 {
return false;
}
match expected_ty {
HirType::Object(obj) => object_type_is_exact_xy_number(obj),
HirType::Named(name) => {
if let Some(alias) = ctx.type_aliases.get(name) {
if expected_type_is_exact_xy_number(ctx, alias, depth + 1) {
return true;
}
}
ctx.interfaces
.get(name)
.map(interface_is_exact_xy_number)
.unwrap_or(false)
}
_ => false,
}
}

fn unboxed_xy_object_literal(
ctx: &FnCtx<'_>,
props: &[(String, Expr)],
expected_ty: Option<&HirType>,
) -> bool {
if !unboxed_object_fields_enabled() {
return false;
}
if props.len() != 2 || props[0].0 != "x" || props[1].0 != "y" {
return false;
}
let Some(expected_ty) = expected_ty else {
return false;
};
expected_type_is_exact_xy_number(ctx, expected_ty, 0)
&& props
.iter()
.all(|(_, value_expr)| is_numeric_expr(ctx, value_expr))
}

fn emit_object_mask_global(ctx: &mut FnCtx<'_>, kind: &str, mask_words: &[u64]) -> String {
if mask_words.is_empty() {
return "null".to_string();
Expand Down Expand Up @@ -253,13 +172,6 @@ fn emit_object_typed_shape_init(
);
}

fn emit_unboxed_object_layout_init(ctx: &mut FnCtx<'_>, obj_handle: &str) {
ctx.block().call_void(
"js_gc_init_unboxed_object_layout",
&[(I64, obj_handle), (I32, "2"), (I64, "3"), (I64, "0")],
);
}

fn is_generator_iterator_object_literal(props: &[(String, Expr)]) -> bool {
if props.len() != 3 {
return false;
Expand Down Expand Up @@ -343,55 +255,6 @@ pub(crate) fn lower_object_literal(
)
});

if !any_method_closure && unboxed_xy_object_literal(ctx, props, expected_ty) {
let mut packed_keys = String::new();
for (k, _) in props {
packed_keys.push_str(k);
packed_keys.push('\0');
}
let keys_idx = ctx.strings.intern(&packed_keys);
let keys_entry = ctx.strings.entry(keys_idx);
let keys_global = format!("@{}", keys_entry.bytes_global);
let keys_len_str = keys_entry.byte_len.to_string();

let mut shape_id: u32 = 0x811c9dc5;
for b in packed_keys.as_bytes() {
shape_id ^= *b as u32;
shape_id = shape_id.wrapping_mul(0x01000193);
}
if shape_id == 0 {
shape_id = 1;
}
let shape_id_str = shape_id.to_string();

let obj_handle = ctx.block().call(
I64,
"js_object_alloc_with_shape",
&[
(I32, &shape_id_str),
(I32, &n_str),
(PTR, &keys_global),
(I32, &keys_len_str),
],
);

let rooted = rooted_handle_begin(ctx, &obj_handle, protect_handle);
for (i, (_, value_expr)) in props.iter().enumerate() {
let v = lower_expr(ctx, value_expr)?;
let idx_str = i.to_string();
let obj_handle = rooted_handle_get(ctx, &rooted);
ctx.block().call_void(
"js_object_set_unboxed_f64_field",
&[(I64, &obj_handle), (I32, &idx_str), (DOUBLE, &v)],
);
}
let obj_handle = rooted_handle_get(ctx, &rooted);
emit_unboxed_object_layout_init(ctx, &obj_handle);
let boxed = nanbox_pointer_inline(ctx.block(), &obj_handle);
rooted_handle_release(ctx, rooted);
return Ok(boxed);
}

if !any_method_closure && field_count > 0 {
// Build packed keys "k1\0k2\0…" interned in the StringPool (shared
// across all literals with the same key set + order).
Expand Down
1 change: 0 additions & 1 deletion crates/perry-codegen/src/gc_call_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ pub(crate) fn classify_direct_callee(name: &str) -> GcCallEffect {
| "js_gc_note_slot_layout"
| "js_gc_note_slot_layout_aware"
| "js_gc_init_typed_shape_layout"
| "js_gc_init_unboxed_object_layout"
// `typed_feedback.rs`: counters/registries only. This intentionally
// does not include feedback wrappers that perform the actual object
// get/set operation.
Expand Down
7 changes: 0 additions & 7 deletions crates/perry-codegen/src/runtime_decls/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
// js_write_barrier_root_heap_word(child_bits: u64)
// js_gc_note_slot_layout(parent_bits: u64, slot_index: u32, value_bits: u64)
// js_gc_init_typed_shape_layout(obj: u64, slot_count: u32, raw_f64_mask_words: *const u64, raw_f64_mask_word_count: u32, pointer_mask_words: *const u64, pointer_mask_word_count: u32)
// js_gc_init_unboxed_object_layout(obj: u64, slot_count: u32, raw_f64_mask: u64, pointer_mask: u64)
module.declare_function("js_write_barrier", VOID, &[I64, I64]);
module.declare_function("js_write_barrier_slot", VOID, &[I64, I64, I64]);
module.declare_function("js_write_barrier_root_nanbox", VOID, &[I64]);
Expand All @@ -155,12 +154,6 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
VOID,
&[I64, I32, PTR, I32, PTR, I32],
);
module.declare_function(
"js_gc_init_unboxed_object_layout",
VOID,
&[I64, I32, I64, I64],
);

// Array methods (Phase B.12).
// - js_array_pop_f64(arr) -> f64 (last element, NaN if empty)
// - js_array_join(arr, sep) -> *mut StringHeader (i64)
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
// loop hung forever. Declaring the slot as I64 routes through the
// same register class the runtime actually reads.
module.declare_function("js_object_set_field", VOID, &[I64, I32, I64]);
module.declare_function("js_object_set_unboxed_f64_field", VOID, &[I64, I32, DOUBLE]);
module.declare_function("js_object_get_unboxed_f64_field", DOUBLE, &[I64, I32]);
module.declare_function("js_object_set_field_by_name", VOID, &[I64, I64, DOUBLE]);
module.declare_function(
"js_object_set_field_by_property_id",
Expand Down
116 changes: 6 additions & 110 deletions crates/perry-codegen/tests/typed_shape_descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,6 @@ use perry_hir::{
ModuleInitKind, Stmt, UpdateOp,
};

static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

struct EnvVarGuard {
key: &'static str,
prev: Option<std::ffi::OsString>,
}

impl EnvVarGuard {
fn set(key: &'static str, value: Option<&str>) -> Self {
let prev = std::env::var_os(key);
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
Self { key, prev }
}
}

impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.prev {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}

fn empty_opts() -> CompileOptions {
CompileOptions {
target: None,
Expand Down Expand Up @@ -331,10 +304,6 @@ fn assert_typed_feedback_setter_after(ir: &str, start_pos: usize, context: &str)
ir.contains("js_object_set_field_by_name"),
"{context} should keep the safe runtime setter as the typed-feedback fallback"
);
assert!(
!after_start.contains("call void @js_object_set_unboxed_f64_field"),
"{context} should not use the raw unboxed field setter for dynamic mutation"
);
}

fn point_module(name: &str, body: Vec<Stmt>) -> Module {
Expand Down Expand Up @@ -936,46 +905,13 @@ fn typed_object_literal_pointer_free_descriptor_precedes_dynamic_mutation() {
);
}

// The `PERRY_UNBOXED_OBJECT_FIELDS` prototype was deleted (Phase 4b cleanup):
// its write path was bit-identical to the default typed-shape path and its
// read side was never implemented. This test pins the default path the flag
// used to bypass: exact `{x, y}` number literals go through the shape-cache
// allocator, indexed setters, and a typed-shape descriptor install.
#[test]
fn unboxed_point_literal_gate_on_emits_raw_setters_and_pointer_free_layout() {
let _lock = ENV_LOCK.lock().unwrap();
let _env = EnvVarGuard::set("PERRY_UNBOXED_OBJECT_FIELDS", Some("1"));
let point_ty = object_type(&[("x", Type::Number), ("y", Type::Number)]);
let module = point_module(
"unboxed_point_on.ts",
vec![
Stmt::Let {
id: 1,
name: "p".to_string(),
ty: point_ty,
mutable: false,
init: Some(Expr::Object(vec![
("x".to_string(), Expr::Number(1.5)),
("y".to_string(), Expr::Number(2.5)),
])),
},
Stmt::Return(Some(Expr::LocalGet(1))),
],
);

let ir = ir_for(module);
assert!(ir.contains("call i64 @js_object_alloc_with_shape"));
assert!(ir.contains("call void @js_object_set_unboxed_f64_field"));
assert!(ir.contains("call void @js_gc_init_unboxed_object_layout"));
assert!(
ir.contains("i32 2, i64 3, i64 0"),
"unboxed point layout should install raw f64 slots for x/y and no pointer slots"
);
assert!(
!ir.contains("call void @js_gc_init_typed_shape_layout"),
"gate-on exact point literals should use the unboxed layout installer"
);
}

#[test]
fn unboxed_point_literal_gate_off_uses_existing_typed_shape_path() {
let _lock = ENV_LOCK.lock().unwrap();
let _env = EnvVarGuard::set("PERRY_UNBOXED_OBJECT_FIELDS", None);
fn point_literal_uses_typed_shape_path() {
let point_ty = object_type(&[("x", Type::Number), ("y", Type::Number)]);
let module = point_module(
"unboxed_point_off.ts",
Expand All @@ -1000,44 +936,4 @@ fn unboxed_point_literal_gate_off_uses_existing_typed_shape_path() {
assert!(ir.contains("call void @js_gc_init_typed_shape_layout"));
assert!(ir.contains("@perry_typed_obj_shape_raw_f64_mask_"));
assert!(ir.contains("constant [1 x i64] [i64 3]"));
assert!(!ir.contains("call void @js_object_set_unboxed_f64_field"));
assert!(!ir.contains("call void @js_gc_init_unboxed_object_layout"));
}

#[test]
fn unboxed_point_dynamic_mutation_still_uses_safe_by_name_setter() {
let _lock = ENV_LOCK.lock().unwrap();
let _env = EnvVarGuard::set("PERRY_UNBOXED_OBJECT_FIELDS", Some("1"));
let point_ty = object_type(&[("x", Type::Number), ("y", Type::Number)]);
let module = point_module(
"unboxed_point_mutation.ts",
vec![
Stmt::Let {
id: 1,
name: "p".to_string(),
ty: point_ty,
mutable: true,
init: Some(Expr::Object(vec![
("x".to_string(), Expr::Number(1.0)),
("y".to_string(), Expr::Number(2.0)),
])),
},
Stmt::Expr(Expr::PropertySet {
object: Box::new(Expr::LocalGet(1)),
property: "x".to_string(),
value: Box::new(Expr::String("heap".to_string())),
}),
Stmt::Return(Some(Expr::LocalGet(1))),
],
);

let ir = ir_for(module);
let layout_pos = ir
.find("call void @js_gc_init_unboxed_object_layout")
.expect("fixture should install unboxed layout");
assert_typed_feedback_setter_after(
&ir,
layout_pos,
"dynamic property mutation after an unboxed layout",
);
}
Loading
Loading