Week in review: 8 June 2026
A recap of what shipped during the week of 8 June. Performance week: a new analyze step in the conversion pipeline that statically infers what each function actually does with its variables, a grab-bag lowering that skips dynamic scoping when none of it is needed, and an optimized $this emission that goes directly through the object reference. End result: the PHPUnit run for the converted test suite is now about half as long as it was at the start of the week.
An analyze step in the conversion pipeline
The transpiler now runs an analyze pass over each PHP function and method before it emits JS. The analyzer answers a small set of questions about every local variable and every parameter: does it ever get passed by reference, does the function take its address, is it ever mutated through aliasing, does it escape the function via a closure capture, does its value get observed across an exception boundary. The answers are stored as flags on the AST node, and the emitter reads those flags when it decides which shape to lower into.
The full Pext scoping layer (the Pscope, Pvar, Pvalue tower from the exporter post) exists to make sure PHP's reference semantics, copy-on-write, and refcount-driven destructors behave correctly under any program. That tower is necessary in general and overkill for the vast majority of individual functions, which never actually take an address or alias anything. The analyzer lets the emitter notice the difference, and the emitter does the rest.
Grab-bag variable lowering
The first place the analyzer feeds into the emitter is in lowering grab-bag locals. For a function whose local variables and parameters are not flagged for any of the questions above, the emitter now lowers them as plain JS let bindings instead of routing them through Pvar and Pvalue. The semantics are still correct, because the analyzer has established that nothing in the function ever does anything that needs the refcount-driven machinery: no & in this scope, no closure captures the address, no destructor depends on when this local goes out of scope.
The shape of the emitted code is significantly closer to hand-written JS. A function that adds two numbers and returns the result now looks like a function that adds two numbers and returns the result, instead of a function that wraps each number in a Pvar, binds them into a Pscope, looks up the operands by name, calls the addition adapter, and unwraps the result for return. The dynamic-scoping path is still available for functions that need it; the analyzer just stops asking for it when it knows it is not needed.
Optimized $this emission
The second place is $this. Inside a method, $this has a fixed set of invariants the language guarantees: it is read-only as a slot (you cannot rebind $this mid-method), it cannot be passed by reference (you cannot write doStuff(&$this)), and its identity is established at method dispatch. None of those invariants needed any of the dynamic scoping machinery. The emitter now treats $this as a direct reference to the receiving object, and reads through to its property slots without the Pvar indirection.
The fast path applies to most method calls in most converted code. Where a method does something exotic (rebinding through Closure::bind, taking the address of an entire object, leaking $this into a closure that survives the call), the analyzer notices and falls back to the general path. The general path is the same one that always existed, just no longer the default for every method.
PHPUnit run time cut in half
The combined effect of the analyze step, the grab-bag lowering, and the $this fast path is a roughly 2x speedup on the converted PHPUnit run. The unit-test pass rate did not change; the same tests pass, the same handful still fail, the suite just gets through them in about half the wall-clock time it used to take. The result is large enough that a couple of CI jobs that had been slow now feel responsive again, and the iteration loop for the Phase 5 testing-stack work is noticeably shorter.
More of the same is queued. There is a small set of other invariants the analyzer can answer cheaply (string immutability of locals, integer-typed parameters, simple foreach iteration patterns), and each one unblocks another lowering shortcut for the emitter. The deterministic correctness baseline is unchanged; the speedups are pure emitter work.