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
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ pub(super) fn compile_function(
for p in &f.params {
let slot = blk.alloca(DOUBLE);
blk.store(DOUBLE, &format!("%arg{}", p.id), &slot);
if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() {
blk.call_void(
"js_shadow_slot_bind",
&[(I32, &slot_idx.to_string()), (PTR, &slot)],
);
}
map.insert(p.id, slot);
}
map
Expand Down
61 changes: 40 additions & 21 deletions crates/perry-codegen/src/expr/array_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ use anyhow::Result;
use perry_hir::Expr;

use super::{
emit_layout_note_slot_on_block, emit_write_barrier_slot_on_block, lower_expr,
emit_jsvalue_slot_store_on_block, expr_produces_non_pointer_bits_by_construction, lower_expr,
nanbox_pointer_inline, FnCtx,
};
use crate::type_analysis::is_numeric_expr;
use crate::types::{DOUBLE, I32, I64, I8, PTR};
use crate::types::{I32, I64, I8, PTR};

/// Lower an array literal `[a, b, c, …]`.
///
Expand Down Expand Up @@ -51,7 +50,9 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
let mut vals = Vec::with_capacity(n);
let mut layout_notes_needed = Vec::with_capacity(n);
for value_expr in elements {
layout_notes_needed.push(!is_numeric_expr(ctx, value_expr));
layout_notes_needed.push(!expr_produces_non_pointer_bits_by_construction(
ctx, value_expr,
));
vals.push(lower_expr(ctx, value_expr)?);
}

Expand All @@ -68,9 +69,9 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
const ELEMENT_SIZE: u64 = 8;
const GC_TYPE_ARRAY: u64 = 1;
const GC_FLAG_ARENA: u64 = 0x02;
// PR #1146: pointer-free hint for slot-layout tracking — empty
// inline literals only. The element-store loop below issues
// per-slot `js_gc_note_slot_layout` for non-empty literals.
// PR #1146: pointer-free hint for slot-layout tracking. The
// element-store loop below only suppresses per-slot notes for
// values whose non-pointer bits are proven by expression shape.
const GC_LAYOUT_POINTER_FREE: u64 = 0x4000;

let total_size = GC_HEADER_SIZE + ARRAY_HEADER_SIZE + (n as u64) * ELEMENT_SIZE;
Expand Down Expand Up @@ -111,6 +112,7 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
// Fast path: commit the bump, compute `data + offset`.
ctx.current_block = fast_idx;
let blk = ctx.block();
// GC_STORE_AUDIT(INIT): arena bump offset is allocator metadata, not a JS heap edge.
blk.store(I64, &new_offset, &offset_field_ptr);
let data_ptr = blk.load(PTR, &state_ptr);
let raw_fast = blk.gep(I8, &data_ptr, &[(I64, &aligned_off)]);
Expand Down Expand Up @@ -146,11 +148,13 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
| (GC_FLAG_ARENA << 8)
| (GC_LAYOUT_POINTER_FREE << 16)
| (total_size << 32);
// GC_STORE_AUDIT(INIT): freshly allocated array header starts pointer-free until slot notes below.
blk.store(I64, &gc_packed.to_string(), &raw);

// Packed ArrayHeader at raw+8 (length low 32 / capacity high 32).
let arr_header_addr = blk.gep(I8, &raw, &[(I64, "8")]);
let arr_header_packed = (n as u64) | ((n as u64) << 32);
// GC_STORE_AUDIT(INIT): freshly allocated ArrayHeader length/capacity, no child pointer.
blk.store(I64, &arr_header_packed.to_string(), &arr_header_addr);

// User pointer = raw + GC_HEADER_SIZE. Computed before the
Expand All @@ -163,12 +167,18 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
for (i, v) in vals.iter().enumerate() {
let offset = (16 + i * 8).to_string();
let elem_ptr = blk.gep_inbounds(I8, &raw, &[(I64, &offset)]);
blk.store(DOUBLE, v, &elem_ptr);
if layout_notes_needed[i] {
let value_bits = blk.bitcast_double_to_i64(v);
let slot_index = i.to_string();
emit_layout_note_slot_on_block(blk, &user_ptr_as_i64, &slot_index, &value_bits);
}
let slot_index = i.to_string();
emit_jsvalue_slot_store_on_block(
blk,
&elem_ptr,
v,
&user_ptr_as_i64,
&slot_index,
layout_notes_needed[i],
&user_ptr_as_i64,
"0",
false,
);
}

return Ok(nanbox_pointer_inline(ctx.block(), &user_ptr_as_i64));
Expand All @@ -186,14 +196,23 @@ pub(crate) fn lower_array_literal(ctx: &mut FnCtx<'_>, elements: &[Expr]) -> Res
for (i, v) in vals.iter().enumerate() {
let offset = (8 + i * 8).to_string();
let elem_ptr = ctx.block().gep_inbounds(I8, &arr_ptr, &[(I64, &offset)]);
ctx.block().store(DOUBLE, v, &elem_ptr);
if layout_notes_needed[i] {
let value_bits = ctx.block().bitcast_double_to_i64(v);
let elem_addr = ctx.block().ptrtoint(&elem_ptr, I64);
let slot_index = i.to_string();
emit_layout_note_slot_on_block(ctx.block(), &arr, &slot_index, &value_bits);
emit_write_barrier_slot_on_block(ctx.block(), &arr, &elem_addr, &value_bits);
}
let elem_addr = if layout_notes_needed[i] {
ctx.block().ptrtoint(&elem_ptr, I64)
} else {
"0".to_string()
};
let slot_index = i.to_string();
emit_jsvalue_slot_store_on_block(
ctx.block(),
&elem_ptr,
v,
&arr,
&slot_index,
layout_notes_needed[i],
&arr,
&elem_addr,
layout_notes_needed[i],
);
}

Ok(nanbox_pointer_inline(ctx.block(), &arr))
Expand Down
38 changes: 20 additions & 18 deletions crates/perry-codegen/src/expr/array_push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ use crate::types::{DOUBLE, I1, I32, I64, I8, PTR};

#[allow(unused_imports)]
use super::{
array_store_needs_layout_note, buffer_alias_metadata_suffix, can_lower_expr_as_i32,
emit_layout_note_slot_on_block, emit_shadow_slot_clear, emit_shadow_slot_update_for_expr,
emit_string_literal_global, 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,
array_store_needs_layout_note, array_store_needs_write_barrier, buffer_alias_metadata_suffix,
can_lower_expr_as_i32, emit_jsvalue_slot_store_on_block, emit_layout_note_slot_on_block,
emit_shadow_slot_clear, emit_shadow_slot_update_for_expr, emit_string_literal_global,
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,
Expand All @@ -55,7 +56,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// go to whichever storage we read from.
let array_expr = Expr::LocalGet(*array_id);
let layout_note_needed = array_store_needs_layout_note(ctx, &array_expr, value);
let write_barrier_needed = !is_numeric_expr(ctx, value);
let write_barrier_needed = array_store_needs_write_barrier(ctx, value);
let v = lower_expr(ctx, value)?;
let arr_box = lower_expr(ctx, &array_expr)?;

Expand Down Expand Up @@ -157,22 +158,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let with_header = blk.add(I64, &byte_offset, "8");
let element_addr = blk.add(I64, &arr_handle, &with_header);
let element_ptr = blk.inttoptr(I64, &element_addr);
blk.store(DOUBLE, &v, &element_ptr);
emit_jsvalue_slot_store_on_block(
blk,
&element_ptr,
&v,
&arr_handle,
&length,
layout_note_needed,
&arr_handle,
&element_addr,
write_barrier_needed,
);
let new_length = blk.add(I32, &length, "1");
let arr_ptr = blk.inttoptr(I64, &arr_handle);
// GC_STORE_AUDIT(POINTER_FREE): array length header update has no child pointer.
blk.store(I32, &new_length, &arr_ptr);
let value_bits = blk.bitcast_double_to_i64(&v);
if layout_note_needed {
emit_layout_note_slot_on_block(blk, &arr_handle, &length, &value_bits);
}
if write_barrier_needed {
emit_write_barrier_slot_on_block(
blk,
&arr_handle,
&element_addr,
&value_bits,
);
}
blk.br(&merge_label);
}

Expand Down Expand Up @@ -251,6 +251,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.block().store(DOUBLE, &new_box, &slot);
} else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() {
let g_ref = format!("@{}", global_name);
// GC_STORE_AUDIT(ROOT): module global array slot is a registered mutable GC root.
ctx.block().store(DOUBLE, &new_box, &g_ref);
} else {
return Err(anyhow!("ArrayPush({}): local not in scope", array_id));
Expand Down Expand Up @@ -314,6 +315,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.block().store(DOUBLE, &new_box, &slot);
} else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() {
let g_ref = format!("@{}", global_name);
// GC_STORE_AUDIT(ROOT): module global array slot is a registered mutable GC root.
ctx.block().store(DOUBLE, &new_box, &g_ref);
} else {
return Err(anyhow!("ArrayPushSpread({}): local not in scope", array_id));
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let byte_ptr = blk.gep_inbounds(I8, &data_ptr, &[(I32, &idx_i32)]);
let byte_val = blk.trunc(I32, &val_i32, I8);
let meta = buffer_alias_metadata_suffix(scope_idx);
// GC_STORE_AUDIT(POINTER_FREE): inline Buffer byte store writes scalar data only.
blk.emit_raw(format!("store i8 {}, ptr {}{}", byte_val, byte_ptr, meta));
return Ok(ctx.block().sitofp(I32, &val_i32, DOUBLE));
}
Expand Down Expand Up @@ -676,6 +677,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.block().store(DOUBLE, &new_box, &slot);
} else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() {
let g_ref = format!("@{}", global_name);
// GC_STORE_AUDIT(ROOT): module global array slot is a registered mutable GC root.
ctx.block().store(DOUBLE, &new_box, &g_ref);
}
let blk = ctx.block();
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/bigint_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.block().store(DOUBLE, &new_box, &slot);
} else if let Some(global_name) = ctx.module_globals.get(set_id).cloned() {
let g_ref = format!("@{}", global_name);
// GC_STORE_AUDIT(ROOT): module global Set slot is a registered mutable GC root.
ctx.block().store(DOUBLE, &new_box, &g_ref);
}
Ok(new_box)
Expand Down
88 changes: 82 additions & 6 deletions crates/perry-codegen/src/expr/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
//! issue #1098. Pure move — no logic changes.

