Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
716 changes: 716 additions & 0 deletions build/PHPStan/Build/TurboDeclarationGenerator.php

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ parameters:
identifier: shipmonk.deadMethod
path: PHPStan/Build/TurboAttributeCollector.php
reportUnmatched: false
-
# called from turbo-ext/bin/generate-declarations.php and
# turbo-ext/bin/side-by-side.php, outside the analysed paths
identifier: shipmonk.deadMethod
path: PHPStan/Build/TurboDeclarationGenerator.php
reportUnmatched: false
-
# called from bin/phpstan before the autoloader, outside the analysed paths
identifier: shipmonk.deadMethod
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '36c579f';
public const EXPECTED_EXTENSION_VERSION = '5b86419';

private static bool $active = false;

Expand Down
26 changes: 21 additions & 5 deletions turbo-ext/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ being ≥0.5% faster is. When the estimate is marginal, don't port.
matches nothing — use single quotes). Run the full test suite now, before
any native work.
2. **Note its parent and interfaces** — the native class is declared with
the twin's real name, final flag, parent and interfaces
(`cls.final()`, `cls.parent(...)`, `cls.implements({...})`), and linked
the twin's real name, final flag, parent and interfaces, and linked
like a PHP declaration: interface methods need declared return types, a
non-final class must dispatch its own non-final methods through the
object's class entry (a PHP subclass may override them). If it is a DI
Expand All @@ -42,9 +41,15 @@ being ≥0.5% faster is. When the estimate is marginal, don't port.
phpstan_turbo` that mirrors the PHP twin method for method (see
`TrinaryLogic.cpp` as the reference; `and`/`or` keyword clashes get a
trailing underscore); registration goes through the `reg::Class` builder
in `reg.h` — one `cls.method("name", flags, requiredArgs, { args... },
lambda)` declaration per method, where the lambda body is only
ZEND_PARSE_PARAMETERS glue + one delegation line (see TrinaryLogic.cpp).
in `reg.h` — one declaration per method: a method that only parses its
parameters and hands them, in order, to a handle member returning
`zv::Val`, `void` or `bool` with a trailing `bool &` out parameter is
`cls.method<&Handle::member, zp::Obj, zp::Bool>("name", flags, { args... },
returns)` with a generated handler; any other glue is a
`cls.method("name", flags, requiredArgs, { args... }, lambda)` whose
lambda parses with `zp::parse<zp::Obj, zp::Opt<zp::Bool>>(execute_data,
...)` (the raw ZEND_PARSE_PARAMETERS macros only for kinds zp does not
cover). Both expand to the engine's own ZPP macros.
Never introduce per-call argument boxing in a registration path — raw
handler pointers only. Use the zero-cost
wrappers in `zv.h` — borrowed `zv::Ref` views vs owned move-only
Expand Down Expand Up @@ -72,6 +77,17 @@ being ≥0.5% faster is. When the estimate is marginal, don't port.
`vendor/turbo-class-map.php` from the attributes (shadowed classes
living in vendor/ cannot carry the attribute and are hardcoded in
`build/TurboAttributeCollector.php`).
5a. **Generate its declarations**: `php turbo-ext/bin/generate-declarations.php`
writes `turbo-ext/src/generated/<Stem>.h` from the twin — `declareClass(cls)`
(final/abstract, parent, the directly implemented interfaces),
`declareProperties(cls)` (the twin's own properties, exactly) and the
`slot::` constants of its instance properties, and `sig::` — each
method's name, flags, arginfo and return type. Call both functions first
in the registration function and register the methods by signature
(`cls.method(sigs::accepts, handler)`, `cls.method<&Handle::accepts,
zp::Obj, zp::Bool>(sigs::accepts)`) instead of spelling them out; side-by-side.php
fails while a header is stale. A class whose native properties deliberately
differ from the twin keeps declaring them by hand.
6. **Check method parity**: `php bin/side-by-side.php` must pass (it also
re-derives the generated `vendor/turbo-*` files from the attributes and
byte-compares them, so a stale autoloader dump fails there).
Expand Down
2 changes: 1 addition & 1 deletion turbo-ext/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ OBJECTS := $(SOURCES:.cpp=.o)
phpstan_turbo.so: $(OBJECTS)
$(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LINK_FLAGS) -o $@ $(OBJECTS)

src/%.o: src/%.cpp src/support.h src/zv.h src/reg.h
src/%.o: src/%.cpp src/support.h src/zv.h src/reg.h $(wildcard src/generated/*.h)
$(CXX) $(CXXFLAGS) -c -o $@ $<

src/main.o: version.stamp
Expand Down
11 changes: 9 additions & 2 deletions turbo-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,13 +373,20 @@ The native sources are C++ that mirrors the PHP implementations they replace:
each shadowed class is a handle class in `namespace phpstanturbo` with the
twin's methods (see `src/TrinaryLogic.cpp` for the reference shape), built on
the zero-cost wrappers in `src/zv.h` — borrowed `zv::Ref` views, owned
move-only `zv::Val` RAII values, range-for HashTable iteration. The wrappers
move-only `zv::Val` RAII values, range-for HashTable iteration, `zv::Args`
argument packs for engine calls — and the shared bodies in `src/TypeTraits.h`
(a member the ports would otherwise repeat verbatim forwards there). The wrappers
compile to the same instructions as the raw zend macros (verified by
interleaved A/B benchmark), so readability costs nothing. Classes register
through the fluent builder in `src/reg.h`, which emits the raw zend
structures with raw handler pointers — no per-call trampoline or argument
boxing; each method's name, flags, signature and parameter-parsing glue live
together in one declaration.
together in one declaration. A method that only parses its parameters and
delegates them is declared by its handle member and parameter kinds
(`cls.method<&UnionType::accepts, zp::Obj, zp::Bool>(...)`) and gets a
generated handler; other glue parses with `zp::parse<...>()`. Both expand to
the engine's own `ZEND_PARSE_PARAMETERS` macros, so the handlers compile to
what the hand-written glue did.
Raw zend form remains where an abstraction would not be provably free —
always with a comment saying so.

Expand Down
58 changes: 58 additions & 0 deletions turbo-ext/bin/generate-declarations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php declare(strict_types = 1);

/**
* Generates turbo-ext/src/generated/<Stem>.h — the class declaration, the
* property slots and the property declarations of every shadowing class,
* derived from its PHP twin (PHPStan\Build\TurboDeclarationGenerator). Run
* it after changing a shadowed class's declaration; side-by-side.php fails
* while a generated header is stale.
*
* Usage: php turbo-ext/bin/generate-declarations.php
*
* Requires vendor/ (run composer install first).
*/

use PHPStan\Build\TurboAttributeCollector;
use PHPStan\Build\TurboDeclarationGenerator;

error_reporting(E_ALL);

$root = dirname(__DIR__, 2);
chdir($root);

require 'vendor/autoload.php';
require_once 'build/PHPStan/Build/TurboAttributeCollector.php';
require_once 'build/PHPStan/Build/TurboDeclarationGenerator.php';

$collected = (new TurboAttributeCollector($root))->collect();
$files = (new TurboDeclarationGenerator($collected['manifest']))->render();

$dir = 'turbo-ext/src/generated';
if (!is_dir($dir) && !mkdir($dir)) {
fwrite(STDERR, "cannot create $dir\n");
exit(1);
}

$written = 0;
foreach ($files as $path => $content) {
if (is_file($path) && file_get_contents($path) === $content) {
continue;
}
file_put_contents($path, $content);
$written++;
}
$removed = 0;
foreach (glob($dir . '/*.h') ?: [] as $existing) {
if (!isset($files[$existing])) {
unlink($existing);
$removed++;
}
}
$withoutProperties = 0;
foreach ($files as $content) {
if (str_contains($content, '/* no declareProperties():')) {
$withoutProperties++;
}
}

printf("%d headers (%d written, %d removed); %d without declareProperties()\n", count($files), $written, $removed, $withoutProperties);
135 changes: 133 additions & 2 deletions turbo-ext/bin/side-by-side.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,31 @@ function parsePhpMethods(string $file): array
return $methods;
}

/**
* The method names behind the sig:: identifiers of a generated header
* (turbo-ext/src/generated/<Stem>.h): identifier => PHP method name.
*
* @return array<string, string>
*/
function generatedSignatureNames(string $stem): array
{
static $cache = [];
if (isset($cache[$stem])) {
return $cache[$stem];
}
$header = 'turbo-ext/src/generated/' . $stem . '.h';
if (!is_file($header)) {
throw new RuntimeException(sprintf('%s does not exist — run php turbo-ext/bin/generate-declarations.php', $header));
}
preg_match_all('~inline constexpr reg::Sig (\w+) = \{ "(\w+)"~', file_get_contents($header), $m, PREG_SET_ORDER);
$names = [];
foreach ($m as [, $identifier, $name]) {
$names[$identifier] = $name;
}

return $cache[$stem] = $names;
}

/**
* @return array<string, array{startLine: int, endLine: int}>
* PHP_METHOD implementations, in source order
Expand All @@ -167,12 +192,21 @@ function parseCppMethods(string $file): array
}
}

// registrations by generated signature name their method through the
// file's `namespace sigs = ptdecl::<Stem>::sig;` alias
$signatures = preg_match('/^namespace sigs = ptdecl::(\w+)::sig;$/m', file_get_contents($file), $alias) === 1
? generatedSignatureNames($alias[1])
: [];

foreach ($lines as $i => $lineText) {
if (
preg_match('/^\s*(?:static\s+)?PHP_METHOD\(\s*\w+\s*,\s*(\w+)\s*\)/', $lineText, $m) !== 1
&& preg_match('/^\s*(?:cls\.|\.)method\("(\w+)"/', $lineText, $m) !== 1
) {
continue;
if (preg_match('/^\s*cls\.(?:method|traitMethod)(?:<[^(]*>)?\(sigs::(\w+)/', $lineText, $sm) !== 1) {
continue;
}
$m = [1 => $signatures[$sm[1]] ?? $sm[1]];
}
// prefer the handle-class member of the same (or underscore-suffixed) name
if (isset($handleClassMembers[$m[1]])) {
Expand Down Expand Up @@ -349,6 +383,20 @@ function checkGeneratedArtifacts(PHPStan\Build\TurboAttributeCollector $collecto
}
}

// the declarations generated from the PHP twins
require_once 'build/PHPStan/Build/TurboDeclarationGenerator.php';
$generated = (new PHPStan\Build\TurboDeclarationGenerator($collected['manifest']))->render();
foreach ($generated as $file => $content) {
if (!is_file($file) || file_get_contents($file) !== $content) {
$problems[] = sprintf('%s is stale — run php turbo-ext/bin/generate-declarations.php', $file);
}
}
foreach (glob('turbo-ext/src/generated/*.h') ?: [] as $file) {
if (!isset($generated[$file])) {
$problems[] = sprintf('%s belongs to no shadowed class — run php turbo-ext/bin/generate-declarations.php', $file);
}
}

return $problems;
}

Expand Down Expand Up @@ -378,8 +426,91 @@ function checkWindowsSources(): array
return $problems;
}

/**
* Every lowercase identifier the native code passes as PT_LC("...") — the
* lowercased method names of by-name calls into userland, $this-dispatch and
* function-table lookups — must name something that exists: a method of a
* PHP class (trait `as` aliases included), a property or constant, an
* internal function or member, a type keyword, or a method the extension
* registers itself. A misspelt name would otherwise only fail when that
* path first runs.
*
* @return list<string> problems
*/
function checkLowercaseNameLiterals(): array
{
$known = array_fill_keys(['array', 'bool', 'callable', 'false', 'float', 'int', 'iterable', 'mixed', 'never', 'null', 'object', 'resource', 'self', 'static', 'string', 'true', 'void', 'parent'], true);
$remember = static function (string $name) use (&$known): void {
$known[strtolower($name)] = true;
};
foreach (['src', 'vendor/nikic/php-parser/lib', 'vendor/ondrejmirtes/better-reflection/src', 'vendor/phpstan/phpdoc-parser/src'] as $dir) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$code = file_get_contents($file->getPathname());
preg_match_all('~\bfunction\s+&?\s*(\w+)\s*\(|\bas\s+(?:(?:public|protected|private)\s+)?(\w+)\s*;|\$(\w+)|\bconst\s+(?:\w+\s+)?(\w+)\s*=~', $code, $m);
foreach ([1, 2, 3, 4] as $group) {
foreach (array_filter($m[$group]) as $name) {
$remember($name);
}
}
}
}
foreach (array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits()) as $className) {
$class = new ReflectionClass($className);
if (!$class->isInternal()) {
continue;
}
foreach ($class->getMethods() as $method) {
$remember($method->getName());
}
foreach ($class->getProperties() as $property) {
$remember($property->getName());
}
}
foreach (get_defined_functions()['internal'] as $function) {
$remember($function);
}
$sources = array_merge(glob('turbo-ext/src/*.cpp'), glob('turbo-ext/src/*.h'), glob('turbo-ext/src/parser/*.cpp'), glob('turbo-ext/src/parser/*.h'));
foreach ($sources as $source) {
preg_match_all('~\.(?:method|traitMethod)(?:<[^(]*>)?\("(\w+)"~', file_get_contents($source), $m);
foreach ($m[1] as $name) {
$remember($name);
}
}

foreach (['__construct', '__destruct', '__call', '__callstatic', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__serialize', '__unserialize', '__tostring', '__invoke', '__set_state', '__clone', '__debuginfo'] as $magic) {
$known[$magic] = true;
}
// consumers of string data rather than member names
$dataConsumers = array_fill_keys(['zend_string_init', 'zend_string_init_interned', 'smart_str_appendl', 'Val::string', 'zv::Val::string'], true);

$problems = [];
foreach ($sources as $source) {
foreach (file($source) as $i => $line) {
preg_match_all('~PT_LC\("([a-z_][a-z0-9_]*)"\)~', $line, $m, PREG_OFFSET_CAPTURE);
foreach ($m[1] as [$name, $offset]) {
if (isset($known[$name])) {
continue;
}
$before = substr($line, 0, $offset - strlen('PT_LC("'));
if (preg_match('~\{\s*$~', $before) === 1) {
continue; // an entry of a { PT_LC("..."), ... } lookup table
}
if (preg_match('~([\w:]+)\s*\((?:[^()]*,\s*)?$~', $before, $consumer) === 1 && isset($dataConsumers[$consumer[1]])) {
continue;
}
$problems[] = sprintf('%s:%d: PT_LC("%s") names no method, property, constant or function', $source, $i + 1, $name);
}
}
}

return $problems;
}

$failed = false;
foreach (array_merge(checkStructure($manifest), checkGeneratedArtifacts($collector, $collected), checkWindowsSources()) as $problem) {
foreach (array_merge(checkStructure($manifest), checkGeneratedArtifacts($collector, $collected), checkWindowsSources(), checkLowercaseNameLiterals()) as $problem) {
printf("✗ %s\n", $problem);
$failed = true;
}
Expand Down
21 changes: 0 additions & 21 deletions turbo-ext/poc/README.md

This file was deleted.

Loading
Loading