Skip to content
Open
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 @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust.
Releases are listed newest first.

## [Unreleased]
- Fixed a heap corruption when an array grown inside a loop is promoted to mixed-element storage (issue #452): with `$vals[] = 1; $vals[] = 2.0;` in a loop body, the earlier push site was typed and lowered against the pre-promotion element type, so from the second iteration it wrote an unboxed scalar into boxed-cell storage — reading that element produced garbage or a segfault. Loop bodies now pre-scan their push sites and fix the array's element type to `mixed` up front (checker and EIR lowering), materializing the promotion once before the loop so every push boxes correctly. Also fixes the silent value corruption and per-conversion leak of the string/float flavour of the same pattern.
- Fixed a constant-propagation miscompile with by-reference calls in a `match` subject (issue #384): `echo match(bump($i)) { ... } . "|" . $i` kept the pre-call constant for `$i` and printed the stale value, because a call's unknown write set was treated as "no writes" instead of forcing conservative invalidation. A read sequenced after any by-reference-mutating call in the same expression now observes the post-call value, matching PHP; pure calls (`gettype`, `strlen`, …) keep their operands foldable.
- Fixed PHP parser/codegen parity for parenthesis-free object instantiation (issue #371): `new Foo`, `new self`, `new static`, `new parent`, and `new $class` now compile with empty constructor arguments, while immediate postfix forms such as `new Foo->bar`, `new Foo::bar()`, `new Foo?->bar`, and `new Foo[0]` are rejected instead of being misparsed.
- Fixed three PHP parity regressions in the EIR backend: indexed array elements such as `$a[0]` can now be passed to by-reference parameters with copy-on-write storage split before mutation (issue #360); `IteratorAggregate::getIterator()` may declare the marker `Traversable` return type while `foreach` still dispatches through the returned object's concrete `Iterator` methods (issue #385); and catchable private/protected method access plus readonly-property write errors now preserve PHP's receiver/RHS evaluation order before throwing `Error` (issue #383). The merge also removes a duplicate `_spl_error_class_id` data symbol that could make post-merge user assembly fail to assemble.
Expand Down
82 changes: 82 additions & 0 deletions src/ir_lower/stmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,75 @@ fn lower_ifdef(
}

/// Lowers a `while` loop.
/// Widens locals whose indexed-array element type joins to `mixed` across the loop body's
/// push sites (issue #452) and materializes the promotion once before the loop. Loop bodies
/// are lowered in a single pass, so without this an early `$a[] = <scalar>` site is emitted
/// as a raw push against the pre-promotion element type even though the back edge brings the
/// promoted `array<mixed>` around, writing an unboxed scalar into boxed-cell storage on
/// iterations >= 2. Converting the array up front (in place, same pointer) and fixing its
/// type to `array<mixed>` makes every push site box its value. `overrides` supplies types
/// for names bound by the loop itself (the `foreach` value/key variables) so a push of the
/// loop variable joins with its real element type. Locals without a materialized slot yet
/// (first assigned inside the body, hence fresh every iteration) are left untouched.
fn widen_loop_grown_arrays(
ctx: &mut LoweringContext<'_, '_>,
body: &[Stmt],
update: Option<&Stmt>,
overrides: &[(&str, PhpType)],
span: Option<Span>,
) {
let names = {
let lookup = |name: &str| -> Option<PhpType> {
if let Some((_, ty)) = overrides.iter().find(|(n, _)| *n == name) {
return Some(ty.clone());
}
// A name with no declared slot yet (first assigned inside the loop body) is
// genuinely unknown at loop entry: report it as such instead of the Mixed
// fallback `local_type` would return, so the prescan does not take Mixed as
// widening evidence for it (mirrors the checker's `env.get` lookup).
if !ctx.local_slots.contains_key(name) {
return None;
}
Some(ctx.local_type(name))
};
crate::types::checker::loop_grown_mixed_array_pushes(body, update, &lookup)
};
for name in names {
if !ctx.local_slots.contains_key(&name) {
continue;
}
let array_value = ctx.load_local(&name, span);
if array_value.ir_type != IrType::Heap(crate::ir::IrHeapKind::Array) {
continue;
}
let mixed_array_ty = PhpType::Array(Box::new(PhpType::Mixed));
let converted = ctx.emit_value(
Op::ArrayToMixed,
vec![array_value.value],
None,
mixed_array_ty.clone(),
Op::ArrayToMixed.default_effects(),
span,
);
ctx.store_mutated_local(&name, converted, mixed_array_ty, span);
}
}

/// Mirrors the checker's `foreach` value binding for the loop-widening prescan: the element
/// type for an indexed/associative array source variable, `mixed` otherwise.
fn foreach_prescan_value_type(ctx: &LoweringContext<'_, '_>, array: &Expr) -> PhpType {
let ExprKind::Variable(name) = &array.kind else {
return PhpType::Mixed;
};
match ctx.local_type(name) {
PhpType::Array(elem) => *elem,
PhpType::AssocArray { value, .. } => *value,
_ => PhpType::Mixed,
}
}

fn lower_while(ctx: &mut LoweringContext<'_, '_>, condition: &Expr, body: &[Stmt]) {
widen_loop_grown_arrays(ctx, body, None, &[], Some(condition.span));
let header = ctx.builder.create_named_block("while.cond", Vec::new());
let body_block = ctx.builder.create_named_block("while.body", Vec::new());
let exit = ctx.builder.create_named_block("while.exit", Vec::new());
Expand Down Expand Up @@ -479,6 +547,7 @@ fn lower_while(ctx: &mut LoweringContext<'_, '_>, condition: &Expr, body: &[Stmt

/// Lowers a `do while` loop.
fn lower_do_while(ctx: &mut LoweringContext<'_, '_>, body: &[Stmt], condition: &Expr) {
widen_loop_grown_arrays(ctx, body, None, &[], Some(condition.span));
let body_block = ctx.builder.create_named_block("do.body", Vec::new());
let cond_block = ctx.builder.create_named_block("do.cond", Vec::new());
let exit = ctx.builder.create_named_block("do.exit", Vec::new());
Expand Down Expand Up @@ -523,6 +592,10 @@ fn lower_for(
if ctx.builder.insertion_block_is_terminated() {
return;
}
let widen_span = condition
.map(|c| c.span)
.or_else(|| body.first().map(|s| s.span));
widen_loop_grown_arrays(ctx, body, update, &[], widen_span);

let header = ctx.builder.create_named_block("for.cond", Vec::new());
let body_block = ctx.builder.create_named_block("for.body", Vec::new());
Expand Down Expand Up @@ -1037,6 +1110,15 @@ fn lower_foreach(
value_by_ref: bool,
body: &[Stmt],
) {
// Widen before the source is lowered so an iterated-and-pushed array is loaded with
// its fixed-point element type, and bind the loop variables' prescan types so a push
// of the foreach value joins with its real element type.
let prescan_value_ty = foreach_prescan_value_type(ctx, array);
let mut overrides: Vec<(&str, PhpType)> = vec![(value_var, prescan_value_ty)];
if let Some(key_var) = key_var {
overrides.push((key_var, PhpType::Mixed));
}
widen_loop_grown_arrays(ctx, body, None, &overrides, Some(array.span));
let source = lower_expr(ctx, array);
let source_php_ty = ctx.builder.value_php_type(source.value);
let source_ty = source_php_ty.codegen_repr();
Expand Down
Loading