diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 71a2f70ede..6a5dd559e3 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -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); } _ => {} diff --git a/tests/codegen/runtime_gc/regressions.rs b/tests/codegen/runtime_gc/regressions.rs index bdf32578f7..96bcde0af2 100644 --- a/tests/codegen/runtime_gc/regressions.rs +++ b/tests/codegen/runtime_gc/regressions.rs @@ -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#" $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 + ); +}