Summary
Boxing an owned object into a Mixed cell leaks the producer's reference: __rt_mixed_from_value retains object payloads (tag 6 → __rt_incref), and __rt_mixed_free_deep correctly releases the child when the cell dies — but EIR lowering treats Op::MixedBox as consuming its operand and never releases the producer's own reference. Net +1 on the payload per boxing of an owning temporary.
The everyday shape is a nullable-object return:
class P { public int $v = 7; }
function g(): ?P { return new P(); } // return coerces to Mixed: object_new (rc=1) → mixed_box (payload rc→2)
$acc = 0;
for ($n = 0; $n < 50; $n++) { $c = g(); $acc += $c->v; }
echo $acc; // 350, correct
Verified on
main (7396e65b), macOS ARM64, --heap-debug: 50 leaked blocks in 50 iterations — exactly the P objects. The boxed cells themselves are freed correctly on rebind (__rt_decref_mixed → __rt_mixed_free_deep → decref_any(child) drops the child from 2 to 1 — never to 0).
IR of the return path:
v5: Heap(Object) php=P own=maybe_owned = object_new … ; producer reference, rc=1
v6: Heap(Mixed) = mixed_box v5 ; runtime retains payload, rc→2
return v6 ; v5's reference never released
Expected
One reference per owner: after boxing, the cell owns the payload; an owning-temporary operand's producer reference must be released (a borrowed operand — e.g. boxing a loaded local — is correct as-is: the retain gives the box its own reference alongside the local's).
Suggested direction
In the Op::MixedBox lowering/emission path(s) in ir_lower, release the operand after boxing when value_is_owning_temporary(operand) — same ownership pattern as the recent throw-of-borrowed fix in #477. Needs an audit of all MixedBox emission sites (return coercion, argument coercion, storage coercion) so the rule is applied uniformly.
Notes
Surfaced while verifying #447 (whose original stored-tryFrom shape is fixed on current main). Enum tryFrom itself no longer exhibits this class (its payload is a persistent singleton); any ?Class/mixed-returning function that produces fresh objects does.
Summary
Boxing an owned object into a Mixed cell leaks the producer's reference:
__rt_mixed_from_valueretains object payloads (tag 6 →__rt_incref), and__rt_mixed_free_deepcorrectly releases the child when the cell dies — but EIR lowering treatsOp::MixedBoxas consuming its operand and never releases the producer's own reference. Net +1 on the payload per boxing of an owning temporary.The everyday shape is a nullable-object return:
Verified on
main(7396e65b), macOS ARM64,--heap-debug: 50 leaked blocks in 50 iterations — exactly thePobjects. The boxed cells themselves are freed correctly on rebind (__rt_decref_mixed→__rt_mixed_free_deep→decref_any(child)drops the child from 2 to 1 — never to 0).IR of the return path:
Expected
One reference per owner: after boxing, the cell owns the payload; an owning-temporary operand's producer reference must be released (a borrowed operand — e.g. boxing a loaded local — is correct as-is: the retain gives the box its own reference alongside the local's).
Suggested direction
In the
Op::MixedBoxlowering/emission path(s) inir_lower, release the operand after boxing whenvalue_is_owning_temporary(operand)— same ownership pattern as the recent throw-of-borrowed fix in #477. Needs an audit of allMixedBoxemission sites (return coercion, argument coercion, storage coercion) so the rule is applied uniformly.Notes
Surfaced while verifying #447 (whose original stored-
tryFromshape is fixed on currentmain). EnumtryFromitself no longer exhibits this class (its payload is a persistent singleton); any?Class/mixed-returning function that produces fresh objects does.