diff --git a/.config/nextest.toml b/.config/nextest.toml index 4c0e34ab89..0e8e4e1da3 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -102,3 +102,11 @@ slow-timeout = { period = "180s", terminate-after = 1 } [[profile.ci.overrides]] filter = 'test(test_eval_aot_callable_named_ref_arg_prep_fatal_cleans_up_stack)' slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.default.overrides]] +filter = 'test(test_eval_declared_method_return_type_values)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(test_eval_declared_method_return_type_values)' +slow-timeout = { period = "180s", terminate-after = 1 } diff --git a/CHANGELOG.md b/CHANGELOG.md index f11a511de7..b71ac88b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Releases are listed newest first. ## [Unreleased] - Added the `--strict-php` flag: the compiler accepts only PHP-compatible constructs. Extension syntax (`ifdef`, `packed class`, `extern`, `ptr_cast`, `buffer_new`, typed local declarations, `ptr`/`buffer` annotations) is rejected at compile time with per-violation diagnostics across the main file, includes, and autoloaded files, while extension builtins (`ptr_*`, `zval_*`, `buffer_*`, `class_attribute_*`) behave exactly as under the PHP interpreter — `function_exists()` reports `false`, calling one is an undefined function with a hint naming the disabled extension, and user code may declare its own functions with those names. Strict mode also reaches `eval()` with PHP's execute-time semantics: extension builtins do not exist inside eval'd fragments (runtime fatal on call, coherent `function_exists`/`is_callable`), extension syntax in a fragment is a runtime parse error, and user functions shadowing extension names stay callable. Programs using compiler preludes (PDO, timezone, image, web) keep compiling; `--define` cannot be combined with the flag. - Added PHP-compatible sessions to `--web` across PHP 8.2–8.5 and every supported target: `$_SESSION`, the complete `session_*()` API and interfaces, file persistence with `flock`, the `php`/`php_binary`/`php_serialize` wire formats, custom save handlers, strict mode, lazy writes, GC, SID and runtime INI configuration, cookies and cache limiters, auto-start, multipart upload progress, and trans-SID rewriting. Session state, locks, and bridge buffers are reset between requests, while invalid names, IDs, serialized input, and every supported callable-handler form follow PHP-compatible validation and failure paths. The web prelude now retains session helpers by reachability, omits the legacy callable-handler adapter when unused, uses a compact auto-start path, and shares callable descriptors and invokers module-wide, keeping ordinary `--web` compilation and assembly size close to the pre-session baseline. +- Fixed by-value `foreach` values corrupting arrays returned from functions (issue #405): concrete `int`, `float`, `bool`, and `string` element layouts now remain consistent across the loop local and function-return boundary, while stored string values retain their borrowed iterator payload before the source array is released. Callers now read the returned values correctly instead of seeing box addresses, empty strings, or a fatal heap-exhaustion error. - Fixed checked-arithmetic Mixed boxes leaking when consumed directly by a parent operation (issue #500): the boxed `int|float` result of runtime `+`/`-`/`*` (and unary `-`) is now released after it is coerced for `%`, bitwise/shift operators, `**`, comparisons, and int-coerced or mixed-key array indexes, so shapes like `($i * 7 + 1) & 0xFFFF` or `$SIN[($i * 7 + 5) & 1023]` in tight loops no longer exhaust the heap. Assignment, call-argument, chained mixed-op, and string-coercion releases stay balanced (no double frees), overflow-to-float promotion is unchanged, and heap-debug stays clean with the EIR optimizer enabled or disabled. - Added PHP-compatible `mb_strlen()` to both native compilation and the Magician `eval()` runtime, including the nullable optional `$encoding` argument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, and catchable `ValueError` for unknown encodings on every supported target. - 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. diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index cea7aa64ad..1fa10bc10b 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1574,6 +1574,17 @@ impl<'m, 'f> LoweringContext<'m, 'f> { { return true; } + // By-value foreach binds either an owned current value or an owned boxed + // Mixed key. Concrete `Str` values are the exception: like `ArrayGet` + // string results they borrow the source container's payload, so treating + // them as owning would free the array's string block out from under it. + match self.builder.value_defining_op(value.value) { + Some(Op::IterCurrentValue) => { + return !matches!(php_type.codegen_repr(), PhpType::Str); + } + Some(Op::IterCurrentKey) => return true, + _ => {} + } matches!( self.builder.value_defining_op(value.value), Some( @@ -1638,11 +1649,6 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::IteratorMethodCall | Op::SplRuntimeCall | Op::FiberRuntimeCall - // By-value foreach binds a fresh OWNED copy of the current - // element/key; without this `store_local` re-acquires it and - // never releases the copy, leaking on every iteration. - | Op::IterCurrentValue - | Op::IterCurrentKey ) ) } diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index f258b6189d..a0534b2859 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -1332,6 +1332,7 @@ fn foreach_value_type(source_ty: &PhpType) -> PhpType { PhpType::Array(elem) => match elem.codegen_repr() { PhpType::Callable => PhpType::Callable, PhpType::Object(class_name) => PhpType::Object(class_name), + elem @ (PhpType::Int | PhpType::Float | PhpType::Str | PhpType::Bool) => elem, _ => PhpType::Mixed, }, PhpType::Object(class_name) if class_name == "Phar" || class_name == "PharData" => { @@ -3330,6 +3331,11 @@ fn coerce_container_to_return_type( { Op::HashToMixed } + (PhpType::Array(source_elem), PhpType::AssocArray { .. }) + if source_elem.as_ref() == &PhpType::Never => + { + Op::ArrayToHash + } _ => return None, }; Some(ctx.emit_value( diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 1594fefe7b..8ecbf226c6 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -296,13 +296,16 @@ impl Checker { /// Infers a concrete array type from return info when the declared return type is a generic `array` hint. /// -/// Returns `Some(PhpType)` only when every non-void return in `return_types` is the same -/// array type (including `array` or `assocArray` shapes). Returns `None` if returns differ, -/// include non-array types, or are all `void`. +/// Returns `Some(PhpType)` only when every non-void, non-empty return in +/// `return_types` is the same array type (including `array` or `assocArray` +/// shapes). An empty indexed array is neutral because it can be materialized in +/// either concrete storage family at the return boundary. Returns `None` if +/// non-empty returns differ, include non-array types, or are all `void`. fn inferred_specific_array_type_from_infos( return_types: &[super::super::returns::ReturnInfo], ) -> Option { let mut specific: Option = None; + let mut empty_array: Option = None; for return_info in return_types { let return_ty = &return_info.ty; if matches!(return_ty, PhpType::Void) { @@ -311,13 +314,17 @@ fn inferred_specific_array_type_from_infos( if !matches!(return_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { return None; } + if matches!(return_ty, PhpType::Array(elem) if elem.as_ref() == &PhpType::Never) { + empty_array = Some(return_ty.clone()); + continue; + } match &specific { None => specific = Some(return_ty.clone()), Some(existing) if existing == return_ty => {} _ => return None, } } - specific + specific.or(empty_array) } /// Returns true when a function return type is a homogeneous array of callables. diff --git a/tests/codegen/arrays/foreach_value_append.rs b/tests/codegen/arrays/foreach_value_append.rs new file mode 100644 index 0000000000..09e7f70a96 --- /dev/null +++ b/tests/codegen/arrays/foreach_value_append.rs @@ -0,0 +1,186 @@ +//! Purpose: +//! Regression tests for appending a by-value `foreach` loop variable into +//! another array that crosses a function-return boundary (issue #405). +//! +//! Called from: +//! - `cargo test` through the `codegen_tests` harness via `crate::support`. +//! +//! Key details: +//! - `foreach_value_type` now keeps concrete scalar element types (`int`, +//! `float`, `bool`, `string`) for the by-value loop variable instead of +//! degrading them to a boxed `Mixed` local. Previously the appended box +//! widened the array under construction to `array` at runtime while +//! the checker-side function signature kept the concrete element type, so +//! the caller read box pointers through the concrete element layout +//! (garbage ints, or a fatal "heap memory exhausted" on string reads). +//! - Concrete `Str` results of `iter_current_value` are borrowed pointers +//! into the source array's payload (like `ArrayGet` string results), NOT +//! owning temporaries: the retaining store must not release them, or the +//! source array's string block is freed while still referenced and the +//! next `__rt_str_persist` reuses it (use-after-free corruption). + +use crate::support::*; + +/// Issue #405 minimal repro: appending the foreach value of an exploded CSV +/// and returning the array previously printed nothing and exhausted the heap +/// when the caller read it back. +#[test] +fn test_foreach_string_value_append_survives_function_return() { + let out = compile_and_run( + r#" Array(Mixed)` runtime conversion (the web `ini_get_all()` CI +/// regression exposed by issue #405). +#[test] +fn test_foreach_string_value_key_preserves_assoc_return_with_empty_branch() { + let out = compile_and_run( + r#"