Conversation
f8d0dcb to
c738392
Compare
|
Sorry @Guikingone, I've found some issues and must postpone your review on this one |
551d0ef to
9c4d101
Compare
ef4b353 to
743e73e
Compare
dd174c5 to
9094735
Compare
A mutating container write-back publishes whatever its helper returned, which is the same pointer whenever copy-on-write found a sole owner and growth did not relocate. Since the ref-cell store began retiring its previous occupant, that released the storage the cell still points at: two `$items[$i] = …` writes through one by-reference variadic freed the collector out from under the next read, and every eval callable form that writes a by-reference variadic returned empty strings. Skip the retirement when nothing was actually replaced, release the cell the attached key-sort separation replaces when its receiver is a raw local, and only retain a key-sort place read when it is a borrow — an element read already hands over an owning temporary.
A named argument the signature does not declare is collected by the variadic tail, so `replace(named: $n)` on `function replace(&...$items)` writes through `$n` exactly like a positional argument in that tail does. Constant propagation only matched declared parameter names, so it kept folding the caller's old value after the call and masked the write-back entirely.
A by-reference `foreach` over an array element now reaches its source through a reference cell, so the sentinel arrives as a boxed Mixed value and `__rt_mixed_unbox` normalizes it to the null tag before the shape dispatch that would touch storage. The statically shaped source still folds the sentinel to zero. Issue #556 asks that no array header be read from a sentinel; assert that one of the two recognitions is present instead of pinning the older shape.
Widening that rule to refcounted payloads dropped `Callable`, which `PhpType::is_refcounted` deliberately excludes, so an extracted descriptor lost its published root slot. Name both shapes.
Two lowering assertions still described the shapes these paths had before the branch changed them: - A scalar array slot handed to a by-reference parameter is now promoted to a managed reference cell instead of exposing a bare interior address. Count both ops, and keep the per-op checks that the exposed reference is a non-heap pointer taken from a boxed parent. - An optimization pass can leave a `Nop` where it removed an instruction, so the union's owned operand release is the next instruction that still does something, not literally the following one.
A declared PHP array reads back as one boxed value, so a by-reference element argument taken from it matched neither concrete receiver branch and skipped the lease that puts its managed cell in the call's unwind ledger. A later argument whose evaluation threw — a destructor firing while a sibling argument detached the array — then left the cell unreleased.
The synthetic alias a by-reference `foreach` binds to a static property was declared Mixed before the binding supplied the slot's actual payload type, and `declare_local` keeps the first declaration. An untyped `public static $rows = [...]` keeps concrete hash storage, so the loop's write-back boxed that hash into a Mixed cell and published it through the symbol — the next `C::$rows[$k]` read then walked a cell as a table and segfaulted.
Widening a concrete-element parent for a `mixed`/`array` by-reference parameter leaves the element in a boxed slot, but the argument was still handed out as a bare interior address. Two aliases of one element — `f($outer[0], $outer[0])` — then disagreed: the second promoted the slot to a reference cell and the first kept pointing at it, so the callee read that cell pointer as the element's value and `count()` rejected an int.
The bump path adds the requested payload size and the 16-byte header straight onto the offset, so a request that is not a multiple of eight — a persisted string of any odd length, the script path among them — misaligned EVERY block carved after it. Generated code reads those blocks with plain word loads and never notices; the eval interpreter dereferences the same blocks as `*mut RuntimeCell` and aborted on Rust's alignment check. That is why the eval by-reference bridge failures looked non-deterministic: they tracked the length of unrelated strings, and any diagnostic that shifted an allocation made them disappear. Round the payload up at the single entry point both the free-list and bump paths share.
A write-back that republishes what __rt_*_ensure_unique returned must not retire the cell's previous occupant: the COW split already dropped the mutator's owner, so the second release freed the argument hash an eval by-reference variadic still shared. RefCellStorePrevious names the two conventions; the mutating builtins keep Retire. Mutating builtins on a global-backed local now resolve a Global receiver place (the _eir_global_* symbol) instead of refusing, and array_reverse on a boxed source no longer keeps the checker's concrete result type.
An eval() in scope widens every local to Mixed, so $o->{$name} reached
the backend ladders boxed and was refused. The get, set and unset
lowerings cast a non-string name first, as php does.
Release-family ops carry the conservative all() effects, so every release_local_slot looked like an opaque user-code boundary and erased global-backed closure facts. They now answer from the released payload type, unions member by member, and the release is analysed against what the slot still holds while the tracking decision follows the widened slot type.
A destructor that stores $this in a new owner (a closure it creates, a global) must not have its storage freed underneath that owner, and __destruct must never run twice. The free path now checks for surviving owners after the call, clears the guard and marks completion in kind bit 17 exactly like the collector does.
The walk now descends into every expression, so a global written inside a closure literal, an assignment prelude or an enum method binds the top-level name to program storage like a named function's would. The Mixed-array backend gaps that kept this a deliberate blind spot are closed; the checker's unset-kill veto cost is documented.
php resolves a name the layout carries only as a strict ancestor's private slot to a dynamic property outside that ancestor. The eval bridge now falls through to dynamic storage instead of refusing, and a generated Mixed-receiver read asks Magician whether the receiver is an eval-declared child before raising the private-access Error.
A runtime-name write on a Mixed receiver that lands in the stdClass arm now hands an eval-declared object to Magician's setter, under the writer's lexical scope, so private slots and the __set guard hold and a refusal is raised as the same Error php throws. A closure created by eval() can now be invoked and rebound from generated code: the boxed-callable ladders wrap an eval callback into an adapter descriptor, and __rt_closure_bind rebinds $this through a hook Magician installs with the eval context.
The declared PHP-array union lowers to the same boxed Mixed cell a mixed variable already holds, so the reference needs no storage change; only the compile-time contract refused a mixed return value bound to array &$p.
# Conflicts: # src/types/checker/functions/call_validation.rs # src/types/checker/functions/resolution/call.rs # src/types/checker/functions/resolution/resolved.rs # src/types/checker/type_compat/declarations.rs # tests/error_tests/misc/functions.rs
The collector sweeps cycle members that still count peers as owners after running their destructors itself; gate the resurrection path on the in-progress guard bit so those blocks are freed instead of kept. Pin the closure/enum global fixtures to PHP's answers now that the shared walk sees nested bodies, and relax the lease cleanup ownership assertion to the cell's last detach.
Element-write helpers (hash_set, hash_unset, hash_append, hash_spread, the element-cell helpers) split under the ensure_unique convention and free the block they replace on growth. The global-receiver write-back still retired the previous pointer, so $_GET with more than sixteen parameters freed the pre-growth table twice inside the web handler and the request-end reset walked a reused block as a hash. Give global and raw-local receivers a publish-only write-back for those sites; the mutating builtins keep their retiring store-back. A store_ref_cell fed by array_to_hash of the same cell's load only publishes too: the conversion already consumed the cell's owner, and the second release freed a by-reference variadic's argument array under a first-class callable (SIGSEGV on aarch64). Pin the string reference-place fixture to the pruned release.
A mutating builtin's receiver on a Mixed-widened local is an unboxed load that lowering releases after the call. prepare_consuming_storeback drops the slot's previous box so that load is the sole owner during the in-place mutation, and the store-back then moved that owner into the new box: the array ended one owner short, sort($names) freed it under the box the slot kept, and an eval() escape walk iterated the reused block until the heap ran out (issue #1031's linux shape). Retain into the box exactly when a later instruction releases the value; receivers lowering never releases (array_multisort) keep the owner transfer.
|
…ss shard The eval shard runner reuses one libtest process for cache reuse, which dropped nextest's per-test slow-timeout and the retries-1 policy the other shards keep. Watch the single pass for progress and stop it when no test completes for the eval budget (180s) or the shard exceeds 30 minutes, then rerun under nextest only what the first pass did not report as ok, each test in its own process with the configured timeout and retry.
Consolidated review — 4 independent reviewersReviewed at Scope note. 3314 files, +152k/-19.6k, ~2100 of them generated docs. Nobody read all of it, and nothing was built or run here — a full codegen suite is a multi-hour build on this machine. So the ownership arithmetic below is traced from the emitted-code shape, not observed under The review was deliberately concentrated on what the PR description itself flags as riskiest: the late CI-stabilization fixes, above all item 7 (the conditional retain in the receiver store-back). 1.
|
Summary
Aligns the Core builtin contract and its AOT and Magician implementations, including the callable, ownership, and runtime-state regressions found during review. The branch was rebased onto main at
217ff6caaearly on; the later main changes (#991, #997, #1003, #1004, #1008) were brought in with a merge commit (3a85bfc2d) because a second rebase of the 700-commit history conflicted with #892's by-ref argument rewrite.The internal Core inventory is 59/59 on both backends. This is not a claim of complete PHP compatibility: the generated PHP 8.5 comparison is 57/62 functions. The inventory excludes
clone,get_error_handler(), andget_exception_handler(); the comparison countsdieandexitseparately as language constructs.Changes
error_reporting()queries, and unhandled user-error termination.PHP_INT_SIZE.Deliberate bounds
fclose()still operates on eval-managed streams. Synchronizing native locals into eval also retains them until the next synchronization or context release.Core/usergrouping.disable_functionsis unsupported, soget_defined_functions()accepts the flag without changing the inventory.get_required_files()remains PHP's alias ofget_included_files().threshold,buffer_size, andfulldo not claim Zend collector-buffer parity.CI stabilization
Fixes landed after the first full CI run, each from a red job on the matrix:
__rt_hash_set, but growth past 16 entries already frees that block;$_GETwith more than 16 parameters freed the table twice and the request-end reset walked a reused string as a hash (every web/session test red on all targets). Element-write sites now publish only; mutating builtins keep their retiring write-back.array_to_hashconsumes the reference cell's array and the followingstore_ref_cellretired it again; the freed block was reused by the persisted key string. The cell store now only publishes when fed by that conversion. Reproduced locally with a zig-backed linux-aarch64 build under qemu and with--heap-debugon x86.globaldeclarations. The shared walk now descends into closure bodies and enum methods, sounset()plus aglobal $xinside a closure keeps the shared symbol and prints PHP's answer; thelocals_retypefixtures pin the new behavior, including the vetoed kill being an ordinary retype in both modes.Closure::bindon an eval callback adapter) are bridged through runtime hooks; ancestor-private names on eval children are answered dynamically; runtime property names are cast to string.mixedvariable is accepted for a by-reference array parameter; container write-backs and scalar releases no longer drop closure facts.prepare_consuming_storebackdrops the slot's previous box so the receiver load is the sole owner during an in-placesort(), but the store-back then moved that owner into the new box while EIR still releases the load: the sorted array was freed under the box, and aneval()escape walk iterated the reused block until the heap ran out (AOT ReflectionClass::getMethods() followed by eval() reflection on the same class hangs forever #1031's shape, which main only avoids by leaking the old box). The receiver store now retains exactly when a later instruction releases the value;array_multisort, whose receivers EIR never releases, keeps the owner transfer. Regression:test_eval_reflection_after_aot_get_methods_and_sorted_local_terminates.--retries 1 --flaky-result pass.Validation
CI run 35119004532 on
abef3186bwas green across the full matrix, and run 35221162825 covers the receiver store-back fix on04024b2b9; run 35231600667 covers the eval shard runner ona3e590e2e(macos-aarch64, linux-aarch64, linux-x86_64 codegen/non-codegen/eval/web/PDO shards, iOS emitter checks, PDO live databases). The linux-aarch64 PDO shard 1/4 needed one rerun after a stuck runner with no log; it passed in its usual six minutes.Local testing was limited to single tests and small families (the affected web, session, GC, globals, hash, variadic and ownership families), per the stabilization instructions.
Closed issues
Verified by running each issue's reproduction on this branch and on current main (
129bf261e), against PHP 8.5.10:Closes Support func_get_args and func_num_args with default parameters #856 —
func_get_args()/func_num_args()in a function with default parameters compile and return the passed arguments (array(0) {}forvalues()).Closes Runtime callable invoker mis-marshals array arguments: bare
arrayparam receives garbage, loop-grown assoc boxed with the indexed tag #925 — the runtime callable invoker hands a barearrayparameter its real payload and boxes a loop-grown assoc with the hash tag; all three probe lines match PHP.Closes Reading $argc inside a closure body compiles and the binary segfaults (with a by-value capture overwrite) #783 — reading
$argcinside a closure with a by-value capture overwrite no longer segfaults; the program prints PHP's77|1(the "Undefined variable" warning is still not emitted).Closes eval() in a parameter default compiles and the binary segfaults #782 —
eval()in a parameter default no longer segfaults; the default is evaluated and the eval-local$astays out of the caller's scope (none|1). PHP rejects the construct at compile time; the issue accepts either outcome.Closes AOT ReflectionClass::getMethods() followed by eval() reflection on the same class hangs forever #1031 — the AOT
getMethods()+ eval reflection program no longer spins (macOS, verified on an arm64 host before and after the fix) or exhausts the heap (linux); the receiver store-back kept the sorted array alive under its box. The wrong casing of the printed names stays under Bug: ReflectionMethod::getName() loses declared method casing #571.Partially addresses #742: engine warnings (
Undefined array key, E_WARNING) andE_USER_WARNINGnow reachset_error_handler();error_get_last()and the mysqli warning sites are still open.Known follow-ups (not in this PR)
Closure::bindof an eval-declared closure from AOT leaks five blocks per bind.