use anyhow::Result;
use perry_hir::Expr;
use perry_hir::{BinaryOp, Expr, UnaryOp};
use perry_types::Type as HirType;

use super::{lower_expr, FnCtx};
use crate::block::LlBlock;
use crate::nanbox::POINTER_MASK_I64;
use crate::type_analysis::is_numeric_expr;
use crate::types::{DOUBLE, I32, I64};

/// Static-type predicate: the type's runtime array layout has no pointer
Expand All @@ -36,11 +35,88 @@ pub(crate) fn expr_has_numeric_pointer_free_array_layout(ctx: &FnCtx<'_>, expr:
.is_some_and(type_has_numeric_pointer_free_array_layout)
}

/// Numeric stores into statically numeric arrays preserve the initial
/// pointer-free layout. Other stores still update the mask so pointer writes
/// and pointer-clearing overwrites on mixed arrays remain precise.
fn local_get_produces_non_pointer_bits_by_dataflow(ctx: &FnCtx<'_>, id: u32) -> bool {
(ctx.i32_counter_slots.contains_key(&id) || ctx.integer_locals.contains(&id))
&& ctx.locals.contains_key(&id)
&& !ctx.boxed_vars.contains(&id)
&& !ctx.closure_captures.contains_key(&id)
&& !ctx.module_globals.contains_key(&id)
}

