Generate the declarative half of the native classes from their PHP twins - #6491
Open
ondrejmirtes wants to merge 12 commits into
Open
ondrejmirtes wants to merge 12 commits into
ondrejmirtes wants to merge 12 commits into
Conversation
A guard whose body is only a return, RETURN_THROWS(), break or continue is written `if (cond) return x;` — mostly the propagation of a pending exception after each engine call. Guards that would grow wider than 160 columns keep their braces; macro bodies and else chains are untouched. Token-identical, so every object compiles byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
A method that only parses its parameters and delegates them in order to a handle member is registered as cls.method<&Handle::member, zp::K...>(...): reg::detail::Bound generates the handler (zv::Val, void, or bool with a trailing bool & out parameter). Every other ZEND_PARSE_PARAMETERS block of the supported kinds is written zp::parse<K...>(execute_data, ...). Both expand to the engine's own ZPP macros per slot, so the handlers compile to the hand-written glue's code (a generated handler differs only in how its error paths are outlined). The 46 parameter blocks of the existing native sources parse through zp::parse; the ports to come register their delegating methods in the bound form, which side-by-side.php accepts. PT_RETURN_VAL, the zv::Val-into-return_value tail the generated handlers share with the hand-written ones, lives in reg.h. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
The `zval args[N]` + one ZVAL_* line per slot of an engine call is a zv::Args pack: the type of each value picks the ZVAL_* macro, nothing is addref'ed, and the pack converts to the zval * the call takes. Everything is inline and fills the same zvals the macros did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
Drop the sentences a header repeats from README.md and the registration code below it: the handle class mirroring the twin with the registration as ABI glue, a bare "declared as ... at activation" restating cls.final()/implements(). File-specific notes stay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
zv::Val, zv::Arr and zv::Str are [[nodiscard]] types (an UNDEF value is a pending exception), and so are the functions documented to signal one through their bool / -1 / NULL result. No call site dropped such a result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
side-by-side.php now verifies every PT_LC("...") lowercase identifier the
native code passes — the lowercased method names of by-name calls,
$this-dispatch and function-table lookups — against the methods (trait
aliases included), properties and constants of src/ and the vendored
libraries, the internal functions and members, the type keywords, PHP's
magic methods and the methods the extension registers itself. Lookup-table
entries and plain string data passed to zend_string_init() and the like
are not names. A misspelt name used to fail only when its path first ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
turbo-ext/bin/generate-declarations.php derives the declarative half of every shadowing class from its PHP twin (PHPStan\Build\TurboDeclarationGenerator) into turbo-ext/src/generated/<Stem>.h: declareClass() with the final / abstract flags, the parent and the directly implemented interfaces, declareProperties() with the twin's own property declarations, and the OBJ_PROP_NUM slot of each instance property. The 13 registrations of the shadowing classes call declareClass() instead of spelling cls.final() / parent() / implements() out; the 6 classes whose native properties equal the twin's call declareProperties() instead of their builder calls (the rest deliberately declare different state). A reflection dump of every native class — modifiers, parent, interfaces, constants, properties with types and defaults, method signatures — is identical before and after. side-by-side.php fails while a generated header is stale. reg.h can declare every property shape a twin spells: reg::Class::property() takes the visibility flags, the kind and the type, and the kinds cover a typed property with no default and one defaulting to null, [], false, a bool or an int, class-typed ones and unions of classes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
The numeric #define PT_*_PROP_* slot macros of the native classes become the generated slot constants (ptdecl::<Stem>::slot, aliased as slots::) — mapped by index, so every object compiles byte-identical. A slot number now follows the twin's declaration instead of being counted by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
The declaration generator also emits each method's signature (name, flags, required count, arginfo, return type) into the class's generated header, and a header per trait the shadowed classes use. Registrations take them — cls.method(sigs::accepts, handler), cls.method<&Handle::accepts, zp::Obj, zp::Bool>(sigs::accepts), cls.traitMethod(sigs::isArray, handler) — instead of spelling names, flags, parameter descriptors and return types out; a bound registration whose parameter kinds no longer match its signature refuses to load the module. The generated signatures are constexpr, and so are the reg::Arg builders they call. Methods of classes without a PHP twin, and those whose native signature deliberately differs from the twin's, keep their hand-written arginfo. The reflection dump of every native class is identical before and after; side-by-side.php resolves sigs:: names through the headers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
The generator also renders ptdecl::<Stem>::registerTraits(cls) for a class whose twin uses traits: the pt_type_trait_*() registrars of those traits, in the twin's order. A class that runs exactly the registrars its twin uses calls it instead of listing them one by one; one that deliberately runs a different set, or runs them in another order, keeps its own calls. No native class uses a trait yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2Qsp9sqyymJYoqgoMkgLT
The patch recorded three measured changes to ondrejmirtes/better-reflection (the getName() memo, the attribute-less short-circuit in getAttributesByName(), the cached-member checks hoisted in ReflectionClass) because vendor/ is git-ignored and they could not be committed. Nothing applies the file, so it only goes stale as the vendored code moves - those changes belong in a pull request against that repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Derives the declarative half of every native class from its PHP twin, and trims the boilerplate the hand-written registrations carried.
Generation
turbo-ext/bin/generate-declarations.php(backed by the newbuild/PHPStan/Build/TurboDeclarationGenerator.php) writesturbo-ext/src/generated/<Stem>.hfrom the twin:declareClass(cls)(final/abstract, parent, directly implemented interfaces) anddeclareProperties(cls)(the twin's own properties, exactly), called first in the registration function instead of being spelled out by hand. A class whose native properties deliberately differ keeps declaring them itself.slot::constants of the instance properties, so the native code stops hardcodingOBJ_PROP_NUMindices. A twin that gains a property used to shift every slot silently.pt_type_trait_*entries a class pulls in.side-by-side.phpfails while a generated header is stale, so a twin that changes shape breaks the build rather than the analysis.Hygiene
zv::Args.[[nodiscard]], so a dropped result is a compile error.side-by-side.phpnow checks everyPT_LC("...")lowercase identifier against the real methods (trait aliases included), properties, constants, internal functions and members, type keywords and magic methods. A misspelt name used to fail only when its code path first ran.Also
turbo-ext/poc/goes away. It held a patch recording three measured changes to the vendoredondrejmirtes/better-reflectionthat could not be committed becausevendor/is git-ignored. Nothing applies the file, so it only goes stale as the vendored code moves — those changes belong in a pull request against that repository.Verification
generate-declarations.phpis a no-op on the committed headers (13 headers (0 written, 0 removed)), at every commit that touches the generator.bin/phpstan analyseoversrc/Type src/Analyser src/Reflection: output identical with the extension on and off (separatetmpDirper leg, activation verified per leg).smoke.php,signature-parity.php(79 methods),side-by-side.phpincluding the new name check: OK.make phpstan,make lint,make cs: clean.make testswith the extension loaded and active: 22016 tests, 97656 assertions, OK (65 skipped).🤖 Generated with Claude Code
https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN