refactor: consume z-engine through its documented API only (audit follow-up) - #14
Conversation
The audit goal: no engine internals outside z-engine. FFI/CData containment
was already clean - what leaked was internal knowledge in non-CData form,
in two cases as latent bugs.
- Strategy layer speaks the package's own vocabulary: SlotAddress plus
native-array SubstitutionPlan/SubstitutionRequest replace the ZEngine
TypeSlot/TypeSubstitutionMap/SlotSubstitutionMap imports. Monomorphizer
translates at the single point where the engine is asked, making AGENTS.md
item 13 true as written - it is now the only src/ class talking to z-engine
(EngineCapabilities keeps its deliberate name-only class_exists probe).
- 'Is the engine booted' is Core::isInitialized(), not isset(Core::$executor):
the wrappers are assigned mid-boot, so the probe reported a half-booted
bridge as ready. Generic::bootstrap() now delegates to Monomorphizer::boot().
- 'Can the engine run here' is Core::isUsable(), not a hand-rolled
filter_var(ini_get('ffi.enable')) - which rejected the supported 'preload'
mode (silently skipping the suite) and booted on hosts z-engine refuses
(fataling the whole suite; both observed). The benchmark harness, which
cannot run without the engine, just boots and relays Core::init()'s
explanation on failure.
- The eviction test uses the new ClassSpecializer::evict() instead of
reconstructing an internal z-engine procedure via Core::$executor and the
@internal HashTable::delete().
- EngineFacts reads struct sizes via Core::sizeOfType(), so no raw FFI\CType
crosses the package boundary.
Requires the z-engine side of this change (Core::isUsable/sizeOfType,
ClassSpecializer::evict) on the resolved z-engine line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019TGQqR7ByjHrVSYKVHPrkJ
|
CI status: all failing jobs share one root cause — Generated by Claude Code |
Follows z-engine's Composer bootstrap (lisachenko/z-engine#21): requiring the autoloader is now the whole boot, in the opcache.preload stage as well, so nothing here initializes the engine and nothing re-derives z-engine's environment rules. - tests/bootstrap.php just requires the autoloader. The filter_var(ini_get('ffi.enable')) check it carried was wrong in both directions - it rejected the supported 'preload' mode (silently skipping the whole engine suite) and booted on hosts z-engine refuses (fataling it); both observed on this container. - preload.php drops its Core::preload() call: z-engine's bootstrap recognises the preload stage and publishes the engine definitions itself. - Monomorphizer::boot() is the idempotent Core::init() - free after a successful auto-boot, and the explanation on a host where that boot was deliberately silent. Generic::bootstrap() still exists for exactly that: it moves the report to application start instead of the first specialization. - benchmarks/bootstrap.php relays that same explanation instead of failing later with a stack trace. - AGENTS.md gains item 14 recording the rule and both failure modes of the hand-rolled check, so it does not come back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TGQqR7ByjHrVSYKVHPrkJ
|
Status update — my earlier comment named Following the maintainer's proposal, this branch now boots nothing: z-engine initializes itself from its Composer bootstrap (lisachenko/z-engine#21), including during the CI here is still red for the one known reason, unchanged: composer resolves This goes green once lisachenko/z-engine#183 lands on the Locally, against z-engine#183 via a path repository: Generated by Claude Code |
The debug-build internal group went red on PHP 8.5 with '4 destructive test(s) skipped - the debug container cannot reach the engine', and the log said nothing more: RequiresEngine checked Core::isInitialized() and skipped in silence, so a gate that fails on skips gave nobody anything to act on. That silence is the regression. z-engine's automatic boot is deliberately quiet on a host it cannot run on, which makes it the caller's job to turn that into an explanation - the contract this package already documents and applies in Monomorphizer::boot(), and that the test trait was left out of. So the trait now does what any code needing the engine does: call the idempotent Core::init(). Free after a successful automatic boot; where that boot never happened at all - an old z-engine without the bootstrap, or ZENGINE_AUTOBOOT=0 - it simply boots and the tests run; and where the engine genuinely cannot start, the skip carries z-engine's own sentence saying why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TGQqR7ByjHrVSYKVHPrkJ
Why
Audit goal: keep all CData/FFI concerns inside z-engine and have this package consume only its API, with no internal knowledge leaking out.
The FFI containment was already clean — no
FFI\CData,FFI::, or raw struct pointer anywhere in this package. What the audit found instead were internal-knowledge leaks in non-CData form, two of them latent bugs:isset(Core::$executor)as an "engine booted" probe (Monomorphizer,RequiresEngine) — the wrappers are assigned mid-boot, so a failed boot read as ready.tests/bootstrap.phpre-derived z-engine's environment rule withfilter_var(ini_get('ffi.enable'), FILTER_VALIDATE_BOOL)— wrong in both directions, and both observed: it rejects the supportedpreloadmode (silently skipping the whole engine suite) and it boots on hosts z-engine refuses (fataling the suite at load).@internalHashTable::delete()throughCore::$executor, reconstructing an internal z-engine procedure.EngineFactsreceived a rawFFI\CTypeviaCore::sizeof(Core::type(...))— the one FFI object crossing the boundary.Strategylayer importedZEngine\Reflection\*value objects, so "Monomorphizer is the only class that talks to z-engine" (AGENTS.md item 13, README) was false.What
SlotAddress(on the existingTemplate\SlotKindenum) plus native-arraySubstitutionPlan/SubstitutionRequest.Monomorphizertranslates them intoTypeSlot/TypeSubstitutionMap/SlotSubstitutionMapat the single point where the engine is asked — item 13 is now true as written. Empty maps staynullat the boundary so placeholder-only templates keep working on z-engine lines predating slot substitution.opcache.preloadstage — sorequire vendor/autoload.phpis the whole boot.tests/bootstrap.phpjust requires the autoloader;preload.phpdrops itsCore::preload()call entirely.Monomorphizer::boot()is the idempotentCore::init(): free after a successful auto-boot, and the explanation on a host where that boot was deliberately silent.Generic::bootstrap()still exists for exactly that — it moves the report to application start instead of the first specialization.RequiresEngineasksCore::isInitialized().ClassSpecializer::evict();EngineFactsusesCore::sizeOfType().docs/long-running.mdupdated;EngineCapabilities' deliberateclass_exists()feature-probe documented so it isn't "fixed" later (it is the only detection that works on the old z-engine lines it exists to detect).Depends on
lisachenko/z-engine#183 — the Composer bootstrap,
ClassSpecializer::evict()andCore::sizeOfType(). Must land on the resolved z-engine line (and merge up tomasterfor the PHP 8.5 jobs) before CI here can go green.CI is currently red for exactly that reason and no other: composer resolves z-engine from packagist, which has none of the new API — e.g. Benchmarks dies at
EngineFacts.php:44onCore::sizeOfType()being undefined.Verified locally (PHP 8.5 container, z-engine#183 via path repo)
phpstan,php-cs-fixer,stubs:checkgreen; analysis suite 15/15 without ext-ffi — a stronger check than before, since the Strategy layer must now load with no z-engine classes resolvable.PreloadTest's child runs on the pinned 8.4 z-engine line under PHP 8.5, so z-engine correctly refuses; unrelated to this diff, green on CI's PHP 8.4.ZEngine\insrc/→ onlyMonomorphizer.php+ the name-only reference inEngineCapabilities.php;Core::$executor,Core::type(andisUsable→ zero matches.🤖 Generated with Claude Code
https://claude.ai/code/session_019TGQqR7ByjHrVSYKVHPrkJ