Skip to content

AOT: builtin class methods are resolved from a hand-written proxy allowlist, so every unlisted method lowers to a silent null — root cause of the one-issue-per-method treadmill (lib/JIT/Context.php findInternalBuiltinInModule, lib/JIT/Call/ExternalMethod.php) #36202

Description

@PurHur

Category

Foundation: · builtin dispatch architecture · child of #36188

Problem

Since 2026-08-30 the tracker closed 81 issues titled AOT: <Class>::<method> … — leftover of <sibling> (#NNNN) (XMLReader ×25, SQLite3 ×12, SimpleXML, XMLWriter, DOM …); 1,066 of the ~2,000 commits of the last 14 days carry the AOT: prefix, most of them one-method fixes. They share one mechanical cause:

  • Functions are found by Context::findInternalBuiltinInModule() (lib/JIT/Context.php:1277-1289), which scans only $module->getFunctions(). Class methods are never looked up there. When resolution fails, :1114 installs new Call\ExternalMethod($proxyName) whose call() emits __value__writeNull (lib/JIT/Call/ExternalMethod.php:41-47) — the call site becomes null with no diagnostic (Self-host P0: JIT external class method calls in bundled lib (PhpParser, etc.) #579).
  • The compensation is a set of hand-maintained allowlists: lib/JIT/XmlReaderInstanceMethodJit.php:12-68 (const METHODS, 28 entries, each annotated // leftover of …), its mirror match in ext/xmlreader/JitXmlReaderMethod.php:16-70, SimpleXml/Xslt/XmlWriter/RandomizerInstanceMethodJit, and ~369 functionProxies[...] = lines in Context.php (SQLite3 :2452-2472, DateTime :2484-2600).
  • Coverage: 771 VM method registrations across 56 files vs 327 distinct class::method JIT proxies → ~73 % of builtin class methods are silent null under AOT today; 40 of 84 ext/ directories contain no Jit* file at all.

The fix is already 90 % built: lib/VM/Builtin/VmClassMethod.php:17abstract class VmClassMethod extends Internal — so every VM method object already implements Call, and its inherited call() (:19-24) throws LogicException('… is not implemented for JIT in this compiler build'). The VM registry is reachable from the JIT: Context::$runtime (:586) → Runtime::$vmContext (lib/Runtime.php:91) → VM\Context::$classes (lib/VM/Context.php:20) → ClassEntry::$methods (lib/VM/ClassEntry.php:52).

php-src reference

  • Zend/zend_API.czend_register_internal_class_ex / zend_register_functions: one registration feeds every executor; there is no second table for the JIT.

PHP implementation target

  1. Context::findInternalClassMethodInModule(string $lc): split on ::, look up $this->runtime->vmContext->classes[$class]->methods[$method], return it (it is a Call). Call it from resolveRegisteredInternalBuiltin() (:1204-1212) before falling back to Call\ExternalMethod at :1114. Result: a method without a lowering fails at compile time with its name, never at runtime with null.
  2. Move lowering onto the VM method class (public function call() overridden in e.g. ext/xmlreader/XmlReaderRead.php), then delete the allowlists (XmlReaderInstanceMethodJit::METHODS, the JitXmlReaderMethod match, the functionProxies[...] block). Adding a method must touch one production file.
  3. ExternalMethod stays only for genuinely external (user-code cross-TU) classes and must be loud by default: drop the array_slice($names, 0, 40) cap at Context.php:1503, make emitStubReachedWarning unconditional, and default PHP_COMPILER_FAIL_ON_EXTERNAL_STUBS=1 for user-script AOT.
  4. Fix the two ad-hoc helper-scope whitelists (isPreRegisterModuleNestedJitKernel / isSpineChunkRuntimeInternalKernel, Context.php:1217-1244) that exist because $this->modules is empty in NestedJIT/SPINE_CHUNK contexts — the direct cause of AOT: substr()/strlen() lower to silent-null stubs inside JitVmHelperLink helpers, while correct in user code #24217 (substr/strlen silent null inside helpers): helper compiles must resolve against $runtime->modules.

Repro

./script/docker-exec.sh -- bash -lc 'source script/php-env.sh && cat > build/m.php <<'"'"'EOP'"'"'
<?php $r = new XMLReader(); $r->XML("<a b=\"1\"/>"); $r->read(); var_dump($r->getAttributeNo(0)); $d = new SQLite3(":memory:"); var_dump($d->lastErrorCode());
EOP
php bin/compile.php -o build/m build/m.php && ./build/m; php build/m.php'

Pick any method not in the allowlists; today it prints NULL under AOT and the right value under Zend.

Done when

  • A fabricated call (new SQLite3(":memory:"))->noSuchMethod() fails the AOT build with the class::method name (not a runtime null); the same for any registered-but-unlowered method
  • grep -c "functionProxies\[" lib/JIT/Context.php < 40; XmlReaderInstanceMethodJit::METHODS and JitXmlReaderMethod match deleted
  • AOT: substr()/strlen() lower to silent-null stubs inside JitVmHelperLink helpers, while correct in user code #24217 repro (substr inside a JitVmHelperLink helper) passes with no whitelist entry
  • script/differential-sweep.sh --aot --repeat 3 unchanged; a new test/differential/cases/ case exercises ≥ 5 previously-null methods per class for XMLReader, SQLite3, SimpleXMLElement, XMLWriter, DOMDocument
  • CONTRIBUTING gains a rule: no new per-method allowlist entries; new methods override call() on the VM class

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    MOST IMPORTANTThis are the most important targetsarea:compilerCompiler / CFG / JITbugSomething isn't workingimplementation-readySpec complete: repro, php-src ref, done-when — safe for workers to claimphase-3:aotPhase 3 – AOT deployment

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions