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
17 changes: 17 additions & 0 deletions crates/perry-codegen/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ impl LlBlock {
self.terminated
}

/// #5093: true if this block contains a `call` to anything other than an
/// `@llvm.*` intrinsic or an inline-asm marker. The class-field versioned
/// loop uses this to verify AT COMPILE TIME that its fast clone came out
/// call-free (no runtime call ⇒ no allocation ⇒ no GC ⇒ the
/// preheader-cached receiver pointer cannot move and the hoisted shape
/// check cannot be invalidated mid-loop). Intrinsic/libm-style calls
/// never enter the perry runtime, so they cannot trigger a collection.
pub fn contains_gc_unsafe_call(&self) -> bool {
self.instructions.iter().any(|line| {
let Some(pos) = line.find("call ") else {
return false;
};
let callee = &line[pos..];
!callee.contains("@llvm.") && !callee.contains(" asm ")
})
}

/// Allocate a fresh SSA register name in the enclosing function's
/// virtual register pool (e.g. `"%r42"`). Safe to call between
/// `gep` / other instructions that may emit sub-registers. Pair with
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ pub(super) fn compile_closure(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: native_facts.index_used_locals(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ pub(super) fn compile_module_entry(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: main_native_facts.index_used_locals(),
Expand Down Expand Up @@ -1176,6 +1177,7 @@ pub(super) fn compile_module_entry(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: init_native_facts.index_used_locals(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ pub(super) fn compile_function(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: native_facts.index_used_locals(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ pub(super) fn compile_method(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: native_facts.index_used_locals(),
Expand Down Expand Up @@ -1304,6 +1305,7 @@ pub(super) fn compile_static_method(
cached_lengths: HashMap::new(),
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
index_used_locals: native_facts.index_used_locals(),
Expand Down
145 changes: 145 additions & 0 deletions crates/perry-codegen/src/expr/class_field_inline_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,151 @@ const OBJ_FLAG_FROZEN_BIT: &str = "1"; // OBJ_FLAG_FROZEN (0x01)
const OBJ_FLAG_HAS_DESCRIPTORS_BIT: &str = "2048"; // OBJ_FLAG_HAS_DESCRIPTORS (0x800)
const F64_EXP_MASK: &str = "9218868437227405312"; // 0x7FF0_0000_0000_0000

/// Emit the `i1` "plain finite number" predicate on a value's raw bits: true
/// iff the exponent field is not all-ones. Rejects ±Inf, every NaN (canonical
/// or boxed), and therefore every NaN-box tag — exactly the values the
/// runtime set contract (`is_plain_number_bits`) refuses to store raw.
pub(crate) fn emit_plain_finite_number_check(
blk: &mut crate::block::LlBlock,
value_bits: &str,
) -> String {
let exp = blk.and(I64, value_bits, F64_EXP_MASK);
blk.icmp_ne(I64, &exp, F64_EXP_MASK)
}

/// #5093 loop versioning: emit the whole-loop shape check in a versioned
/// loop's preheader.
///
/// This is the hoisted form of [`emit_class_field_inline_precheck`]: the same
/// strict subset of the runtime `class_field_fast_contract`, evaluated ONCE
/// before loop entry, branching to `fast_label` (the fast clone's preheader)
/// when the monomorphic shape holds and to `slow_label` (the slow clone's
/// preheader, i.e. today's guarded loop) otherwise. Evaluating it once is
/// sound only because the fast clone's body is call-free (matcher-enforced in
/// `stmt/loops.rs`): with no calls there is no allocation, so no GC can move
/// the object or run any of the runtime paths that mutate class_id /
/// keys_array / field_count / the typed-layout intact bit / the frozen bit /
/// the process-global enable flag mid-loop.
///
/// `max_field_index` is the largest packed slot index the loop touches
/// (`field_count ugt max_field_index` covers every access). `require_raw_f64`
/// adds the per-object typed-layout intact check (any raw-f64 read or write in
/// the loop); `require_not_frozen` adds the frozen-bit check (any write in the
/// loop). Per-store value checks are NOT emitted here — the fast clone's
/// stores keep their inline plain-finite check and side-exit to `slow_label`.
///
/// Returns `(obj_ptr, shape_ok)`: the SSA name of the receiver object pointer
/// (`inttoptr` of `obj_handle`) and the accumulated `i1` shape predicate,
/// both emitted in the deref block. The deref block is deliberately left
/// UNTERMINATED with `ctx.current_block` pointing at it: the caller lowers
/// the fast clone first, verifies it really came out call-free
/// (`LlBlock::contains_gc_unsafe_call`), and only then terminates the deref
/// block — `cond_br(shape_ok, fast, slow)` on success, or an unconditional
/// branch to the slow clone if some unpredicted lowering path emitted a call
/// (never enter a fast clone whose call-freeness is unproven). The deref
/// block dominates the fast preheader, so the fast clone may use `obj_ptr`
/// directly for raw slot access.
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_class_field_loop_preheader_check(
ctx: &mut FnCtx,
obj_bits: &str,
obj_handle: &str,
expected_class_id: &str,
expected_keys: &str,
max_field_index: u32,
require_raw_f64: bool,
require_not_frozen: bool,
slow_label: &str,
) -> (String, String) {
let deref_idx = ctx.new_block("class_field_loop.preheader.deref");
let deref_label = ctx.block_label(deref_idx);
let max_field_index_str = max_field_index.to_string();

// Gate: enable flag first (volatile — the runtime flips it sticky 0 -> 1
// when descriptors / typed feedback / verify mode come into use), then
// prove the receiver is a real heap object before dereferencing.
{
let blk = ctx.block();
let flag = blk.load_volatile(I8, "@PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED");
let flag_ok = blk.icmp_eq(I8, &flag, "0");
let tag = blk.lshr(I64, obj_bits, "48");
let is_ptr = blk.icmp_eq(I64, &tag, POINTER_TAG_HI16);
let above_band = blk.icmp_ugt(I64, obj_handle, HANDLE_BAND_TOP);
let ptr_safe = blk.and(I1, &is_ptr, &above_band);
let can_inline = blk.and(I1, &ptr_safe, &flag_ok);
blk.cond_br(&can_inline, &deref_label, slow_label);
}

ctx.current_block = deref_idx;
{
let blk = ctx.block();
let obj_ptr = blk.inttoptr(I64, obj_handle);

// GcHeader (precedes the object by 8 bytes): obj_type @-8 (i8),
// gc_flags @-7 (i8), _reserved @-6 (i16).
let gtype_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-8")]);
let gtype = blk.load(I8, &gtype_ptr);
let gtype_ok = blk.icmp_eq(I8, &gtype, GC_TYPE_OBJECT);

let gflags_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-7")]);
let gflags = blk.load(I8, &gflags_ptr);
let fwd = blk.and(I8, &gflags, GC_FLAG_FORWARDED_I8);
let not_fwd = blk.icmp_eq(I8, &fwd, "0");

let res_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-6")]);
let reserved = blk.load(I16, &res_ptr);

// ObjectHeader: object_type @0 (i32)==REGULAR, class_id @4 (i32),
// field_count @12 (i32), keys_array @16 (i64).
let object_type = blk.load(I32, &obj_ptr);
let ot_ok = blk.icmp_eq(I32, &object_type, OBJECT_TYPE_REGULAR);

let cid_ptr = blk.gep(I8, &obj_ptr, &[(I64, "4")]);
let class_id = blk.load(I32, &cid_ptr);
let cid_ok = blk.icmp_eq(I32, &class_id, expected_class_id);

let fc_ptr = blk.gep(I8, &obj_ptr, &[(I64, "12")]);
let field_count = blk.load(I32, &fc_ptr);
let fc_ok = blk.icmp_ugt(I32, &field_count, &max_field_index_str);

let ka_ptr = blk.gep(I8, &obj_ptr, &[(I64, "16")]);
let keys_array = blk.load(I64, &ka_ptr);
let ka_ok = blk.icmp_eq(I64, &keys_array, expected_keys);

let mut acc = blk.and(I1, &gtype_ok, &not_fwd);
acc = blk.and(I1, &acc, &ot_ok);
acc = blk.and(I1, &acc, &cid_ok);
acc = blk.and(I1, &acc, &fc_ok);
acc = blk.and(I1, &acc, &ka_ok);

// #5654: a receiver that has ever had a property / accessor descriptor
// installed on it needs the guard's descriptor-aware dispatch (an
// accessor must fire on reads, a non-writable slot must reject
// stores). Instance-level installs no longer flip the process-global
// gate, so the hoisted check must vet the per-object flag — once, for
// the whole loop: installing a descriptor mid-loop would require a
// runtime call, which the call-free fast clone cannot make.
let has_desc = blk.and(I16, &reserved, OBJ_FLAG_HAS_DESCRIPTORS_BIT);
let no_desc = blk.icmp_eq(I16, &has_desc, "0");
acc = blk.and(I1, &acc, &no_desc);

if require_raw_f64 {
let intact = blk.and(I16, &reserved, TYPED_LAYOUT_INTACT_BIT);
let intact_ok = blk.icmp_ne(I16, &intact, "0");
acc = blk.and(I1, &acc, &intact_ok);
}

if require_not_frozen {
let frozen = blk.and(I16, &reserved, OBJ_FLAG_FROZEN_BIT);
let not_frozen = blk.icmp_eq(I16, &frozen, "0");
acc = blk.and(I1, &acc, &not_frozen);
}

// No terminator: the caller branches after verifying the fast clone.
(obj_ptr, acc)
}
}

/// Emit the inline class-field shape pre-check.
///
/// Before calling, the caller must have already created `fast_label` (the slot
Expand Down
57 changes: 56 additions & 1 deletion crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,20 @@ pub(crate) struct FnCtx<'a> {
/// `i` in bounds.
pub packed_f64_loop_facts: Vec<PackedF64LoopFact>,

/// #5093: scoped loop-versioning facts for monomorphic class-field loops.
/// Pushed only around the FAST clone of `lower_class_field_versioned_for`
/// (`stmt/loops.rs`): the loop preheader already proved the receiver's
/// exact class shape (class_id, keys identity, field_count, typed-layout
/// intact bit, not-frozen, inline-guard enable flag), and the matcher
/// proved the fast body is call-free (no allocation ⇒ no GC ⇒ the cached
/// `obj_ptr` cannot move and the shape cannot change mid-loop). Inside
/// that clone, `recv.field` GET/SET on a tracked raw-f64 field lowers to
/// a bare GEP+load/store on `obj_ptr` with no guard and no fallback call;
/// SET keeps an inline plain-finite-number check that side-exits to the
/// slow clone's preheader BEFORE committing any side effect of the
/// current iteration.
pub class_field_loop_facts: Vec<ClassFieldLoopFact>,

/// Parallel i32 counter slots for integer loop counters that are
/// used as bounded array indices. When a for-loop counter is in
/// `integer_locals` AND appears in `bounded_index_pairs`, `lower_for`
Expand Down Expand Up @@ -1157,6 +1171,47 @@ pub(crate) struct PackedF64LoopFact {
pub allow_holes: bool,
}

/// #5093: one fact per (receiver, versioned loop). See
/// `FnCtx::class_field_loop_facts` for the safety argument.
#[derive(Debug, Clone)]
pub(crate) struct ClassFieldLoopFact {
/// LocalId of the loop-invariant receiver (plain local or module global).
pub recv_local_id: u32,
pub scope_id: u32,
/// Class the preheader check proved exactly (by class_id compare).
pub class_name: String,
/// SSA name of the receiver object pointer, `inttoptr`'d in the
/// preheader's deref block. Dominates every block of the fast clone and
/// is stable for the clone's whole lifetime because the fast body is
/// call-free (no allocation ⇒ no GC ⇒ no evacuation).
pub obj_ptr: String,
/// Slow clone's preheader label. A raw-f64 store whose value fails the
/// inline plain-finite check branches here; the slow clone re-executes
/// the current iteration from scratch (no side effect has committed yet).
pub side_exit_label: String,
/// property name -> packed slot index. Every entry is a declared raw-f64
/// candidate field validated by the matcher via
/// `class_field_global_index` / `class_field_declared_type`.
pub fields: std::collections::BTreeMap<String, u32>,
}

/// Find the innermost active class-field loop fact covering
/// `(recv_local_id, class_name, property)`. Returns the fact and the packed
/// slot index of the field.
pub(crate) fn class_field_loop_fact_lookup<'f>(
facts: &'f [ClassFieldLoopFact],
recv_local_id: u32,
class_name: &str,
property: &str,
) -> Option<(&'f ClassFieldLoopFact, u32)> {
facts.iter().rev().find_map(|fact| {
if fact.recv_local_id != recv_local_id || fact.class_name != class_name {
return None;
}
fact.fields.get(property).map(|idx| (fact, *idx))
})
}

impl<'a> FnCtx<'a> {
pub fn next_loop_proof_scope_id(&mut self) -> u32 {
let id = self.next_loop_proof_scope_id;
Expand Down Expand Up @@ -1235,7 +1290,7 @@ pub(crate) use index_get::packed_f64_loop_index_parts;
mod index_set;
mod instance_misc1;
pub(crate) use instance_misc1::builtin_parent_reserved_class_id;
mod class_field_inline_guard;
pub(crate) mod class_field_inline_guard;
mod js_runtime;
mod literals_vars;
mod logical_collections;
Expand Down
63 changes: 63 additions & 0 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,69 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
ctx.class_ids.get(&class_name),
ctx.class_keys_globals.get(&class_name).cloned(),
) {
// #5093 loop versioning: inside the fast clone of a
// class-field versioned loop, a tracked field read on
// the proven receiver lowers to a bare slot load on
// the preheader-cached object pointer — no shape
// check, no guard call, no fallback (the preheader
// proved the shape once and the call-free clone keeps
// it true; see stmt/loops.rs).
let loop_fact_ptr = match object.as_ref() {
Expr::LocalGet(recv_id) => crate::expr::class_field_loop_fact_lookup(
&ctx.class_field_loop_facts,
*recv_id,
&class_name,
property,
)
.filter(|(_, loop_idx)| *loop_idx == field_index)
.map(|(fact, _)| fact.obj_ptr.clone()),
_ => None,
};
if let Some(obj_ptr) = loop_fact_ptr {
let field_idx_str = field_index.to_string();
let blk = ctx.block();
let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]);
let field_ptr = blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]);
let val = blk.load(DOUBLE, &field_ptr);
let fast = LoweredValue {
semantic: SemanticKind::JsNumber,
rep: NativeRep::F64,
llvm_ty: DOUBLE,
value: val.clone(),
};
ctx.record_lowered_value_with_access_mode_and_facts(
"ClassFieldGet",
None,
"class_field_get.loop_raw_f64_load",
&fast,
Some(BoundsState::Guarded {
guard_id: "class_field_loop_preheader_check".to_string(),
}),
None,
Some(BufferAccessMode::CheckedNative),
None,
None,
None,
vec![raw_f64_layout_fact(
None,
"consumed",
"class_field_loop_preheader_check",
None,
)],
Vec::new(),
false,
false,
vec![
format!("class={}", class_name),
format!("field={}", property),
format!("field_index={}", field_idx_str),
"receiver_proof=loop_preheader_shape_check".to_string(),
"field_layout=raw_f64_slot_array".to_string(),
"loop_versioning=class_field_fast_clone".to_string(),
],
);
return Ok(val);
}
let recv_box = lower_expr(ctx, object)?;
let key_idx = ctx.strings.intern(property);
let key_handle_global =
Expand Down
Loading
Loading