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/6796-object-meta-phase-b.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
perf(runtime): #6759 Phase B — `ObjectHeader` grows a trailing `meta: *mut ObjectMeta` slot (24→32 bytes; 20→24 ILP32) backed by a new GC-arena `GC_TYPE_OBJECT_META` record; shaped objects' custom `[[Prototype]]` moves out of the address-keyed mutex registry into the per-object record (two-load reads, structural death — no stale-address hazard). Six hardcoded `obj+24` codegen field bases converted to the target-aware header size (one was latently wrong on arm64_32); `perry-ffi` mirror + layout assertion extended.
23 changes: 16 additions & 7 deletions crates/perry-codegen/src/codegen/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use super::string_pool::emit_string_pool;
/// destructured.
pub(super) struct ModuleArtifactsCtx<'a> {
pub llmod: &'a mut LlModule,
pub target_triple: &'a str,
pub strings: &'a mut StringPool,
pub hir: &'a HirModule,
pub import_function_prefixes: &'a std::collections::HashMap<String, String>,
Expand Down Expand Up @@ -181,6 +182,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
// rest are shared borrows.
let ModuleArtifactsCtx {
llmod,
target_triple,
strings,
hir,
import_function_prefixes,
Expand Down Expand Up @@ -338,13 +340,20 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
.typed_f64_receiver_methods
.get(&(class.name.clone(), method.name.clone()))
{
compile_typed_f64_receiver_method(llmod, class, method, method_names, receiver)
.with_context(|| {
format!(
"lowering typed-f64 receiver method clone '{}::{}'",
class.name, method.name
)
})?;
compile_typed_f64_receiver_method(
llmod,
class,
method,
method_names,
receiver,
crate::target_layout::object_header_size_bytes(target_triple),
)
.with_context(|| {
format!(
"lowering typed-f64 receiver method clone '{}::{}'",
class.name, method.name
)
})?;
}
if cross_module
.typed_i32_methods
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@ pub(super) fn compile_typed_f64_receiver_method(
method: &Function,
methods: &HashMap<(String, String), String>,
receiver: &TypedReceiverMethodInfo,
header_skip: u64,
) -> Result<()> {
let generic_name = methods
.get(&(class.name.clone(), method.name.clone()))
Expand All @@ -1077,7 +1078,7 @@ pub(super) fn compile_typed_f64_receiver_method(

let value = {
let blk = lf.block_mut(0).unwrap();
lower_typed_f64_receiver_body(blk, &method.params, &method.body, receiver)?
lower_typed_f64_receiver_body(blk, &method.params, &method.body, receiver, header_skip)?
};
lf.block_mut(0).unwrap().ret(DOUBLE, &value);
Ok(())
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// see the doc on that fn for the split rationale.
emit_module_artifacts(ModuleArtifactsCtx {
llmod: &mut llmod,
target_triple: &triple,
strings: &mut strings,
hir,
import_function_prefixes: &opts.import_function_prefixes,
Expand Down
47 changes: 38 additions & 9 deletions crates/perry-codegen/src/codegen/typed_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,9 +1602,18 @@ pub(crate) fn lower_typed_string_body(
lower_typed_string_body_with_seed_locals(blk, params, body, HashMap::new())
}

fn lower_typed_f64_receiver_field(blk: &mut crate::block::LlBlock, field_index: u32) -> String {
fn lower_typed_f64_receiver_field(
blk: &mut crate::block::LlBlock,
field_index: u32,
header_skip: u64,
) -> String {
let obj_ptr = blk.inttoptr(crate::types::I64, "%this_obj");
let fields_base = blk.gep(crate::types::I8, &obj_ptr, &[(crate::types::I64, "24")]);
let header_skip_str = header_skip.to_string();
let fields_base = blk.gep(
crate::types::I8,
&obj_ptr,
&[(crate::types::I64, &header_skip_str)],
);
let field_index_str = field_index.to_string();
let field_ptr = blk.gep(
crate::types::DOUBLE,
Expand All @@ -1619,6 +1628,7 @@ fn lower_typed_f64_receiver_expr_with_env(
expr: &Expr,
locals: &HashMap<u32, String>,
receiver: &TypedReceiverMethodInfo,
header_skip: u64,
) -> anyhow::Result<String> {
match expr {
Expr::Number(n) => Ok(crate::nanbox::double_literal(*n)),
Expand All @@ -1633,22 +1643,34 @@ fn lower_typed_f64_receiver_expr_with_env(
let Some(field_index) = receiver.field_index(property) else {
anyhow::bail!("typed-f64 receiver clone cannot lower unproven receiver field")
};
Ok(lower_typed_f64_receiver_field(blk, field_index))
Ok(lower_typed_f64_receiver_field(
blk,
field_index,
header_skip,
))
}
Expr::Unary {
op: UnaryOp::Pos,
operand,
} => lower_typed_f64_receiver_expr_with_env(blk, operand, locals, receiver),
} => lower_typed_f64_receiver_expr_with_env(blk, operand, locals, receiver, header_skip),
Expr::Unary {
op: UnaryOp::Neg,
operand,
} => {
let v = lower_typed_f64_receiver_expr_with_env(blk, operand, locals, receiver)?;
let v = lower_typed_f64_receiver_expr_with_env(
blk,
operand,
locals,
receiver,
header_skip,
)?;
Ok(blk.fneg(&v))
}
Expr::Binary { op, left, right } => {
let l = lower_typed_f64_receiver_expr_with_env(blk, left, locals, receiver)?;
let r = lower_typed_f64_receiver_expr_with_env(blk, right, locals, receiver)?;
let l =
lower_typed_f64_receiver_expr_with_env(blk, left, locals, receiver, header_skip)?;
let r =
lower_typed_f64_receiver_expr_with_env(blk, right, locals, receiver, header_skip)?;
Ok(match op {
BinaryOp::Add => blk.fadd(&l, &r),
BinaryOp::Sub => blk.fsub(&l, &r),
Expand All @@ -1672,6 +1694,7 @@ pub(crate) fn lower_typed_f64_receiver_body(
params: &[perry_hir::Param],
body: &[Stmt],
receiver: &TypedReceiverMethodInfo,
header_skip: u64,
) -> anyhow::Result<String> {
let mut locals = HashMap::new();
for param in params {
Expand All @@ -1689,15 +1712,21 @@ pub(crate) fn lower_typed_f64_receiver_body(
init: Some(expr),
..
} if is_f64_type(ty) => {
let value = lower_typed_f64_receiver_expr_with_env(blk, expr, &locals, receiver)?;
let value = lower_typed_f64_receiver_expr_with_env(
blk,
expr,
&locals,
receiver,
header_skip,
)?;
locals.insert(*id, value);
}
_ => anyhow::bail!("typed-f64 receiver clone cannot lower non-straight-line statement"),
}
}
match last {
Stmt::Return(Some(expr)) => {
lower_typed_f64_receiver_expr_with_env(blk, expr, &locals, receiver)
lower_typed_f64_receiver_expr_with_env(blk, expr, &locals, receiver, header_skip)
}
_ => anyhow::bail!("typed-f64 receiver clone requires a final return value"),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/index_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// receiver had an ArrayHeader (8-byte header) layout. That's
// a load-bearing assumption for `arr[i] = v` against an
// unknown-typed receiver where `is_array_expr` couldn't
// narrow it statically — but ObjectHeader is 24 bytes plus
// narrow it statically — but the header spans object_header_size_bytes(...) bytes, then inline slots, plus
// `max(field_count, 8)` inline slots, so writing at offset
// `8 + idx*8` for any `idx ≥ 7` overflows the object's
// allocation and corrupts the adjacent heap object. The
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1212,8 +1212,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
};
if let Some(obj_ptr) = loop_fact_ptr {
let field_idx_str = field_index.to_string();
let header_skip =
crate::target_layout::object_header_size_bytes(ctx.target_triple)
.to_string();
let blk = ctx.block();
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]);
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]);
let field_ptr = blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]);
let val = blk.load(DOUBLE, &field_ptr);
let fast = LoweredValue {
Expand Down
6 changes: 4 additions & 2 deletions crates/perry-codegen/src/expr/property_get/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,10 @@ pub(crate) fn lower_raw_f64_class_field_get_for_number_context(
};
if let Some(obj_ptr) = loop_fact_ptr {
let field_idx_str = field_index.to_string();
let header_skip =
crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();
let blk = ctx.block();
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]);
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]);
let field_ptr = blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]);
let val = blk.load(DOUBLE, &field_ptr);
let fast = LoweredValue {
Expand Down Expand Up @@ -480,9 +482,9 @@ pub(crate) fn lower_raw_f64_class_field_get_for_number_context(
.cond_br(&guard_pass, &fast_label, &fallback_label);

ctx.current_block = fast_idx;
let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();
let blk = ctx.block();
let obj_ptr = blk.inttoptr(I64, &obj_handle);
let header_skip = "24".to_string();
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]);
let field_ptr = blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]);
let val_fast = blk.load(DOUBLE, &field_ptr);
Expand Down
7 changes: 6 additions & 1 deletion crates/perry-codegen/src/expr/property_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
ctx.current_block = store_idx;
{
let header_skip =
crate::target_layout::object_header_size_bytes(
ctx.target_triple,
)
.to_string();
let blk = ctx.block();
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]);
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]);
let field_ptr =
blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]);
// No raw-f64 canonicalization call is needed:
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,13 @@ fn lower_new_impl(
// target-compiled runtime (no-op on 64-bit; see `target_layout`).
let object_header_size: u64 =
crate::target_layout::object_header_size_bytes(ctx.target_triple);
// #6759 Phase B: pointer width for the trailing `meta` header
// field (computed here, before `ctx.block()` mutably borrows).
let meta_ptr_size: u64 = if crate::target_layout::target_is_ilp32(ctx.target_triple) {
4
} else {
8
};
const FIELD_SLOT_SIZE: u64 = 8;
// Inline-slot floor — MUST match perry-runtime `object::INLINE_SLOT_FLOOR`
// (they independently pad `new` objects to the same minimum; a mismatch
Expand Down Expand Up @@ -660,6 +667,17 @@ fn lower_new_impl(
// GC_STORE_AUDIT(INIT): keys_array edge is installed before publishing the new object.
blk.store(I64, &keys_ptr, &oh_addr_3);

// #6759 Phase B: null the `meta` record pointer — the LAST header
// field, at header offset (object_header_size - pointer_size).
// Pointer-width store: on ILP32 the field is 4 bytes at a
// 4-aligned offset, and an i64 store there would violate the
// arm64_32 `i64:64` ABI alignment (and spill into slot 0).
let meta_off = GC_HEADER_SIZE + object_header_size - meta_ptr_size;
let meta_addr = blk.gep(I8, &raw, &[(I64, &meta_off.to_string())]);
// GC_STORE_AUDIT(INIT): fresh inline object starts with no per-object meta record (#6759 B).
let meta_store_ty = if meta_ptr_size == 4 { I32 } else { I64 };
blk.store(meta_store_ty, "0", &meta_addr);

// PerryTS/perry#4717: zero-fill the field slots with `undefined`, mirroring
// `js_object_alloc_with_parent` (runtime object/alloc.rs), which deliberately
// initializes ALL `max(field_count, 8)` slots "to prevent stale data from
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-codegen/src/lower_call/scalar_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,10 +675,11 @@ fn emit_materialized_scalar_receiver_direct_field_store(
value: &str,
) {
let field_idx_str = field_index.to_string();
let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();
let field_ptr = {
let blk = ctx.block();
let obj_ptr = blk.inttoptr(I64, obj_handle);
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]);
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, &header_skip)]);
blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)])
};
let is_raw_f64 = crate::type_analysis::class_field_declared_type(ctx, class_name, field)
Expand Down
43 changes: 24 additions & 19 deletions crates/perry-codegen/src/target_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@ pub fn target_is_ilp32(target_triple: &str) -> bool {
|| target_triple.starts_with("wasm32")
|| target_triple.starts_with("i686")
|| target_triple.starts_with("i386")
// x32: 64-bit ISA with 32-bit pointers — the `x86_64` prefix alone
// would misclassify it as LP64.
|| target_triple.ends_with("gnux32")
}

