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
7 changes: 5 additions & 2 deletions .github/workflows/phar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ permissions:
env:
# The php-parser version whose grammar tables and semantic actions the
# native parser engine (turbo-ext/src/parser/) was ported against.
SUPPORTED_PHP_PARSER_VERSION: "v5.8.0"
SUPPORTED_PHP_PARSER_VERSION: "v5.9.0"
# Composer for the turbo container legs, pinned by checksum. The distro
# composers are unusable there: jammy's 2.2.6 fatals under PHP 8.5, and
# Alpine's /usr/bin/composer is a shell wrapper hardwired to the
Expand Down Expand Up @@ -205,7 +205,7 @@ jobs:
echo "composer.lock: $INSTALLED, native engine ported against: $SUPPORTED_PHP_PARSER_VERSION"
if [ "$INSTALLED" != "$SUPPORTED_PHP_PARSER_VERSION" ]; then
echo "::error::nikic/php-parser was updated to $INSTALLED but turbo-ext/src/parser/ was ported against $SUPPORTED_PHP_PARSER_VERSION."
echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin."
echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php and turbo-ext/tests/parser-upstream-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin."
exit 1
fi

Expand Down Expand Up @@ -527,6 +527,9 @@ jobs:
- name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)"
run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -d memory_limit=4G turbo-ext/tests/parser-corpus.php

- name: "Parser upstream corpus (the same differential over php-parser's own test cases)"
run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/parser-upstream-corpus.php

- name: "Upload extension artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
12 changes: 6 additions & 6 deletions compiler/box/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions compiler/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"nette/php-generator": "3.6.9",
"nette/schema": "^1.2.2",
"nette/utils": "^3.2.5",
"nikic/php-parser": "^5.8.0",
"nikic/php-parser": "^5.9.0",
"ondram/ci-detector": "^4.0",
"ondrejmirtes/better-reflection": "6.73.0.4",
"ondrejmirtes/composer-attribute-collector": "^1.2.0",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class TurboExtensionEnabler
* version is the short SHA of the last commit touching turbo-ext/src/,
* enforced by the phar.yml turbo-version job.
*/
public const EXPECTED_EXTENSION_VERSION = 'e8e7544';
public const EXPECTED_EXTENSION_VERSION = '0867753';

private static bool $typeCombinatorCacheEnabled = false;