fn expr_produces_numeric_bits_by_construction(ctx: &FnCtx<'_>, expr: &Expr) -> bool {
match expr {
Expr::Integer(_) | Expr::Number(_) | Expr::DateNow | Expr::NumberCoerce(_) => true,
Expr::LocalGet(id) => local_get_produces_non_pointer_bits_by_dataflow(ctx, *id),
Expr::Unary { op, operand } => match op {
UnaryOp::Neg | UnaryOp::Pos | UnaryOp::BitNot => {
expr_produces_numeric_bits_by_construction(ctx, operand)
}
UnaryOp::Not => false,
},
Expr::Binary { op, left, right } => {
!matches!(op, BinaryOp::Add)
&& expr_produces_numeric_bits_by_construction(ctx, left)
&& expr_produces_numeric_bits_by_construction(ctx, right)
}
Expr::Conditional {
then_expr,
else_expr,
..
} => {
expr_produces_numeric_bits_by_construction(ctx, then_expr)
&& expr_produces_numeric_bits_by_construction(ctx, else_expr)
}
Expr::Sequence(exprs) => exprs
.last()
.is_some_and(|last| expr_produces_numeric_bits_by_construction(ctx, last)),
_ => false,
}
}

pub(crate) fn expr_produces_non_pointer_bits_by_construction(ctx: &FnCtx<'_>, expr: &Expr) -> bool {
match expr {
Expr::Undefined
| Expr::Null
| Expr::Bool(_)
| Expr::Compare { .. }
| Expr::Void(_)
| Expr::BooleanCoerce(_)
| Expr::IsNaN(_)
| Expr::IsFinite(_)
| Expr::NumberIsNaN(_)
| Expr::NumberIsFinite(_)
| Expr::NumberIsInteger(_)
| Expr::NumberIsSafeInteger(_) => true,
Expr::Unary {
op: UnaryOp::Not, ..
} => true,
Expr::Conditional {
then_expr,
else_expr,
..
} => {
expr_produces_non_pointer_bits_by_construction(ctx, then_expr)
&& expr_produces_non_pointer_bits_by_construction(ctx, else_expr)
}
Expr::Sequence(exprs) => exprs
.last()
.is_some_and(|last| expr_produces_non_pointer_bits_by_construction(ctx, last)),
_ => expr_produces_numeric_bits_by_construction(ctx, expr),
}
}

/// Stores into statically numeric arrays may preserve the initial
/// pointer-free layout only when the stored value's bits are known from
/// expression construction, not from TypeScript's local type alone. Other
/// stores update the mask so pointer writes and pointer-clearing overwrites
/// on mixed arrays remain precise.
pub(crate) fn array_store_needs_layout_note(ctx: &FnCtx<'_>, array: &Expr, value: &Expr) -> bool {
!(expr_has_numeric_pointer_free_array_layout(ctx, array) && is_numeric_expr(ctx, value))
!(expr_has_numeric_pointer_free_array_layout(ctx, array)
&& expr_produces_non_pointer_bits_by_construction(ctx, value))
}

pub(crate) fn array_store_needs_write_barrier(ctx: &FnCtx<'_>, value: &Expr) -> bool {
!expr_produces_non_pointer_bits_by_construction(ctx, value)
}

/// `lower_expr` variant that hands an expected-type hint down to the
Expand Down
Loading