/// `std::mem::size_of::<perry_runtime::object::ObjectHeader>()` for the target.
///
/// `ObjectHeader` is four `u32`s (`object_type`, `class_id`, `parent_class_id`,
/// `field_count` = 16 bytes) followed by one pointer (`keys_array`): 8 bytes +
/// 8-byte alignment → 24 on 64-bit; 4 bytes → 20 on ILP32. Inline object
/// allocation, header init, and the property inline-cache fast path all use
/// this as the field-region base (`fields = obj + object_header_size_bytes`).
/// It MUST equal the runtime's `size_of::<ObjectHeader>()`, or inline-
/// constructed objects and runtime-FFI field access diverge by 4 bytes and
/// every property read/write is corrupt. (The closure header `type_tag` offset
/// has the analogous problem; that one is handled runtime-side via
/// `perry_runtime::closure::CLOSURE_TYPE_TAG_OFFSET` / `offset_of!`.)
/// `field_count` = 16 bytes) followed by two pointers (`keys_array`, and the
/// #6759 Phase B `meta` record pointer): 16 bytes → 32 on 64-bit; 8 bytes → 24
/// on ILP32. Inline object allocation, header init, and the property
/// inline-cache fast path all use this as the field-region base
/// (`fields = obj + object_header_size_bytes`). It MUST equal the runtime's
/// `size_of::<ObjectHeader>()`, or inline-constructed objects and runtime-FFI
/// field access diverge and every property read/write is corrupt. (The closure
/// header `type_tag` offset has the analogous problem; that one is handled
/// runtime-side via `perry_runtime::closure::CLOSURE_TYPE_TAG_OFFSET` /
/// `offset_of!`.)
pub fn object_header_size_bytes(target_triple: &str) -> u64 {
if target_is_ilp32(target_triple) {
20
} else {
24
} else {
32
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand All @@ -48,14 +52,15 @@ mod tests {

#[test]
fn object_header_size_matches_pointer_width() {
// 64-bit targets: 4×u32 + 8-byte aligned pointer = 24. Must stay 24 so
// the shipping arm64 / x86_64 IR is byte-identical to before this fix.
assert_eq!(object_header_size_bytes("aarch64-apple-darwin"), 24);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos"), 24);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos-sim"), 24);
assert_eq!(object_header_size_bytes("x86_64-unknown-linux-gnu"), 24);
// arm64_32 watchOS (Series 4–8 / SE): 4×u32 + 4-byte pointer = 20.
assert_eq!(object_header_size_bytes("arm64_32-apple-watchos"), 20);
// 64-bit targets: 4×u32 + two 8-byte-aligned pointers (keys_array +
// #6759 meta) = 32.
assert_eq!(object_header_size_bytes("aarch64-apple-darwin"), 32);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos"), 32);
assert_eq!(object_header_size_bytes("aarch64-apple-watchos-sim"), 32);
assert_eq!(object_header_size_bytes("x86_64-unknown-linux-gnu"), 32);
// arm64_32 watchOS (Series 4–8 / SE): 4×u32 + two 4-byte pointers = 24.
assert_eq!(object_header_size_bytes("x86_64-unknown-linux-gnux32"), 24);
assert_eq!(object_header_size_bytes("arm64_32-apple-watchos"), 24);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ pub struct ObjectHeader {
pub field_count: u32,
/// Runtime array of object keys, or null for class instances.
pub keys_array: *mut ArrayHeader,
/// Per-object metadata record (#6759 Phase B), or null when the object
/// has none. Opaque to FFI consumers — never dereferenced across the
/// boundary, mirrored only so the header size and field-region offset
/// stay in lockstep with the runtime.
pub meta: *mut core::ffi::c_void,
}

/// Header for a runtime-allocated Buffer or Uint8Array payload.
Expand Down Expand Up @@ -165,6 +170,10 @@ mod layout_tests {
offset_of!(ObjectHeader, keys_array),
offset_of!(perry_runtime::ObjectHeader, keys_array)
);
assert_eq!(
offset_of!(ObjectHeader, meta),
offset_of!(perry_runtime::ObjectHeader, meta)
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/heap_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ unsafe fn node_label(rec: &NodeRec) -> (u32, String) {
GC_TYPE_TYPED_ARRAY => (NODE_TYPE_NATIVE, "TypedArray".to_string()),
GC_TYPE_DATE_CELL => (NODE_TYPE_OBJECT, "Date".to_string()),
GC_TYPE_TEMPORAL => (NODE_TYPE_NATIVE, "Temporal".to_string()),
GC_TYPE_OBJECT_META => (NODE_TYPE_NATIVE, "ObjectMeta".to_string()),
_ => (NODE_TYPE_NATIVE, "native".to_string()),
}
}
Expand Down
Loading
Loading