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.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Releases are listed newest first.
- Fixed list destructuring after non-fallthrough null guards: `break` and `continue` branches now preserve the complementary non-null type, allowing guarded `?array` values to be unpacked safely. Associative-array right-hand sides with positional integer keys are also accepted with adaptive `mixed` element types, while unguarded nullable and non-array values remain compile-time errors.
- Fixed heap-typed functions whose `try` body and every `catch` terminate being assigned a fabricated implicit return. Dead `try`/`catch` joins, including joins after `finally`, are now explicitly unreachable, so object and array return paths compile consistently with EIR optimization enabled or disabled.
- Added PHP's builtin `UnhandledMatchError` as an `Error` subclass for explicit `new`, `throw`, `catch`, and `instanceof` use, with inherited `Throwable` constructor and method signatures plus autoload recognition. The implicit no-arm/no-default `match` path remains the existing fatal runtime error rather than a catchable object.
- Fixed by-reference `foreach` over a missing array element segfaulting (issue #556): the by-reference path was left out of scope by the #526/#533 read-side hardening, so the null-container sentinel from a missed read was handed to the copy-on-write helpers as a real array pointer and then re-read as a live array header on every iteration. `__rt_array_ensure_unique` and `__rt_hash_ensure_unique` now recognize the sentinel and return it unchanged (hardening every copy-on-write call site), foreach initialization folds a sentinel source to the canonical zero pointer in the iterator's private slot while the user-visible variable stays null, and the by-reference live-length read substitutes zero for a null source. The direct form reports the missing key while the `?? []` form silently iterates its empty default; both skip the loop body and continue instead of crashing. Ordinary by-reference mutation, aliasing, and PHP's visit-appended-elements semantics are unchanged, and heap-debug stays clean with `--ir-opt` enabled or disabled across every supported target.

## [0.26.2]
- Added experimental PHP `eval()` support across macOS ARM64, Linux ARM64, and Linux x86_64. Eligible literal fragments are parsed at compile time and lowered to native EIR, including direct or scope-backed caller-local synchronization; dynamic strings and unsupported literal shapes fall back to the optional statically linked `elephc-magician` EvalIR interpreter. Within the supported eval subset, the fallback preserves caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, ownership/COW behavior, and PHP-visible diagnostics without requiring PHP or the Zend Engine. Bridge linking is automatic when required and can be forced with `--with-eval`; generated builtin documentation now reports AOT and eval availability separately.
Expand Down
52 changes: 50 additions & 2 deletions src/codegen/lower_inst/iterators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ pub(super) fn lower_iter_start(ctx: &mut FunctionContext<'_>, inst: &Instruction
}
if by_ref {
ensure_unique_static_iter_source(ctx, source, &source_kind)?;
if matches!(
source_kind,
IteratorSourceKind::Indexed { .. } | IteratorSourceKind::Hash
) {
// -- normalize a missed-read sentinel source to the canonical zero pointer --
// Only the iterator's private slot is normalized; the origin local keeps the
// sentinel so the user-visible value stays null. `IterNext` re-reads the live
// length from this slot every iteration, so folding the sentinel here keeps
// that hot path on the cheap zero check (issue #556).
crate::codegen::sentinels::emit_normalize_null_container_to_zero(
ctx.emitter,
result_reg,
abi::secondary_scratch_reg(ctx.emitter),
);
}
}
abi::store_at_offset(ctx.emitter, result_reg, offset - ITER_SOURCE_OFFSET_DELTA);
if matches!(source_kind, IteratorSourceKind::DynamicIterable) {
Expand Down Expand Up @@ -380,6 +395,12 @@ fn iter_start_is_by_ref(inst: &Instruction) -> bool {
}

/// Splits statically typed array/hash sources before by-reference iteration.
///
/// A null/sentinel source — the value a missed read such as `foreach ($a[7] as &$v)`
/// materializes — has nothing to split: the copy-on-write helpers recognize both the zero
/// pointer and the in-band `NULL_SENTINEL` and return them unchanged (issue #556). The
/// origin local and SSA slot therefore keep the sentinel; the caller normalizes only the
/// iterator's private source slot afterwards.
fn ensure_unique_static_iter_source(
ctx: &mut FunctionContext<'_>,
source: ValueId,
Expand Down Expand Up @@ -869,6 +890,33 @@ fn snapshot_indexed_array_length(ctx: &mut FunctionContext<'_>, offset: usize) {
abi::store_at_offset(ctx.emitter, len_reg, offset - ITER_SNAPSHOT_LEN_OFFSET_DELTA);
}

/// Loads the live entry count from the indexed-array header in `array_reg` into `len_reg`,
/// substituting zero for a zero source pointer. Used by the by-reference `IterNext`
/// advancement, whose source slot `IterStart` has already normalized: a missed-read
/// sentinel was folded to zero there, so the per-iteration hot path needs only the cheap
/// zero check instead of the full null-container guard (issue #556). `len_reg` must not
/// alias `array_reg`.
fn load_live_indexed_array_length(ctx: &mut FunctionContext<'_>, array_reg: &str, len_reg: &str) {
let null_label = ctx.next_label("iter_len_null_source");
let done_label = ctx.next_label("iter_len_done");
match ctx.emitter.target.arch {
Arch::AArch64 => {
ctx.emitter
.instruction(&format!("cbz {}, {}", array_reg, null_label)); // zero sources (normalized missed reads) have no header to read
}
Arch::X86_64 => {
ctx.emitter
.instruction(&format!("test {}, {}", array_reg, array_reg)); // is the source the canonical zero container pointer?
ctx.emitter.instruction(&format!("jz {}", null_label)); // zero sources (normalized missed reads) have no header to read
}
}
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
abi::emit_jump(ctx.emitter, &done_label);
ctx.emitter.label(&null_label);
abi::emit_load_int_immediate(ctx.emitter, len_reg, 0);
ctx.emitter.label(&done_label);
}

/// Boxes a concrete iterator method result when the EIR result slot expects `Mixed`.
fn box_iterator_method_result_if_needed(
ctx: &mut FunctionContext<'_>,
Expand Down Expand Up @@ -1252,7 +1300,7 @@ fn lower_indexed_iter_next_aarch64(
if by_ref {
let array_reg = abi::int_result_reg(ctx.emitter);
abi::load_at_offset(ctx.emitter, array_reg, offset - ITER_SOURCE_OFFSET_DELTA);
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
load_live_indexed_array_length(ctx, array_reg, len_reg);
ctx.emitter.instruction(&format!("cmp {}, {}", index_reg, len_reg)); // compare the candidate offset against the live array length
} else {
abi::load_at_offset(ctx.emitter, len_reg, offset - ITER_SNAPSHOT_LEN_OFFSET_DELTA);
Expand Down Expand Up @@ -1283,7 +1331,7 @@ fn lower_indexed_iter_next_x86_64(
if by_ref {
let array_reg = abi::symbol_scratch_reg(ctx.emitter);
abi::load_at_offset(ctx.emitter, array_reg, offset - ITER_SOURCE_OFFSET_DELTA);
abi::emit_load_from_address(ctx.emitter, len_reg, array_reg, 0);
load_live_indexed_array_length(ctx, array_reg, len_reg);
ctx.emitter.instruction(&format!("cmp {}, {}", index_reg, len_reg)); // compare the candidate offset against the live array length
} else {
abi::load_at_offset(ctx.emitter, len_reg, offset - ITER_SNAPSHOT_LEN_OFFSET_DELTA);
Expand Down
29 changes: 23 additions & 6 deletions src/codegen_support/runtime/arrays/array_ensure_unique.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ use crate::codegen_support::platform::Arch;

/// Splits a shared array before mutation using copy-on-write (COW) semantics.
///
/// Null arrays (x0=0) are returned immediately as they are trivially unique.
/// For non-null arrays: if refcount > 1, clones the array and decrements the
/// Null arrays (x0=0) and the in-band null-container sentinel that missed reads
/// materialize (issue #556) are returned unchanged: neither carries a heap header,
/// so they must never reach the refcount load below.
/// For real arrays: if refcount > 1, clones the array and decrements the
/// original's refcount; if refcount <= 1, returns the array unchanged.
///
/// Dispatches to `emit_array_ensure_unique_linux_x86_64` on x86_64; emits
Expand All @@ -32,8 +34,15 @@ pub fn emit_array_ensure_unique(emitter: &mut Emitter) {
emitter.comment("--- runtime: array_ensure_unique ---");
emitter.label_global("__rt_array_ensure_unique");

// -- null arrays are already trivially unique --
// -- null and sentinel-null arrays are already trivially unique --
emitter.instruction("cbz x0, __rt_array_ensure_unique_done"); // null inputs do not need copy-on-write splitting
crate::codegen_support::abi::emit_load_int_immediate(
emitter,
"x9",
crate::codegen_support::sentinels::NULL_SENTINEL,
);
emitter.instruction("cmp x0, x9"); // does the array carry the in-band null-container sentinel?
emitter.instruction("b.eq __rt_array_ensure_unique_done"); // sentinel-null arrays from missed reads have no header to split

// -- only shared arrays need to be cloned --
emitter.instruction("ldr w9, [x0, #-12]"); // load the current array refcount from the uniform header
Expand All @@ -59,9 +68,10 @@ pub fn emit_array_ensure_unique(emitter: &mut Emitter) {

/// Emits the x86_64 Linux implementation of `__rt_array_ensure_unique`.
///
/// Mirrors the ARM64 logic: null inputs return immediately; shared arrays
/// (refcount > 1) are cloned via `__rt_array_clone_shallow` and the original's
/// refcount is decremented; unique arrays (refcount <= 1) are returned unchanged.
/// Mirrors the ARM64 logic: null and sentinel-null inputs return immediately;
/// shared arrays (refcount > 1) are cloned via `__rt_array_clone_shallow` and the
/// original's refcount is decremented; unique arrays (refcount <= 1) are returned
/// unchanged.
///
/// Input: rdi = candidate indexed-array pointer
/// Output: rax = unique indexed-array pointer
Expand All @@ -73,6 +83,13 @@ fn emit_array_ensure_unique_linux_x86_64(emitter: &mut Emitter) {
emitter.instruction("mov rax, rdi"); // default to returning the original indexed-array pointer when no copy-on-write split is needed
emitter.instruction("test rdi, rdi"); // null indexed-array pointers are already trivially unique
emitter.instruction("je __rt_array_ensure_unique_done"); // return immediately for null inputs without touching heap metadata
crate::codegen_support::abi::emit_load_int_immediate(
emitter,
"r10",
crate::codegen_support::sentinels::NULL_SENTINEL,
);
emitter.instruction("cmp rdi, r10"); // does the indexed array carry the in-band null-container sentinel?
emitter.instruction("je __rt_array_ensure_unique_done"); // sentinel-null arrays from missed reads have no header to split
emitter.instruction("mov r10d, DWORD PTR [rdi - 12]"); // load the current indexed-array refcount from the uniform heap header
emitter.instruction("cmp r10d, 1"); // does the indexed array have more than one logical owner?
emitter.instruction("jbe __rt_array_ensure_unique_done"); // refcount <= 1 means the indexed array can be mutated in place
Expand Down
18 changes: 17 additions & 1 deletion src/codegen_support/runtime/arrays/hash_ensure_unique.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use crate::codegen_support::emit::Emitter;
use crate::codegen_support::platform::Arch;

/// hash_ensure_unique: split a shared hash table before mutation.
/// Null hashes and the in-band null-container sentinel that missed reads materialize
/// (issue #556) are returned unchanged: neither carries a heap header to split.
/// Input: x0 = candidate hash pointer
/// Output: x0 = unique hash pointer (original or cloned)
pub fn emit_hash_ensure_unique(emitter: &mut Emitter) {
Expand All @@ -24,8 +26,15 @@ pub fn emit_hash_ensure_unique(emitter: &mut Emitter) {
emitter.comment("--- runtime: hash_ensure_unique ---");
emitter.label_global("__rt_hash_ensure_unique");

// -- null hashes are already trivially unique --
// -- null and sentinel-null hashes are already trivially unique --
emitter.instruction("cbz x0, __rt_hash_ensure_unique_done"); // null inputs do not need copy-on-write splitting
crate::codegen_support::abi::emit_load_int_immediate(
emitter,
"x9",
crate::codegen_support::sentinels::NULL_SENTINEL,
);
emitter.instruction("cmp x0, x9"); // does the hash carry the in-band null-container sentinel?
emitter.instruction("b.eq __rt_hash_ensure_unique_done"); // sentinel-null hashes from missed reads have no header to split

// -- only shared hashes need to be cloned --
emitter.instruction("ldr w9, [x0, #-12]"); // load the current hash refcount from the uniform header
Expand Down Expand Up @@ -60,6 +69,13 @@ fn emit_hash_ensure_unique_linux_x86_64(emitter: &mut Emitter) {
emitter.instruction("mov rax, rdi"); // default to returning the original associative-array pointer when no copy-on-write split is needed
emitter.instruction("test rdi, rdi"); // null associative-array pointers are already trivially unique
emitter.instruction("je __rt_hash_ensure_unique_done"); // return immediately for null inputs without touching heap metadata
crate::codegen_support::abi::emit_load_int_immediate(
emitter,
"r10",
crate::codegen_support::sentinels::NULL_SENTINEL,
);
emitter.instruction("cmp rdi, r10"); // does the associative array carry the in-band null-container sentinel?
emitter.instruction("je __rt_hash_ensure_unique_done"); // sentinel-null hashes from missed reads have no header to split
emitter.instruction("mov r10d, DWORD PTR [rdi - 12]"); // load the current associative-array refcount from the uniform heap header
emitter.instruction("cmp r10d, 1"); // does the associative array have more than one logical owner?
emitter.instruction("jbe __rt_hash_ensure_unique_done"); // refcount <= 1 means the associative array can be mutated in place
Expand Down
25 changes: 25 additions & 0 deletions src/codegen_support/sentinels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ pub(crate) fn emit_branch_if_null_container(
}
}

/// Replaces the in-band `NULL_SENTINEL` in `value_reg` with a zero pointer, leaving any
/// other value untouched. Clobbers `scratch_reg` with the sentinel bit pattern. Used where
/// a null container must be representable by the cheap zero check alone — e.g. the
/// by-reference foreach iterator source slot, whose live length is re-read every iteration
/// (issue #556). Zero-pointer inputs are already normal and pass through unchanged.
pub(crate) fn emit_normalize_null_container_to_zero(
emitter: &mut Emitter,
value_reg: &str,
scratch_reg: &str,
) {
match emitter.target.arch {
Arch::AArch64 => {
super::abi::emit_load_int_immediate(emitter, scratch_reg, NULL_SENTINEL);
emitter.instruction(&format!("cmp {}, {}", value_reg, scratch_reg)); // does the container carry the in-band null sentinel?
emitter.instruction(&format!("csel {}, xzr, {}, eq", value_reg, value_reg)); // fold the sentinel into the canonical zero container pointer
}
Arch::X86_64 => {
super::abi::emit_load_int_immediate(emitter, scratch_reg, NULL_SENTINEL);
emitter.instruction(&format!("cmp {}, {}", value_reg, scratch_reg)); // does the container carry the in-band null sentinel?
emitter.instruction(&format!("mov {}, 0", scratch_reg)); // materialize the zero replacement without disturbing the comparison flags
emitter.instruction(&format!("cmove {}, {}", value_reg, scratch_reg)); // fold the sentinel into the canonical zero container pointer
}
}
}

/// Branches to `label` when the tagged scalar in the result registers is PHP null
/// (tag register == null tag).
pub(crate) fn emit_branch_if_tagged_scalar_null(emitter: &mut Emitter, label: &str) {
Expand Down
Loading
Loading