Week in review: 29 June 2026
A recap of what shipped during the week of 29 June. Three items this week: Bun is now a supported runtime for Pext-converted code, a new closed-world conversion flag opts a project into ingesting the entire source set as one unit, and an early POC of pext-viz visualises the control flow of a converted project.
Bun as a supported runtime for converted code
Bun has been on the roadmap for a while; the WASM work covered in the 15 June recap unblocked it. The runtime modules that used to bind against Node-specific N-API packages (PCRE via a native oniguruma binding, LibXML via libxmljs2) were the two hard blockers, and swapping both onto the new pext-wasm-* packages removed the last dependency on NODE_MODULE_VERSION. Bun's JavaScriptCore engine now happily loads every transpiled artifact the runtime produces.
A single non-obvious issue surfaced during the bring-up. Bun's JIT inlines short arrow functions more aggressively than V8, which broke pext-reflection's caller-frame discovery: a () => new Error() used to capture the caller's stack frame got inlined out of existence, and ReflectionClass::getFileName then returned the wrong path. The fix was to rewrite the frame-capture as a try/throw/catch, which the inliner leaves alone under both engines. With that in place, the converted PHPUnit suite passes at parity on Bun (labelled pun in the internal test matrix, alongside pext for Node) and finishes in comparable wall-clock time.
The closed-world conversion flag
The transpiler has always converted files one at a time. That works because PHP itself has no cross-file compile step; you can add a file without touching the rest. What it does not do is let the transpiler reason across files. Under pext.json:"closedWorld": true, the pipeline switches to a two-pass shape: first, every file is annotated in isolation, then an inter-file pass runs over the whole source set with those annotations available, and only then does each file emit. The output is byte-identical to the per-file flow until a specific inter-file analysis is turned on.
Closed-world is infrastructure only. Each analysis that rides on it is its own flag layered on top. The first shipped one is asyncColoring, which builds the inter-procedural call graph, seeds it from the runtime modules' awaitable and awaitable_methods manifests, and propagates async and await through the reverse call graph. The design is covered at more length in the async/await SAPI post from earlier this month. Other analyses (purity inference, dead-code elimination across files, escape analysis at call sites) will each be their own flag on the same closed-world scaffolding.
The trade-off is what it always is for whole-program analysis: you give up incremental compilation and pick up cross-file precision. For a Pext user, that means the closed-world pipeline is opt-in per project, and for the case where you know your source set and want the extra precision, the ceiling on what the transpiler can prove goes up considerably.
pext-viz POC for control-flow visualisation
The last item is early-stage: a POC for a module that visualises the control flow of a PHP codebase after the closed-world analysis has run over it. The picture that comes out is roughly a call graph with the async colouring overlaid, plus the escape edges from the closure analysis, plus the class-hierarchy edges that autoloading walked. The intent is to make the inter-file information the pipeline already computes visible to a human reader, so an engineer looking at a migration decision can see the shape of their own codebase.
The immediate use cases are internal. Deciding which subsystem to migrate first is a decision an architect wants to make with the call graph in front of them. Understanding why a given handler was coloured async (and which downstream call justified it) is easier from a picture than from a text report. Whether pext-viz becomes a shipped module or stays a companion tool is a decision for later; for now the POC is enough to prove the closed-world pipeline is producing information a visualisation layer can consume.