Skip to content

Restore instanceof WP_Parser for the native parser#393

Merged
adamziel merged 2 commits intocodex/native-lazy-ast-facadefrom
adamziel/parser-instanceof
May 1, 2026
Merged

Restore instanceof WP_Parser for the native parser#393
adamziel merged 2 commits intocodex/native-lazy-ast-facadefrom
adamziel/parser-instanceof

Conversation

@adamziel
Copy link
Copy Markdown
Collaborator

@adamziel adamziel commented May 1, 2026

Addresses Jan's note on #381: in native mode, new WP_MySQL_Parser(...) instanceof WP_Parser returns false because the native-mode class extends the Rust-registered WP_MySQL_Native_Parser, which has no WP_Parser in its chain. Existing downstream code doing if ($parser instanceof WP_Parser) silently skipped the parser whenever the extension was loaded.

This restores the contract by always extending the pure-PHP WP_Parser and pulling the native-mode behaviour in via a trait:

class WP_MySQL_Parser extends WP_Parser {
    use WP_MySQL_Native_Parser_Impl;
}

WP_MySQL_Native_Parser_Impl owns the composed WP_MySQL_Native_Parser instance and the four-method delegation surface (parse, next_query, get_query_ast, reset_tokens). WP_Parser's protected state ($grammar, $tokens, $position) is initialised by parent::__construct and stays inert — the trait's overrides never read it.

Adding a public method later means adding it to the trait — the class file itself is two lines and doesn't need touching.

Why a trait, not a private property?

A bare property would also work, but the trait keeps the class file expressing only the routing decision (extends WP_Parser + use Rust_Implementation;). The implementation lives in one place, symmetric to where a future PHP-mode trait could live if we ever want to mirror the structure. Behaviour-wise the two are equivalent.

Performance

The trait adds one extra method-call frame per public-API call. The public API is parse(), next_query(), get_query_ast(), reset_tokens() — called once per query. The actual parsing work happens inside the native call, so the delegation overhead is a small constant per query, not a multiplier on the parsing work.

The Parser Delegation Perf workflow runs tests/tools/run-parser-benchmark.php (parses the full MySQL server-suite corpus, ~70k queries) three times on this PR and three times on the PR base, on the same runner, with the extension loaded both times. The comparison goes into the job summary on every push.

Test plan

  • PHP-only PHPUnit suite passes.
  • PHPUnit suite passes with the native extension loaded; the new WP_MySQL_Parser_Instanceof_Tests confirm instanceof WP_Parser and instanceof WP_MySQL_Parser both hold.
  • Parser Delegation Perf workflow shows the delegation cost is within noise.

Pre-PR, the native-mode WP_MySQL_Parser was `extends WP_MySQL_Native_Parser`
(a Rust class with no WP_Parser in its chain), so callers' existing
`if ($parser instanceof WP_Parser)` checks silently dropped the
parser when the extension was loaded.

This restores the contract by always extending WP_Parser and pulling the
native-mode behaviour in via a trait. The trait owns the composed
WP_MySQL_Native_Parser instance and the four-method delegation surface;
the class file itself is two lines. Adding a public method now means
adding it to the trait — no further wiring.

WP_Parser's protected state ($grammar, $tokens, $position) stays inert
in native mode: parent::__construct still initialises it, but the trait's
overrides never read it.

Adds a regression test for the instanceof contract and a CI workflow
that benchmarks parse() across the full MySQL server-suite corpus on
this branch and on the PR base, on the same runner. The delta is the
delegation overhead — one extra method-call frame per query.
The SQLite driver passes a WP_MySQL_Native_Token_Stream object as the
$tokens argument when the extension is loaded; pre-PR this worked
because the parent class was the Rust WP_MySQL_Native_Parser, which
accepts either an array or the stream object. The trait's constructor
needs the same flexibility — drop the array typehint and pass an empty
array to WP_Parser::__construct, whose tokens/position state is inert
in native mode.
@adamziel
Copy link
Copy Markdown
Collaborator Author

adamziel commented May 1, 2026

Parser delegation perf

CI run 25216548485. Three back-to-back runs of tests/tools/run-parser-benchmark.php on each branch, same runner, native extension loaded both times, 69,577 queries from the MySQL server suite.

run baseline (extends WP_MySQL_Native_Parser) this PR (extends WP_Parser + trait)
1 14.740s / 4720 qps 14.356s / 4846 qps
2 14.659s / 4746 qps 14.452s / 4814 qps
3 14.556s / 4780 qps 14.350s / 4848 qps
median 14.659s / 4746 qps 14.404s / 4836 qps

Median delta: −0.26s (−1.7%) duration, +1.9% qps — i.e. the trait/composition variant measured faster than baseline. That's not a real win; the trait adds an extra WP_MySQL_Native_Parser instantiation per query plus one delegating PHP method-call frame on parse(), which can only be neutral or slower in principle. The 2% gap is run-to-run noise on a shared GHA runner — the dominant cost per query is the parsing work itself (~210µs/query at 4800 qps), and the delegation overhead is single-digit microseconds, well below what this benchmark resolves.

The honest summary: the delegation cost is invisible at this granularity, which is what we wanted to confirm before fixing the instanceof WP_Parser regression. The trait/composition pattern restores the contract Jan flagged at no measurable perf cost.

Other CI

PHP 8.2 / Rust extension and SQLite driver / Rust extension both pass on the latest commit, which means the new WP_MySQL_Parser_Instanceof_Tests regression test runs and verifies instanceof WP_Parser and instanceof WP_MySQL_Parser both hold under the extension.

@adamziel adamziel merged commit 2b595f3 into codex/native-lazy-ast-facade May 1, 2026
25 checks passed
@adamziel adamziel deleted the adamziel/parser-instanceof branch May 1, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant