Inspiration: Go
defer· Layer: lexer + parser + compiler + runtime · Commit:f261dc6
defer CALL; schedules a call to run when the enclosing function returns by any
path — normal return or exception — in LIFO order. It replaces nested
try / finally for cleanup:
function copy(string $src, string $dst): void {
$in = fopen($src, 'r');
defer fclose($in); // runs on return OR throw
$out = fopen($dst, 'w');
defer fclose($out); // runs before fclose($in) (LIFO)
stream_copy_to_stream($in, $out);
}The callee and the arguments are evaluated at the defer point; only the
invocation is delayed.
$x = 1;
defer printf("%d\n", $x); // captures 1...
$x = 2; // ...so this prints 1, not 2
defer $obj->m(); // captures *this* $obj even if $obj is reassigned laterThe operand must be a function / method / static call. Positional and spread
(...$a) arguments are supported; named arguments are not (v1), and any
non-call operand is a compile error.
zend_compile_defer() desugars each statement to a hidden per-frame collector:
$<hidden> ??= new DeferScope(); // created lazily, once per frame
$<hidden>->push(CALLEE(...), [ARGS]); // FCC captures callee NOW; [ARGS] eval NOWCALLEE(...)uses first-class-callable syntax, which evaluates the receiver / closure immediately (Go-faithful callee capture).[ARGS]is an array literal built from the call's argument list; a spread...$ais carried over verbatim, so its elements are flattened into the captured array at the defer point.- The hidden storage is a compiler-allocated CV with an internal, NUL-prefixed
name unreachable from userland, so the
DeferScopeis owned solely by the frame. Its__destructfires exactly when the frame's locals are freed — on normal return and during exception unwind — and replays the collected calls LIFO.
Because the callee and args are captured up front, deferred calls don't depend on other locals still being alive at teardown.
A runtime detail worth noting: DeferScope declares private array $entries = [],
whose default is the engine's immutable empty array. SEPARATE_ARRAY won't clone
it (its refcount is 1), so push installs a fresh owned array on the first call and
appends thereafter.
- A defer runs during teardown — after the return value is computed — so it can't change the return value.
- Don't throw from a defer. PHP's destructor-exception handling is restrictive,
and there is no clean
recover()analogue. DeferScopebecomes a global class (used only by the desugar) and so is a reserved name.deferis only semi-reserved — a contextual keyword in statement position; it stays usable as a method / constant name (no BC break).
smoke.sh— smoke tests (LIFO, exception path, loop, capture, spread, static call, return value, non-call error, semi-reserved name)tests/defer.phpt