Back to blog

pext-vanillify: AI post-processing for non-canonical JS patterns

The conversion pipeline produces correct JavaScript. Some of that JavaScript is not idiomatic JavaScript, because the PHP it came from was leaning on language features (the tokenizer extension, magic methods, reflection used for caller introspection) that have no JS-native equivalent. pext-vanillify is a post-processing pass that takes the converted output and rewrites specific patterns into something a JS engineer would have written by hand. It uses an LLM, it runs after the deterministic pipeline, and it logs every change it makes.

What pext-vanillify is

Pext's main pipeline is a deterministic PHP-to-JS transpiler. Given the same input it produces the same output, and that output is the correctness baseline. pext-vanillify sits after that pipeline as an opt-in second pass. It takes the converted JS, applies a small set of named skills (each one a prompt plus a verification harness), and emits a rewritten file alongside a log of every site it touched. Nothing is silent: every vanillified region is marked with a comment naming the skill and linking to the original PHP intent so the change is reviewable.

The set of skills is deliberately small. Vanillify is not for refactors the developer should be doing themselves. It is for the handful of patterns the transpiler can faithfully convert but cannot make idiomatic, because being idiomatic means understanding why the original code was written that way and picking a different mechanism. That is the kind of judgment call an AI is well-suited to and a deterministic pass is not.

Skill 1: detokenize Kint's caller introspection

kint-php/kint is a debug dumper. It exposes a function called like d($value) that dumps the value, and supports unary modifiers like +d($value), !d($value), ~d($value) on the call site to change the output mode (expand deeply, expand textually, force a specific renderer, and so on). The way Kint implements this is delightful and dirty: at runtime it tokenizes the caller's source file, finds the call site by line number, and reads the modifier characters off the tokens preceding the function name. The output mode is therefore controlled by the prefix at the call site, without any explicit argument being passed.

The transpiler converts this faithfully. The runtime has a tokenizer module that returns tokens with the same shape PHP does, so the conversion works and Kint runs. It is, however, exactly the kind of thing a JS engineer reading the output would flag in code review. The intent (let the caller pick the output mode) is great. The mechanism (re-tokenize my caller's source to figure out which sigil they used) is not how anyone would write this in JS.

The detokenize skill reads the Kint call sites and the dispatcher, identifies the modifiers (+, !, ~, and friends) as a fixed enum of control values, and refactors the entry-point function to accept the mode as an explicit parameter. Existing call sites in user code are rewritten to pass that parameter instead of relying on the sigil. The result is a Kint that is structurally the same, exposes the same set of behaviours, and reads like a normal JS dumper with named options. Every rewritten site is logged.

Skill 2: detokenize CI4's class-from-file resolution

CodeIgniter 4 uses the tokenizer for a different purpose. Several places in the framework need to answer "given this file path, what fully-qualified class does it define?". The implementation reads the file, runs token_get_all on it, walks the tokens looking for a T_NAMESPACE followed by a T_CLASS, assembles the qualified name, and returns it. This works, the converted version works, and it is again not how anyone would write this in JS, because JS has a different mechanism for the same question: the path-to-class mapping is the PSR-4 autoloader, which already knows the answer.

The detokenize skill replaces the tokenizer-based resolver with a PSR-4-backed lookup. Given a file path, the new implementation reverses the autoloader's namespace prefix mapping to compute the qualified class name directly, with a per-process cache because the result is stable for the lifetime of the request. For files that fall outside the configured PSR-4 roots (test fixtures, ad-hoc loaders), the skill leaves a documented fallback in place that still uses the runtime's tokenizer, and emits a runtime log so the slot is visible in operations.

The behavioural surface is the same. The performance is better, because the autoloader lookup is a hash lookup and a couple of string operations instead of opening the file and tokenizing it. The code reads like a normal JS module.

The shape of a vanillify skill

A vanillify skill is three things: a target detector (a deterministic check that says "this file or this region matches the pattern this skill knows how to rewrite"), a prompt that explains the intent and the target idiom, and a verification harness that confirms the rewrite preserves the externally observable behaviour. The harness is the safety rail. A skill that cannot verify itself does not run.

For the Kint skill the verification is a fixture of representative dump invocations and a comparison of the produced output strings, before and after. For the CI4 skill the verification is the framework's own tests over the autoloader and the class-from-file lookup, plus a synthetic set of PSR-4 mappings the skill resolves against. If either harness fails, the rewrite is rolled back and the skill produces a report instead of a patched file.

What's next

The first two skills (Kint's modifier sigils, CI4's class-from-file lookup) are in. The next two on the list are reflection-based dependency injection in CI4's services container (which has a JS-native pattern in the form of explicit factory functions) and dynamic property creation through __set on data-bag objects (which has a JS-native pattern in the form of typed records). Both have prompts drafted; both need verification harnesses before they can ship.

Pext-vanillify is opt-in and off by default. The deterministic pipeline is, and remains, the correctness baseline. If you want to see vanillify run against a chunk of your own codebase, book a demo.