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 heap corruption when arrays grown inside loops are promoted to mixed-element storage (issue #452). Loop analysis now covers `$a[] =`, `$a[$i] =`, and `array_push($a, …)` sites and uses inferred call and assignment value types before lowering, so every write uses consistent boxed storage without widening homogeneous typed rebuilds such as `MultipleIterator::detachIterator`. This also fixes the silent value corruption and per-conversion leak in the string/float form of the same pattern.
- Fixed symbolic defaults for object-typed parameters: enum-case defaults are now resolved and type-checked semantically after class schemas are complete across functions, methods and constructors, constructor-promoted properties, and closures, including named, `self::`, `static::`, and `parent::` receivers. Missing cases and scalar class constants are rejected instead of slipping through syntactic typing, while `ReflectionParameter::getDefaultValueConstantName()` preserves the source-level constant spelling.
- Added PHP-compatible `::class` support on object expressions: `$object::class` now returns the receiver's concrete runtime class name, evaluates the receiver exactly once, and rejects statically known non-object receivers, while existing named and static forms remain unchanged.
- Added support for `static $x;` function-static declarations without an initializer, in both the native parser and the Magician `eval()` parser: the missing initializer desugars to `= null`, matching PHP, where `static $x;` and `static $x = null;` are identical (including `isset()` behavior).
Expand Down
29 changes: 26 additions & 3 deletions src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7142,7 +7142,7 @@ fn expand_static_indexed_spread_args(args: &[Expr]) -> Vec<Expr> {
}

/// Returns the best available return type for a function-like call.
fn call_return_type(
pub(super) fn call_return_type(
ctx: &LoweringContext<'_, '_>,
name: &str,
operands: &[crate::ir::ValueId],
Expand Down Expand Up @@ -8462,7 +8462,7 @@ fn scoped_constant_value_type_for_ir(
}

/// Returns the element/value type for an array-access expression used inside a literal.
fn array_access_expr_value_type_for_ir(
pub(super) fn array_access_expr_value_type_for_ir(
ctx: &LoweringContext<'_, '_>,
array: &Expr,
) -> Option<PhpType> {
Expand Down Expand Up @@ -8490,7 +8490,7 @@ fn array_access_expr_value_type_for_ir(
}

/// Returns the declared type for an object property expression used inside a literal.
fn property_access_expr_type_for_ir(
pub(super) fn property_access_expr_type_for_ir(
ctx: &LoweringContext<'_, '_>,
object: &Expr,
property: &str,
Expand All @@ -8511,6 +8511,18 @@ fn property_access_expr_type_for_ir(
.map(|(_, ty)| normalize_value_php_type(ty.codegen_repr()))
}

/// Returns the declared result type for an instance method call before its receiver is lowered.
pub(super) fn method_call_expr_type_for_ir(
ctx: &LoweringContext<'_, '_>,
object: &Expr,
method: &str,
) -> Option<PhpType> {
let class_name = instance_callable_object_class(ctx, object)?;
let method_key = php_symbol_key(method);
class_method_signature(ctx, &class_name, &method_key)
.map(|signature| normalize_value_php_type(signature.return_type.codegen_repr()))
}

/// Merges associative-array value types for EIR storage metadata.
fn merge_ir_assoc_value_type(left: PhpType, right: PhpType) -> PhpType {
if left == right {
Expand Down Expand Up @@ -14341,6 +14353,17 @@ fn static_method_implementation_signature<'a>(
.and_then(|class_info| class_info.static_methods.get(&key))
}

/// Returns the declared result type for a static method call before its arguments are lowered.
pub(super) fn static_method_call_expr_type_for_ir(
ctx: &LoweringContext<'_, '_>,
receiver: &StaticReceiver,
method: &str,
) -> Option<PhpType> {
static_method_implementation_signature(ctx, receiver, method)
.or_else(|| lexical_instance_static_call_signature(ctx, receiver, method))
.map(|signature| normalize_value_php_type(signature.return_type.codegen_repr()))
}

/// Returns the instance-method signature used by `self::method()` or `parent::method()`.
fn lexical_instance_static_call_signature<'a>(
ctx: &'a LoweringContext<'_, '_>,
Expand Down
140 changes: 138 additions & 2 deletions src/ir_lower/stmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ use crate::ir_lower::context::{
};
use crate::ir_lower::effects_lookup;
use crate::ir_lower::expr::{
array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment,
array_access_element_result_type, array_access_expr_value_type_for_ir, call_return_type,
coerce_to_int_at_span, lower_callable_array_for_assignment,
lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr,
method_call_expr_type_for_ir, property_access_expr_type_for_ir,
reflection_arg_array_binding_for_expr, reflection_class_binding_for_expr,
reflection_function_binding_for_expr, reflection_method_binding_for_expr,
reflection_property_binding_for_expr, static_callable_binding_for_expr,
string_op_uses_scratch_storage, type_satisfies_array_access_for_ir,
static_method_call_expr_type_for_ir, string_op_uses_scratch_storage,
type_satisfies_array_access_for_ir,
};
use crate::names::{php_symbol_key, property_hook_set_method};
use crate::parser::ast::{
Expand Down Expand Up @@ -529,8 +532,127 @@ fn lower_ifdef(
ctx.clear_static_callable_locals();
}

/// 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,
&mut |expr| infer_loop_growth_value_type(ctx, expr),
)
};
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);
}
}

/// Returns the value type that EIR lowering already knows before it emits a loop body.
fn infer_loop_growth_value_type(
ctx: &LoweringContext<'_, '_>,
expr: &Expr,
) -> Option<PhpType> {
match &expr.kind {
ExprKind::Variable(name) if ctx.local_slots.contains_key(name) => {
Some(ctx.local_type(name))
}
ExprKind::FunctionCall { name, .. } => {
Some(call_return_type(ctx, name.as_str(), &[]))
}
ExprKind::MethodCall { object, method, .. } => {
method_call_expr_type_for_ir(ctx, object, method)
}
ExprKind::StaticMethodCall {
receiver, method, ..
} => static_method_call_expr_type_for_ir(ctx, receiver, method),
ExprKind::PropertyAccess { object, property } => {
property_access_expr_type_for_ir(ctx, object, property)
}
ExprKind::ArrayAccess { array, .. } => {
array_access_expr_value_type_for_ir(ctx, array)
}
ExprKind::This => ctx.current_class.clone().map(PhpType::Object),
ExprKind::NewObject { class_name, .. } => {
Some(PhpType::Object(class_name.as_str().to_string()))
}
ExprKind::ErrorSuppress(inner) => infer_loop_growth_value_type(ctx, inner),
ExprKind::Ternary {
then_expr,
else_expr,
..
} => {
let then_ty = infer_loop_growth_value_type(ctx, then_expr)?;
let else_ty = infer_loop_growth_value_type(ctx, else_expr)?;
if then_ty == else_ty {
Some(then_ty)
} else {
Some(PhpType::Mixed)
}
}
_ => None,
}
}

/// 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,
}
}

/// Lowers a `while` loop.
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 @@ -563,6 +685,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 @@ -607,6 +730,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 @@ -1219,6 +1346,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
Loading