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
14 changes: 13 additions & 1 deletion src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7623,7 +7623,19 @@ fn release_stringified_source_if_owned(
return;
}
match ctx.builder.value_php_type(source.value).codegen_repr() {
PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => {
// Boxed Mixed sources are safe to release here as well: the backend
// lowers `cast Mixed -> Str` through `__rt_mixed_cast_string`, which
// detaches the result from the box (string payloads are persisted into
// an independent copy; int/float/bool payloads render into fresh
// buffers), so the produced string never aliases the released cell.
// Skipping Mixed leaked every owned boxed temporary that flowed into a
// string coercion — e.g. `echo $row[1] . "\n"` inside a by-value
// `foreach` leaked the `$row[1]` element box each iteration (issue
// #527). `Union` codegen-reprs to `Mixed`, so this arm covers it too.
PhpType::Object(_)
| PhpType::Array(_)
| PhpType::AssocArray { .. }
| PhpType::Mixed => {
crate::ir_lower::ownership::release_if_owned(ctx, source, span);
}
_ => {}
Expand Down
71 changes: 71 additions & 0 deletions tests/codegen/runtime_gc/regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,3 +1230,74 @@ echo "done";
"promoting an indexed array literal to hash storage must free the source array (issue #408)"
);
}

/// Regression test for issue #527: a by-value `foreach` over an array of arrays
/// whose body string-coerces an element read (`echo $row[1] . "\n"`) must not
/// leak. The `$row[1]` read produces an owned boxed Mixed temporary; the
/// implicit Mixed→string coercion detaches its result, so the source box must
/// be released after the cast instead of leaking two heap blocks per iteration
/// (the box plus the string payload it owns).
#[test]
fn test_regression_527_foreach_value_element_string_coercion_heap_debug_clean() {
let out = compile_and_run_with_heap_debug(
r#"<?php
$a = [];
for ($i = 0; $i < 4; $i++) { $a[] = ['kw', 'word' . $i]; }
foreach ($a as $row) { echo $row[1] . "\n"; }
"#,
);
assert!(out.success, "program failed: {}", out.stderr);
assert_eq!(out.stdout, "word0\nword1\nword2\nword3\n");
assert!(
out.stderr.contains("HEAP DEBUG: leak summary: clean"),
"expected a clean heap, got: {}",
out.stderr
);
}

/// Regression test for issue #527 (post-loop semantics): after a by-value
/// `foreach` ends, the loop variable must still hold the LAST element like PHP,
/// and string-coercing an element of that surviving copy must release the
/// element box so the heap stays clean at exit.
#[test]
fn test_regression_527_foreach_value_var_survives_loop_and_heap_stays_clean() {
let out = compile_and_run_with_heap_debug(
r#"<?php
$a = [];
for ($i = 0; $i < 4; $i++) { $a[] = ['kw', 'word' . $i]; }
foreach ($a as $row) {}
echo $row[1] . "\n";
"#,
);
assert!(out.success, "program failed: {}", out.stderr);
assert_eq!(out.stdout, "word3\n");
assert!(
out.stderr.contains("HEAP DEBUG: leak summary: clean"),
"expected a clean heap, got: {}",
out.stderr
);
}

/// Regression test for issue #527 (key + value form): iterating an associative
/// array of arrays with string keys binds both an owned key copy and an owned
/// value copy per iteration; string-coercing `$row[1]` inside the body must
/// release the element box each iteration and end with a clean heap.
#[test]
fn test_regression_527_foreach_assoc_key_value_element_coercion_heap_debug_clean() {
let out = compile_and_run_with_heap_debug(
r#"<?php
$m = [];
$m['alpha'] = ['kw', 'wordA'];
$m['beta'] = ['kw', 'wordB'];
$m['gamma'] = ['kw', 'wordC'];
foreach ($m as $k => $row) { echo $k . '=' . $row[1] . "\n"; }
"#,
);
assert!(out.success, "program failed: {}", out.stderr);
assert_eq!(out.stdout, "alpha=wordA\nbeta=wordB\ngamma=wordC\n");
assert!(
out.stderr.contains("HEAP DEBUG: leak summary: clean"),
"expected a clean heap, got: {}",
out.stderr
);
}
Loading