Expand Down
16 changes: 9 additions & 7 deletions turbo-ext/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ being ≥0.5% faster is. When the estimate is marginal, don't port.
(arginfo parameter names must match the PHP twin exactly — named arguments),
full `make tests` with the extension loaded, and byte-identical analysis
output with the extension loaded vs. not loaded. Anything touching
`src/parser/` additionally runs `turbo-ext/tests/parser-corpus.php`
(byte-identical ASTs over the whole corpus); a php-parser version bump in
composer.lock requires the same.
`src/parser/` additionally runs `turbo-ext/tests/parser-corpus.php` and
`turbo-ext/tests/parser-upstream-corpus.php` (byte-identical ASTs over
the whole corpus and over php-parser's own test cases); a php-parser
version bump in composer.lock requires the same.
9. **Benchmark** (protocol below). ≤0.5% → revert the port, keep the PHP
extraction only if it stands on its own.
10. **Version bump** (only when `turbo-ext/src/` changed): commit the
Expand Down Expand Up @@ -163,10 +164,11 @@ Follow README.md's "Updating php-parser" procedure. The agent-relevant traps:
- `ParserEngine::reduce` dispatches via `PN_REDUCE_SPLIT_1/2` from the generated
`ParserRunnerActionsSplit.h`; the generator rebalances the three action
files automatically.
- The corpus differential (`tests/parser-corpus.php`) is the acceptance bar
— byte-identical serialized ASTs, errors, and token streams — and it only
proves what the corpus contains: new syntax needs fixtures in the repo
before the check means anything for it.
- The corpus differentials are the acceptance bar — byte-identical
serialized ASTs, errors, and token streams. `tests/parser-upstream-corpus.php`
runs php-parser's own test cases at the installed commit, so it covers new
syntax as soon as the lock file moves; `tests/parser-corpus.php` only
proves what the repo contains, so new syntax also needs fixtures here.
- Finish with both pins: `SUPPORTED_PHP_PARSER_VERSION` in
`.github/workflows/phar.yml` plus the extension version bump in
`TurboExtensionEnabler` (`src/parser/` changed).
Expand Down
17 changes: 12 additions & 5 deletions turbo-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ differential coverage (in `$covered` there, or via its dedicated script).

## The native parser engine

`src/parser/` reimplements php-parser 5.8.0's LALR engine and node building
`src/parser/` reimplements php-parser 5.9.0's LALR engine and node building
(`PhpParser\ParserAbstract` + the generated `Parser\Php8`), shadowed through
the `PHPStan\Parser\ParserRunner` seam. The parsing tables are read at run
time from the first `Php8` parser object seen — they are generated data, so
Expand All @@ -195,7 +195,12 @@ non-string inputs fall back to `$parser->parse()`.
Because the input domain is "all PHP source code", method-level parity is not
enough here: `tests/parser-corpus.php` parses thousands of files with both
implementations and requires byte-identical serialized ASTs, identical
collected errors, and identical token streams. It runs in CI on every build.
collected errors, and identical token streams. `tests/parser-upstream-corpus.php`
runs the same comparison over every input of php-parser's own test suite at
the installed commit (fetched from GitHub, or pass a checkout) — upstream's
cases cover every grammar rule, error recovery and version-gated behavior,
including syntax PHPStan's own code does not use yet. Both run in CI on every
build.

### Updating php-parser

Expand Down Expand Up @@ -228,9 +233,11 @@ The CI version job pins the php-parser version the engine was ported against
reported as warnings — delete them once their replacement is handled.
Never hand-edit the generated files: CI regenerates and diffs them.
3. **Verify**: strict build, then `php turbo-ext/tests/parser-corpus.php`
until byte-identical over the whole corpus. New PHP syntax is only covered
once fixtures using it exist in the repo — PHPStan's own test data for the
new syntax provides them; make sure they land before or with the bump.
and `php turbo-ext/tests/parser-upstream-corpus.php` until byte-identical
over both corpora. The upstream corpus covers the new version's syntax the
moment the lock file points at it; the repo corpus only covers it once
fixtures using it exist — PHPStan's own test data for the new syntax, or
`tests/parser-fixtures/`; make sure they land before or with the bump.
Then the full test suite and `make phpstan` with the extension loaded,
and `tests/parser-bench.php` to confirm the speedup held.
4. **Bump both pins**: `SUPPORTED_PHP_PARSER_VERSION` in the workflow, and —
Expand Down
2 changes: 1 addition & 1 deletion turbo-ext/src/parser/ParserEngine.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* phpstanturbo::ParserEngine — native port of php-parser 5.8.0's LALR engine
* phpstanturbo::ParserEngine — native port of php-parser 5.9.0's LALR engine
* and node building (vendor/nikic/php-parser: ParserAbstract.php +
* Parser/Php8.php), structured to mirror ParserAbstract method for method:
* doParse(), getAttributes(), emitError(), the semantic-action helpers
Expand Down
2 changes: 1 addition & 1 deletion turbo-ext/src/parser/ParserRunner.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* PHPStanTurbo\ParserRunner — native LALR engine for php-parser 5.8.0.
* PHPStanTurbo\ParserRunner — native LALR engine for php-parser 5.9.0.
* phpstanturbo::ParserEngine mirrors PhpParser\ParserAbstract::parse()/doParse()
* exactly; the generated reduce actions live in ParserRunnerActions*.cpp, the
* ported semantic helpers in ParserRunnerHelpers.cpp. PHP twin:
Expand Down
30 changes: 15 additions & 15 deletions turbo-ext/src/parser/ParserRunnerActions2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,34 +292,35 @@ bool ParserEngine::reduceRange2(int rule, int stackPos)
}
/* rule 330 */
case 330: {
semValue = arrayOf(PN_SEM(3, 2));
semValue = zv::Arr::empty();
return true;
}
/* rule 331 */
case 331: {
semValue = zv::Arr::empty();
semValue = PN_SEM(4, 2);
return true;
}
/* rule 332 */
case 332: {
semValue = PN_SEM(4, 2);
zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(4, 1, 4), PN_SEM(4, 2), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
semValue = arrayOf(t1);
return true;
}
/* rule 333 */
case 333: {
zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(4, 1, 4), PN_SEM(4, 2), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
semValue = arrayOf(t1);
zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(3, 1, 1), PN_SEM(3, 1), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
semValue = arrayOf(t1, PN_SEM(3, 3));
return true;
}
/* rule 334 */
case 334: {
semValue = arrayOf(PN_SEM(3, 2));
semValue = arrayOf(PN_SEM(1, 1));
return true;
}
/* rule 335 */
case 335: {
zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(3, 1, 1), PN_SEM(3, 1), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
semValue = arrayOf(t1, PN_SEM(3, 3));
pushOnto(PN_SEM(3, 1), PN_SEM(3, 3));
semValue = PN_SEM(3, 1);
return true;
}
/* rule 336 */
Expand All @@ -335,33 +336,32 @@ bool ParserEngine::reduceRange2(int rule, int stackPos)
}
/* rule 338 */
case 338: {
semValue = newNode("Node\\VariadicPlaceholder", PN_ATTRS(1, 1, 1));
semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(true), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
return true;
}
/* rule 339 */
case 339: {
semValue = arrayOf(PN_SEM(1, 1));
semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(false), zv::Val::boolean(true), nullptr /* ctor default for omitted $name */);
return true;
}
/* rule 340 */
case 340: {
pushOnto(PN_SEM(3, 1), PN_SEM(3, 3));
semValue = PN_SEM(3, 1);
semValue = newNode("Node\\VariadicPlaceholder", PN_ATTRS(1, 1, 1));
return true;
}
/* rule 341 */
case 341: {
semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(true), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */);
semValue = newNode("Node\\ArgPlaceholder", PN_ATTRS(1, 1, 1), nullptr);
return true;
}
/* rule 342 */
case 342: {
semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(false), zv::Val::boolean(true), nullptr /* ctor default for omitted $name */);
semValue = newNode("Node\\Arg", PN_ATTRS(3, 1, 3), PN_SEM(3, 3), zv::Val::boolean(false), zv::Val::boolean(false), PN_SEM(3, 1));
return true;
}
/* rule 343 */
case 343: {
semValue = newNode("Node\\Arg", PN_ATTRS(3, 1, 3), PN_SEM(3, 3), zv::Val::boolean(false), zv::Val::boolean(false), PN_SEM(3, 1));
semValue = newNode("Node\\ArgPlaceholder", PN_ATTRS(3, 1, 3), PN_SEM(3, 1));
return true;
}
/* rule 344 */
Expand Down
8 changes: 4 additions & 4 deletions turbo-ext/src/parser/ParserRunnerHelpers.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* ParserRunnerHelpers.cpp — native ports of php-parser 5.8.0's semantic-action
* ParserRunnerHelpers.cpp — native ports of php-parser 5.9.0's semantic-action
* helper methods (ParserAbstract.php lines 560-1341) plus the Node-side
* fromString builders they rely on (Int_/String_/Float_::fromString,
* Name::prepareName, Modifiers::verify*), as phpstanturbo::ParserEngine
Expand Down Expand Up @@ -305,7 +305,7 @@ static zend_string *stripTrailingNewline(zend_string *s)
return r;
}

/* String_::codePointToUtf8() — caller guarantees num <= 0x1FFFFF */
/* String_::codePointToUtf8() — caller guarantees num <= 0x10FFFF */
static void utf8Append(smart_str *out, zend_ulong num)
{
if (num <= 0x7F) {
Expand Down Expand Up @@ -424,8 +424,8 @@ zend_string *ParserEngine::parseEscapeSequences(zend_string *strIn, bool hasQuot
break;
}
ParsedNum cp = baseToNum(s + i + 3, digits, 16);
/* hexdec overflow → PHP_INT_MAX → codePointToUtf8 throws; > 0x1FFFFF throws */
if (cp.isDouble || cp.lval > 0x1FFFFF) {
/* hexdec overflow → PHP_INT_MAX → codePointToUtf8 throws; > 0x10FFFF throws */
if (cp.isDouble || cp.lval > 0x10FFFF) {
smart_str_free(&out);
zend_string_release(str);
fatalError("Invalid UTF-8 codepoint escape sequence: Codepoint too large", zv::Arr::empty());
Expand Down
Loading
Loading