diff --git a/.gitattributes b/.gitattributes
index cfa4eea..11aad01 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,7 @@
* text=auto eol=lf
*.pp eol=lf linguist-language=EBNF
*.pp2 eol=lf linguist-language=EBNF
+*.pp3 eol=lf linguist-language=EBNF
# GIT
.editorconfig export-ignore
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a7df15d..3abc5cb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [ '8.4', '8.5' ]
+ php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
os: [ ubuntu-latest, macos-latest, windows-latest ]
stability: [ lowest, stable ]
steps:
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index d95cfb6..5c5d69d 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -24,7 +24,7 @@
strict: true,
));
-return new Config()
+return (new Config())
->setUnsupportedPhpVersionAllowed(true)
->setParallelConfig(ParallelConfigFactory::detect())
->setRules([
@@ -277,7 +277,7 @@
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
'yoda_style' => false,
- 'new_expression_parentheses' => true,
+ 'new_expression_parentheses' => ['use_parentheses' => true],
])
->setCacheFile(__DIR__ . '/vendor/.cache.php-cs-fixer')
->setFinder($files);
diff --git a/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php
new file mode 100644
index 0000000..1deb87f
--- /dev/null
+++ b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php
@@ -0,0 +1,23 @@
+
+ */
+ private const REPEATS = 16;
+
+ /**
+ * @return iterable
+ */
+ public static function stringsDataProvider(): iterable
+ {
+ yield 'plain' => ['string' => '"example"', 'double' => true];
+ yield 'plain long' => ['string' => '"' . \str_repeat(self::LOREM, 8) . '"', 'double' => true];
+ yield 'quote' => ['string' => '"a\"b"', 'double' => true];
+ yield 'few specials' => ['string' => '"line\nand\ttab"', 'double' => true];
+ yield 'many specials' => ['string' => '"' . \str_repeat('a\nb\tc\r', self::REPEATS) . '"', 'double' => true];
+ yield 'many backslashes' => ['string' => '"' . \str_repeat('a\\\\b', self::REPEATS) . '"', 'double' => true];
+ yield 'one hexadecimal' => ['string' => '"a\x41b"', 'double' => true];
+ yield 'numeric mix' => ['string' => '"\x41\101\u{1F600}\x42"', 'double' => true];
+ yield 'many numeric' => ['string' => '"' . \str_repeat('\x41\101\u{48}', self::REPEATS) . '"', 'double' => true];
+ yield 'single quoted plain' => ['string' => "'" . \str_repeat(self::LOREM, 8) . "'", 'double' => false];
+ yield 'single quoted escaped' => ['string' => "'" . \str_repeat('a\\\\b\\\'c', self::REPEATS) . "'", 'double' => false];
+ }
+
+ /**
+ * @param array{string: non-empty-string, double: bool} $params
+ */
+ abstract public function benchDecodeString(array $params): void;
+}
diff --git a/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php
new file mode 100644
index 0000000..05592bc
--- /dev/null
+++ b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php
@@ -0,0 +1,23 @@
+
+ */
+ protected const CORPUS_PACKAGES = [
+ 'doctrine/collections' => '*',
+ 'doctrine/lexer' => '*',
+ 'guzzlehttp/guzzle' => '*',
+ 'guzzlehttp/promises' => '*',
+ 'guzzlehttp/psr7' => '*',
+ 'illuminate/support' => '*',
+ 'monolog/monolog' => '*',
+ 'nikic/php-parser' => '*',
+ 'phpdocumentor/reflection-docblock' => '*',
+ 'phpstan/extension-installer' => '*',
+ 'phpstan/php-8-stubs' => '*',
+ 'phpstan/phpdoc-parser' => '*',
+ 'phpstan/phpstan' => '*',
+ 'phpstan/phpstan-beberlei-assert' => '*',
+ 'phpstan/phpstan-deprecation-rules' => '*',
+ 'phpstan/phpstan-dibi' => '*',
+ 'phpstan/phpstan-doctrine' => '*',
+ 'phpstan/phpstan-mockery' => '*',
+ 'phpstan/phpstan-nette' => '*',
+ 'phpstan/phpstan-php-parser' => '*',
+ 'phpstan/phpstan-phpunit' => '*',
+ 'phpstan/phpstan-strict-rules' => '*',
+ 'phpstan/phpstan-symfony' => '*',
+ 'phpstan/phpstan-webmozart-assert' => '*',
+ 'psalm/attributes' => '*',
+ 'psalm/plugin-mockery' => '*',
+ 'psalm/plugin-phpunit' => '*',
+ 'psalm/plugin-symfony' => '*',
+ 'phpunit/php-code-coverage' => '*',
+ 'phpunit/php-file-iterator' => '*',
+ 'phpunit/php-text-template' => '*',
+ 'phpunit/php-timer' => '*',
+ 'phpunit/phpunit' => '*',
+ 'psr/container' => '*',
+ 'psr/http-message' => '*',
+ 'psr/log' => '*',
+ 'ralouphie/getallheaders' => '*',
+ 'sebastian/comparator' => '*',
+ 'sebastian/diff' => '*',
+ 'sebastian/environment' => '*',
+ 'sebastian/exporter' => '*',
+ 'sebastian/recursion-context' => '*',
+ 'symfony/console' => '*',
+ 'symfony/event-dispatcher' => '*',
+ 'symfony/finder' => '*',
+ 'symfony/http-foundation' => '*',
+ 'symfony/polyfill-ctype' => '*',
+ 'symfony/polyfill-mbstring' => '*',
+ 'symfony/process' => '*',
+ 'symfony/service-contracts' => '*',
+ 'symfony/var-dumper' => '*',
+ 'twig/twig' => '*',
+ 'vimeo/psalm' => '*',
+ 'webmozart/assert' => '*',
+ ];
+
+ /**
+ * The PHP version the corpus is resolved for.
+ *
+ * The corpus is read and never run, so the version it is installed for is
+ * not the one the benchmark runs on: it is pinned so that the corpus is
+ * the same everywhere, and so that a package supporting no version as new
+ * as the one at hand is installed all the same.
+ *
+ * @var non-empty-string
+ */
+ protected const CORPUS_PHP_VERSION = '8.3.0';
+
+ /**
+ * Each set contains every DocBlock of a single package of the corpus.
+ *
+ * The corpus is installed by the provider itself, because it is executed
+ * in a separate process before any benchmark is launched.
+ *
+ * @return iterable}>
+ * @throws \JsonException
+ */
+ public static function docBlocksDataProvider(): iterable
+ {
+ $metadata = self::getCorpusMetadata();
+
+ foreach (\array_keys(self::CORPUS_PACKAGES) as $package) {
+ $directory = \realpath($metadata['versions'][$package]['install_path'] ?? '');
+
+ if ($directory === false) {
+ throw new \RuntimeException(\sprintf(
+ 'The "%s" package of the benchmark corpus is not installed',
+ $package,
+ ));
+ }
+
+ $docblocks = self::extractDocBlocks($directory);
+
+ if ($docblocks === []) {
+ continue;
+ }
+
+ yield $package => ['docblocks' => $docblocks];
+ }
+ }
+
+ /**
+ * Installs the corpus (if required) and returns the Composer metadata
+ * of the installed packages.
+ *
+ * @return array{versions: array}
+ * @throws \JsonException
+ */
+ private static function getCorpusMetadata(): array
+ {
+ $installed = self::CORPUS_DIRECTORY . '/vendor/composer/installed.php';
+
+ // The manifest is generated from the {@see CORPUS_PACKAGES} list, so
+ // that an edit of the list is enough to update the corpus.
+ $outdated = self::updateCorpusManifest();
+
+ if ($outdated || !\is_file(self::CORPUS_DIRECTORY . '/composer.lock')) {
+ self::composer('update');
+ } elseif (!\is_file($installed)) {
+ self::composer('install');
+ }
+
+ /** @var array{versions: array} */
+ return require $installed;
+ }
+
+ /**
+ * Writes the "composer.json" of the corpus and returns {@see true} in
+ * case of the manifest has been changed (and therefore the installed
+ * packages are outdated).
+ */
+ private static function updateCorpusManifest(): bool
+ {
+ $pathname = self::CORPUS_DIRECTORY . '/composer.json';
+
+ $expected = \json_encode([
+ 'name' => 'type-lang/bench-corpus',
+ 'description' => 'A disposable sandbox of the third-party packages that are '
+ . 'used as a real-world DocBlock corpus. The directory is generated by '
+ . 'the benchmarks and can be safely removed.',
+ 'type' => 'project',
+ 'license' => 'MIT',
+ 'require' => self::CORPUS_PACKAGES,
+ 'config' => [
+ 'preferred-install' => ['*' => 'dist'],
+ 'allow-plugins' => false,
+ 'platform' => ['php' => self::CORPUS_PHP_VERSION],
+ ],
+ 'minimum-stability' => 'stable',
+ 'prefer-stable' => true,
+ ], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR);
+
+ if (@\file_get_contents($pathname) === $expected) {
+ return false;
+ }
+
+ if (!\is_dir(self::CORPUS_DIRECTORY) && !@\mkdir(self::CORPUS_DIRECTORY, recursive: true)) {
+ throw new \RuntimeException(\sprintf(
+ 'Could not create the benchmark corpus directory "%s"',
+ self::CORPUS_DIRECTORY,
+ ));
+ }
+
+ if (\file_put_contents($pathname, $expected) === false) {
+ throw new \RuntimeException(\sprintf(
+ 'Could not write the benchmark corpus manifest "%s"',
+ $pathname,
+ ));
+ }
+
+ return true;
+ }
+
+ /**
+ * @param non-empty-string $command
+ */
+ private static function composer(string $command): void
+ {
+ $process = new Process(
+ command: [
+ ...self::getComposerBinary(),
+ $command,
+ '--working-dir=' . self::CORPUS_DIRECTORY,
+ '--no-interaction',
+ '--no-progress',
+ '--no-audit',
+ '--no-plugins',
+ '--no-scripts',
+ '--prefer-dist',
+ ],
+ // The installation of the corpus may take a few minutes on the
+ // first run, so it should not be interrupted by a timeout.
+ timeout: null,
+ );
+
+ if ($process->run() !== 0) {
+ throw new \RuntimeException(\sprintf(
+ "Could not install the benchmark corpus:\n%s",
+ \trim($process->getErrorOutput() . "\n" . $process->getOutput()),
+ ));
+ }
+ }
+
+ /**
+ * @return non-empty-list
+ */
+ private static function getComposerBinary(): array
+ {
+ $finder = new ExecutableFinder();
+
+ foreach (['composer', 'composer.phar'] as $name) {
+ $pathname = $finder->find($name);
+
+ if ($pathname === null || $pathname === '') {
+ continue;
+ }
+
+ // A phar is not executable on all platforms, so it is passed
+ // to the PHP binary that runs the benchmarks.
+ return \str_ends_with($pathname, '.phar')
+ ? [\PHP_BINARY, $pathname]
+ : [$pathname];
+ }
+
+ throw new \RuntimeException(\sprintf(
+ 'The "composer" executable is required to install the benchmark corpus, '
+ . 'but it could not be found. Please install the corpus manually '
+ . 'using the "composer install --working-dir=%s" command',
+ self::CORPUS_DIRECTORY,
+ ));
+ }
+
+ /**
+ * Extracts every DocBlock of every PHP file of the given directory.
+ *
+ * @param non-empty-string $directory
+ * @return list
+ */
+ private static function extractDocBlocks(string $directory): array
+ {
+ $result = [];
+
+ foreach (self::getSourceFiles($directory) as $pathname) {
+ $source = @\file_get_contents($pathname);
+
+ if ($source === false || $source === '') {
+ continue;
+ }
+
+ foreach (\PhpToken::tokenize($source) as $token) {
+ if ($token->is(\T_DOC_COMMENT) && $token->text !== '') {
+ $result[] = $token->text;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param non-empty-string $directory
+ * @return iterable
+ */
+ private static function getSourceFiles(string $directory): iterable
+ {
+ $files = new \RecursiveIteratorIterator(
+ iterator: new \RecursiveDirectoryIterator(
+ directory: $directory,
+ flags: \FilesystemIterator::SKIP_DOTS
+ | \FilesystemIterator::CURRENT_AS_FILEINFO,
+ ),
+ );
+
+ /** @var \SplFileInfo $file */
+ foreach ($files as $file) {
+ $pathname = $file->getPathname();
+
+ if ($file->isFile() && $file->getExtension() === 'php' && $pathname !== '') {
+ yield $pathname;
+ }
+ }
+ }
+
+ /**
+ * @param array{docblocks: list} $params
+ */
+ abstract public function benchParseDocBlock(array $params): void;
+}
diff --git a/libs/phpdoc/tests/Bench/PHPStanParserBench.php b/bench/PhpDoc/Tools/PHPStanParserBench.php
similarity index 55%
rename from libs/phpdoc/tests/Bench/PHPStanParserBench.php
rename to bench/PhpDoc/Tools/PHPStanParserBench.php
index 2e265ca..d159a12 100644
--- a/libs/phpdoc/tests/Bench/PHPStanParserBench.php
+++ b/bench/PhpDoc/Tools/PHPStanParserBench.php
@@ -2,10 +2,12 @@
declare(strict_types=1);
-namespace TypeLang\PhpDoc\Tests\Bench;
+namespace TypeLang\Bench\PhpDoc\Tools;
use PhpBench\Attributes\BeforeMethods;
+use PhpBench\Attributes\Groups;
use PhpBench\Attributes\Iterations;
+use PhpBench\Attributes\ParamProviders;
use PhpBench\Attributes\RetryThreshold;
use PhpBench\Attributes\Revs;
use PhpBench\Attributes\Warmup;
@@ -16,8 +18,9 @@
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;
-#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)]
-final readonly class PHPStanParserBench extends DocBlockParserBench
+#[Groups(['phpstan']), Revs(3), Warmup(10), Iterations(5)]
+#[BeforeMethods('prepare'), RetryThreshold(5)]
+final class PHPStanParserBench extends DocBlockParserBench
{
private Lexer $lexer;
private PhpDocParser $parser;
@@ -37,10 +40,17 @@ public function prepare(): void
$this->parser = new PhpDocParser($config, $typeParser, $constExprParser);
}
- public function benchParseDocBlock(): void
+ #[ParamProviders('docBlocksDataProvider')]
+ public function benchParseDocBlock(array $params): void
{
- $iterator = new TokenIterator($this->lexer->tokenize(self::DOC_BLOCK_SAMPLE));
-
- $this->parser->parse($iterator);
+ foreach ($params['docblocks'] as $docblock) {
+ try {
+ $this->parser->parse(new TokenIterator($this->lexer->tokenize($docblock)));
+ } catch (\Throwable) {
+ // A real-world corpus contains DocBlocks that some of the tools
+ // are not able to parse. They are skipped so that every tool
+ // is measured on the same corpus.
+ }
+ }
}
}
diff --git a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php
new file mode 100644
index 0000000..d8c565f
--- /dev/null
+++ b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php
@@ -0,0 +1,41 @@
+parser = DocBlockFactory::createInstance();
+ }
+
+ #[ParamProviders('docBlocksDataProvider')]
+ public function benchParseDocBlock(array $params): void
+ {
+ foreach ($params['docblocks'] as $docblock) {
+ try {
+ $this->parser->create($docblock);
+ } catch (\Throwable) {
+ // A real-world corpus contains DocBlocks that some of the tools
+ // are not able to parse. They are skipped so that every tool
+ // is measured on the same corpus.
+ }
+ }
+ }
+}
diff --git a/bench/PhpDoc/Tools/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php
new file mode 100644
index 0000000..9bd2577
--- /dev/null
+++ b/bench/PhpDoc/Tools/TypeLangParserBench.php
@@ -0,0 +1,41 @@
+parser = DocBlockParser::createDefault();
+ }
+
+ #[ParamProviders('docBlocksDataProvider')]
+ public function benchParseDocBlock(array $params): void
+ {
+ foreach ($params['docblocks'] as $docblock) {
+ try {
+ $this->parser->parse($docblock);
+ } catch (\Throwable) {
+ // A real-world corpus contains DocBlocks that some of the tools
+ // are not able to parse. They are skipped so that every tool
+ // is measured on the same corpus.
+ }
+ }
+ }
+}
diff --git a/bench/var/.gitignore b/bench/var/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/bench/var/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/composer.json b/composer.json
index cc49d4e..202428f 100644
--- a/composer.json
+++ b/composer.json
@@ -7,7 +7,7 @@
{"type": "path", "url": "libs/*"}
],
"require": {
- "php": "^8.4",
+ "php": "^8.1",
"type-lang/parser": "^2.0",
"type-lang/phpdoc": "^2.0",
"type-lang/printer": "^2.0",
@@ -15,25 +15,26 @@
"type-lang/types": "^2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.95.13",
+ "friendsofphp/php-cs-fixer": "^3.95",
"jetbrains/phpstorm-attributes": "^1.2",
- "phpbench/phpbench": "^1.7.0",
- "phpdocumentor/reflection-docblock": "^6.0.3",
- "phplrt/compiler": "^3.7.5",
- "phpstan/phpdoc-parser": "^2.3.3",
- "phpstan/phpstan": "^2.2.5",
- "phpstan/phpstan-deprecation-rules": "^2.0.4",
- "phpstan/phpstan-strict-rules": "^2.0.11",
- "phpunit/phpunit": "^13.2.4",
- "symfony/var-dumper": "^7.4|^8.0"
+ "phpbench/phpbench": "^1.2",
+ "phpdocumentor/reflection-docblock": "^6.0",
+ "phplrt/compiler": "^4.1",
+ "phpstan/phpdoc-parser": "^2.3",
+ "phpstan/phpstan": "^2.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"autoload-dev": {
"psr-4": {
- "TypeLang\\Parser\\Tests\\": "libs/parser/tests",
- "TypeLang\\Printer\\Tests\\": "libs/printer/tests",
- "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests",
- "TypeLang\\Reader\\Tests\\": "libs/reader/tests",
- "TypeLang\\Type\\Tests\\": "libs/types/tests"
+ "TypeLang\\Bench\\": "bench",
+ "TypeLang\\Parser\\Tests\\": "libs/parser/tests",
+ "TypeLang\\Printer\\Tests\\": "libs/printer/tests",
+ "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests",
+ "TypeLang\\Reader\\Tests\\": "libs/reader/tests",
+ "TypeLang\\Type\\Tests\\": "libs/types/tests"
}
},
"extra": {
@@ -56,7 +57,8 @@
"phpcs:check": ["phpcs", "cs", "psr", "per"]
},
"scripts": {
- "bench": "phpbench run libs/phpdoc/tests --report=default",
+ "bench": "phpbench run ./bench --report=compare",
+ "bench:census": "php bench/PhpDoc/bin/census",
"test:unit": "phpunit --testdox",
"linter:check": "phpstan analyse --configuration phpstan.neon --memory-limit 256M",
"linter:baseline": "@linter:check --generate-baseline",
diff --git a/docs/Specification/Appendix B -- Grammar Summary.md b/docs/Specification/Appendix B -- Grammar Summary.md
index 61b2c81..db3afdb 100644
--- a/docs/Specification/Appendix B -- Grammar Summary.md
+++ b/docs/Specification/Appendix B -- Grammar Summary.md
@@ -16,7 +16,7 @@ Letter :: one of
- `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z`
- `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m`
- `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z`
-- "Any byte from U+0080 to U+00FF"
+- "Any byte from 0x80 to 0xFF"
Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
@@ -27,7 +27,14 @@ Ignored ::
- Whitespace
- Comment
-Whitespace :: "Any Unicode whitespace character"
+Whitespace :: one of
+
+- "Horizontal Tab (U+0009)"
+- "Line Feed (U+000A)"
+- "Vertical Tab (U+000B)"
+- "Form Feed (U+000C)"
+- "Carriage Return (U+000D)"
+- "Space (U+0020)"
LineTerminator :: one of "New Line (U+000A)" "Carriage Return (U+000D)"
@@ -63,8 +70,8 @@ Punctuator :: one of
- `?` `|` `&` `*` `,` `:` `=`
- `(` `)` `[` `]` `{` `}`
-- `<` `>` `<=` `>=`
-- `::` `\` `...` `#[`
+- `<` `>`
+- `::` `\` `...`
NameToken :: NameStart NameContinue\* [lookahead != NameContinue]
@@ -80,11 +87,13 @@ NameContinue ::
- `_`
- `-`
-ReservedWord :: one of `true` `false` `null` `is`
+ReservedWord :: one of `true` `false` `null` `is` `not`
+
+Variable :: `$` NameStart VariableContinue\*
-Variable :: `$` NameStart NameContinue\*
+VariableContinue :: NameContinue but not `-`
-ThisVariable :: `$this` [lookahead != NameContinue]
+ThisVariable :: `$this` [lookahead != VariableContinue]
**Literal Tokens**
@@ -94,8 +103,12 @@ NullLiteral :: `null` [lookahead != NameContinue]
NegativeSign :: `-`
+Sign :: one of `-` `+`
+
DigitSeparator :: `_`
+Digits :: Digit (DigitSeparator? Digit)\*
+
IntLiteral ::
- BinaryIntLiteral
@@ -103,24 +116,38 @@ IntLiteral ::
- HexIntLiteral
- DecimalIntLiteral
-DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\*
+DecimalIntLiteral ::
-BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\*
+- Sign? NonZeroDigit (DigitSeparator? Digit)\*
+- Sign? `0`
+
+NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9`
+
+BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits
BinaryIndicator :: one of `b` `B`
+BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\*
+
BinaryDigit :: one of `0` `1`
-OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\*
+OctalIntLiteral ::
+
+- Sign? `0` OctalIndicator OctalDigits
+- Sign? `0` (DigitSeparator? OctalDigit)+
OctalIndicator :: one of `o` `O`
+OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\*
+
OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7`
-HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\*
+HexIntLiteral :: Sign? `0` HexIndicator HexDigits
HexIndicator :: one of `x` `X`
+HexDigits :: HexDigit (DigitSeparator? HexDigit)\*
+
HexDigit :: one of
- `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
@@ -133,13 +160,13 @@ FloatLiteral ::
- TrailingFloatLiteral
- ExponentFloatLiteral
-LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart?
+LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart?
-TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart?
+TrailingFloatLiteral :: Sign? `.` Digits ExponentPart?
-ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart
+ExponentFloatLiteral :: Sign? Digits ExponentPart
-ExponentPart :: ExponentIndicator NegativeSign? Digit+
+ExponentPart :: ExponentIndicator Sign? Digits
ExponentIndicator :: one of `e` `E`
@@ -176,6 +203,8 @@ UnicodeEscape :: `u` `{` HexDigit+ `}`
**Types**
+Document : Type
+
Type : Expression
Expression : ConditionalType
@@ -192,7 +221,7 @@ ConditionalOperand :
- Type
- Variable
-ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>`
+ConditionalOperator : one of `is` `is not`
**Logical Types**
@@ -251,11 +280,13 @@ NamedType : Name (TemplateArguments | ShapeFields)?
TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>`
-TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType)
+TemplateArgument : TemplateArgumentHint | TemplateArgumentValue
+
+TemplateArgumentHint : NameToken TemplateArgumentValue
-TemplateArgumentHint : Identifier Type
+TemplateArgumentValue : Wildcard | Type
-TemplateArgumentType : Type
+Wildcard : `*`
**Literal and Constant Types**
@@ -273,9 +304,23 @@ ClassConstant : Name `::` Identifier
ConstantMask :
-- Name `*`
-- Name `::` Identifier `*`
-- Name `::` `*`
+- GlobalConstantMask
+- ClassConstantMask
+
+GlobalConstantMask :
+
+- Name MaskTail
+- Name `\` MaskTail
+- LeadingMask
+
+ClassConstantMask :
+
+- Name `::` Identifier MaskTail?
+- Name `::` MaskTail
+
+MaskTail : Wildcard (Identifier Wildcard)\* Identifier?
+
+LeadingMask : Wildcard Identifier MaskTail?
**Shape Types**
@@ -288,7 +333,7 @@ ShapeBody :
ShapeFieldList : ShapeField (`,` ShapeField)\*
-ShapeField : AttributeGroups? (ExplicitField | ImplicitField)
+ShapeField : ExplicitField | ImplicitField
ExplicitField : ShapeKey `?`? `:` ShapeValue
@@ -308,34 +353,27 @@ UnsealedShape : `...` TemplateArguments?
**Callable Types**
-CallableType : Name `(` CallableParameters? `)` CallableReturnType?
-
-CallableParameters : CallableParameter (`,` CallableParameter)\* `,`?
-
-CallableReturnType : `:` Type
+CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType?
-CallableParameter : AttributeGroups? CallableParameterBody `=`?
+TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>`
-CallableParameterBody :
+TemplateParameter : Identifier TemplateBound\* TemplateDefault?
-- `...`? Type ParameterModifiers? Variable?
-- ParameterModifiers? Variable
+TemplateBound :
-ParameterModifiers :
+- UpperBound
+- LowerBound
-- `&` `...`?
-- `...` `&`?
+UpperBound : UpperBoundOperator Type
-**Attributes**
+UpperBoundOperator : one of `of` `as`
-AttributeGroups : AttributeGroup+
+LowerBound : `super` Type
-AttributeGroup : `#[` AttributeList `,`? `]`
+TemplateDefault : `=` Type
-AttributeList : Attribute (`,` Attribute)\*
-
-Attribute : Name AttributeArguments?
+CallableParameters : CallableParameter (`,` CallableParameter)\* `,`?
-AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)`
+CallableReturnType : `:` Type
-AttributeArgument : Type
+CallableParameter : Type `&`? `...`? Variable? `=`?
diff --git a/docs/Specification/Section 1 -- Overview.md b/docs/Specification/Section 1 -- Overview.md
index b612849..28aa172 100644
--- a/docs/Specification/Section 1 -- Overview.md
+++ b/docs/Specification/Section 1 -- Overview.md
@@ -103,8 +103,6 @@ others:
- Escape, hexadecimal and Unicode sequences inside double-quoted string
literals.
- Global constant masks such as `JSON_*`.
-- Attributes (`#[...]`) on template arguments, shape fields and callable
- parameters.
- A uniform conditional (ternary) type syntax that also permits comparison
operators and Yoda-style operands.
diff --git a/docs/Specification/Section 2 -- Lexical Grammar.md b/docs/Specification/Section 2 -- Lexical Grammar.md
index e68d8ac..f9d3506 100644
--- a/docs/Specification/Section 2 -- Lexical Grammar.md
+++ b/docs/Specification/Section 2 -- Lexical Grammar.md
@@ -56,7 +56,7 @@ Letter :: one of
- `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z`
- `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m`
- `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z`
-- "Any byte from U+0080 to U+00FF"
+- "Any byte from 0x80 to 0xFF"
Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
@@ -72,23 +72,36 @@ otherwise insignificant. Any amount of {Ignored} MAY appear before and after
every lexical {Token}.
Note: Although {Ignored} between two tokens is never itself significant, the
-_presence_ of separating whitespace is what distinguishes a {NameToken} that
-is immediately followed by another token from a {NameToken} that introduces a
-template argument hint (see
+_presence_ of {Whitespace} standing directly behind a {NameToken} is what
+distinguishes a {NameToken} that is immediately followed by another token
+from a {NameToken} that introduces a template argument hint (see
[Template Argument Hints](#sec-Template-Argument-Hints)). This is the only
-place in the grammar where the presence of separating whitespace is
-observable to the syntactic grammar.
+place in the grammar where {Ignored} is observable to the syntactic grammar,
+and the one place a {Comment} does not stand for the whitespace it is written
+among: a comment MAY follow that whitespace, but MUST NOT stand in its stead.
### White Space
-Whitespace :: "Any Unicode whitespace character"
+Whitespace :: one of
-Whitespace separates tokens and improves the legibility of the source text. It
-includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed
-(U+000A) and carriage return (U+000D) characters. TypeLang does not
-distinguish between horizontal whitespace and line terminators, except within
-the multi-word `is not` operator (see
-[Conditional Types](#sec-Conditional-Types)).
+- "Horizontal Tab (U+0009)"
+- "Line Feed (U+000A)"
+- "Vertical Tab (U+000B)"
+- "Form Feed (U+000C)"
+- "Carriage Return (U+000D)"
+- "Space (U+0020)"
+
+Whitespace separates tokens and improves the legibility of the source text.
+TypeLang does not distinguish between horizontal whitespace and line
+terminators, and a {Comment} stands wherever whitespace stands, save for the
+one place the presence of the whitespace itself is read (see
+[Ignored Tokens](#sec-Ignored-Tokens)).
+
+Note: The six characters above are the whole of it. A source text is read as
+a sequence of bytes (see [Source Text](#sec-Source-Text)), so a character that
+parts words in a script of its own — a no-break space (U+00A0), say — is no
+whitespace here: every byte it is written of falls in the range a {Letter}
+covers, and it is read as a part of the name it stands in.
### Comments
@@ -116,11 +129,6 @@ comment_ begins with `/*` and continues up to and including the next `*/`.
Comments are {Ignored} and have no bearing on the meaning of a document.
-Note: The `#` line comment marker and the `#[` attribute marker share a
-leading character. Because lexical analysis prefers the longest match, the
-sequence `#[` is always scanned as the start of an attribute (see
-[Attributes](#sec-Attributes)) rather than as the start of a comment.
-
## Lexical Tokens
Token ::
@@ -145,13 +153,12 @@ Punctuator :: one of
- `?` `|` `&` `*` `,` `:` `=`
- `(` `)` `[` `]` `{` `}`
-- `<` `>` `<=` `>=`
-- `::` `\` `...` `#[`
+- `<` `>`
+- `::` `\` `...`
TypeLang documents use punctuation to describe structure. Several punctuators
-share a leading character (for example, `<` and `<=`; `:` and `::`; `.` in
-`...`; `#` in `#[`); in every such case, the longest matching punctuator is
-taken.
+share a leading character (for example, `:` and `::`; `.` in `...`); in every
+such case, the longest matching punctuator is taken.
### Names
@@ -179,6 +186,14 @@ the [PHP identifier grammar](https://www.php.net/manual/en/language.variables.ba
is that the dash character is additionally permitted in any non-leading
position.
+A source text is read as a sequence of bytes rather than of codepoints, and
+every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a
+{Letter} covers. A name MAY therefore be written in any script.
+
+```typescript
+Проект\Тип
+```
+
A {NameToken} is always the longest possible valid sequence; it MUST NOT be
followed by a {NameContinue} character.
@@ -213,41 +228,57 @@ ParseException: Syntax error, unexpected "type"
### Reserved Words
-ReservedWord :: one of `true` `false` `null` `is`
+ReservedWord :: one of `true` `false` `null` `is` `not`
The words `true`, `false` and `null` are _literal_ keywords (see
-[Literal Tokens](#sec-Literal-Tokens)), and `is` is the conditional operator
-keyword (see [Conditional Types](#sec-Conditional-Types)). All four are
-matched case-insensitively, and only when not immediately followed by a
-{NameContinue} character.
+[Literal Tokens](#sec-Literal-Tokens)), and `is` and `not` are the
+conditional operator keywords (see
+[Conditional Types](#sec-Conditional-Types)). Each is matched only when not
+immediately followed by a {NameContinue} character. The three literal
+keywords are matched case-insensitively, while `is` and `not` are matched
+the one way they are spelled.
+
+Wherever the source contains one of these words, it is scanned as the
+corresponding literal or operator token and MUST NOT be scanned as a
+{NameToken}. It remains an {Identifier} all the same (see
+[Names and Namespaces](#sec-Names-and-Namespaces)), so a reserved word is
+shadowed rather than forbidden: it stands as a name wherever the token it is
+scanned as denotes no type of its own.
+
+A literal keyword standing alone, in a position where a type is expected, is
+read as the {LiteralType} it denotes and not as the {NamedType} it would
+otherwise be (see [Primary Types](#sec-Primary-Types)):
-When the source contains one of these words standing alone, in a position
-where a type is expected, it is scanned as the corresponding literal or
-operator token and MUST NOT be scanned as a {NameToken}. Consequently, a bare
-reserved word cannot be used as a type name.
+```typescript
+TrUe
+```
-**Counter-example.** A bare reserved word is scanned as a literal, not a type
-name.
+Behind a namespace separator or another segment, where no literal is
+expected, the same word is a name segment. This makes `\true` a reference to
+a type literally named `true`, distinct from the `true` literal:
-```typescript counter-example
-TrUe
+```typescript
+\true
```
-A reserved word MAY, however, appear as an {Identifier} _inside_ a qualified
-{Name} — that is, when it is preceded by a namespace separator or another
-identifier (see [Names and Namespaces](#sec-Names-and-Namespaces)). For
-example, `\true` references a type literally named `true`, whereas the bare
-`true` is the boolean literal.
+The conditional operator keywords denote no type of their own, so nothing
+shadows them, and a bare `is` or `not` is an ordinary named type:
+
+```typescript
+not
+```
### Variable
-Variable :: `$` NameStart NameContinue\*
+Variable :: `$` NameStart VariableContinue\*
-ThisVariable :: `$this` [lookahead != NameContinue]
+VariableContinue :: NameContinue but not `-`
+
+ThisVariable :: `$this` [lookahead != VariableContinue]
A {Variable} token begins with a dollar sign (`$`) followed by a sequence
-matching the body of a {NameToken}. Variables are used to name callable
-parameters (see [Callable Types](#sec-Callable-Types)) and as operands in
+matching the body of a {NameToken}, save for the dash a variable cannot
+carry. Variables are used to name callable parameters (see [Callable Types](#sec-Callable-Types)) and as operands in
conditional types (see [Conditional Types](#sec-Conditional-Types)).
The special variable `$this` is recognised as a distinct token and
@@ -289,17 +320,30 @@ IntLiteral ::
NegativeSign :: `-`
+Sign :: one of `-` `+`
+
DigitSeparator :: `_`
An integer literal denotes a value of the PHP `int` type. Binary, octal,
decimal and hexadecimal radixes are supported, each optionally prefixed with a
-{NegativeSign}. Within the digits of any integer literal, underscores
-({DigitSeparator}) MAY appear freely as visual separators; they carry no
-meaning and do not affect the denoted value.
+{Sign}. A {NegativeSign} makes the denoted value negative; a leading `+` denotes
+the same value the absence of a {Sign} does. Underscores ({DigitSeparator}) MAY
+appear as visual separators; they carry no meaning and do not affect the denoted
+value. A {DigitSeparator} MUST stand between two digits, and therefore may
+neither lead a run of digits, nor trail one, nor stand beside another
+{DigitSeparator}.
+
+Digits :: Digit (DigitSeparator? Digit)\*
+
+**Decimal.** A decimal literal other than a lone `0` MUST NOT begin with a
+zero, which is the mark of an {OctalIntLiteral}.
+
+DecimalIntLiteral ::
-**Decimal.**
+- Sign? NonZeroDigit (DigitSeparator? Digit)\*
+- Sign? `0`
-DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\*
+NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9`
```typescript
1_000_000
@@ -307,35 +351,61 @@ DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\*
**Binary.** Prefixed with `0b` or `0B`; digits are `0` and `1` only.
-BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\*
+BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits
BinaryIndicator :: one of `b` `B`
+BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\*
+
BinaryDigit :: one of `0` `1`
```typescript
0b1010_1101
```
-**Octal.** Prefixed with `0o` or `0O`; digits are `0` through `7` only.
+**Octal.** Prefixed with `0o` or `0O` or, as octal has been written since long
+before that prefix, with a leading `0` alone; digits are `0` through `7` only.
+A `0123` denotes the same value a `0o123` does.
+
+OctalIntLiteral ::
-OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\*
+- Sign? `0` OctalIndicator OctalDigits
+- Sign? `0` (DigitSeparator? OctalDigit)+
OctalIndicator :: one of `o` `O`
+OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\*
+
OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7`
```typescript
0o42
```
+```typescript
+04_23
+```
+
+**Counter-example.** A leading zero makes a literal an octal, so a digit
+outside the octal radix may not follow one.
+
+```typescript counter-example
+08
+```
+
+```
+ParseException: Syntax error, unexpected "8"
+```
+
**Hexadecimal.** Prefixed with `0x` or `0X`; digits are `0` through `9` and `a`
through `f`, in either case.
-HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\*
+HexIntLiteral :: Sign? `0` HexIndicator HexDigits
HexIndicator :: one of `x` `X`
+HexDigits :: HexDigit (DigitSeparator? HexDigit)\*
+
HexDigit :: one of
- `0` `1` `2` `3` `4` `5` `6` `7` `8` `9`
@@ -358,9 +428,9 @@ ParseException: Syntax error, unexpected "42"
**Static Semantics.**
-A {DecimalIntLiteral} whose first digit is `0` and which is longer than a
-single character is interpreted in the _legacy octal_ radix (base 8), for
-compatibility with historical PHP source. Thus, `042` denotes the value 34.
+An {OctalIntLiteral} written without an {OctalIndicator} is the _legacy octal_
+spelling kept for compatibility with historical PHP source. It is read in the
+same radix (base 8) the prefixed spelling is, so `042` denotes the value 34.
A conforming implementation MUST retain the original (raw) lexeme of every
integer literal. When the denoted value exceeds the range representable by the
@@ -376,19 +446,19 @@ FloatLiteral ::
- TrailingFloatLiteral
- ExponentFloatLiteral
-LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart?
+LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart?
-TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart?
+TrailingFloatLiteral :: Sign? `.` Digits ExponentPart?
-ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart
+ExponentFloatLiteral :: Sign? Digits ExponentPart
-ExponentPart :: ExponentIndicator NegativeSign? Digit+
+ExponentPart :: ExponentIndicator Sign? Digits
ExponentIndicator :: one of `e` `E`
A floating-point literal denotes a value of the PHP `float` type. It MUST
contain either a decimal point or an exponent, or both, and MAY be prefixed
-with a {NegativeSign}.
+with a {Sign}.
Either the leading run of digits (before the decimal point) or the trailing
run (after it) MAY be omitted, but not both:
@@ -405,13 +475,25 @@ run (after it) MAY be omitted, but not both:
1.
```
-Scientific notation uses the case-insensitive `e` indicator followed by an
-optionally negative decimal exponent:
+Scientific notation uses the case-insensitive `e` indicator followed by a
+decimal exponent, which takes a {Sign} of its own:
```typescript
10e-2
```
+```typescript
+10e+2
+```
+
+Every run of digits a float is written of accepts the {DigitSeparator} under
+the same rule an integer does, and, unlike a {DecimalIntLiteral}, MAY begin
+with a zero:
+
+```typescript
+2_3.4_5e-6_7
+```
+
**Counter-example.** A lone decimal point is not a valid float.
```typescript counter-example
diff --git a/docs/Specification/Section 3 -- Types.md b/docs/Specification/Section 3 -- Types.md
index 3090ac6..dabbfe8 100644
--- a/docs/Specification/Section 3 -- Types.md
+++ b/docs/Specification/Section 3 -- Types.md
@@ -47,7 +47,7 @@ ConditionalOperand :
- Type
- Variable
-ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>`
+ConditionalOperator : one of `is` `is not`
A _conditional type_ (also called a ternary type) selects between two types
based on a comparison between a _subject_ and a _comparand_. It is written as
@@ -58,9 +58,9 @@ condition holds, a `:`, and the type chosen otherwise.
T is string ? int : bool
```
-The negative-equality operator is spelled `is not`. At the lexical level, the
-two words MUST be separated only by horizontal whitespace and are scanned as a
-single token; no line terminator may appear between them.
+The negative-equality operator is spelled `is not`. The two words are separate
+tokens, so whitespace and comments MAY stand between them the way they MAY
+stand between any other two tokens.
```typescript
T is not string ? int : bool
@@ -77,11 +77,6 @@ $value is array ? non-empty-array : null
array is $value ? non-empty-array : null
```
-In addition to `is` and `is not`, the relational operators `<`, `>`, `<=` and
-`>=` MAY be used as the condition operator. This is an extension beyond what
-PHPStan and Psalm accept; see
-[Relationship to Other Tools](#sec-Relationship-to-Other-Tools).
-
Note: A bare {Variable} is only a valid type when it carries a
{ConditionalSuffix}. A {Variable} MUST NOT otherwise stand alone as a type;
the only variable-like token that may appear as a primary type on its own is
@@ -189,19 +184,16 @@ User[]
User[][]
```
-**Counter-example.** The legacy list syntax does not accept a key type between
-the brackets; for that, use [offset access](#sec-Offset-Access) or the modern
-`array<…>` generic.
+The legacy list syntax accepts no key type between the brackets; for that,
+use [offset access](#sec-Offset-Access) or the modern `array<…>` generic. A
+`User[int]` is a well-formed document all the same — an _offset access_ type
+(see below) rather than a list, since `int` is a valid {Type}. It is the
+absence of a type between the brackets that selects the list reading:
-```typescript counter-example
+```typescript
User[int]
```
-This document, however, parses `User[int]` successfully — as an
-_offset access_ type (see below) rather than a list — because `int` is a
-valid {Type}. It is the absence of a type between the brackets that selects
-the list interpretation.
-
### Offset Access
A pair of square brackets enclosing a {Type} forms an _offset access_ type,
@@ -275,6 +267,14 @@ that immediately follows it: `*` selects a
[generic or shape](#sec-Generic-Types) named type; any other following token
(or the end of the document) leaves a plain named type.
+Note: A bare {ReservedWord} is likewise both a {LiteralType} and, through
+{Identifier}, a {Name} of a single segment (see
+[Reserved Words](#sec-Reserved-Words)). Where one source could be read as
+either, the {LiteralType} is the one taken, so a `true` denotes the boolean
+literal and never a type named `true`. An `is` and a `not` denote no literal
+of their own, and nothing is therefore left to shadow the named type each of
+them is.
+
## Names and Namespaces
Name :
@@ -307,13 +307,12 @@ Example\Name
A separator MAY appear at the start of a name or between two segments, but
MUST NOT appear at the end.
-**Reserved words within names.** The reserved words `true`, `false`, `null`
-and `is` (see [Reserved Words](#sec-Reserved-Words)) MAY appear as an
-{Identifier} segment of a {Name}. In isolation, such a word is scanned as a
-literal or operator and so cannot stand as a bare type name; but in a
-qualified position — following a separator or another segment — it denotes a
-name segment. This makes `\null` a reference to a type named `null`, distinct
-from the `null` literal.
+**Reserved words within names.** Every reserved word — `true`, `false`,
+`null`, `is` and `not` (see [Reserved Words](#sec-Reserved-Words)) — MAY
+appear as an {Identifier} segment of a {Name}. In a qualified position —
+following a separator or another segment — such a word is a name segment and
+nothing besides. This makes `\null` a reference to a type named `null`,
+distinct from the `null` literal.
**Counter-example.** A name cannot end with a separator.
@@ -348,13 +347,16 @@ implementation and is outside the scope of this specification.
TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>`
-TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType)
+TemplateArgument : TemplateArgumentHint | TemplateArgumentValue
-TemplateArgumentType : Type
+TemplateArgumentValue : Wildcard | Type
+
+Wildcard : `*`
A _generic type_ supplies a named type with one or more _template arguments_,
-each of which is itself a {Type}. Arguments are enclosed in angle brackets
-(`<` and `>`) and separated by commas. A trailing comma is permitted.
+each of which is itself a {Type} or a {Wildcard}. Arguments are enclosed in
+angle brackets (`<` and `>`) and separated by commas. A trailing comma is
+permitted.
Validating the number of arguments, their bounds, and their nesting is the
responsibility of the implementation, not of this grammar, which imposes no
@@ -391,9 +393,9 @@ describes only use sites, and therefore only template arguments.
### Template Argument Hints
-TemplateArgumentHint : Identifier Type
+TemplateArgumentHint : NameToken TemplateArgumentValue
-A template argument MAY carry a single leading _hint_: an {Identifier} placed
+A template argument MAY carry a single leading _hint_: a {NameToken} placed
before the argument's type. Hints are used by tooling — for example, to
express [call-site variance](https://phpstan.org/blog/guide-to-call-site-generic-variance#call-site-variance)
with identifiers such as `in`, `out`, `covariant`, or `contravariant`.
@@ -402,40 +404,36 @@ with identifiers such as `in`, `out`, `covariant`, or `contravariant`.
HashMap
```
-At the lexical level, the hint and the type it modifies MUST be separated by
-whitespace; this separation is what distinguishes a hint from the start of
-the argument's own type (see [Ignored Tokens](#sec-Ignored-Tokens)). Each
-argument may carry at most one hint.
+At the lexical level, {Whitespace} MUST stand directly behind the hint; this
+separation is what distinguishes a hint from the start of the argument's own
+type, and a {Comment} does not stand in its place (see
+[Ignored Tokens](#sec-Ignored-Tokens)). Each argument may carry at most one
+hint.
-**Counter-example.** A hint must be a single valid identifier.
+**Counter-example.** A hint is a single {NameToken}. A second one reads as
+the bound of a [template parameter](#sec-Callable-Types), and a parameter
+list belongs to a callable, so the statement is refused where the `(` it
+would go on with is missing.
```typescript counter-example
HashMap
```
```
-ParseException: Syntax error, unexpected "Request"
+ParseException: Syntax error, unexpected end of input
```
-**Counter-example.** A hint cannot be a reserved word: `is`, `true`, `false`
-and `null` are always scanned as the corresponding operator or literal token
-(see [Reserved Words](#sec-Reserved-Words)) and never as a hint {Identifier}.
+**Counter-example.** A hint cannot be a reserved word: `true`, `false`,
+`null`, `is` and `not` are always scanned as the corresponding literal or
+operator token (see [Reserved Words](#sec-Reserved-Words)) and never as a
+{NameToken}.
```typescript counter-example
HashMap
```
```
-ParseException: Syntax error, unexpected "Request"
-```
-
-### Template Argument Attributes
-
-Each template argument MAY additionally be prefixed with one or more
-[attribute groups](#sec-Attributes), providing metadata for the argument.
-
-```typescript
-HashMap<#[name("key")] T, #[name("value")] U>
+ParseException: Syntax error, unexpected ">"
```
## Literal Types
@@ -536,49 +534,92 @@ ParseException: Syntax error, unexpected "\"
ConstantMask :
-- Name `*`
-- Name `::` Identifier `*`
-- Name `::` `*`
+- GlobalConstantMask
+- ClassConstantMask
+
+GlobalConstantMask :
+
+- Name MaskTail
+- Name `\` MaskTail
+- LeadingMask
-A _constant mask_ denotes a family of constants whose names share a common
-prefix. A mask MUST terminate with an asterisk (`*`).
+ClassConstantMask :
-A global constant mask matches every global constant beginning with the
-prefix:
+- Name `::` Identifier MaskTail?
+- Name `::` MaskTail
+
+MaskTail : Wildcard (Identifier Wildcard)\* Identifier?
+
+LeadingMask : Wildcard Identifier MaskTail?
+
+A _constant mask_ denotes a family of constants whose names are written of the
+same segments, in the same order, with a {Wildcard} standing wherever the name
+is left unsaid. A mask MUST contain at least one {Wildcard}, and its segments
+and wildcards alternate, so two wildcards in a row denote nothing a single one
+does not.
+
+A global constant mask matches every global constant the segments describe:
```typescript
JSON_*
```
-A class constant mask matches every constant of a class whose name begins
-with the prefix; the prefix MAY be omitted entirely, in which case the mask
-matches every constant of the class:
+The name of a global constant MAY be left unsaid from its very beginning, and
+the namespace it belongs to MAY be written in front of it. A {Name} standing in
+front of a {MaskTail} carries the namespace and, in its last segment, the
+leading segment of the mask; a {Name} separated from the mask by a `\` is a
+namespace whole, and the mask then matches every constant that namespace holds:
+
+```typescript
+*_SUFFIX
+```
+
+```typescript
+Path\To\JSON_*
+```
+
+```typescript
+Path\To\*
+```
+
+A class constant mask matches every constant of a class the segments describe;
+the leading segment MAY be omitted entirely, in which case the mask matches
+every constant of the class:
```typescript
Path\To\ClassName::PREFIX_*
```
+```typescript
+Path\To\ClassName::*_SUFFIX
+```
+
+```typescript
+Path\To\ClassName::PREFIX_*_SUFFIX
+```
+
```typescript
Path\To\ClassName::*
```
-**Counter-example.** A global mask must have a prefix; a lone asterisk is not
-a type.
+**Counter-example.** A global mask must have at least one segment of a name,
+whether it stands in front of a wildcard or behind it; a lone asterisk, which
+would match every constant there is, is not a type.
```typescript counter-example
*
```
```
-ParseException: Syntax error, unexpected "*"
+ParseException: Syntax error, unexpected end of input
```
-**Counter-example.** The asterisk must be the final character of the mask.
+**Counter-example.** Two wildcards in a row are not a mask.
```typescript counter-example
-Path\To\ClassName::PREFIX_*_SUFFIX
+Path\To\ClassName::PREFIX_**
```
```
-ParseException: Syntax error, unexpected "_SUFFIX"
+ParseException: Syntax error, unexpected "*"
```
diff --git a/docs/Specification/Section 4 -- Structural Types.md b/docs/Specification/Section 4 -- Structural Types.md
index e38ac4e..f6de26f 100644
--- a/docs/Specification/Section 4 -- Structural Types.md
+++ b/docs/Specification/Section 4 -- Structural Types.md
@@ -1,9 +1,8 @@
# Structural Types
-This section defines the structural type forms — _shapes_ and _callables_ —
-and the _attribute_ syntax shared by several constructs. Each of these forms
-builds on the [named type](#sec-Named-Types) grammar introduced in the
-previous section.
+This section defines the structural type forms — _shapes_ and _callables_.
+Each of these forms builds on the [named type](#sec-Named-Types) grammar
+introduced in the previous section.
## Shape Types
@@ -45,7 +44,7 @@ array{}
### Shape Fields
-ShapeField : AttributeGroups? (ExplicitField | ImplicitField)
+ShapeField : ExplicitField | ImplicitField
ExplicitField : ShapeKey `?`? `:` ShapeValue
@@ -157,24 +156,9 @@ keys — using the same angle-bracket syntax as generics:
array{ user: User, ... }
```
-### Shape Field Attributes
-
-Each shape field MAY be prefixed with one or more
-[attribute groups](#sec-Attributes):
-
-```typescript
-App\Domain\User{
- #[name("user_name")]
- userName: non-empty-string,
- #[skip_when_empty]
- friends: list,
- ...
-}
-```
-
## Callable Types
-CallableType : Name `(` CallableParameters? `)` CallableReturnType?
+CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType?
CallableParameters : CallableParameter (`,` CallableParameter)\* `,`?
@@ -182,8 +166,10 @@ CallableReturnType : `:` Type
A _callable type_ describes a function-like value. It is a
[name](#sec-Names-and-Namespaces) — commonly `callable` or `Closure`, but any
-name is accepted — followed by a parenthesized, possibly empty, parameter
-list, and an optional return type introduced by a colon.
+name is accepted — optionally followed by the
+[template parameters](#sec-Template-Parameters) it declares, then by a
+parenthesized, possibly empty, parameter list, and an optional return type
+introduced by a colon.
```typescript
callable()
@@ -197,185 +183,214 @@ callable(): void
Closure(int<0, max>, callable(?C): mixed): void
```
-### Callable Parameters
+### Template Parameters
-CallableParameter : AttributeGroups? CallableParameterBody `=`?
+TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>`
-CallableParameterBody :
+TemplateParameter : Identifier TemplateBound\* TemplateDefault?
-- `...`? Type ParameterModifiers? Variable?
-- ParameterModifiers? Variable
+TemplateBound :
-ParameterModifiers :
+- UpperBound
+- LowerBound
-- `&` `...`?
-- `...` `&`?
+UpperBound : UpperBoundOperator Type
-A parameter is described by its type, optionally followed by a name. A
-parameter MAY also be given by name alone, without a type.
+UpperBoundOperator : one of `of` `as`
-```typescript
-callable(Type)
-```
+LowerBound : `super` Type
-```typescript
-callable(Type $name)
-```
+TemplateDefault : `=` Type
-```typescript
-callable($name)
-```
+A callable type MAY declare the _template parameters_ it introduces, written
+as a `<...>` list between the name and the parameter list. Each parameter is
+an {Identifier} followed by as many _bounds_ as are put on it.
-**Named Parameters.** A name beginning with `$` MAY follow the parameter's
-type, [permitting the argument to be passed by name](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments),
-exactly as in PHP.
+A parameter accepts three kinds of limit, each of which MUST be written at
+most once. The two bounds MAY be written in either order, while a
+{TemplateDefault} MUST be written last: a bound standing behind it belongs to
+the {Type} of the default rather than to the parameter.
+
+- An {UpperBound} narrows the parameter from above: the argument is to be a
+ subtype of the given {Type}. Its two operators denote the same thing, and a
+ conforming implementation MUST retain the one that was written.
+- A {LowerBound} bounds the parameter from below: the argument is to be a
+ supertype of the given {Type}.
+- A {TemplateDefault} is the {Type} the parameter takes when no argument is
+ supplied. It bounds nothing.
+
+Each operator is matched the one way it is spelled, so an `OF` bounds nothing
+an `of` does.
```typescript
-callable(A $a, B, C)
+callable(T): T
```
-**Output Parameters.** An ampersand (`&`) placed _after_ the parameter type
-marks the parameter as passed by reference (an _output_ parameter).
+```typescript
+callable(T): T
+```
```typescript
-callable(T&)
+Closure(T, U): V
```
```typescript
-callable(T &$name)
+callable(T): void
```
-**Counter-example.** The ampersand must follow the type; it must not precede
-it.
+**Counter-example.** A word that bounds nothing is not an operator.
```typescript counter-example
-callable(&T)
+callable(): void
```
```
-ParseException: Syntax error, unexpected "T"
+ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super"
```
-**Optional Parameters.** A trailing `=` marks a parameter as optional: the
-caller MAY omit the corresponding argument.
+**Counter-example.** Neither is a word that is spelled the way an operator is
+spelled, but written in a case of its own.
-```typescript
-callable(T=)
+```typescript counter-example
+callable(): void
```
-```typescript
-callable(T &$name=)
+```
+ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super"
```
-**Variadic Parameters.** An ellipsis (`...`) marks a parameter as variadic. It
-MAY be written either before the type or after it, immediately before the
-name.
+**Counter-example.** Each kind of limit is written at most once.
-```typescript
-callable(...T)
+```typescript counter-example
+callable(): void
```
-```typescript
-callable(T ...$name)
```
-
-```typescript
-callable(T &...$name)
+ParseException: Template parameter cannot have more than one upper bound
```
-**Counter-example.** The ellipsis may appear in only one position; a
-parameter that is variadic in both the prefix and suffix positions is an
-error.
+**Counter-example.** A bound does not stand behind a default.
```typescript counter-example
-callable(...T...)
+callable(): void
```
```
-ParseException: Either prefix or postfix variadic syntax should be used, but not both
+ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself
```
-**Counter-example.** A variadic parameter is already optional and therefore
-must not additionally carry a default marker.
+A `<...>` list is written the same way whether it declares template parameters
+or supplies [template arguments](#sec-Generic-Types), and only the `(` that
+follows a parameter list tells them apart. A list that no `(` follows is
+therefore a list of template arguments, which describes no bounds.
+
+**Counter-example.** Bounds belong to a callable alone.
```typescript counter-example
-callable(T ...$name=)
+Collection
```
```
-ParseException: Cannot have variadic param with a default
+ParseException: Syntax error, unexpected end of input
```
+### Callable Parameters
+
+CallableParameter : Type `&`? `...`? Variable? `=`?
-### Callable Parameter Attributes
+A parameter is described by its type, optionally followed by the reference
+and the variadic markers, by a name and by the default marker — in that
+order and in no other. The type is the only required part.
-Each callable parameter MAY be prefixed with one or more
-[attribute groups](#sec-Attributes):
+```typescript
+callable(Type)
+```
```typescript
-Example\Functor(#[type] int $a): void
+callable(Type $name)
```
-## Attributes
+**Counter-example.** A parameter given by name alone, without a type, is
+not permitted.
-AttributeGroups : AttributeGroup+
+```typescript counter-example
+callable($name)
+```
-AttributeGroup : `#[` AttributeList `,`? `]`
+```
+ParseException: Syntax error, unexpected ")"
+```
-AttributeList : Attribute (`,` Attribute)\*
+**Named Parameters.** A name beginning with `$` MAY follow the parameter's
+type, [permitting the argument to be passed by name](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments),
+exactly as in PHP.
-Attribute : Name AttributeArguments?
+```typescript
+callable(A $a, B, C)
+```
-AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)`
+**Output Parameters.** An ampersand (`&`) placed _after_ the parameter type
+marks the parameter as passed by reference (an _output_ parameter).
-AttributeArgument : Type
+```typescript
+callable(T&)
+```
-An _attribute_ attaches arbitrary, implementation-defined metadata to the
-construct it precedes. The syntax mirrors
-[PHP attributes](https://www.php.net/manual/en/language.attributes.php): each
-attribute is a [name](#sec-Names-and-Namespaces) optionally followed by a
-parenthesized list of arguments, and each argument is itself an arbitrary
-{Type} — including, recursively, a type that carries attributes of its own.
+```typescript
+callable(T &$name)
+```
-Attributes are written in groups delimited by `#[` and `]`. A construct MAY
-carry several attributes within one group, separated by commas, and several
-groups in sequence. Attributes may be attached to a
-[template argument](#sec-Template-Argument-Attributes), a
-[shape field](#sec-Shape-Field-Attributes), or a
-[callable parameter](#sec-Callable-Parameter-Attributes).
+**Counter-example.** The ampersand must follow the type; it must not precede
+it.
-A single attribute with one argument:
+```typescript counter-example
+callable(&T)
+```
-```typescript
-Example\Functor(#[type] int $a): void
```
+ParseException: Syntax error, unexpected "&"
+```
+
+**Optional Parameters.** A trailing `=` marks a parameter as optional: the
+caller MAY omit the corresponding argument.
-Several attributes within one group:
+```typescript
+callable(T=)
+```
```typescript
-HashMap<#[name("key"), out] T>
+callable(T &$name=)
```
-Several groups in sequence:
+**Variadic Parameters.** An ellipsis (`...`) placed after the parameter type
+marks the parameter as variadic. Where a parameter carries both markers, the
+ampersand comes first.
```typescript
-array{
- #[serialize("onSerialize")]
- #[deserialize("onDeserialize")]
- test?: App\Domain\User,
-}
+callable(T ...$name)
```
-**Counter-example.** An attribute name must be a valid {Name}; a literal is
-not permitted in that position.
+```typescript
+callable(T &...$name)
+```
+
+**Counter-example.** The ellipsis must follow the type; it must not precede
+it.
```typescript counter-example
-Collection<#[42] User>
+callable(...T)
```
```
-ParseException: Syntax error, unexpected "42"
+ParseException: Syntax error, unexpected "..."
```
-Note: Because the `#[` attribute marker is scanned as a single token by the
-[lexical grammar](#sec-Comments), it is never confused with the `#`
-line-comment marker.
+**Counter-example.** A variadic parameter is already optional and therefore
+must not additionally carry a default marker.
+
+```typescript counter-example
+callable(T ...$name=)
+```
+
+```
+ParseException: Cannot have variadic param with a default
+```
diff --git a/docs/Writerside/cfg/static/spec.html b/docs/Writerside/cfg/static/spec.html
index 4ad11cd..09f6a23 100644
--- a/docs/Writerside/cfg/static/spec.html
+++ b/docs/Writerside/cfg/static/spec.html
@@ -74,7 +74,6 @@
Ignored tokens improve readability and separate lexical tokens, but are otherwise insignificant. Any amount of Ignored MAY appear before and after every lexical Token.
-
-Note
-Although Ignored between two tokens is never itself significant, the presence of separating whitespace is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where the presence of separating whitespace is observable to the syntactic grammar.
+
+Note
+Although Ignored between two tokens is never itself significant, the presence of Whitespace standing directly behind a NameToken is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where Ignored is observable to the syntactic grammar, and the one place a Comment does not stand for the whitespace it is written among: a comment MAY follow that whitespace, but MUST NOT stand in its stead.
Whitespace separates tokens and improves the legibility of the source text. It includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed (U+000A) and carriage return (U+000D) characters. TypeLang does not distinguish between horizontal whitespace and line terminators, except within the multi-word is not operator (see Conditional Types).
+
Whitespace separates tokens and improves the legibility of the source text. TypeLang does not distinguish between horizontal whitespace and line terminators, and a Comment stands wherever whitespace stands, save for the one place the presence of the whitespace itself is read (see Ignored Tokens).
+
+Note
+The six characters above are the whole of it. A source text is read as a sequence of bytes (see Source Text), so a character that parts words in a script of its own — a no-break space (U+00A0), say — is no whitespace here: every byte it is written of falls in the range a Letter covers, and it is read as a part of the name it stands in.
LineTerminator (or the end of the source, whichever occurs first). A block comment begins with /* and continues up to and including the next */.
Comments are Ignored and have no bearing on the meaning of a document.
-
-Note
-The # line comment marker and the #[ attribute marker share a leading character. Because lexical analysis prefers the longest match, the sequence #[ is always scanned as the start of an attribute (see Attributes) rather than as the start of a comment.
TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, < and <=; : and ::; . in ...; # in #[); in every such case, the longest matching punctuator is taken.
+
TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, : and ::; . in ...); in every such case, the longest matching punctuator is taken.
A NameToken is the raw lexical unit that is later assembled, together with the namespace separator, into the syntactic Name production (see Names and Namespaces); the two are distinct non-terminals of, respectively, the lexical and the syntactic grammar. A NameToken MUST begin with a Letter or an underscore (_) and MAY continue with any Letter, Digit, underscore, or dash (-). The only difference from the PHP identifier grammar is that the dash character is additionally permitted in any non-leading position.
+
A source text is read as a sequence of bytes rather than of codepoints, and every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a Letter covers. A name MAY therefore be written in any script.
+
Проект\Тип
+
A NameToken is always the longest possible valid sequence; it MUST NOT be followed by a NameContinue character.
The words true, false and null are literal keywords (see Literal Tokens), and is is the conditional operator keyword (see Conditional Types). All four are matched case-insensitively, and only when not immediately followed by a NameContinue character.
-
When the source contains one of these words standing alone, in a position where a type is expected, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. Consequently, a bare reserved word cannot be used as a type name.
-
Counter-example. A bare reserved word is scanned as a literal, not a type name.
The words true, false and null are literal keywords (see Literal Tokens), and is and not are the conditional operator keywords (see Conditional Types). Each is matched only when not immediately followed by a NameContinue character. The three literal keywords are matched case-insensitively, while is and not are matched the one way they are spelled.
+
Wherever the source contains one of these words, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. It remains an Identifier all the same (see Names and Namespaces), so a reserved word is shadowed rather than forbidden: it stands as a name wherever the token it is scanned as denotes no type of its own.
+
A literal keyword standing alone, in a position where a type is expected, is read as the LiteralType it denotes and not as the NamedType it would otherwise be (see Primary Types):
+
TrUe
+
+
Behind a namespace separator or another segment, where no literal is expected, the same word is a name segment. This makes \true a reference to a type literally named true, distinct from the true literal:
+
\true
+
+
The conditional operator keywords denote no type of their own, so nothing shadows them, and a bare is or not is an ordinary named type:
+
not
-
A reserved word MAY, however, appear as an Identifierinside a qualified Name — that is, when it is preceded by a namespace separator or another identifier (see Names and Namespaces). For example, \true references a type literally named true, whereas the bare true is the boolean literal.
A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).
+
A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken, save for the dash a variable cannot carry. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).
The special variable $this is recognised as a distinct token and additionally denotes the current object type when used as a primary type (see Primary Types).
An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a NegativeSign. Within the digits of any integer literal, underscores (DigitSeparator) MAY appear freely as visual separators; they carry no meaning and do not affect the denoted value.
An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a Sign. A NegativeSign makes the denoted value negative; a leading + denotes the same value the absence of a Sign does. Underscores (DigitSeparator) MAY appear as visual separators; they carry no meaning and do not affect the denoted value. A DigitSeparator MUST stand between two digits, and therefore may neither lead a run of digits, nor trail one, nor stand beside another DigitSeparator.
Octal. Prefixed with 0o or 0O; digits are 0 through 7 only.
+
Octal. Prefixed with 0o or 0O or, as octal has been written since long before that prefix, with a leading 0 alone; digits are 0 through 7 only. A 0123 denotes the same value a 0o123 does.
A DecimalIntLiteral whose first digit is 0 and which is longer than a single character is interpreted in the legacy octal radix (base 8), for compatibility with historical PHP source. Thus, 042 denotes the value 34.
+
An OctalIntLiteral written without an OctalIndicator is the legacy octal spelling kept for compatibility with historical PHP source. It is read in the same radix (base 8) the prefixed spelling is, so 042 denotes the value 34.
A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the implementation’s platform integer type, the numeric value MUST be clamped to the nearest representable bound (the platform’s PHP_INT_MAX or PHP_INT_MIN) while the raw lexeme MUST be preserved unchanged.
A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a NegativeSign.
+
A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a Sign.
Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both:
Every run of digits a float is written of accepts the DigitSeparator under the same rule an integer does, and, unlike a DecimalIntLiteral, MAY begin with a zero:
+
2_3.4_5e-6_7
+
Counter-example. A lone decimal point is not a valid float.
A conditional type (also called a ternary type) selects between two types based on a comparison between a subject and a comparand. It is written as the subject, an operator, the comparand, a ?, the type chosen when the condition holds, a :, and the type chosen otherwise.
Tisstring? int : bool
-
The negative-equality operator is spelled is not. At the lexical level, the two words MUST be separated only by horizontal whitespace and are scanned as a single token; no line terminator may appear between them.
+
The negative-equality operator is spelled is not. The two words are separate tokens, so whitespace and comments MAY stand between them the way they MAY stand between any other two tokens.
Tis not string? int : bool
Either the subject or the comparand MAY be a Variable instead of a Type, including in “Yoda” order, where the variable appears on the right:
Note
A bare Variable is only a valid type when it carries a ConditionalSuffix. A Variable MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is $this (see Primary Types).
The legacy list syntax accepts no key type between the brackets; for that, use offset access or the modern array<…> generic. A User[int] is a well-formed document all the same — an offset access type (see below) rather than a list, since int is a valid Type. It is the absence of a type between the brackets that selects the list reading:
+
User[int]
-
This document, however, parses User[int] successfully — as an offset access type (see below) rather than a list — because int is a valid Type. It is the absence of a type between the brackets that selects the list interpretation.
+Note
+A bare ReservedWord is likewise both a LiteralType and, through Identifier, a Name of a single segment (see Reserved Words). Where one source could be read as either, the LiteralType is the one taken, so a true denotes the boolean literal and never a type named true. An is and a not denote no literal of their own, and nothing is therefore left to shadow the named type each of them is.
A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end.
-
Reserved words within names. The reserved words true, false, null and is (see Reserved Words) MAY appear as an Identifier segment of a Name. In isolation, such a word is scanned as a literal or operator and so cannot stand as a bare type name; but in a qualified position — following a separator or another segment — it denotes a name segment. This makes \null a reference to a type named null, distinct from the null literal.
+
Reserved words within names. Every reserved word — true, false, null, is and not (see Reserved Words) — MAY appear as an Identifier segment of a Name. In a qualified position — following a separator or another segment — such a word is a name segment and nothing besides. This makes \null a reference to a type named null, distinct from the null literal.
Counter-example. A name cannot end with a separator.
A generic type supplies a named type with one or more template arguments, each of which is itself a Type. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.
+
A generic type supplies a named type with one or more template arguments, each of which is itself a Type or a Wildcard. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.
Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no such limits.
A template argument MAY carry a single leading hint: an Identifier placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.
+
A template argument MAY carry a single leading hint: a NameToken placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.
HashMap<array-key, covariant Request>
-
At the lexical level, the hint and the type it modifies MUST be separated by whitespace; this separation is what distinguishes a hint from the start of the argument’s own type (see Ignored Tokens). Each argument may carry at most one hint.
-
Counter-example. A hint must be a single valid identifier.
At the lexical level, Whitespace MUST stand directly behind the hint; this separation is what distinguishes a hint from the start of the argument’s own type, and a Comment does not stand in its place (see Ignored Tokens). Each argument may carry at most one hint.
+
Counter-example. A hint is a single NameToken. A second one reads as the bound of a template parameter, and a parameter list belongs to a callable, so the statement is refused where the ( it would go on with is missing.
Counter-example. A hint cannot be a reserved word: is, true, false and null are always scanned as the corresponding operator or literal token (see Reserved Words) and never as a hint Identifier.
Counter-example. A hint cannot be a reserved word: true, false, null, is and not are always scanned as the corresponding literal or operator token (see Reserved Words) and never as a NameToken.
A constant mask denotes a family of constants whose names are written of the same segments, in the same order, with a Wildcard standing wherever the name is left unsaid. A mask MUST contain at least one Wildcard, and its segments and wildcards alternate, so two wildcards in a row denote nothing a single one does not.
+
A global constant mask matches every global constant the segments describe:
JSON_*
-
A class constant mask matches every constant of a class whose name begins with the prefix; the prefix MAY be omitted entirely, in which case the mask matches every constant of the class:
+
The name of a global constant MAY be left unsaid from its very beginning, and the namespace it belongs to MAY be written in front of it. A Name standing in front of a MaskTail carries the namespace and, in its last segment, the leading segment of the mask; a Name separated from the mask by a \ is a namespace whole, and the mask then matches every constant that namespace holds:
+
*_SUFFIX
+
+
Path\To\JSON_*
+
+
Path\To\*
+
+
A class constant mask matches every constant of a class the segments describe; the leading segment MAY be omitted entirely, in which case the mask matches every constant of the class:
Path\To\ClassName::PREFIX_*
+
Path\To\ClassName::*_SUFFIX
+
+
Path\To\ClassName::PREFIX_*_SUFFIX
+
Path\To\ClassName::*
-
Counter-example. A global mask must have a prefix; a lone asterisk is not a type.
Counter-example. A global mask must have at least one segment of a name, whether it stands in front of a wildcard or behind it; a lone asterisk, which would match every constant there is, is not a type.
This section defines the structural type forms — shapes and callables — and the attribute syntax shared by several constructs. Each of these forms builds on the named type grammar introduced in the previous section.
+
This section defines the structural type forms — shapes and callables. Each of these forms builds on the named type grammar introduced in the previous section.
ParseException: Cannot mix explicit and implicit shape keys
Duplicate explicit keys are not permitted. No two explicit fields of the same shape may denote the same key, regardless of which of the five key forms each uses.
A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — followed by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.
+
A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — optionally followed by the template parameters it declares, then by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.
A callable type MAY declare the template parameters it introduces, written as a <...> list between the name and the parameter list. Each parameter is an Identifier followed by as many bounds as are put on it.
+
A parameter accepts three kinds of limit, each of which MUST be written at most once. The two bounds MAY be written in either order, while a TemplateDefault MUST be written last: a bound standing behind it belongs to the Type of the default rather than to the parameter.
+
+
An UpperBound narrows the parameter from above: the argument is to be a subtype of the given Type. Its two operators denote the same thing, and a conforming implementation MUST retain the one that was written.
+
A LowerBound bounds the parameter from below: the argument is to be a supertype of the given Type.
+
A TemplateDefault is the Type the parameter takes when no argument is supplied. It bounds nothing.
+
+
Each operator is matched the one way it is spelled, so an OF bounds nothing an of does.
+
callable<T>(T):T
+
+
callable<Tof Some>(T):T
+
+
Closure<Tof Some,Usuper Any,V= int>(T,U):V
+
+
callable<Tof Some super Any = int>(T):void
+
+
Counter-example. A word that bounds nothing is not an operator.
ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself
+
+
A <...> list is written the same way whether it declares template parameters or supplies template arguments, and only the ( that follows a parameter list tells them apart. A list that no ( follows is therefore a list of template arguments, which describes no bounds.
+
Counter-example. Bounds belong to a callable alone.
A parameter is described by its type, optionally followed by a name. A parameter MAY also be given by name alone, without a type.
+
A parameter is described by its type, optionally followed by the reference and the variadic markers, by a name and by the default marker — in that order and in no other. The type is the only required part.
callable(Type)
callable(Type $name)
-
callable($name)
+
Counter-example. A parameter given by name alone, without a type, is not permitted.
Optional Parameters. A trailing = marks a parameter as optional: the caller MAY omit the corresponding argument.
callable(T=)
callable(T&$name=)
-
Variadic Parameters. An ellipsis (...) marks a parameter as variadic. It MAY be written either before the type or after it, immediately before the name.
-
callable(...T)
-
+
Variadic Parameters. An ellipsis (...) placed after the parameter type marks the parameter as variadic. Where a parameter carries both markers, the ampersand comes first.
callable(T...$name)
callable(T&...$name)
-
Counter-example. The ellipsis may appear in only one position; a parameter that is variadic in both the prefix and suffix positions is an error.
An attribute attaches arbitrary, implementation-defined metadata to the construct it precedes. The syntax mirrors PHP attributes: each attribute is a name optionally followed by a parenthesized list of arguments, and each argument is itself an arbitrary Type — including, recursively, a type that carries attributes of its own.
-
Attributes are written in groups delimited by #[ and ]. A construct MAY carry several attributes within one group, separated by commas, and several groups in sequence. Attributes may be attached to a template argument, a shape field, or a callable parameter.
AAppendix: Notation Conventions
diff --git a/docs/Writerside/tl.tree b/docs/Writerside/tl.tree
index e6d987a..a68d237 100644
--- a/docs/Writerside/tl.tree
+++ b/docs/Writerside/tl.tree
@@ -29,6 +29,7 @@
topic="comparison.md">
+
diff --git a/docs/Writerside/topics/language/basic-types.md b/docs/Writerside/topics/language/basic-types.md
index d59c4ad..825f436 100644
--- a/docs/Writerside/topics/language/basic-types.md
+++ b/docs/Writerside/topics/language/basic-types.md
@@ -115,3 +115,96 @@ The namespace delimiter can be used in conjunction with keywords such as `true`,
+
+## The $this Type {id="this-type"}
+
+The `$this` variable denotes the type of the object the type is written on. It
+is a type of its own, so it stands wherever a type stands: inside a union,
+behind a question mark, or as the type a
+[callable](callable-types.md) returns.
+
+> `$this` is the one variable that is a type. Any other variable names a
+> [callable parameter](callable-types.md#named-parameters) or an operand of a
+> [conditional type](conditional-types.md), and is not a type on its own.
+> {style="note"}
+
+
+
+
+> The type of the current object.
+> ```typescript
+> $this
+> ```
+
+> The current object type, or nothing at all.
+> ```typescript
+> ?$this
+> ```
+
+> A callable returning the object it was called on.
+> ```typescript
+> callable(): $this
+> ```
+
+
+
+
+> Any other variable is not a type.
+> ```typescript
+> $var
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected end of input, T_EQ expected
+> ```
+> {style="warning"}
+
+
+
+
+## Comments
+
+A type MAY carry comments, and they are read the way whitespace is: they part
+tokens and mean nothing of their own.
+
+A **line comment** begins with either a `//` or a `#` marker and runs up to the
+end of the line. A **block comment** begins with a `/*` and runs up to and
+including the next `*/`.
+
+
+
+
+> Block comment standing between two tokens.
+> ```typescript
+> int /* comment */ | string
+> ```
+
+> Line comment ending a line of a type written across several.
+> ```typescript
+> int | // comment
+> string
+> ```
+
+> A `#` marker begins a line comment as well.
+> ```typescript
+> # comment
+> int
+> ```
+
+
+
+
+> A comment is not a type, so a source made of one alone describes nothing.
+> ```typescript
+> // comment
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected end of input
+> ```
+> {style="warning"}
+
+
+
diff --git a/docs/Writerside/topics/language/callable-types.md b/docs/Writerside/topics/language/callable-types.md
index 092f87d..7945d5e 100644
--- a/docs/Writerside/topics/language/callable-types.md
+++ b/docs/Writerside/topics/language/callable-types.md
@@ -148,8 +148,8 @@ parameter description.
### Variadic Parameters
-Variadic parameters are indicated by the "`...`" and can be placed either
-_before the type_ or _before the parameter name._
+Variadic parameters are indicated by the "`...`" placed after the type. Where
+a parameter carries both markers, the ampersand ("`&`") comes first.
> Variadic parameter cannot be optional since they are already optional.
{style="warning"}
@@ -157,24 +157,6 @@ _before the type_ or _before the parameter name._
-> Callable type with one variadic parameter.
-> ```typescript
-> foo(...T)
-> ```
-
-> Callable type with one variadic named parameter.
-> ```typescript
-> foo(...T $name)
-> ```
-
-> Callable type with one variadic output named parameter.
-> ```typescript
-> foo(...T &$name)
-> ```
-
-
-
-
> Callable type with one variadic parameter.
> ```typescript
> foo(T...)
@@ -193,9 +175,9 @@ _before the type_ or _before the parameter name._
-> The ellipses (`...`) must come before or after the type.
+> The ellipsis (`...`) must be placed after the parameter's type.
> ```typescript
-> foo(...T...)
+> foo(...T)
> ```
>
> An error similar to the one below should occur
@@ -204,6 +186,17 @@ _before the type_ or _before the parameter name._
> ```
> {style="warning"}
+> The ampersand (`&`) must be placed before the ellipsis (`...`).
+> ```typescript
+> foo(T ...&$name)
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected "&"
+> ```
+> {style="warning"}
+
> Variadic parameter cannot be optional.
> ```typescript
> foo(T ...$name=)
@@ -218,47 +211,100 @@ _before the type_ or _before the parameter name._
+## Template Parameters
+
+A callable MAY declare the template parameters it introduces, written as a
+`<...>` list between the name and the parameter list. Each parameter is a
+name, optionally followed by the bounds put on it.
+
+A parameter accepts three kinds of limit, each written at most once. The two
+bounds are written in either order, and the default is written last: a bound
+behind it would read as a bound of the default itself.
-## Attributes
+* `of T` or `as T` — the **upper bound**: the argument is to be a subtype
+ of `T`. The two words mean the same and are kept as they are written.
+* `super T` — the **lower bound**: the argument is to be a supertype of `T`.
+* `= T` — the **default**: the type the parameter takes when no argument is
+ passed. It bounds nothing.
-
+The words are not case-sensitive, so an `OF` reads the same way an `of` does.
-Each callable parameter allows you to define list of additional attributes.
-An attribute is additional metadata for a parameter.
+> A `<...>` that no parenthesis follows is a list of
+> [template arguments](generic-types.md), not of template parameters, and
+> template arguments describe no bounds.
+> {style="note"}
-> Simple attribute with one argument for each callable parameter.
+> Callable type declaring one template parameter.
> ```typescript
-> Example\Functor(#[type(int8)] int $a): void
+> callable(T): T
> ```
-> Multiple attributes in one group.
+> Callable type declaring a bounded template parameter.
> ```typescript
-> Example\Functor(#[type(int8), const] int&): void
+> callable(T): T
> ```
-> Multiple attribute groups.
+> Every kind of bound, and several parameters at once.
> ```typescript
-> Example\OnCreate(
-> #[deprecated]
-> #[inline]
-> (callback(T): void) $callback=,
-> ): void
+> Closure(T, U): V
+> ```
+
+> One parameter carrying every limit at once.
+> ```typescript
+> callable(T): void
> ```
-> Only valid identifiers are allowed.
+> Bounds belong to a callable alone, so a type used with template arguments
+> describes none.
+> ```typescript
+> Collection
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected end of input
+> ```
+> {style="warning"}
+
+> Only `of`, `as` and `super` bound a parameter.
+> ```typescript
+> callable(): void
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Template parameter cannot be bounded with "whatever",
+> expected one of "of", "as" or "super"
+> ```
+> {style="warning"}
+
+> Each kind of limit is written at most once.
+> ```typescript
+> callable(): void
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Template parameter cannot have more than one upper bound
+> ```
+> {style="warning"}
+
+> A bound cannot stand behind the default, since it would read as a bound
+> of the default itself: the `of Some` below bounds the `int`, not the `T`.
> ```typescript
-> Example\Functor(#[42] int $a): void
+> callable(): void
> ```
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "42"
+> ParseException: Template parameter default must be written last, since
+> a bound behind it reads as a bound of the default itself
> ```
> {style="warning"}
diff --git a/docs/Writerside/topics/language/comparison.md b/docs/Writerside/topics/language/comparison.md
index d0840f7..c72ae0c 100644
--- a/docs/Writerside/topics/language/comparison.md
+++ b/docs/Writerside/topics/language/comparison.md
@@ -7,21 +7,8 @@ static code analysis tools: [PHPStan](https://phpstan.org/) and [Psalm](https://
- PHPStan: [https://phpstan.org](https://phpstan.org/writing-php-code/phpdoc-types)
- Psalm: [https://psalm.dev](https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/)
-
-
-There are others that are not currently tested for compatibility.
-For example:
-
-
-
-
-If you wish, you can add information about this software using a
-
- pull request to the documentation
-.
-
-
+- Phan: [https://github.com/phan](https://github.com/phan/phan/wiki/About-Union-Types)
+- phpDocumentor: [https://docs.phpdoc.org](https://docs.phpdoc.org/guide/guides/types.html)
> Below is a comparison list of all syntactic structures (grammar).
> The logical component (the physical existence of the type) is **NOT**
@@ -40,26 +27,103 @@ General table across all type parsing capabilities
+## Methodology
+
+Each verdict below is the answer of the tool's own parser, asked directly, so
+the table says what the tools do rather than what they are said to do.
+
+
+
+
Tool
+
Version
+
A type is supported when
+
+
+
+ TypeLang
+
+
type-lang/parser 2.x
+
TypeParser::parse() returns a node
+
+
+
+ Psalm
+
+
vimeo/psalm 6.5
+
Psalm\Type::parseString() returns a type carrying what was written
+
+
+
+ PHPStan
+
+
phpstan/phpdoc-parser 2.3
+
TypeParser::parse() reads the source whole, leaving no trailing input
+
+
+
+ Phan
+
+
phan/phan 5.5
+
+ UnionType::fromStringInContext() returns something other than
+ a class name made of the whole source
+
+
+
+
+ phpDocumentor
+
+
phpdocumentor/type-resolver 2.0
+
TypeResolver::resolve() returns a type, the source read whole
+
+
+
+> Two of these tools answer even where they did not understand the question, and
+> the rules above are what tells the two apart.
+>
+> Phan reads whatever it cannot parse as a class name, so a `0b1010` comes back
+> as a class called `\0b1010` rather than as an error. phpDocumentor builds on
+> `phpstan/phpdoc-parser` and keeps what it managed to read, so a `JSON_*` comes
+> back as the `JSON_` in front of it, the mask silently dropped. A verdict here
+> counts neither.
+> {style="note"}
+
## Basic Types
Below is a list of simple, logical and other common types.
@@ -76,27 +140,43 @@ Below is a list of simple, logical and other common types.
@@ -494,24 +827,26 @@ Below is a list of conditional types.
class_alias()
-
-
-
func_get_arg()
-
func_get_args()
-
func_num_args()
-
+
+
+ A parenthesis behind a name ends the type, whatever the name is,
+ so the functions PHPStan reads in a condition are read by rules
+ of its own rather than by the type language.
+
+
+ (func_num_args() is 1 ? A : B)
+ // Unexpected token "(", expected ')' at offset 14
+
+ The $decimal carries the value written out in base
+ 10, so a number too large for the platform's int is
+ still readable in full. The $value beside it is the
+ native one, and that is the only one a platform limit applies to.
+
// Input
9999999999999999999
// Stored
- TypeLang\Parser\Node\Literal\IntLiteralNode {
+ TypeLang\Type\Literal\IntLiteralNode {
+offset: 0
- +raw: "9999999999999999999"
+value: 9223372036854775807
+ +raw: "9999999999999999999"
+ +decimal: "9999999999999999999"
}
-
+
// Input
9999999999999999999
@@ -863,22 +1138,55 @@ Below is a list of literal types/lexemes.
Open in psalm.dev
-
+
+
+ The literal is kept as it was written, so nothing is lost while
+ the type is read. The number does not survive the analysis that
+ follows, though: what comes out of it is the nearest
+ int the platform carries.
+
+ A shape follows an array, a list or an
+ object, and no other name.
+
+
+
+
-## Callables Types
+## Callable Types
Below is a list of grammar of callable (function) types.
@@ -1617,27 +1986,43 @@ Below is a list of grammar of callable (function) types.
-
+
// OK
- callable(T&): U
+ callable(T&): U
// Bug: Intersection types must be all objects,
// Psalm\Type\Atomic\TInt provided in docblock
- callable(int&): U
+ callable(int&): U
Open in psalm.dev
@@ -1778,47 +2195,49 @@ Below is a list of grammar of callable (function) types.
-
+
// OK
- callable(T&=): U
+ callable(T&): U
// Bug: Intersection types must be all objects,
// Psalm\Type\Atomic\TInt provided in docblock
- callable(int&=): U
+ callable(int&): U
- Open in psalm.dev
+ Open in psalm.dev
-
- This expression is incorrect because the "variadic"
- lexeme ... must be present in the parameter
- in a single copy. The parser must throw a syntax or
- semantic error.
-
+
-
+
+
+ The ellipsis stands behind the type, the way PHP itself writes it.
+ The prefix form was read by earlier versions of the parser and is
+ a syntax error now.
+
- callable(...int ...$name)
- // Internal Psalm error on line ...:
- // Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree
+ callable(Type...): T
- Open in psalm.dev
-
+
- callable(...int ...$name)
- // PHPDoc tag @return has invalid value (callable(...int ...$name): T):
- // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ...
+ // Written
+ callable(...Type): T
+
+
+ // Read
+ callable(): T
- Open in phpstan.org
- This expression is incorrect because any variadic
- parameter is already optional. The parser must
- throw a syntax or semantic error.
+ This expression is incorrect because any variadic parameter is
+ already optional. The parser must throw a syntax or semantic
+ error.
-
+
// OK: Cannot have variadic param with a default in docblock
callable(...T=): T
@@ -1961,87 +2406,153 @@ Below is a list of grammar of callable (function) types.
+
+ This expression is incorrect because the "variadic" lexeme
+ ... must be present in the parameter in a single
+ copy. The parser must throw a syntax or semantic error.
+
-
+
- callable(...Type $name): T
- // Internal Psalm error on line ...:
+ callable(...int ...$name)
+ // Internal Psalm error on line ...:
// Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree
- Open in psalm.dev
-
-
-
- callable(...Type $name): T
- // PHPDoc tag @return has invalid value (callable(...Type $name): T):
- // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ...
-
- Open in phpstan.org
+ Open in psalm.dev
+ An as comes back as an of, where TypeLang
+ keeps the bound as it was written.
+
- // OK
- callable(T &...$name): U
+ // Written
+ callable<T as Some>(T): T
- // Bug: Intersection types must be all objects,
- // Psalm\Type\Atomic\TInt provided in docblock
- callable(int &...$name): U
-
- Open in psalm.dev
-
-
-
- callable(...Type &$name): T
- // PHPDoc tag @return has invalid value (callable(...Type &$name): T):
- // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ...
+ // Read
+ callable<T of Some>(T): T
- Open in phpstan.org
diff --git a/docs/Writerside/topics/language/conditional-types.md b/docs/Writerside/topics/language/conditional-types.md
index 409005a..b3854d9 100644
--- a/docs/Writerside/topics/language/conditional-types.md
+++ b/docs/Writerside/topics/language/conditional-types.md
@@ -101,42 +101,6 @@ The `is` keyword checks whether the subject **is** of the compared type, and
-## Comparison Operators
-
-In addition to the `is` / `is not` equality checks, TypeLang also allows
-comparing the subject using the `<`, `>`, `<=` and `>=` operators.
-
-
-
-
-> Less than.
-> ```typescript
-> T < int<0, 100> ? A : B
-> ```
-
-> Greater than.
-> ```typescript
-> T > 0 ? A : B
-> ```
-
-> Less than or equal.
-> ```typescript
-> T <= 100 ? A : B
-> ```
-
-> Greater than or equal.
-> ```typescript
-> T >= 0 ? A : B
-> ```
-
-
-
-
-> This is a TypeLang-specific extension: neither PHPStan nor Psalm support
-> comparison operators in conditional types, only `is` and `is not`. See the
-> [syntax comparison](comparison.md) page for details.
-> {style="note"}
-
## Yoda-Style Conditions
Since the subject and the compared type can both be arbitrary type
diff --git a/docs/Writerside/topics/language/const-types.md b/docs/Writerside/topics/language/const-types.md
index 5ede64b..4867836 100644
--- a/docs/Writerside/topics/language/const-types.md
+++ b/docs/Writerside/topics/language/const-types.md
@@ -85,26 +85,59 @@ a double colon (`::`) character, and then the constant name.
## Constant Masks
-A reference to a certain set of constants can be defined using a mask. The
-use of masks is identical to regular constants, but must be terminated with
-an asterisk (`*`).
+A reference to a certain set of constants can be defined using a mask: A
+sequence of name segments with an asterisk (`*`) standing wherever the name
+is left unsaid.
+
+A mask is made of at least one asterisk, and its segments and asterisks
+alternate, so two asterisks in a row describe nothing a single one does not.
Prefixes on class constants can be omitted, so type will mean any class constant.
+A global constant may be left unsaid from its very beginning as well, and the
+namespace it belongs to may be written in front of the mask. A name separated
+from the mask by a backslash is a namespace whole, so the mask then means any
+constant that namespace holds.
-> Reference to any constant with the `JSON_*` prefix.
+> Reference to any constant with the `JSON_` prefix.
> ```typescript
> JSON_*
> ```
+> Reference to any constant with the `_SUFFIX` suffix.
+> ```typescript
+> *_SUFFIX
+> ```
+
+> Reference to any constant with the `JSON_` prefix in the `Path\To` namespace.
+> ```typescript
+> Path\To\JSON_*
+> ```
+
+> Reference to any constant of the `Path\To` namespace.
+> ```typescript
+> Path\To\*
+> ```
+
> Reference to any class constant with the `PREFIX_` prefix.
> ```typescript
> Path\To\ClassName::PREFIX_*
> ```
+> Reference to any class constant with the `_SUFFIX` suffix.
+> ```typescript
+> Path\To\ClassName::*_SUFFIX
+> ```
+
+> Reference to any class constant written of the `PREFIX_` and `_SUFFIX`
+> segments, in that order.
+> ```typescript
+> Path\To\ClassName::PREFIX_*_SUFFIX
+> ```
+
> Reference to any class constant
> ```typescript
> Path\To\ClassName::*
@@ -113,25 +146,27 @@ Prefixes on class constants can be omitted, so type will mean any class constant
-> It is not allowed to omit prefixes from global constants.
+> A global mask needs at least one segment of a name, whether it stands in
+> front of the asterisk or behind it. A lone asterisk would mean any constant
+> there is, and that is not a type.
> ```typescript
> *
> ```
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "*"
+> ParseException: Syntax error, unexpected end of input
> ```
> {style="warning"}
-> The asterisk (`*`) must be the final character.
+> Two asterisks in a row are not a mask.
> ```typescript
-> Path\To\ClassName::PREFIX_*_SUFFIX
+> Path\To\ClassName::PREFIX_**
> ```
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "_SUFFIX"
+> ParseException: Syntax error, unexpected "*"
> ```
> {style="warning"}
diff --git a/docs/Writerside/topics/language/generic-types.md b/docs/Writerside/topics/language/generic-types.md
index 8905c84..2685cb2 100644
--- a/docs/Writerside/topics/language/generic-types.md
+++ b/docs/Writerside/topics/language/generic-types.md
@@ -114,7 +114,7 @@ used, for example, in static analyzers to indicate the
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "Request"
+> ParseException: Syntax error, unexpected end of input
> ```
> {style="warning"}
@@ -122,35 +122,41 @@ used, for example, in static analyzers to indicate the
-## List Syntax
+## Wildcard Arguments
-In addition to modern list declarations such as `list` or
-`array`, the legacy `int[]` syntax is allowed.
+A template argument may be left unsaid, written as an asterisk (`*`) in the
+place of the type. A wildcard argument accepts a [hint](#call-site-hints)
+the same way any other argument does.
-> List (array) of `User` objects.
+> Any argument at all.
> ```typescript
-> User[]
+> Collection<*>
> ```
-> List of list (nested array) of `User` objects.
+> A wildcard beside a type.
> ```typescript
-> User[][]
+> HashMap
+> ```
+
+> A wildcard carrying a hint.
+> ```typescript
+> Collection
> ```
-> Incorrect syntax (was [used in the PSR](https://github.com/php-fig/event-dispatcher/blob/1.0.0/src/ListenerProviderInterface.php#L14) by mistake).
+> An asterisk is a template argument alone and is not a type of its own.
> ```typescript
-> User[int]
+> int|*
> ```
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "int"
+> ParseException: Syntax error, unexpected "*"
> ```
> {style="warning"}
@@ -158,42 +164,35 @@ In addition to modern list declarations such as `list` or
-## Attributes
-
-
+## List Syntax
-Each template argument allows you to define list of additional attributes.
-An attribute is additional metadata for an argument.
+In addition to modern list declarations such as `list` or
+`array`, the legacy `int[]` syntax is allowed.
-> Simple attribute with one argument for each template argument.
-> ```typescript
-> HashMap<#[name("key")] T, #[name("value")] U>
-> ```
-
-> Multiple attributes in one group.
+> List (array) of `User` objects.
> ```typescript
-> HashMap<#[name("key"), out] T>
+> User[]
> ```
-> Multiple attribute groups.
+> List of list (nested array) of `User` objects.
> ```typescript
-> HashMap<#[name("key")] #[out] T>
+> User[][]
> ```
-> Only valid identifiers are allowed.
+> Incorrect syntax (was [used in the PSR](https://github.com/php-fig/event-dispatcher/blob/1.0.0/src/ListenerProviderInterface.php#L14) by mistake).
> ```typescript
-> Collection<#[42] User>
+> User[int]
> ```
>
> An error similar to the one below should occur
> ```
-> ParseException: Syntax error, unexpected "42"
+> ParseException: Syntax error, unexpected "int"
> ```
> {style="warning"}
diff --git a/docs/Writerside/topics/language/literal-types.md b/docs/Writerside/topics/language/literal-types.md
index 4a7bdaf..6b8baaa 100644
--- a/docs/Writerside/topics/language/literal-types.md
+++ b/docs/Writerside/topics/language/literal-types.md
@@ -125,13 +125,58 @@ representation. The braces are required in the sequence. E.g. `"\u{41}" === "A"`
The syntax of integer numbers is similar to the PHP language. Binary, octal,
decimal and hexadecimal number systems are supported.
-> Please note that in addition to numbers, underscores (`_`) are allowed.
+> Please note that in addition to numbers, underscores (`_`) are allowed as a
+> separator. A separator stands between two digits and nowhere else, so it
+> may neither lead a number, nor trail it, nor stand beside another separator.
> {style="note"}
Decimal numbers can contain any digits from `0` to `9` (leading `0` not allowed)
and must match the regular expression `[1-9][0-9]*`. For any other numbers,
-a special prefixed format is used, described below. Negative values are prefixed
-with a minus (`-`).
+a special prefixed format is used, described below. A value may be prefixed by
+a sign: a minus (`-`) makes it negative, and a plus (`+`) says what the absence
+of a sign already says.
+
+> A number is kept whole, however large it is. The node carries both the
+> native `$value`, which a platform limit applies to, and the `$decimal`
+> beside it — the value written out in base 10 as a string, so a number too
+> large for an `int` is still readable in full.
+> {style="note"}
+
+
+
+
+> A long number written with separators.
+> ```typescript
+> 42_04
+> ```
+
+
+
+
+> A separator cannot trail a number.
+> ```typescript
+> 42_
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected "_"
+> ```
+> {style="warning"}
+
+> Two separators in a row are not allowed.
+> ```typescript
+> 4__2
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected "__2"
+> ```
+> {style="warning"}
+
+
+
### Binary
@@ -212,6 +257,11 @@ numbers between `0` and `7` and must match the regular expression
> 0o42_23
> ```
+> Number `275` in "legacy" octal format with `_` delimiters.
+> ```typescript
+> 04_23
+> ```
+
@@ -226,6 +276,18 @@ numbers between `0` and `7` and must match the regular expression
> ```
> {style="warning"}
+> A leading `0` makes a number octal, so a digit outside the octal range
+> cannot follow one.
+> ```typescript
+> 08
+> ```
+>
+> An error similar to the one below should occur
+> ```
+> ParseException: Syntax error, unexpected "8"
+> ```
+> {style="warning"}
+
@@ -273,7 +335,18 @@ The syntax of float numbers is similar to the PHP language. Basic floating point
syntax and scientific notation are supported.
Every floating point number accepts the format `[0-9]+\.[0-9]+`. The leading or
-trailing number may be omitted. Negative values are prefixed with a minus (`-`.
+trailing number may be omitted. A value may be prefixed by a sign: a minus (`-`)
+makes it negative, and a plus (`+`) says what the absence of a sign already says.
+
+> A number is kept whole, however large it is. The node carries both the
+> native `$value`, which a platform limit applies to, and the `$decimal`
+> beside it — the value written out in base 10 as a string, so a number too
+> large for an `int` is still readable in full.
+> {style="note"}
+
+Every run of digits a float is written of — the one before the dot, the one
+after it and the exponent alike — accepts the underscore (`_`) separator under
+the same rule an integer does.
@@ -298,6 +371,17 @@ trailing number may be omitted. Negative values are prefixed with a minus (`-`.
> -0.9
> ```
+> Signed floating point literal value (equivalent of `0.9`).
+> ```typescript
+> +0.9
+> ```
+
+> Floating point literal value written with separators in every part
+> (equivalent of `23.45e-67`).
+> ```typescript
+> 2_3.4_5e-6_7
+> ```
+
@@ -352,6 +436,18 @@ writing out an inconveniently long string of digits.
> 10e-2
> ```
+> Also number `1000.0` in scientific notation: The exponent takes a sign of
+> its own, and a plus says what the absence of a sign already says.
+
+> A number is kept whole, however large it is. The node carries both the
+> native `$value`, which a platform limit applies to, and the `$decimal`
+> beside it — the value written out in base 10 as a string, so a number too
+> large for an `int` is still readable in full.
+> {style="note"}
+> ```typescript
+> 10e+2
+> ```
+
diff --git a/docs/Writerside/topics/language/shape-types.md b/docs/Writerside/topics/language/shape-types.md
index 772f062..15d2f25 100644
--- a/docs/Writerside/topics/language/shape-types.md
+++ b/docs/Writerside/topics/language/shape-types.md
@@ -196,62 +196,3 @@ array{
}
```
-
-
-## Attributes
-
-
-
-Each shape field allows you to define list of additional attributes.
-An attribute is additional metadata for a field.
-
-
-
-
-> Simple attribute with one argument for each shape field.
-> ```typescript
-> App\Domain\User{
-> #[name("user_name")]
-> userName: non-empty-string,
-> #[skip_when_empty]
-> friends: list,
-> ...
-> }
-> ```
-
-> Multiple attributes in one group.
-> ```typescript
-> App\Domain\User{
-> #[complexity(100), skip_when_empty]
-> friends: list,
-> }
-> ```
-
-> Multiple attribute groups.
-> ```typescript
-> array{
-> #[serialize("onSerialize")]
-> #[deserialize("onDeserialize")]
-> test?: App\Domain\User,
-> }
-> ```
-
-
-
-
-> Only valid identifiers are allowed.
-> ```typescript
-> Collection{
-> #[42]
-> test?: App\Domain\User,
-> }
-> ```
->
-> An error similar to the one below should occur
-> ```
-> ParseException: Syntax error, unexpected "42"
-> ```
-> {style="warning"}
-
-
-
diff --git a/docs/Writerside/topics/parser.md b/docs/Writerside/topics/parser.md
index 49fcfed..e585432 100644
--- a/docs/Writerside/topics/parser.md
+++ b/docs/Writerside/topics/parser.md
@@ -2,8 +2,8 @@
-Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict and
-tolerant parsing modes, feature toggling, and grammar checking.
+Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict,
+partial and validating reading modes, feature toggling, and grammar checking.
@@ -21,7 +21,7 @@ out of it, checking the grammar along the way.
**Requirements:**
-* `PHP >= 8.4`
+* `PHP >= 8.1`
## Quick Start
@@ -40,23 +40,25 @@ var_dump($type);
```php
object(TypeLang\Type\NamedTypeNode)#1 (4) {
["offset"]=> int(0)
- ["name"]=> object(TypeLang\Type\Name)#2 (3) {
+ ["name"]=> object(TypeLang\Type\Name)#2 (5) {
["offset"]=> int(0)
- ["segments"]=> array(1) {
+ ["parts"]=> array(1) {
[0]=> object(TypeLang\Type\Identifier)#3 (2) {
["offset"]=> int(0)
["value"]=> string(5) "array"
}
}
+ ["first"]=> object(TypeLang\Type\Identifier)#3 { ... }
+ ["last"]=> object(TypeLang\Type\Identifier)#3 { ... }
["isFullyQualified"]=> bool(false)
}
["arguments"]=> NULL
["fields"]=> object(TypeLang\Type\Shape\FieldsListNode)#4 (3) {
["offset"]=> int(7)
["items"]=> array(1) {
- [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (5) { ... }
+ [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (4) { ... }
}
- ["sealed"]=> bool(true)
+ ["isSealed"]=> bool(true)
}
}
```
@@ -68,30 +70,32 @@ node classes themselves (`TypeLang\Type\*`) belong to the separate
`type-lang/types` package — plain AST Nodes.
> If a statement cannot be parsed, `parse()` throws an exception implementing
-> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [tolerant
-> mode](tolerant-mode.md) page for a way to parse partially valid input
+> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [partial
+> parsing](tolerant-mode.md) page for a way to read partially valid input
> instead of failing outright.
> {style="note"}
-## Strict vs. Tolerant Parsing
+## Three Ways of Reading a Source
-`TypeParser` implements two parsing strategies, both declared on
-`TypeParserInterface`:
+`TypeParser` asks the grammar three different questions, all three declared
+on `TypeParserInterface`:
* `parse(): TypeNode` — strict mode. Requires the whole input to be a
syntactically valid type statement; throws a `ParserExceptionInterface` on
the first error.
-* `parseTolerant(): ParsedResult` — tolerant mode. Parses as much of the
- input as it can and returns a `TypeLang\Parser\ParsedResult` object
- containing the (possibly partial) type and the offset up to which the
- source was actually consumed — regardless of what follows. Useful for
- phpdoc/docblock parsing where a type declaration is followed by a
- free-text description. See [Tolerant mode](tolerant-mode.md).
+* `partial(): ParsedResult` — reads as much of the input as the grammar
+ describes and returns a `TypeLang\Parser\Partial\ParsedResult` carrying the
+ type built out of the read part and the offset the reading stopped at,
+ whatever follows it. Useful for phpdoc/docblock parsing, where a type
+ declaration is followed by a free-text description. See
+ [](tolerant-mode.md).
+* `validate(): CheckResult` — tells whether the source is a type, building
+ nothing of it. This is the cheapest of the three whenever the type itself
+ is of no use. See [](tolerant-mode.md#validation).
## Parser Arguments
-The `parse()`/`parseTolerant()` methods accept the source code in
-any of the following forms:
+All three methods accept the source code in any of the following forms:
@@ -137,10 +141,11 @@ Enable or disable individual language constructs (generics, shapes,
unions, ...) — see [](features.md).
-
+
-Parse a type declaration embedded in free-form text, such as a phpdoc
-annotation — see [](tolerant-mode.md).
+Read a type declaration embedded in free-form text, such as a phpdoc
+annotation, or ask whether a source is a type at all — see
+[](tolerant-mode.md).
diff --git a/docs/Writerside/topics/parser/features.md b/docs/Writerside/topics/parser/features.md
index 96c2510..53533f3 100644
--- a/docs/Writerside/topics/parser/features.md
+++ b/docs/Writerside/topics/parser/features.md
@@ -31,7 +31,7 @@ $parser->parse('42');
```
TypeLang\Parser\Exception\SemanticParseException:
-Literal values not allowed in "42" at column 1
+Literal values not allowed in "42"
```
To override one or more flags on an *existing* parser, use `TypeParser::withFeatures()`
@@ -59,7 +59,7 @@ $features = new TypeParserFeatures()
| Flag | Enables syntax | Error message fragment |
|------------------|------------------------------------|------------------------------------------|
| `literals` | `42`, `"foo"`, `true`, `null`, ... | `Literal values not allowed` |
-| `generics` | `Example` | `Template arguments not allowed` |
+| `generics` | `Example`, `callable(T): T` | `Template arguments not allowed` |
| `hints` | `Example` | `Template argument hints not allowed` |
| `lists` | `Example[]` | `Square bracket list types not allowed` |
| `offsets` | `Example[Type]` | `Type offsets not allowed` |
@@ -68,7 +68,6 @@ $features = new TypeParserFeatures()
| `unions` | `T\|U` | `Union types not allowed` |
| `intersections` | `T&U` | `Intersection types not allowed` |
| `conditions` | `T is U ? A : B` | `Conditional expressions not allowed` |
-| `attributes` | `#[attr]` on fields/args/params | see below |
### Literals
@@ -84,7 +83,7 @@ $parser->parse('42');
```
```
-Literal values not allowed in "42" at column 1
+Literal values not allowed in "42"
```
### Generics
@@ -101,7 +100,19 @@ $parser->parse('Example');
```
```
-Template arguments not allowed in "Example" at column 8
+Template arguments not allowed in "Example"
+```
+
+The same flag closes the other side of the angle brackets: the
+[template parameters](callable-types.md#template-parameters) a callable
+declares.
+
+```php
+$parser->parse('callable(T): T');
+```
+
+```
+Template parameters not allowed in "callable(T): T"
```
### Hints
@@ -117,7 +128,7 @@ $parser->parse('Example');
```
```
-Template argument hints not allowed in "Example" at column 9
+Template argument hints not allowed in "Example"
```
### Lists
@@ -133,7 +144,7 @@ $parser->parse('Example[]');
```
```
-Square bracket list types not allowed in "Example[]" at column 1
+Square bracket list types not allowed in "Example[]"
```
### Offsets
@@ -151,7 +162,7 @@ $parser->parse('Example[Type]');
```
```
-Type offsets not allowed in "Example[Type]" at column 1
+Type offsets not allowed in "Example[Type]"
```
### Callables
@@ -167,7 +178,7 @@ $parser->parse('fn(): void');
```
```
-Callable types not allowed in "fn(): void" at column 1
+Callable types not allowed in "fn(): void"
```
### Shapes
@@ -185,7 +196,7 @@ $parser->parse(<<<'PHP'
```
```
-Shape fields not allowed in "array{foo: T}" on line 1 at column 6
+Shape fields not allowed in "array{foo: T}"
```
### Unions
@@ -199,7 +210,7 @@ $parser->parse('T|U');
```
```
-Union types not allowed in "T|U" at column 1
+Union types not allowed in "T|U"
```
### Intersections
@@ -213,7 +224,7 @@ $parser->parse('T&U');
```
```
-Intersection types not allowed in "T&U" at column 1
+Intersection types not allowed in "T&U"
```
### Conditions
@@ -230,50 +241,5 @@ $parser->parse('T is U ? 23 : 42');
```
```
-Conditional expressions not allowed in "T is U ? 23 : 42" at column 1
-```
-
-### Attributes
-
-Unlike the other flags, `attributes` is checked independently at every
-place attributes may occur — currently on
-[shape fields](shape-types.md#attributes), on
-[template arguments](generic-types.md#attributes), and on
-[callable parameters](callable-types.md#attributes) — each reporting its own
-message.
-
-```php
-$parser = new TypeParser(new TypeParserFeatures(
- attributes: false,
-));
-
-$parser->parse(<<<'PHP'
- array{
- #[name("new_name"), skip_when_empty]
- oldName: int,
- }
- PHP);
-```
-
-```
-Shape field attributes not allowed in "array{\n #[name("new_name"),
-…" (33+) on line 2 at column 5
-```
-
-```php
-$parser->parse('Collection<#[a] T>');
-```
-
-```
-Template argument attributes not allowed in "Collection<#[a] T>"
-at column 12
-```
-
-```php
-$parser->parse('callable(#[a] int $x): void');
-```
-
-```
-Callable parameter attributes not allowed in "callable(#[a] int $x): void"
-at column 10
+Conditional expressions not allowed in "T is U ? 23 : 42"
```
diff --git a/docs/Writerside/topics/parser/tolerant-mode.md b/docs/Writerside/topics/parser/tolerant-mode.md
index b1e8fce..c8124c9 100644
--- a/docs/Writerside/topics/parser/tolerant-mode.md
+++ b/docs/Writerside/topics/parser/tolerant-mode.md
@@ -1,15 +1,15 @@
-# Tolerant Mode
+# Partial Parsing
-Parse as much of a type declaration as possible instead of failing on the
-first syntax error, and recover the offset at which parsing actually stopped.
+Read as much of a type declaration as the grammar describes instead of failing
+on the first syntax error, recover the offset the reading stopped at, or ask
+whether a source is a type at all.
-The "tolerant" analysis mode allows parsing a type grammar embedded in other
-arbitrary data, without requiring the full input to be a valid type
-statement.
+Partial parsing allows reading a type grammar embedded in other arbitrary
+data, without requiring the full input to be a valid type statement.
This is convenient, for example, for analyzing phpdoc (docblocks): a
`@return` annotation contains a type declaration followed by a free-text
@@ -17,20 +17,32 @@ description, and there is no delimiter between the two other than "the type
grammar stops making sense here".
Unlike `parse()`, which either returns a fully valid `TypeNode` or throws,
-`TypeParser::parseTolerant()` always returns a `TypeLang\Parser\ParsedResult`
-object:
+`TypeParser::partial()` always returns a
+`TypeLang\Parser\Partial\ParsedResult`, and which of the three it is says how
+much of the source the grammar described.
-```php
-final readonly class ParsedResult
-{
- public TypeNode $type;
-
- /**
- * Last processed token offset, in bytes.
- */
- public int $offset;
-}
-```
+
+
+
+The source is a type whole, so the reading stopped at the very end of it.
+Carries the `$type` built out of it and the `$offset` the reading stopped at.
+
+
+
+
+The grammar described the beginning of the source alone. This is a successful
+result as well, since a type has been built either way — the rest of the
+source simply begins at the `$offset`.
+
+
+
+
+The source opens no type at all, so nothing has been built of it. Carries the
+`$message` of what stands in the way, the `$position` (line and column) and
+the `$offset` the reading stopped at.
+
+
+
## Basic Usage
@@ -39,8 +51,9 @@ use TypeLang\Parser\TypeParser;
$parser = new TypeParser();
-$result = $parser->parseTolerant('int and more text');
+$result = $parser->partial('int and more text');
+var_dump($result::class); // PartialParsedResult
var_dump($result->type->name->toString()); // string(3) "int"
var_dump($result->offset); // int(4)
```
@@ -60,8 +73,8 @@ var_dump($remainder); // string(13) "and more text"
> ```php
> $parser->parse('int and more text');
> // TypeLang\Parser\Exception\UnexpectedTokenException:
-> // Syntax error, unexpected "and " in "int and more text"
-> // at column 5
+> // Syntax error, unexpected "and " (T_NAME_WITH_SPACE),
+> // T_NS_DELIMITER expected in "int and more text"
> ```
> {style="note"}
@@ -82,7 +95,7 @@ $content = <<<'PHP'
$content = substr($content, strlen('@return '));
// "Example Returns something very interesting!"
-$result = $parser->parseTolerant($content);
+$result = $parser->partial($content);
var_dump($result->type);
```
@@ -110,17 +123,17 @@ The description can then be recovered from the reported offset:
$description = substr($content, $result->offset);
var_dump($description);
-// string(36) "Returns something very interesting!"
+// string(35) "Returns something very interesting!"
```
## Recovering from Incomplete Constructs
-Tolerant mode does not attempt to *repair* broken syntax — it stops at the
+Partial parsing does not attempt to *repair* broken syntax — it stops at the
last point where a complete sub-rule was matched and treats everything past
it as trailing content, however implausible that trailing content looks.
```php
-$result = $parser->parseTolerant('array{');
+$result = $parser->partial('array{');
var_dump($result->type->name->toString()); // string(5) "array"
var_dump($result->offset); // int(5)
@@ -129,3 +142,57 @@ var_dump($result->offset); // int(5)
Here the unterminated `{` of the shape-fields list is never entered, so the
parser falls back to the last valid statement — the bare `array` named type —
and reports offset `5`, right before the `{`.
+
+## Validation
+
+Where the type itself is of no use and the only question is whether the
+source is one, `TypeParser::validate()` asks exactly that and builds nothing.
+It returns a `TypeLang\Parser\Validation\CheckResult`, and which of the three
+it is answers the question.
+
+
+
+
+The source is a type whole, so nothing stands in the way of it.
+
+
+
+
+The source is no type of its own. Carries the `$message` of what stands in
+the way, the `$position` (line and column) and the `$offset` the reading
+stopped at.
+
+
+
+
+The grammar described the beginning of the source alone. This is a failure as
+well, since a check asks about the source whole, and the rest of it begins at
+the `$offset`.
+
+
+
+
+```php
+use TypeLang\Parser\TypeParser;
+use TypeLang\Parser\Validation\SuccessfulCheckResult;
+
+$parser = new TypeParser();
+
+var_dump($parser->validate('array{ field: result }')::class);
+// TypeLang\Parser\Validation\SuccessfulCheckResult
+
+var_dump($parser->validate('array{ field: result } and more')::class);
+// TypeLang\Parser\Validation\PartialCheckResult
+
+var_dump($parser->validate('array{')::class);
+// TypeLang\Parser\Validation\PartialCheckResult
+
+// A check for a source that is a type whole:
+$isType = $parser->validate($source) instanceof SuccessfulCheckResult;
+```
+
+> A `PartialCheckResult` **is** a `FailureCheckResult`, and a
+> `PartialParsedResult` **is** a `SuccessfulParsedResult`. Each hierarchy is
+> shaped around the question its method asks: a check asks about the source
+> whole, whereas a partial parse is content with the beginning of it.
+> {style="note"}
diff --git a/docs/Writerside/topics/phpdoc.md b/docs/Writerside/topics/phpdoc.md
index 308f0e7..f0b79c1 100644
--- a/docs/Writerside/topics/phpdoc.md
+++ b/docs/Writerside/topics/phpdoc.md
@@ -28,7 +28,7 @@ comment.
**Requirements:**
-* `PHP >= 8.4`
+* `PHP >= 8.1`
* `ext-mbstring` optional
diff --git a/docs/Writerside/topics/phpdoc/combinators.md b/docs/Writerside/topics/phpdoc/combinators.md
index 901c5c6..10b3cd5 100644
--- a/docs/Writerside/topics/phpdoc/combinators.md
+++ b/docs/Writerside/topics/phpdoc/combinators.md
@@ -43,7 +43,7 @@ whatever is left entirely.
## Built-in Combinators
-Fourteen combinators cover every built-in tag's grammar between them:
+Fifteen combinators cover every built-in tag's grammar between them:
@@ -56,6 +56,13 @@ Fourteen combinators cover every built-in tag's grammar between them:
Visibility
Method or property visibility.
+
+
AssertSubject
+
CodeReference
+
What an assertion is written of: $value,
+ $this->items, $this->getItems()
+ or self::$instances.
+
AuthorName
string
diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md
index b28a6ca..cbb8279 100644
--- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md
@@ -9,15 +9,16 @@ and its counterpart
[@phan-assert-if-true](phan-assert-if-true-tag.md).
```
-"@phan-assert-if-false" [ ]
+"@phan-assert-if-false" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfFalseTag extends TypedVariableTag {}
+final class AssertIfFalseTag extends AssertionTag {}
```
The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md
index 16caa52..33a49a1 100644
--- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md
@@ -9,15 +9,16 @@ and its counterpart
[@phan-assert-if-false](phan-assert-if-false-tag.md).
```
-"@phan-assert-if-true" [ ]
+"@phan-assert-if-true" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfTrueTag extends TypedVariableTag {}
+final class AssertIfTrueTag extends AssertionTag {}
```
The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md
index b1bdf0b..e2b475a 100644
--- a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md
@@ -10,15 +10,16 @@ siblings — see [@phan-assert-if-true](phan-assert-if-true-tag.md) and
same assertion family.
```
-"@phan-assert" [ ]
+"@phan-assert" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phan-assert` tag produces an `AssertTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertTag extends TypedVariableTag {}
+final class AssertTag extends AssertionTag {}
```
The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phpstan-assert](phpstan-assert-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md
index f53c00c..bf000f8 100644
--- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md
@@ -7,15 +7,16 @@ function returns `false`, defined by PHPStan. It is one of the
[@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags.
```
-"@phpstan-assert-if-false" [ ]
+"@phpstan-assert-if-false" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phpstan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfFalseTag extends TypedVariableTag {}
+final class AssertIfFalseTag extends AssertionTag {}
```
The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md
index b4fce07..f9149ee 100644
--- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md
@@ -7,15 +7,16 @@ function returns `true`, defined by PHPStan. It is one of the
[@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags.
```
-"@phpstan-assert-if-true" [ ]
+"@phpstan-assert-if-true" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phpstan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfTrueTag extends TypedVariableTag {}
+final class AssertIfTrueTag extends AssertionTag {}
```
The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md
index 8f0b35c..ac1a469 100644
--- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md
@@ -8,15 +8,16 @@ narrowed to a given type after the call, defined by PHPStan. It has
boolean-returning function returns a particular value.
```
-"@phpstan-assert" [ ]
+"@phpstan-assert" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@phpstan-assert` tag produces an `AssertTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertTag extends TypedVariableTag {}
+final class AssertTag extends AssertionTag {}
```
The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md
index b701939..643d082 100644
--- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md
@@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside
[@psalm-assert](psalm-assert-tag.md).
```
-"@psalm-assert-if-false" [ ]
+"@psalm-assert-if-false" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@psalm-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfFalseTag extends TypedVariableTag {}
+final class AssertIfFalseTag extends AssertionTag {}
```
The same `AssertIfFalseTag` is produced by [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md
index e60f763..0ba0d99 100644
--- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md
@@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside
[@psalm-assert](psalm-assert-tag.md).
```
-"@psalm-assert-if-true" [ ]
+"@psalm-assert-if-true" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@psalm-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertIfTrueTag extends TypedVariableTag {}
+final class AssertIfTrueTag extends AssertionTag {}
```
The same `AssertIfTrueTag` is produced by [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same
diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md
index d1a2c27..798f8f0 100644
--- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md
+++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md
@@ -7,15 +7,16 @@ narrowed to a given type after the call returns. It is defined by
the static analyzer Psalm.
```
-"@psalm-assert" [ ]
+"@psalm-assert" [ "!" | "=" | "!=" ] [ ]
```
Parsing a `@psalm-assert` tag produces an `AssertTag` instance, carrying the
-asserted `$type` and `$variable` alongside the `$name` and optional
-`$description` every [Tag](phpdoc.md#tag) already provides.
+asserted `$type`, the `$subject` it is asserted of and the `$operator`
+relating them, alongside the `$name` and optional `$description` every
+[Tag](phpdoc.md#tag) already provides.
```php
-final class AssertTag extends TypedVariableTag {}
+final class AssertTag extends AssertionTag {}
```
The same `AssertTag` is produced by [@phpstan-assert](phpstan-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same
diff --git a/docs/Writerside/topics/printer.md b/docs/Writerside/topics/printer.md
index 1c95d77..caa6646 100644
--- a/docs/Writerside/topics/printer.md
+++ b/docs/Writerside/topics/printer.md
@@ -38,7 +38,7 @@ It ships with two printers that render the very same AST differently:
**Requirements:**
-* `PHP >= 8.4`
+* `PHP >= 8.1`
* `ext-mbstring` optional
## Quick Start
diff --git a/docs/Writerside/topics/reader.md b/docs/Writerside/topics/reader.md
index 6874963..d9c5270 100644
--- a/docs/Writerside/topics/reader.md
+++ b/docs/Writerside/topics/reader.md
@@ -35,7 +35,7 @@ phpdoc is the job of the PHPDoc parser.
**Requirements:**
-* `PHP >= 8.4`
+* `PHP >= 8.1`
* `ext-mbstring` optional
## Quick Start
diff --git a/libs/parser/.gitattributes b/libs/parser/.gitattributes
index cfa4eea..11aad01 100644
--- a/libs/parser/.gitattributes
+++ b/libs/parser/.gitattributes
@@ -1,6 +1,7 @@
* text=auto eol=lf
*.pp eol=lf linguist-language=EBNF
*.pp2 eol=lf linguist-language=EBNF
+*.pp3 eol=lf linguist-language=EBNF
# GIT
.editorconfig export-ignore
diff --git a/libs/parser/.github/workflows/tests.yml b/libs/parser/.github/workflows/tests.yml
index a7df15d..3abc5cb 100644
--- a/libs/parser/.github/workflows/tests.yml
+++ b/libs/parser/.github/workflows/tests.yml
@@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [ '8.4', '8.5' ]
+ php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
os: [ ubuntu-latest, macos-latest, windows-latest ]
stability: [ lowest, stable ]
steps:
diff --git a/libs/parser/README.md b/libs/parser/README.md
index f2a0433..308d97f 100644
--- a/libs/parser/README.md
+++ b/libs/parser/README.md
@@ -33,7 +33,7 @@ composer require type-lang/parser
```
**Requirements:**
-- PHP 8.4+
+- PHP 8.1+
## Usage
diff --git a/libs/parser/bin/build b/libs/parser/bin/build
index 4ab89a7..accea66 100644
--- a/libs/parser/bin/build
+++ b/libs/parser/bin/build
@@ -4,6 +4,9 @@
declare(strict_types=1);
use Phplrt\Compiler\Compiler;
+use Phplrt\Compiler\Generator\ClassModifier;
+use Phplrt\Compiler\Generator\TargetPhpVersion;
+use Phplrt\Source\FileSource;
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
trigger_error('The console should be invoked via the CLI '
@@ -31,20 +34,16 @@ if (!class_exists(Compiler::class)) {
exit(-1);
}
-$grammar = (new Compiler())
- ->load(new SplFileInfo(__DIR__ . '/../resources/grammar.pp2'))
- ->build()
- ->withClassReference('TypeLang\\Type')
- ->withClassReference('TypeLang\\Parser\\Exception')
- ->generate();
-
-file_put_contents(__DIR__ . '/../resources/grammar.php', $grammar);
-
-//
-// Postprocess and optimize output grammar
-//
-
-$data = file_get_contents(__DIR__ . '/../resources/grammar.php');
-
-// Replace functions to static one
-file_put_contents(__DIR__ . '/../resources/grammar.php', $data);
+(new Compiler())
+ ->load(FileSource::createFromPathname(__DIR__ . '/../resources/grammar.pp3'))
+ ->generate()
+ ->withNamespaceName('TypeLang\\Parser\\Internal')
+ ->withClassName('CompiledExecutor')
+ ->withClassModifier(ClassModifier::Abstract)
+ // The parser carries the features it recognizes a source with, so it
+ // is declared as a mutable class rather than a readonly one.
+ ->withReadonly(false)
+ ->withTargetPhpVersion(TargetPhpVersion::Php81)
+ ->withClassImport('TypeLang\\Type')
+ ->withClassImport('TypeLang\\Parser\\Exception')
+ ->save(__DIR__ . '/../src/Internal/CompiledExecutor.php');
diff --git a/libs/parser/composer.json b/libs/parser/composer.json
index bc78049..2813faa 100644
--- a/libs/parser/composer.json
+++ b/libs/parser/composer.json
@@ -9,11 +9,9 @@
"issues": "https://github.com/php-type-language/parser/issues"
},
"require": {
- "php": "^8.4",
+ "php": "^8.1",
"type-lang/types": "^2.0",
- "phplrt/lexer": "^3.7",
- "phplrt/parser": "^3.7",
- "phplrt/source": "^3.7"
+ "phplrt/runtime": "^4.1"
},
"autoload": {
"psr-4": {
@@ -22,9 +20,9 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
- "phplrt/compiler": "^3.7.5",
- "phpstan/phpstan": "^2.2.5",
- "phpunit/phpunit": "^13.2.4"
+ "phplrt/compiler": "^4.1",
+ "phpstan/phpstan": "^2.2",
+ "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0"
},
"autoload-dev": {
"psr-4": {
diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon
index 018e46e..bf3ffa2 100644
--- a/libs/parser/phpstan.neon
+++ b/libs/parser/phpstan.neon
@@ -1,6 +1,16 @@
parameters:
level: max
+ phpVersion:
+ min: 80100
+ max: 80599
+ treatPhpDocTypesAsCertain: false
fileExtensions:
- php
paths:
- src
+ excludePaths:
+ analyse:
+ # The parser is generated out of the grammar by the phplrt compiler and
+ # is not written by hand.
+ - src/Internal/CompiledExecutor.php
+ reportUnmatchedIgnoredErrors: false
diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php
deleted file mode 100644
index 408de44..0000000
--- a/libs/parser/resources/grammar.php
+++ /dev/null
@@ -1,804 +0,0 @@
-,
- * ...
- * },
- * skip: list,
- * grammar: array,
- * reducers: array,
- * transitions?: array
- * }
- */
-return [
- 'initial' => 59,
- 'tokens' => [
- 'default' => [
- 'T_DQ_STRING_LITERAL' => '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"',
- 'T_SQ_STRING_LITERAL' => '\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'',
- 'T_PFX_FLOAT_LITERAL' => '\\-?[0-9]++\\.[0-9]*+(?:[eE]-?[0-9]++)?',
- 'T_SFX_FLOAT_LITERAL' => '\\-?[0-9]*+\\.[0-9]++(?:[eE]-?[0-9]++)?',
- 'T_EXP_LITERAL' => '\\-?[0-9]++[eE]-?[0-9]++',
- 'T_BIN_INT_LITERAL' => '\\-?0[bB][01_]++',
- 'T_OCT_INT_LITERAL' => '\\-?0[oO][0-7_]++',
- 'T_HEX_INT_LITERAL' => '\\-?0[xX][0-9a-fA-F_]++',
- 'T_DEC_INT_LITERAL' => '\\-?[0-9][0-9_]*+',
- 'T_BOOL_LITERAL' => '(?i)(?:true|false)(?![a-zA-Z0-9\\-_\\x80-\\xff])',
- 'T_NULL_LITERAL' => '(?i)null(?![a-zA-Z0-9\\-_\\x80-\\xff])',
- 'T_NEQ' => '(?i)is\\h++not(?![a-zA-Z0-9\\-_\\x80-\\xff])',
- 'T_EQ' => '(?i)is(?![a-zA-Z0-9\\-_\\x80-\\xff])',
- 'T_THIS' => '\\$this\\b',
- 'T_VARIABLE' => '\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+',
- 'T_NAME_WITH_SPACE' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+\\s++',
- 'T_NAME' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+',
- 'T_LTE' => '<=',
- 'T_GTE' => '>=',
- 'T_ANGLE_BRACKET_OPEN' => '<',
- 'T_ANGLE_BRACKET_CLOSE' => '>',
- 'T_PARENTHESIS_OPEN' => '\\(',
- 'T_PARENTHESIS_CLOSE' => '\\)',
- 'T_BRACE_OPEN' => '\\{',
- 'T_BRACE_CLOSE' => '\\}',
- 'T_ATTR_OPEN' => '#\\[',
- 'T_SQUARE_BRACKET_OPEN' => '\\[',
- 'T_SQUARE_BRACKET_CLOSE' => '\\]',
- 'T_COMMA' => ',',
- 'T_ELLIPSIS' => '\\.\\.\\.',
- 'T_DOUBLE_COLON' => '::',
- 'T_COLON' => ':',
- 'T_ASSIGN' => '=',
- 'T_NS_DELIMITER' => '\\\\',
- 'T_QMARK' => '\\?',
- 'T_OR' => '\\|',
- 'T_AMP' => '&',
- 'T_ASTERISK' => '\\*',
- 'T_COMMENT' => '(?://|#)[^\\r\\n]*+',
- 'T_DOC_COMMENT' => '/\\*.*?\\*/',
- 'T_WHITESPACE' => '\\s++',
- ],
- ],
- 'skip' => [
- 'T_COMMENT',
- 'T_DOC_COMMENT',
- 'T_WHITESPACE',
- ],
- 'transitions' => [],
- 'grammar' => [
- new Phplrt\Parser\Grammar\Concatenation([6, 3, 7]),
- new Phplrt\Parser\Grammar\Concatenation([3, 10]),
- new Phplrt\Parser\Grammar\Alternation([0, 1]),
- new Phplrt\Parser\Grammar\Alternation([11, 12, 13, 14, 15]),
- new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false),
- new Phplrt\Parser\Grammar\Concatenation([4, 3]),
- new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false),
- new Phplrt\Parser\Grammar\Repetition(5, 0, INF),
- new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false),
- new Phplrt\Parser\Grammar\Concatenation([8, 3]),
- new Phplrt\Parser\Grammar\Repetition(9, 0, INF),
- new Phplrt\Parser\Grammar\Lexeme('T_NAME', true),
- new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true),
- new Phplrt\Parser\Grammar\Lexeme('T_EQ', true),
- new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true),
- new Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24, 25]),
- new Phplrt\Parser\Grammar\Concatenation([2, 39]),
- new Phplrt\Parser\Grammar\Concatenation([2, 43, 44]),
- new Phplrt\Parser\Grammar\Alternation([17, 18, 19]),
- new Phplrt\Parser\Grammar\Alternation([30, 31]),
- new Phplrt\Parser\Grammar\Alternation([32, 33, 34]),
- new Phplrt\Parser\Grammar\Alternation([35, 36, 37, 38]),
- new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_VARIABLE', true),
- new Phplrt\Parser\Grammar\Lexeme('T_THIS', true),
- new Phplrt\Parser\Grammar\Alternation([26, 27]),
- new Phplrt\Parser\Grammar\Lexeme('T_THIS', true),
- new Phplrt\Parser\Grammar\Lexeme('T_DQ_STRING_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_SQ_STRING_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_PFX_FLOAT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_SFX_FLOAT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_EXP_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_BIN_INT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_OCT_INT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_HEX_INT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_DEC_INT_LITERAL', true),
- new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', false),
- new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true),
- new Phplrt\Parser\Grammar\Concatenation([3, 40]),
- new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true),
- new Phplrt\Parser\Grammar\Lexeme('T_DOUBLE_COLON', false),
- new Phplrt\Parser\Grammar\Alternation([41, 3, 42]),
- new Phplrt\Parser\Grammar\Concatenation([57, 58]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([46, 45]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', false),
- new Phplrt\Parser\Grammar\Repetition(47, 0, INF),
- new Phplrt\Parser\Grammar\Optional(48),
- new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', false),
- new Phplrt\Parser\Grammar\Concatenation([49, 45, 50, 51, 52]),
- new Phplrt\Parser\Grammar\Repetition(155, 1, INF),
- new Phplrt\Parser\Grammar\Concatenation([16, 59]),
- new Phplrt\Parser\Grammar\Concatenation([59]),
- new Phplrt\Parser\Grammar\Optional(54),
- new Phplrt\Parser\Grammar\Alternation([55, 56]),
- new Phplrt\Parser\Grammar\Concatenation([175]),
- new Phplrt\Parser\Grammar\Concatenation([67, 71, 72]),
- new Phplrt\Parser\Grammar\Concatenation([107, 59]),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false),
- new Phplrt\Parser\Grammar\Optional(60),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false),
- new Phplrt\Parser\Grammar\Optional(61),
- new Phplrt\Parser\Grammar\Concatenation([2, 62, 63, 64, 65]),
- new Phplrt\Parser\Grammar\Concatenation([74, 73]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([68, 67]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Repetition(69, 0, INF),
- new Phplrt\Parser\Grammar\Optional(70),
- new Phplrt\Parser\Grammar\Concatenation([78, 79]),
- new Phplrt\Parser\Grammar\Optional(54),
- new Phplrt\Parser\Grammar\Concatenation([93, 92]),
- new Phplrt\Parser\Grammar\Alternation([83, 86, 88, 90, 80]),
- new Phplrt\Parser\Grammar\Lexeme('T_ASSIGN', true),
- new Phplrt\Parser\Grammar\Alternation([75, 76]),
- new Phplrt\Parser\Grammar\Optional(77),
- new Phplrt\Parser\Grammar\Concatenation([28]),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', true),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Concatenation([81, 82, 80]),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', true),
- new Phplrt\Parser\Grammar\Concatenation([84, 85, 80]),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Concatenation([87, 80]),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', true),
- new Phplrt\Parser\Grammar\Concatenation([89, 80]),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Concatenation([94, 95]),
- new Phplrt\Parser\Grammar\Optional(91),
- new Phplrt\Parser\Grammar\Concatenation([96, 106]),
- new Phplrt\Parser\Grammar\Optional(28),
- new Phplrt\Parser\Grammar\Concatenation([59]),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', true),
- new Phplrt\Parser\Grammar\Optional(97),
- new Phplrt\Parser\Grammar\Concatenation([98, 99]),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', true),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Optional(101),
- new Phplrt\Parser\Grammar\Concatenation([102, 103]),
- new Phplrt\Parser\Grammar\Alternation([100, 104]),
- new Phplrt\Parser\Grammar\Optional(105),
- new Phplrt\Parser\Grammar\Lexeme('T_COLON', false),
- new Phplrt\Parser\Grammar\Concatenation([123, 126]),
- new Phplrt\Parser\Grammar\Concatenation([121, 122]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([110, 109]),
- new Phplrt\Parser\Grammar\Optional(111),
- new Phplrt\Parser\Grammar\Concatenation([108, 112]),
- new Phplrt\Parser\Grammar\Optional(109),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Lexeme('T_BRACE_OPEN', false),
- new Phplrt\Parser\Grammar\Alternation([113, 114]),
- new Phplrt\Parser\Grammar\Optional(115),
- new Phplrt\Parser\Grammar\Lexeme('T_BRACE_CLOSE', false),
- new Phplrt\Parser\Grammar\Concatenation([116, 117, 118, 119]),
- new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true),
- new Phplrt\Parser\Grammar\Optional(53),
- new Phplrt\Parser\Grammar\Concatenation([129, 130]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([124, 123]),
- new Phplrt\Parser\Grammar\Repetition(125, 0, INF),
- new Phplrt\Parser\Grammar\Concatenation([131, 134, 135, 133]),
- new Phplrt\Parser\Grammar\Concatenation([133]),
- new Phplrt\Parser\Grammar\Optional(54),
- new Phplrt\Parser\Grammar\Alternation([127, 128]),
- new Phplrt\Parser\Grammar\Alternation([18, 19, 3, 23, 21]),
- new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true),
- new Phplrt\Parser\Grammar\Concatenation([59]),
- new Phplrt\Parser\Grammar\Optional(132),
- new Phplrt\Parser\Grammar\Lexeme('T_COLON', false),
- new Phplrt\Parser\Grammar\Alternation([53, 120]),
- new Phplrt\Parser\Grammar\Optional(136),
- new Phplrt\Parser\Grammar\Concatenation([2, 137]),
- new Phplrt\Parser\Grammar\Concatenation([176]),
- new Phplrt\Parser\Grammar\Optional(142),
- new Phplrt\Parser\Grammar\Concatenation([139, 140]),
- new Phplrt\Parser\Grammar\Concatenation([145, 146, 147, 59, 148, 59]),
- new Phplrt\Parser\Grammar\Concatenation([28, 142]),
- new Phplrt\Parser\Grammar\Alternation([141, 143]),
- new Phplrt\Parser\Grammar\Alternation([149, 150, 151, 152, 153, 154]),
- new Phplrt\Parser\Grammar\Alternation([59, 28]),
- new Phplrt\Parser\Grammar\Lexeme('T_QMARK', false),
- new Phplrt\Parser\Grammar\Lexeme('T_COLON', false),
- new Phplrt\Parser\Grammar\Lexeme('T_EQ', true),
- new Phplrt\Parser\Grammar\Lexeme('T_NEQ', true),
- new Phplrt\Parser\Grammar\Lexeme('T_GTE', true),
- new Phplrt\Parser\Grammar\Lexeme('T_LTE', true),
- new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', true),
- new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', true),
- new Phplrt\Parser\Grammar\Concatenation([158, 156, 159, 160]),
- new Phplrt\Parser\Grammar\Concatenation([161, 164]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Lexeme('T_ATTR_OPEN', false),
- new Phplrt\Parser\Grammar\Optional(157),
- new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false),
- new Phplrt\Parser\Grammar\Concatenation([2, 166]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([162, 161]),
- new Phplrt\Parser\Grammar\Repetition(163, 0, INF),
- new Phplrt\Parser\Grammar\Concatenation([171, 167, 172, 173, 174]),
- new Phplrt\Parser\Grammar\Optional(165),
- new Phplrt\Parser\Grammar\Concatenation([59]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Concatenation([168, 167]),
- new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false),
- new Phplrt\Parser\Grammar\Repetition(169, 0, INF),
- new Phplrt\Parser\Grammar\Optional(170),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false),
- new Phplrt\Parser\Grammar\Concatenation([144]),
- new Phplrt\Parser\Grammar\Concatenation([177, 180]),
- new Phplrt\Parser\Grammar\Concatenation([181, 184]),
- new Phplrt\Parser\Grammar\Lexeme('T_OR', false),
- new Phplrt\Parser\Grammar\Concatenation([178, 176]),
- new Phplrt\Parser\Grammar\Optional(179),
- new Phplrt\Parser\Grammar\Concatenation([185]),
- new Phplrt\Parser\Grammar\Lexeme('T_AMP', false),
- new Phplrt\Parser\Grammar\Concatenation([182, 177]),
- new Phplrt\Parser\Grammar\Optional(183),
- new Phplrt\Parser\Grammar\Alternation([188, 186]),
- new Phplrt\Parser\Grammar\Concatenation([189, 191]),
- new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true),
- new Phplrt\Parser\Grammar\Concatenation([187, 186]),
- new Phplrt\Parser\Grammar\Alternation([197, 29, 20, 66, 138]),
- new Phplrt\Parser\Grammar\Concatenation([192, 193, 194]),
- new Phplrt\Parser\Grammar\Repetition(190, 0, INF),
- new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_OPEN', true),
- new Phplrt\Parser\Grammar\Optional(59),
- new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false),
- new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false),
- new Phplrt\Parser\Grammar\Concatenation([195, 59, 196]),
- ],
- 'reducers' => [
- 0 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Name($children, true);
- },
- 1 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Name($children, false);
- },
- 3 => static function (Phplrt\Parser\Context $ctx, $children) {
- return Type\Identifier::createFromString($children->getValue());
- },
- 16 => static function (Phplrt\Parser\Context $ctx, $children) {
- return Type\Identifier::createFromString($children->getValue());
- },
- 17 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if ($this->features->literals === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset);
- }
-
- return $children;
- },
- 18 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\ConstMaskNode($children[0]);
- },
- 19 => static function (Phplrt\Parser\Context $ctx, $children) {
- // :: "*"
- if (\count($children) === 3) {
- return new Type\ClassConstMaskNode(
- $children[0],
- $children[1],
- );
- }
-
- // ::
- if ($children[1] instanceof Type\Identifier) {
- return new Type\ClassConstNode(
- $children[0],
- $children[1],
- );
- }
-
- // :: "*"
- return new Type\ClassConstMaskNode($children[0]);
- },
- 21 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return $this->stringPool[$token] ??= $children;
- },
- 22 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\FloatLiteralNode::parse($token->getValue());
- },
- 23 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue());
- },
- 24 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\BoolLiteralNode::parse($token->getValue());
- },
- 25 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Literal\NullLiteralNode($children->getValue());
- },
- 28 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\VariableLiteralNode::parse($token->getValue());
- },
- 29 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\VariableLiteralNode::parse($token->getValue());
- },
- 30 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue());
- },
- 31 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$token" variable is an auto-generated
- $token = $ctx->lastProcessedToken;
-
- return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue());
- },
- 45 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $hint = $attributes = null;
-
- if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset);
- }
-
- $attributes = \array_shift($children);
- }
-
- $type = \array_pop($children);
-
- if (\reset($children) !== false) {
- if ($this->features->hints === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset);
- }
-
- $hint = \reset($children);
- }
-
- return new Type\Template\TemplateArgumentNode(
- $type,
- $hint,
- $attributes,
- );
- },
- 53 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if ($this->features->generics === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset);
- }
-
- return new Type\Template\TemplateArgumentListNode($children);
- },
- 54 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Attribute\AttributeGroupListNode($children);
- },
- 60 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Callable\CallableParameterListNode($children);
- },
- 66 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $name = \array_shift($children);
-
- if ($this->features->callables === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset);
- }
-
- $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode
- ? \array_shift($children)
- : new Type\Callable\CallableParameterListNode();
-
- return new Type\CallableTypeNode(
- name: $name,
- parameters: $parameters,
- type: $children[0] ?? null,
- );
- },
- 67 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $result = \end($children);
-
- if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset);
- }
-
- $result->attributes = $children[0];
- }
-
- return $result;
- },
- 73 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if (\count($children) === 1) {
- return $children[0];
- }
-
- if ($children[0]->isVariadic) {
- throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset);
- }
-
- $children[0]->isOptional = true;
-
- return $children[0];
- },
- 75 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if (\count($children) === 1) {
- return $children[0];
- }
-
- if ($children[1]->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
-
- $children[1]->isVariadic = true;
-
- return $children[1];
- },
- 76 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if (!\is_array($children)) {
- return $children;
- }
-
- $result = \end($children);
-
- foreach ($children as $modifier) {
- if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) {
- switch ($modifier->getName()) {
- case 'T_AMP':
- $result->isOutput = true;
- break;
- case 'T_ELLIPSIS':
- if ($result->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
- $result->isVariadic = true;
- break;
- }
- }
- }
-
- return $result;
- },
- 80 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Callable\CallableParameterNode(null, $children[0]);
- },
- 92 => static function (Phplrt\Parser\Context $ctx, $children) {
- if (\count($children) === 1) {
- return $children[0];
- }
-
- $children[0]->name = $children[1];
-
- return $children[0];
- },
- 94 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $result = \reset($children);
-
- foreach ($children as $modifier) {
- if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) {
- switch ($modifier->getName()) {
- case 'T_AMP':
- $result->isOutput = true;
- break;
- case 'T_ELLIPSIS':
- if ($result->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
- $result->isVariadic = true;
- break;
- }
- }
- }
-
- return $result;
- },
- 96 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Callable\CallableParameterNode($children[0]);
- },
- 108 => static function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $explicit = [];
- $implicit = false;
-
- foreach ($children as $field) {
- if ($field instanceof Type\Shape\ExplicitFieldNode) {
- $key = $field->index;
-
- if (\in_array($key, $explicit, true)) {
- throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset);
- }
-
- $explicit[] = $key;
- } else {
- $implicit = true;
- }
- }
-
- if ($explicit !== [] && $implicit) {
- throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset);
- }
-
- return new Type\Shape\FieldsListNode($children);
- },
- 120 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if ($children === []) {
- return new Type\Shape\FieldsListNode();
- }
-
- if ($this->features->shapes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset);
- }
-
- $parameters = null;
-
- if (\end($children) instanceof Type\Template\TemplateArgumentListNode) {
- $parameters = \array_pop($children);
- }
-
- $fields = \reset($children) instanceof Type\Shape\FieldsListNode
- ? \array_shift($children)
- : new Type\Shape\FieldsListNode();
-
- if ($children !== []) {
- $fields->sealed = false;
- }
-
- return \array_filter([$parameters, $fields]);
- },
- 123 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $result = \end($children);
-
- if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset);
- }
-
- $result->attributes = $children[0];
- }
-
- return $result;
- },
- 127 => static function (Phplrt\Parser\Context $ctx, $children) {
- $name = $children[0];
- $value = \array_pop($children);
-
- // In case of "nullable" suffix defined
- $optional = \count($children) === 2;
-
- return match (true) {
- $name instanceof Type\Literal\IntLiteralNode
- => new Type\Shape\NumericFieldNode($name, $value, $optional),
- $name instanceof Type\Literal\StringLiteralNode
- => new Type\Shape\StringNamedFieldNode($name, $value, $optional),
- $name instanceof Type\ClassConstNode
- => new Type\Shape\ClassConstFieldNode($name, $value, $optional),
- $name instanceof Type\ClassConstMaskNode
- => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional),
- $name instanceof Type\ConstMaskNode
- => new Type\Shape\ConstMaskFieldNode($name, $value, $optional),
- default => new Type\Shape\NamedFieldNode($name, $value, $optional),
- };
- },
- 128 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Shape\ImplicitFieldNode($children[0]);
- },
- 138 => static function (Phplrt\Parser\Context $ctx, $children) {
- $fields = $parameters = null;
-
- // Shape fields
- if (\end($children) instanceof Type\Shape\FieldsListNode) {
- $fields = \array_pop($children);
- }
-
- // Template parameters
- if (\end($children) instanceof Type\Template\TemplateArgumentListNode) {
- $parameters = \array_pop($children);
- }
-
- return new Type\NamedTypeNode(
- $children[0],
- $parameters,
- $fields,
- );
- },
- 144 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $count = \count($children);
-
- if ($count === 1) {
- return $children[0];
- }
-
- if ($this->features->conditions === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset);
- }
-
- $condition = match ($children[1]->getName()) {
- 'T_EQ' => new Type\Condition\EqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_NEQ' => new Type\Condition\NotEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode(
- $children[0],
- $children[2],
- ),
- default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid(
- $children[1]->getValue(),
- $offset,
- ),
- };
-
- return new Type\TernaryExpressionNode(
- $condition,
- $children[3],
- $children[4],
- );
- },
- 155 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Attribute\AttributeGroupNode($children);
- },
- 161 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Attribute\AttributeNode(
- $children[0],
- );
- },
- 165 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Attribute\AttributeArgumentListNode($children);
- },
- 167 => static function (Phplrt\Parser\Context $ctx, $children) {
- return new Type\Attribute\AttributeArgumentNode($children[0]);
- },
- 176 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if (\count($children) === 2) {
- if ($this->features->unions === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset);
- }
-
- return new Type\UnionTypeNode($children[0], $children[1]);
- }
-
- return $children;
- },
- 177 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- if (\count($children) === 2) {
- if ($this->features->intersections === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset);
- }
-
- return new Type\IntersectionTypeNode($children[0], $children[1]);
- }
-
- return $children;
- },
- 185 => static function (Phplrt\Parser\Context $ctx, $children) {
- if (\is_array($children)) {
- return new Type\NullableTypeNode($children[1]);
- }
-
- return $children;
- },
- 186 => function (Phplrt\Parser\Context $ctx, $children) {
- // The "$offset" variable is an auto-generated
- $offset = $ctx->lastProcessedToken->getOffset();
-
- $statement = \array_shift($children);
-
- foreach ($children as $child) {
- switch (true) {
- // In case of list type
- case $child === true:
- if ($this->features->lists === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset);
- }
-
- $statement = new Type\TypesListNode($statement);
- break;
- // In case of offset access type
- case $child instanceof Type\TypeNode:
- if ($this->features->offsets === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset);
- }
-
- $statement = new Type\TypeOffsetAccessNode($statement, $child);
- break;
- default:
- throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected(
- \get_debug_type($child),
- $offset,
- );
- }
- }
-
- return $statement;
- },
- 190 => static function (Phplrt\Parser\Context $ctx, $children) {
- return $children[1] ?? true;
- },
- ],
-];
diff --git a/libs/parser/resources/grammar.pp2 b/libs/parser/resources/grammar.pp2
deleted file mode 100644
index 0c02b43..0000000
--- a/libs/parser/resources/grammar.pp2
+++ /dev/null
@@ -1,173 +0,0 @@
-
-/**
- * -----------------------------------------------------------------------------
- * Language Syntax Summary
- * -----------------------------------------------------------------------------
- */
-
-%include grammar/lexemes // Lexeme/Token Definitions
-%include grammar/common // Common Utils
-%include grammar/literals // Literal Definitions
-%include grammar/template-arguments // T
-%include grammar/callable // callable(mixed): void
-%include grammar/shape-fields // array { key: int, ... }
-%include grammar/named-type // Map, non-empty-string>
-%include grammar/ternary // T is A ? B : C
-%include grammar/attribute // #[attr]
-
-%pragma root Type
-
-/**
- * -----------------------------------------------------------------------------
- * Type Statement
- * -----------------------------------------------------------------------------
- *
- * Constant references to external definitions or
- * describe the type in some way.
- *
- */
-
-Type
- : Expression()
- ;
-
-/**
- * -----------------------------------------------------------------------------
- * Ternary Expression
- * -----------------------------------------------------------------------------
- *
- * Ternary conditional expressions, like:
- * - T is A ? B : C - for equality type conditions.
- * - T is not A ? B ? C - for non-equality type conditions.
- *
- */
-
-Expression
- : TernaryExpressionOrLogicalType()
- ;
-
-
-/**
- * -----------------------------------------------------------------------------
- * Logical Statements
- * -----------------------------------------------------------------------------
- *
- * Logical statements denote union or intersection types, like:
- * - A | B | C - for union type references.
- * - A & B & C - for intersection type references.
- *
- */
-
-LogicalType
- : UnionType()
- ;
-
-UnionType -> {
- if (\count($children) === 2) {
- if ($this->features->unions === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset);
- }
-
- return new Type\UnionTypeNode($children[0], $children[1]);
- }
-
- return $children;
-}
- : IntersectionType() (::T_OR:: UnionType())?
- ;
-
-IntersectionType -> {
- if (\count($children) === 2) {
- if ($this->features->intersections === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset);
- }
-
- return new Type\IntersectionTypeNode($children[0], $children[1]);
- }
-
- return $children;
-}
- : UnaryType() (::T_AMP:: IntersectionType())?
- ;
-
-/**
- * -----------------------------------------------------------------------------
- * Unary Statements
- * -----------------------------------------------------------------------------
- *
- * Unary statements denote terminal types, like:
- * - A - for type reference.
- * - ?A - for nullable type reference.
- * - 'some' - for string literal reference.
- * - etc...
- *
- */
-
-UnaryType
- : PrefixedNullableType()
- ;
-
-// stmt = ?Type
-PrefixedNullableType -> {
- if (\is_array($children)) {
- return new Type\NullableTypeNode($children[1]);
- }
-
- return $children;
-}
- : TypesList()
- | TypesList()
- ;
-
-TypesList -> {
- $statement = \array_shift($children);
-
- foreach ($children as $child) {
- switch (true) {
- // In case of list type
- case $child === true:
- if ($this->features->lists === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset);
- }
-
- $statement = new Type\TypesListNode($statement);
- break;
- // In case of offset access type
- case $child instanceof Type\TypeNode:
- if ($this->features->offsets === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset);
- }
-
- $statement = new Type\TypeOffsetAccessNode($statement, $child);
- break;
- default:
- throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected(
- \get_debug_type($child),
- $offset,
- );
- }
- }
-
- return $statement;
-}
- : PrimaryType()
- TypeListOrOffsetSuffix()*
- ;
-
-// - Returns `true` in case of a list type (`T[]`).
-// - Returns `NamedTypeNode` in case of an offset access type (`T[Y]`).
-TypeListOrOffsetSuffix -> {
- return $children[1] ?? true;
-}
- :
- Type()?
- ::T_SQUARE_BRACKET_CLOSE::
- ;
-
-PrimaryType
- : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE::
- | ThisLiteral()
- | Literal()
- | CallableType()
- | NamedType()
- ;
diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3
new file mode 100644
index 0000000..6b17c1e
--- /dev/null
+++ b/libs/parser/resources/grammar.pp3
@@ -0,0 +1,209 @@
+
+/**
+ * -----------------------------------------------------------------------------
+ * Language Syntax Summary
+ * -----------------------------------------------------------------------------
+ */
+
+%include grammar/lexemes
+%include grammar/common
+%include grammar/literals
+%include grammar/template-arguments // T
+%include grammar/template-parameters // callable(): void
+%include grammar/callable // callable(mixed): void
+%include grammar/shape-fields // array { key: int, ... }
+%include grammar/named-type // Map, non-empty-string>
+
+// The lexer reads bytes rather than codepoints, so a "Тип" is a name the way a "Type" is
+%pragma lexer.pcre.disable u
+
+%pragma root Type
+
+/**
+ * -----------------------------------------------------------------------------
+ * Type Statement
+ * -----------------------------------------------------------------------------
+ */
+
+// The logical operators and the condition are read by this very rule rather
+// than by a level of their own: every type the reading goes through is read
+// through each of those levels, while an operator stands in only a few of
+// them.
+Type -> {
+ if (\count($children) === 1) {
+ return $children[0];
+ }
+
+ // The suffix of a condition is the only thing the rule reads that carries
+ // a flag of its own, which is what the operands are told from
+ $condition = null;
+
+ for ($i = 1, $count = \count($children); $i < $count; ++$i) {
+ if (\is_bool($children[$i])) {
+ $condition = \array_splice($children, $i);
+
+ break;
+ }
+ }
+
+ // The operands joined by an ampersand bind tighter, so they are collected
+ // into a group of their own, and the groups are what a union is made of
+ $groups = [];
+ $group = [$children[0]];
+ $isUnion = false;
+ $isIntersection = false;
+
+ for ($i = 1, $count = \count($children); $i < $count; $i += 2) {
+ if ($children[$i]->id === self::T_OR) {
+ $isUnion = true;
+ $groups[] = $group;
+ $group = [$children[$i + 1]];
+
+ continue;
+ }
+
+ $isIntersection = true;
+ $group[] = $children[$i + 1];
+ }
+
+ $groups[] = $group;
+
+ // The operand that binds tighter is the one reported first, the way the
+ // reading would have reached it
+ if ($isIntersection && $this->features->intersections === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset);
+ }
+
+ if ($isUnion && $this->features->unions === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset);
+ }
+
+ $statements = [];
+
+ foreach ($groups as $group) {
+ $statements[] = \count($group) === 1
+ ? $group[0]
+ : new Type\IntersectionTypeNode($group, $group[0]->offset);
+ }
+
+ $statement = \count($statements) === 1
+ ? $statements[0]
+ : new Type\UnionTypeNode($statements, $offset);
+
+ if ($condition === null) {
+ return $statement;
+ }
+
+ if ($this->features->conditions === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset);
+ }
+
+ $comparison = $condition[0]
+ ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset)
+ : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset);
+
+ return new Type\TernaryExpressionNode(
+ $comparison,
+ $condition[2],
+ $condition[3],
+ $offset,
+ );
+}
+ : UnaryType() LogicalTail()* TernaryExpressionSuffix()?
+ | Variable() TernaryExpressionSuffix()
+ ;
+
+TernaryExpressionSuffix
+ : TernaryExpressionOperator() (Type() | Variable())
+ ::T_QMARK:: Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is true of")
+ ::T_COLON:: @error("Syntax error, unexpected {token}, a condition must be parted with a colon \":\"")
+ Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is false of")
+ ;
+
+// An "is" or an "is not"
+TernaryExpressionOperator -> {
+ return \count($children) === 2;
+}
+ : ?
+ ;
+
+/**
+ * -----------------------------------------------------------------------------
+ * Logical Statements
+ * -----------------------------------------------------------------------------
+ */
+
+LogicalTail
+ : UnaryType() @error("Syntax error, unexpected {token}, a union type must carry a type after the vertical bar \"|\", for example \"T|U\"")
+ | UnaryType() @error("Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand \"&\", for example \"T&U\"")
+ ;
+
+/**
+ * -----------------------------------------------------------------------------
+ * Unary Statements
+ * -----------------------------------------------------------------------------
+ */
+
+UnaryType
+ : NullableType()
+ | ListOrOffsetType()
+ ;
+
+NullableType -> {
+ return new Type\NullableTypeNode($children[0], $offset);
+}
+ : ::T_QMARK:: ListOrOffsetType() @error("Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example \"?T\"")
+ ;
+
+ListOrOffsetType -> {
+ $statement = $children[0];
+ $count = \count($children);
+
+ for ($i = 1; $i < $count; ++$i) {
+ $child = $children[$i];
+
+ switch (true) {
+ case $child === true:
+ if ($this->features->lists === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset);
+ }
+
+ $statement = new Type\TypesListNode($statement, $offset);
+ break;
+ case $child instanceof Type\TypeNode:
+ if ($this->features->offsets === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset);
+ }
+
+ $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset);
+ break;
+ default:
+ throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected(
+ \get_debug_type($child),
+ $source,
+ $offset,
+ );
+ }
+ }
+
+ return $statement;
+}
+ : PrimaryType()
+ ListOrOffsetSuffix()*
+ ;
+
+ListOrOffsetSuffix -> {
+ return $children[0] ?? true;
+}
+ : ::T_SQUARE_BRACKET_OPEN::
+ Type()?
+ ::T_SQUARE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an offset must be closed with a bracket \"]\"")
+ ;
+
+PrimaryType
+ : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a group must be closed with a bracket \")\"")
+ | ThisType()
+ | LiteralType()
+ | NamedType()
+ | LeadingMaskType()
+ ;
diff --git a/libs/parser/resources/grammar/attribute.pp2 b/libs/parser/resources/grammar/attribute.pp2
deleted file mode 100644
index 51319c6..0000000
--- a/libs/parser/resources/grammar/attribute.pp2
+++ /dev/null
@@ -1,44 +0,0 @@
-
-AttributeGroupsList -> {
- return new Type\Attribute\AttributeGroupListNode($children);
-}
- : AttributeGroup()+
- ;
-
-AttributeGroup -> {
- return new Type\Attribute\AttributeGroupNode($children);
-}
- : ::T_ATTR_OPEN::
- AttributesList() ::T_COMMA::?
- ::T_SQUARE_BRACKET_CLOSE::
- ;
-
-AttributesList
- : Attribute() (
- ::T_COMMA:: Attribute()
- )*
- ;
-
-Attribute -> {
- return new Type\Attribute\AttributeNode(
- $children[0],
- );
-}
- : Name() AttributeArguments()?
- ;
-
-AttributeArguments -> {
- return new Type\Attribute\AttributeArgumentListNode($children);
-}
- : ::T_PARENTHESIS_OPEN::
- AttributeArgument() (
- ::T_COMMA:: AttributeArgument()
- )* ::T_COMMA::?
- ::T_PARENTHESIS_CLOSE::
- ;
-
-AttributeArgument -> {
- return new Type\Attribute\AttributeArgumentNode($children[0]);
-}
- : Type()
- ;
diff --git a/libs/parser/resources/grammar/callable.pp2 b/libs/parser/resources/grammar/callable.pp2
deleted file mode 100644
index b33a316..0000000
--- a/libs/parser/resources/grammar/callable.pp2
+++ /dev/null
@@ -1,189 +0,0 @@
-
-CallableType -> {
- $name = \array_shift($children);
-
- if ($this->features->callables === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset);
- }
-
- $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode
- ? \array_shift($children)
- : new Type\Callable\CallableParameterListNode();
-
- return new Type\CallableTypeNode(
- name: $name,
- parameters: $parameters,
- type: $children[0] ?? null,
- );
-}
- : Name()
- ::T_PARENTHESIS_OPEN::
- CallableParameters()?
- ::T_PARENTHESIS_CLOSE::
- CallableReturnType()?
- ;
-
-CallableParameters -> {
- return new Type\Callable\CallableParameterListNode($children);
-}
- : CallableParameter()
- ( ::T_COMMA:: CallableParameter() )*
- ::T_COMMA::?
- ;
-
-CallableParameter -> {
- $result = \end($children);
-
- if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset);
- }
-
- $result->attributes = $children[0];
- }
-
- return $result;
-}
- : AttributeGroupsList()?
- MaybeDefaultCallableParameter()
- ;
-
-// Expects expression:
-// - ""
-// - " ="
-MaybeDefaultCallableParameter -> {
- if (\count($children) === 1) {
- return $children[0];
- }
-
- if ($children[0]->isVariadic) {
- throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset);
- }
-
- $children[0]->isOptional = true;
- return $children[0];
-}
- : ( MaybePrefixedVariadicTypedNamedCallableParameter()
- | MaybeModifiersNamedCallableParameter() )
- ?
- ;
-
-// Expects expression:
-// - "&..."
-// - "...&"
-// - "..."
-// - "&"
-// - ""
-MaybeModifiersNamedCallableParameter -> {
- if (!\is_array($children)) {
- return $children;
- }
-
- $result = \end($children);
-
- foreach ($children as $modifier) {
- if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) {
- switch ($modifier->getName()) {
- case 'T_AMP':
- $result->isOutput = true;
- break;
- case 'T_ELLIPSIS':
- if ($result->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
- $result->isVariadic = true;
- break;
- }
- }
- }
-
- return $result;
-}
- : MaybeNamedCallableParameter()
- | MaybeNamedCallableParameter()
- | MaybeNamedCallableParameter()
- | MaybeNamedCallableParameter()
- | MaybeNamedCallableParameter()
- ;
-
-// Expects expression:
-// - "$"
-MaybeNamedCallableParameter -> {
- return new Type\Callable\CallableParameterNode(null, $children[0]);
-}
- : VariableLiteral()
- ;
-
-MaybePrefixedVariadicTypedNamedCallableParameter -> {
- if (\count($children) === 1) {
- return $children[0];
- }
-
- if ($children[1]->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
-
- $children[1]->isVariadic = true;
- return $children[1];
-}
- : ? MaybeTypedNamedCallableParameter()
- ;
-
-// Expects expression:
-// - " $"
-// - ""
-MaybeTypedNamedCallableParameter -> {
- if (\count($children) === 1) {
- return $children[0];
- }
-
- $children[0]->name = $children[1];
- return $children[0];
-}
- : MaybeModifiersTypedCallableParameter()
- VariableLiteral()?
- ;
-
-// Expects expression:
-// - "...&"
-// - "&..."
-// - "&"
-// - "..."
-// - ""
-MaybeModifiersTypedCallableParameter -> {
- $result = \reset($children);
-
- foreach ($children as $modifier) {
- if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) {
- switch ($modifier->getName()) {
- case 'T_AMP':
- $result->isOutput = true;
- break;
- case 'T_ELLIPSIS':
- if ($result->isVariadic) {
- throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset);
- }
- $result->isVariadic = true;
- break;
- }
- }
- }
-
- return $result;
-}
- : TypedCallableParameter()
- ( ?
- | ? )?
- ;
-
-// Expects expression:
-// - ""
-TypedCallableParameter -> {
- return new Type\Callable\CallableParameterNode($children[0]);
-}
- : Type()
- ;
-
-CallableReturnType
- : ::T_COLON:: Type()
- ;
diff --git a/libs/parser/resources/grammar/callable.pp3 b/libs/parser/resources/grammar/callable.pp3
new file mode 100644
index 0000000..7326ab5
--- /dev/null
+++ b/libs/parser/resources/grammar/callable.pp3
@@ -0,0 +1,67 @@
+
+// The "(T, U): V" of a callable type
+CallableSuffix
+ : ::T_PARENTHESIS_OPEN::
+ CallableParameters()
+ ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \")\"")
+ CallableReturnType()?
+ ;
+
+CallableParameters -> {
+ return new Type\Callable\CallableParameterListNode($children, $offset);
+}
+ : ( CallableParameter()
+ ( ::T_COMMA:: CallableParameter() )*
+ ::T_COMMA::?
+ )?
+ ;
+
+CallableParameter -> {
+ $parameter = new Type\Callable\CallableParameterNode(
+ $children[0],
+ null,
+ false,
+ false,
+ false,
+ $offset,
+ );
+
+ for ($i = 1, $count = \count($children); $i < $count; ++$i) {
+ $child = $children[$i];
+
+ if ($child instanceof Type\VariableNode) {
+ $parameter->name = $child;
+
+ continue;
+ }
+
+ switch ($child->id) {
+ case self::T_AMP:
+ $parameter->isOutput = true;
+ break;
+
+ case self::T_ELLIPSIS:
+ $parameter->isVariadic = true;
+ break;
+
+ default:
+ if ($parameter->isVariadic) {
+ throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset);
+ }
+
+ $parameter->isOptional = true;
+ }
+ }
+
+ return $parameter;
+}
+ : Type()
+ ?
+ ?
+ Variable()?
+ ?
+ ;
+
+CallableReturnType
+ : ::T_COLON:: Type() @error("Syntax error, unexpected {token}, a callable must carry a type after the colon \":\"")
+ ;
diff --git a/libs/parser/resources/grammar/common.pp2 b/libs/parser/resources/grammar/common.pp2
deleted file mode 100644
index 8662158..0000000
--- a/libs/parser/resources/grammar/common.pp2
+++ /dev/null
@@ -1,33 +0,0 @@
-
-Name
- : FullQualifiedName()
- | RelativeName()
- ;
-
-FullQualifiedName -> {
- return new Type\Name($children, true);
-}
- : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())*
- ;
-
-RelativeName -> {
- return new Type\Name($children, false);
-}
- : Identifier() (::T_NS_DELIMITER:: Identifier())*
- ;
-
-Identifier -> {
- return Type\Identifier::createFromString($children->getValue());
-}
- :
- |
- |
- |
- |
- ;
-
-IdentifierWithExtraSpace -> {
- return Type\Identifier::createFromString($children->getValue());
-}
- :
- ;
diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3
new file mode 100644
index 0000000..657138a
--- /dev/null
+++ b/libs/parser/resources/grammar/common.pp3
@@ -0,0 +1,68 @@
+
+// "Some\Any" or "\Some\Any"
+Name -> {
+ // "Some\Any" starts at a segment of its own
+ if ($children[0] instanceof Type\Identifier) {
+ return new Type\Name($children, false, $offset);
+ }
+
+ // "\Some\Any" carries the separator in front of its segments, and the
+ // name itself is the part that follows it
+ $segments = \array_slice($children, 1);
+
+ return new Type\Name($segments, true, $segments[0]->offset);
+}
+ : ? Identifier() NameTail()*
+ ;
+
+NameTail
+ : ::T_NS_DELIMITER:: Identifier() @error("Syntax error, unexpected {token}, a name must carry a segment after the separator, for example \"Some\Any\"")
+ ;
+
+Identifier -> {
+ return new Type\Identifier(
+ $children->id === self::T_NAME_WITH_SPACE
+ ? \rtrim($children->value)
+ : $children->value,
+ $offset,
+ );
+}
+ :
+ |
+ |
+ |
+ |
+ |
+ |
+ ;
+
+IdentifierWithExtraSpace -> {
+ return new Type\Identifier(\rtrim($children->value), $offset);
+}
+ :
+ ;
+
+// Any "$variable" (including "$this")
+Variable -> {
+ return new Type\VariableNode(
+ new Type\Identifier(\substr($children->value, 1), $offset),
+ $offset,
+ );
+}
+ :
+ |
+ ;
+
+// The "$this" of a "callable(): $this"
+ThisType -> {
+ return new Type\ThisNode($offset);
+}
+ :
+ ;
+
+// "*" in "T<*>" or "JSON_*"
+WildcardType -> {
+ return new Type\WildcardNode($offset);
+}
+ :
+ ;
diff --git a/libs/parser/resources/grammar/lexemes.pp2 b/libs/parser/resources/grammar/lexemes.pp2
deleted file mode 100644
index 4edb9e5..0000000
--- a/libs/parser/resources/grammar/lexemes.pp2
+++ /dev/null
@@ -1,53 +0,0 @@
-
-// Literals
-
-%token T_DQ_STRING_LITERAL "([^"\\]*(?:\\.[^"\\]*)*)"
-%token T_SQ_STRING_LITERAL '([^'\\]*(?:\\.[^'\\]*)*)'
-%token T_PFX_FLOAT_LITERAL \-?[0-9]++\.[0-9]*+(?:[eE]-?[0-9]++)?
-%token T_SFX_FLOAT_LITERAL \-?[0-9]*+\.[0-9]++(?:[eE]-?[0-9]++)?
-%token T_EXP_LITERAL \-?[0-9]++[eE]-?[0-9]++
-%token T_BIN_INT_LITERAL \-?0[bB][01_]++
-%token T_OCT_INT_LITERAL \-?0[oO][0-7_]++
-%token T_HEX_INT_LITERAL \-?0[xX][0-9a-fA-F_]++
-%token T_DEC_INT_LITERAL \-?[0-9][0-9_]*+
-%token T_BOOL_LITERAL (?i)(?:true|false)(?![a-zA-Z0-9\-_\x80-\xff])
-%token T_NULL_LITERAL (?i)null(?![a-zA-Z0-9\-_\x80-\xff])
-
-// Identifier
-
-%token T_NEQ (?i)is\h++not(?![a-zA-Z0-9\-_\x80-\xff])
-%token T_EQ (?i)is(?![a-zA-Z0-9\-_\x80-\xff])
-%token T_THIS \$this\b
-%token T_VARIABLE \$[a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+
-%token T_NAME_WITH_SPACE [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+\s++
-%token T_NAME [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+
-
-// Special Chars
-
-%token T_LTE <=
-%token T_GTE >=
-%token T_ANGLE_BRACKET_OPEN <
-%token T_ANGLE_BRACKET_CLOSE >
-%token T_PARENTHESIS_OPEN \(
-%token T_PARENTHESIS_CLOSE \)
-%token T_BRACE_OPEN \{
-%token T_BRACE_CLOSE \}
-%token T_ATTR_OPEN #\[
-%token T_SQUARE_BRACKET_OPEN \[
-%token T_SQUARE_BRACKET_CLOSE \]
-%token T_COMMA ,
-%token T_ELLIPSIS \.\.\.
-%token T_DOUBLE_COLON ::
-%token T_COLON :
-%token T_ASSIGN =
-%token T_NS_DELIMITER \\
-%token T_QMARK \?
-%token T_OR \|
-%token T_AMP &
-%token T_ASTERISK \*
-
-// Other
-
-%skip T_COMMENT (?://|#)[^\r\n]*+
-%skip T_DOC_COMMENT /\*.*?\*/
-%skip T_WHITESPACE \s++
diff --git a/libs/parser/resources/grammar/lexemes.pp3 b/libs/parser/resources/grammar/lexemes.pp3
new file mode 100644
index 0000000..d8351c8
--- /dev/null
+++ b/libs/parser/resources/grammar/lexemes.pp3
@@ -0,0 +1,126 @@
+
+/**
+ * -----------------------------------------------------------------------------
+ * Pattern Fragments
+ * -----------------------------------------------------------------------------
+ */
+
+//
+// Identifiers
+//
+
+%fragment IDENT_HEAD [a-zA-Z_\x80-\xff]
+%fragment IDENT_TAIL [a-zA-Z0-9\-_\x80-\xff]
+
+// The "foo_1" of a "$foo_1", which carries no dash
+%fragment VAR_TAIL [a-zA-Z0-9_\x80-\xff]
+
+%fragment KEYWORD_END (?!(?&IDENT_TAIL))
+
+//
+// Numbers
+//
+
+// The "-" of a "-42"
+%fragment SIGN [-+]
+
+// The "F" of a "0xFF"
+%fragment DIGIT_HEAD [0-9]
+%fragment BIN_HEAD [01]
+%fragment OCT_HEAD [0-7]
+%fragment HEX_HEAD [0-9a-fA-F]
+
+%fragment DEC_HEAD [1-9]
+
+%fragment DIGIT_TAIL _?(?&DIGIT_HEAD)
+%fragment DEC_TAIL _?(?&DIGIT_HEAD)
+%fragment BIN_TAIL _?(?&BIN_HEAD)
+%fragment OCT_TAIL _?(?&OCT_HEAD)
+%fragment HEX_TAIL _?(?&HEX_HEAD)
+
+// The "FE_DE" of a "0xFE_DE"
+%fragment DIGITS (?&DIGIT_HEAD)(?&DIGIT_TAIL)*+
+%fragment DEC_DIGITS (?&DEC_HEAD)(?&DEC_TAIL)*+
+%fragment BIN_DIGITS (?&BIN_HEAD)(?&BIN_TAIL)*+
+%fragment OCT_DIGITS (?&OCT_HEAD)(?&OCT_TAIL)*+
+%fragment HEX_DIGITS (?&HEX_HEAD)(?&HEX_TAIL)*+
+
+// A "42.0", a "42." or a ".42"
+%fragment FRACTION (?&DIGITS)\.(?:(?&DIGITS))?|\.(?&DIGITS)
+
+// An "e2", an "E-2" or an "e+2"
+%fragment EXPONENT [eE](?&SIGN)?(?&DIGITS)
+
+/**
+ * -----------------------------------------------------------------------------
+ * Lexeme/Token Definitions
+ * -----------------------------------------------------------------------------
+ */
+
+%skip T_WHITESPACE \s++
+
+// An "is"
+%token T_EQ is(?&KEYWORD_END)
+// A "not"
+%token T_NOT not(?&KEYWORD_END)
+// A "true" or a "TRUE"
+%token T_TRUE_LITERAL (?i:true)(?&KEYWORD_END)
+// A "false" or a "FALSE"
+%token T_FALSE_LITERAL (?i:false)(?&KEYWORD_END)
+// A "null" or a "NULL"
+%token T_NULL_LITERAL (?i:null)(?&KEYWORD_END)
+
+// Identifiers
+
+%token T_NAME_WITH_SPACE (?&IDENT_HEAD)(?&IDENT_TAIL)*+\s++
+// An "int", a "non-empty-string" or a "_42"
+%token T_NAME (?&IDENT_HEAD)(?&IDENT_TAIL)*+
+
+%token T_NS_DELIMITER \\
+%token T_OR \|
+%token T_COMMA ,
+%token T_DOUBLE_COLON ::
+%token T_COLON :
+%token T_ANGLE_BRACKET_OPEN <
+%token T_ANGLE_BRACKET_CLOSE >
+%token T_QMARK \?
+%token T_BRACE_OPEN \{
+%token T_BRACE_CLOSE \}
+%token T_PARENTHESIS_OPEN \(
+%token T_PARENTHESIS_CLOSE \)
+%token T_SQUARE_BRACKET_OPEN \[
+%token T_SQUARE_BRACKET_CLOSE \]
+%token T_AMP &
+%token T_ELLIPSIS \.\.\.
+%token T_ASSIGN =
+%token T_ASTERISK \*
+
+// Variables
+
+// A "$this"
+%token T_THIS \$this(?!(?&VAR_TAIL))
+// A "$value" or a "$_"
+%token T_VARIABLE \$(?&IDENT_HEAD)(?&VAR_TAIL)*+
+
+// Literals
+
+// A double quoted "text"
+%token T_DQ_STRING_LITERAL "[^"\\]*+(?:\\.[^"\\]*+)*+"
+%token T_SQ_STRING_LITERAL '[^'\\]*+(?:\\.[^'\\]*+)*+'
+// A "42.0", a "42.", a ".42", a "42e2", a "-1.5e+3" or a "2_3.4_5e-6_7"
+%token T_FLOAT_LITERAL (?&SIGN)?(?:(?&FRACTION)(?:(?&EXPONENT))?|(?&DIGITS)(?&EXPONENT))
+// A "0b1010", a "0B0001_1000" or a "-0b10"
+%token T_BIN_INT_LITERAL (?&SIGN)?0[bB](?&BIN_DIGITS)
+// A "0o42", a "0O42", a "042" or an "04_23"
+%token T_OCT_INT_LITERAL (?&SIGN)?0(?:[oO](?&OCT_DIGITS)|(?&OCT_TAIL)++)
+// A "0xDEAD", a "0XDeaD" or a "0xFE_DE"
+%token T_HEX_INT_LITERAL (?&SIGN)?0[xX](?&HEX_DIGITS)
+// A "42", a "+42_04" or a "0"
+%token T_DEC_INT_LITERAL (?&SIGN)?(?:(?&DEC_DIGITS)|0)
+
+// Comments
+
+// A "// text" or a "# text"
+%skip T_COMMENT (?://|#)[^\r\n]*+
+// A "/* text */"
+%skip T_DOC_COMMENT /\*.*?\*/
diff --git a/libs/parser/resources/grammar/literals.pp2 b/libs/parser/resources/grammar/literals.pp2
deleted file mode 100644
index 2a0e750..0000000
--- a/libs/parser/resources/grammar/literals.pp2
+++ /dev/null
@@ -1,111 +0,0 @@
-
-Literal
- : RawLiteral()
- | ConstMaskLiteral()
- | ClassConstLiteral()
- ;
-
-RawLiteral -> {
- if ($this->features->literals === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset);
- }
- return $children;
-}
- : StringLiteral()
- | FloatLiteral()
- | IntLiteral()
- | BoolLiteral()
- | NullLiteral()
- ;
-
-VariableLiteral -> {
- return Type\Literal\VariableLiteralNode::parse($token->getValue());
-}
- :
- |
- ;
-
-ThisLiteral -> {
- return Type\Literal\VariableLiteralNode::parse($token->getValue());
-}
- :
- ;
-
-StringLiteral -> { return $this->stringPool[$token] ??= $children; }
- : DoubleQuotedStringLiteral()
- | SingleQuotedStringLiteral()
- ;
-
-DoubleQuotedStringLiteral -> {
- return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue());
-}
- :
- ;
-
-SingleQuotedStringLiteral -> {
- return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue());
-}
- :
- ;
-
-FloatLiteral -> {
- return Type\Literal\FloatLiteralNode::parse($token->getValue());
-}
- :
- |
- |
- ;
-
-IntLiteral -> {
- return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue());
-}
- :
- |
- |
- |
- ;
-
-BoolLiteral -> {
- return Type\Literal\BoolLiteralNode::parse($token->getValue());
-}
- :
- ;
-
-NullLiteral -> {
- return new Type\Literal\NullLiteralNode($children->getValue());
-}
- :
- ;
-
-ConstMaskLiteral -> {
- return new Type\ConstMaskNode($children[0]);
-}
- : Name() ::T_ASTERISK::
- ;
-
-ClassConstLiteral -> {
- // :: "*"
- if (\count($children) === 3) {
- return new Type\ClassConstMaskNode(
- $children[0],
- $children[1],
- );
- }
-
- // ::
- if ($children[1] instanceof Type\Identifier) {
- return new Type\ClassConstNode(
- $children[0],
- $children[1],
- );
- }
-
- // :: "*"
- return new Type\ClassConstMaskNode($children[0]);
-}
- : Name() ::T_DOUBLE_COLON::
- ( Identifier()
- | Identifier()
- |
- )
- ;
diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3
new file mode 100644
index 0000000..dea24e6
--- /dev/null
+++ b/libs/parser/resources/grammar/literals.pp3
@@ -0,0 +1,90 @@
+
+LiteralType -> {
+ if ($this->features->literals === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset);
+ }
+ return $children;
+}
+ : ScalarType()
+ | NullLiteralType()
+ ;
+
+ScalarType
+ : StringLiteralType()
+ | FloatLiteralType()
+ | IntLiteralType()
+ | TrueLiteralType()
+ | FalseLiteralType()
+ ;
+
+StringLiteralType
+ : DoubleQuotedStringLiteralType()
+ | SingleQuotedStringLiteralType()
+ ;
+
+DoubleQuotedStringLiteralType -> {
+ return new Type\Literal\StringLiteralNode(
+ StringDecoder::unpackAndDecode($children->value, true),
+ $children->value,
+ $offset,
+ );
+}
+ :
+ ;
+
+SingleQuotedStringLiteralType -> {
+ return new Type\Literal\StringLiteralNode(
+ StringDecoder::unpackAndDecode($children->value, false),
+ $children->value,
+ $offset,
+ );
+}
+ :
+ ;
+
+FloatLiteralType -> {
+ return new Type\Literal\FloatLiteralNode(
+ FloatDecoder::decode($children->value),
+ $children->value,
+ $offset,
+ );
+}
+ :
+ ;
+
+IntLiteralType -> {
+ $decoded = IntDecoder::decode($children->value);
+
+ return new Type\Literal\IntLiteralNode(
+ $decoded->value,
+ $children->value,
+ $decoded->decimal,
+ $offset,
+ );
+}
+ :
+ |
+ |
+ |
+ ;
+
+TrueLiteralType -> {
+ return new Type\Literal\BoolLiteralNode(true, $children->value, $offset);
+}
+ :
+ ;
+
+FalseLiteralType -> {
+ return new Type\Literal\BoolLiteralNode(false, $children->value, $offset);
+}
+ :
+ ;
+
+NullLiteralType -> {
+ return new Type\Literal\NullLiteralNode(
+ $children->value,
+ $offset,
+ );
+}
+ :
+ ;
diff --git a/libs/parser/resources/grammar/named-type.pp2 b/libs/parser/resources/grammar/named-type.pp2
deleted file mode 100644
index 300654d..0000000
--- a/libs/parser/resources/grammar/named-type.pp2
+++ /dev/null
@@ -1,22 +0,0 @@
-
-NamedType -> {
- $fields = $parameters = null;
-
- // Shape fields
- if (\end($children) instanceof Type\Shape\FieldsListNode) {
- $fields = \array_pop($children);
- }
-
- // Template parameters
- if (\end($children) instanceof Type\Template\TemplateArgumentListNode) {
- $parameters = \array_pop($children);
- }
-
- return new Type\NamedTypeNode(
- $children[0],
- $parameters,
- $fields,
- );
-}
- : Name() (TemplateArguments() | ShapeFields())?
- ;
diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3
new file mode 100644
index 0000000..a76e73e
--- /dev/null
+++ b/libs/parser/resources/grammar/named-type.pp3
@@ -0,0 +1,118 @@
+
+/**
+ * -----------------------------------------------------------------------------
+ * Named Types
+ * -----------------------------------------------------------------------------
+ */
+
+// A name is read by this very rule rather than by one of its own: a type is
+// named far more often than anything else is read, so every rule the reading
+// goes through on its way here is paid for by the whole grammar.
+NamedType -> {
+ $head = $children[0];
+ $suffix = $children[1] ?? null;
+
+ // "Some\Any\*" and "Some\Any\SOME_*"
+ //
+ // A mask closes the type it is written at the end of, so it is read as a
+ // suffix of a name and never carries a suffix of its own.
+ if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) {
+ $parts = $head->parts;
+
+ if ($suffix instanceof Type\MaskNode) {
+ // "Some\Any\*"
+ $mask = $suffix;
+ } else {
+ // "Some\Any\SOME_*"
+ $segment = \array_pop($parts);
+ $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset);
+ }
+
+ return new Type\ConstMaskNode(
+ $mask,
+ $parts === []
+ ? $head->isFullyQualified
+ : new Type\Name($parts, $head->isFullyQualified, $head->offset),
+ $offset,
+ );
+ }
+
+ switch (true) {
+ // Some\Any
+ case $suffix === null:
+ return new Type\NamedTypeNode($children[0], null, null, $offset);
+
+ // Some\Any
+ case $suffix instanceof Type\Template\TemplateArgumentListNode:
+ return new Type\NamedTypeNode($children[0], $suffix, null, $offset);
+
+ // Some\Any{name: T, ...}
+ case $suffix instanceof Type\Shape\FieldsListNode:
+ return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset);
+
+ // Some\Any(T, U): V
+ case $suffix instanceof Type\Callable\CallableParameterListNode:
+ if ($this->features->callables === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset);
+ }
+
+ return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset);
+
+ // Some\Any(T): U
+ case $suffix instanceof Type\Template\TemplateParameterListNode:
+ if ($this->features->callables === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset);
+ }
+
+ return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset);
+
+ // Some\Any::CONST, Some\Any::CONST_* and Some\Any::*
+ default:
+ if (\count($children) === 3 && $children[2] instanceof Type\Identifier) {
+ return new Type\ClassConstNode($children[0], $children[2], $offset);
+ }
+
+ return new Type\ClassConstMaskNode(
+ $children[0],
+ new Type\MaskNode(\array_slice($children, 2), $children[2]->offset),
+ $offset,
+ );
+ }
+}
+ : Name() NamedTypeSuffix()?
+ ;
+
+NamedTypeSuffix
+ : ClassConstSuffix()
+ | CallableSuffix()
+ | TemplateArguments() !::T_PARENTHESIS_OPEN::
+ | TemplateParameters() CallableSuffix()
+ | ShapeFields()
+ | MaskTail()
+ | NamespacedMask()
+ ;
+
+// "*_SOME" — a mask that carries no name in front of it
+LeadingMaskType -> {
+ return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset);
+}
+ : WildcardType() Identifier() MaskTail()?
+ ;
+
+MaskTail
+ : WildcardType() ( Identifier() WildcardType() )* Identifier()?
+ ;
+
+NamespacedMask -> {
+ return new Type\MaskNode($children, $offset);
+}
+ : ::T_NS_DELIMITER:: MaskTail()
+ ;
+
+// The "::CONST", "::CONST_*", "::*" and "::A*B" of a class constant
+ClassConstSuffix
+ :
+ ( Identifier() MaskTail()?
+ | MaskTail()
+ ) @error("Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example \"Some::CONST\"")
+ ;
diff --git a/libs/parser/resources/grammar/shape-fields.pp2 b/libs/parser/resources/grammar/shape-fields.pp2
deleted file mode 100644
index 0c1a2fe..0000000
--- a/libs/parser/resources/grammar/shape-fields.pp2
+++ /dev/null
@@ -1,128 +0,0 @@
-
-ShapeFields -> {
- if ($children === []) {
- return new Type\Shape\FieldsListNode();
- }
-
- if ($this->features->shapes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset);
- }
-
- $parameters = null;
-
- if (\end($children) instanceof Type\Template\TemplateArgumentListNode) {
- $parameters = \array_pop($children);
- }
-
- $fields = \reset($children) instanceof Type\Shape\FieldsListNode
- ? \array_shift($children)
- : new Type\Shape\FieldsListNode();
-
- if ($children !== []) {
- $fields->sealed = false;
- }
-
- return \array_filter([$parameters, $fields]);
-}
- : ::T_BRACE_OPEN::
- (
- (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?)
- | ShapeFieldsUnsealed()?
- )
- ::T_COMMA::?
- ::T_BRACE_CLOSE::
- ;
-
-ShapeFieldsUnsealed
- : TemplateArguments()?
- ;
-
-ShapeFieldsList -> {
- $explicit = [];
- $implicit = false;
-
- foreach ($children as $field) {
- if ($field instanceof Type\Shape\ExplicitFieldNode) {
- $key = $field->index;
-
- if (\in_array($key, $explicit, true)) {
- throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset);
- }
-
- $explicit[] = $key;
- } else {
- $implicit = true;
- }
- }
-
- if ($explicit !== [] && $implicit) {
- throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset);
- }
-
- return new Type\Shape\FieldsListNode($children);
-}
- : ShapeField() (
- ::T_COMMA:: ShapeField()
- )*
- ;
-
-ShapeField -> {
- $result = \end($children);
-
- if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset);
- }
-
- $result->attributes = $children[0];
- }
-
- return $result;
-}
- : AttributeGroupsList()? (
- ExplicitField()
- | ImplicitField()
- )
- ;
-
-ExplicitField -> {
- $name = $children[0];
- $value = \array_pop($children);
-
- // In case of "nullable" suffix defined
- $optional = \count($children) === 2;
-
- return match (true) {
- $name instanceof Type\Literal\IntLiteralNode
- => new Type\Shape\NumericFieldNode($name, $value, $optional),
- $name instanceof Type\Literal\StringLiteralNode
- => new Type\Shape\StringNamedFieldNode($name, $value, $optional),
- $name instanceof Type\ClassConstNode
- => new Type\Shape\ClassConstFieldNode($name, $value, $optional),
- $name instanceof Type\ClassConstMaskNode
- => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional),
- $name instanceof Type\ConstMaskNode
- => new Type\Shape\ConstMaskFieldNode($name, $value, $optional),
- default => new Type\Shape\NamedFieldNode($name, $value, $optional),
- };
-}
- : ShapeKey() ()? ::T_COLON:: ShapeValue()
- ;
-
-ImplicitField -> {
- return new Type\Shape\ImplicitFieldNode($children[0]);
-}
- : ShapeValue()
- ;
-
-ShapeKey
- : ConstMaskLiteral()
- | ClassConstLiteral()
- | Identifier()
- | IntLiteral()
- | StringLiteral()
- ;
-
-ShapeValue
- : Type()
- ;
diff --git a/libs/parser/resources/grammar/shape-fields.pp3 b/libs/parser/resources/grammar/shape-fields.pp3
new file mode 100644
index 0000000..bdd0479
--- /dev/null
+++ b/libs/parser/resources/grammar/shape-fields.pp3
@@ -0,0 +1,138 @@
+
+ShapeFields -> {
+ $count = \count($children);
+
+ if ($count === 0) {
+ return [new Type\Shape\FieldsListNode([], true, $offset)];
+ }
+
+ if ($this->features->shapes === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset);
+ }
+
+ $parameters = null;
+
+ // The "..." suffix
+ if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) {
+ $parameters = $children[$count - 1];
+ --$count;
+ }
+
+ if ($children[0] instanceof Type\Shape\FieldsListNode) {
+ $fields = $children[0];
+ $unsealed = $count > 1;
+ } else {
+ $fields = new Type\Shape\FieldsListNode([], true, $offset);
+ $unsealed = $count > 0;
+ }
+
+ if ($unsealed) {
+ $fields->isSealed = false;
+ }
+
+ if ($parameters === null) {
+ return [$fields];
+ }
+
+ return [$fields, $parameters];
+}
+ : ::T_BRACE_OPEN::
+ (
+ (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?)
+ | ShapeFieldsUnsealed()?
+ )
+ ::T_COMMA::?
+ ::T_BRACE_CLOSE:: @error("Syntax error, unexpected {token}, a shape must be closed with a brace \"}\"")
+ ;
+
+ShapeFieldsUnsealed
+ : TemplateArguments()?
+ ;
+
+ShapeFieldsList -> {
+ $explicit = false;
+ $implicit = false;
+ $indexed = [];
+
+ foreach ($children as $field) {
+ if (!$field instanceof Type\Shape\ExplicitFieldNode) {
+ $implicit = true;
+
+ continue;
+ }
+
+ $explicit = true;
+
+ if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) {
+ continue;
+ }
+
+ $key = $field->getIndex();
+
+ if (isset($indexed[$key])) {
+ throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset);
+ }
+
+ $indexed[$key] = true;
+ }
+
+ if ($explicit && $implicit) {
+ throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset);
+ }
+
+ return new Type\Shape\FieldsListNode($children, true, $offset);
+}
+ : ShapeField() (
+ ::T_COMMA:: ShapeField()
+ )*
+ ;
+
+ShapeField
+ : ExplicitField()
+ | ImplicitField()
+ ;
+
+ExplicitField -> {
+ $count = \count($children);
+ $name = $children[0];
+ $value = $children[$count - 1];
+
+ $optional = $count === 3;
+
+ return match (true) {
+ // "a", "true" and "null"
+ $name instanceof Type\Identifier
+ => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset),
+ $name instanceof Type\Literal\IntLiteralNode,
+ $name instanceof Type\Literal\StringLiteralNode
+ => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset),
+ // "Some::CONST", "Some::CONST_*" and "JSON_*"
+ $name instanceof Type\ClassConstNode,
+ $name instanceof Type\ClassConstMaskNode,
+ $name instanceof Type\ConstMaskNode
+ => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset),
+ default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset),
+ };
+}
+ : ShapeKey() ()? ::T_COLON:: ShapeValue() @error("Syntax error, unexpected {token}, a shape field must carry a type after the colon \":\"")
+ ;
+
+ImplicitField -> {
+ return new Type\Shape\ImplicitFieldNode($children[0], false, $offset);
+}
+ : ShapeValue()
+ ;
+
+ShapeKey
+ : Identifier() &ShapeKeyEnd()
+ | PrimaryType()
+ ;
+
+// The "?:" and ":" a shape key is followed by
+ShapeKeyEnd
+ : ::T_QMARK::? ::T_COLON::
+ ;
+
+ShapeValue
+ : Type()
+ ;
diff --git a/libs/parser/resources/grammar/template-arguments.pp2 b/libs/parser/resources/grammar/template-arguments.pp2
deleted file mode 100644
index e0cecce..0000000
--- a/libs/parser/resources/grammar/template-arguments.pp2
+++ /dev/null
@@ -1,55 +0,0 @@
-
-TemplateArguments -> {
- if ($this->features->generics === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset);
- }
-
- return new Type\Template\TemplateArgumentListNode($children);
-}
- : ::T_ANGLE_BRACKET_OPEN::
- TemplateArgument() (
- ::T_COMMA:: TemplateArgument()
- )* ::T_COMMA::?
- ::T_ANGLE_BRACKET_CLOSE::
- ;
-
-TemplateArgument -> {
- $hint = $attributes = null;
-
- if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) {
- if ($this->features->attributes === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset);
- }
-
- $attributes = \array_shift($children);
- }
-
- $type = \array_pop($children);
-
- if (\reset($children) !== false) {
- if ($this->features->hints === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset);
- }
-
- $hint = \reset($children);
- }
-
- return new Type\Template\TemplateArgumentNode(
- $type,
- $hint,
- $attributes,
- );
-}
- : AttributeGroupsList()? (
- TemplateHintedArgument()
- | TemplateSimpleArgument()
- )
- ;
-
-TemplateSimpleArgument
- : Type()
- ;
-
-TemplateHintedArgument
- : IdentifierWithExtraSpace() Type()
- ;
diff --git a/libs/parser/resources/grammar/template-arguments.pp3 b/libs/parser/resources/grammar/template-arguments.pp3
new file mode 100644
index 0000000..c8b8e3d
--- /dev/null
+++ b/libs/parser/resources/grammar/template-arguments.pp3
@@ -0,0 +1,43 @@
+/**
+ * -----------------------------------------------------------------------------
+ * Template Arguments
+ * -----------------------------------------------------------------------------
+ */
+
+TemplateArguments -> {
+ if ($this->features->generics === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset);
+ }
+
+ return new Type\Template\TemplateArgumentListNode($children, $offset);
+}
+ : ::T_ANGLE_BRACKET_OPEN::
+ TemplateArgument() @error("Syntax error, unexpected {token}, an argument list must carry at least one argument") (
+ ::T_COMMA:: TemplateArgument()
+ )* ::T_COMMA::?
+ ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an argument list must be closed with a bracket \">\"")
+ ;
+
+TemplateArgument -> {
+ if (\is_array($children)) {
+ if ($this->features->hints === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset);
+ }
+
+ return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset);
+ }
+
+ return new Type\Template\TemplateArgumentNode($children, null, $offset);
+}
+ : TemplateHintedArgument()
+ | TemplateSimpleArgument()
+ ;
+
+TemplateSimpleArgument
+ : WildcardType()
+ | Type()
+ ;
+
+TemplateHintedArgument
+ : IdentifierWithExtraSpace() TemplateSimpleArgument()
+ ;
diff --git a/libs/parser/resources/grammar/template-parameters.pp3 b/libs/parser/resources/grammar/template-parameters.pp3
new file mode 100644
index 0000000..7d316cf
--- /dev/null
+++ b/libs/parser/resources/grammar/template-parameters.pp3
@@ -0,0 +1,79 @@
+/**
+ * -----------------------------------------------------------------------------
+ * Template Parameters
+ * -----------------------------------------------------------------------------
+ */
+
+TemplateParameters -> {
+ if ($this->features->generics === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset);
+ }
+
+ return new Type\Template\TemplateParameterListNode($children, $offset);
+}
+ : ::T_ANGLE_BRACKET_OPEN::
+ TemplateParameter() @error("Syntax error, unexpected {token}, a parameter list must carry at least one parameter") (
+ ::T_COMMA:: TemplateParameter()
+ )* ::T_COMMA::?
+ ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \">\"")
+ ;
+
+TemplateParameter -> {
+ $upper = $lower = $default = null;
+
+ foreach (\array_slice($children, 1) as $limit) {
+ if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) {
+ if ($default !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset);
+ }
+
+ $default = $limit;
+
+ continue;
+ }
+
+ if ($default !== null) {
+ throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset);
+ }
+
+ switch ($limit->operator->value) {
+ case 'of':
+ case 'as':
+ if ($upper !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset);
+ }
+
+ $upper = $limit;
+ break;
+
+ case 'super':
+ if ($lower !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset);
+ }
+
+ $lower = $limit;
+ break;
+
+ default:
+ throw Exception\TemplateBoundException::becauseOperatorIsUnknown(
+ $limit->operator->value,
+ $source,
+ $limit->operator->offset,
+ );
+ }
+ }
+
+ return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset);
+}
+ : Identifier() ( TemplateBoundEdge() | TemplateDefault() )*
+ ;
+
+TemplateBoundEdge -> {
+ return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset);
+}
+ : Identifier() Type()
+ ;
+
+TemplateDefault
+ : ::T_ASSIGN:: Type()
+ ;
diff --git a/libs/parser/resources/grammar/ternary.pp2 b/libs/parser/resources/grammar/ternary.pp2
deleted file mode 100644
index cd86fdc..0000000
--- a/libs/parser/resources/grammar/ternary.pp2
+++ /dev/null
@@ -1,71 +0,0 @@
-
-TernaryExpressionOrLogicalType -> {
- $count = \count($children);
-
- if ($count === 1) {
- return $children[0];
- }
-
- if ($this->features->conditions === false) {
- throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset);
- }
-
- $condition = match ($children[1]->getName()) {
- 'T_EQ' => new Type\Condition\EqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_NEQ' => new Type\Condition\NotEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode(
- $children[0],
- $children[2],
- ),
- 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode(
- $children[0],
- $children[2],
- ),
- default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid(
- $children[1]->getValue(),
- $offset,
- ),
- };
-
- return new Type\TernaryExpressionNode(
- $condition,
- $children[3],
- $children[4],
- );
-}
- : LogicalType() OptionalTernaryExpressionSuffix()
- | VariableLiteral() TernaryExpressionSuffix()
- ;
-
-OptionalTernaryExpressionSuffix
- : TernaryExpressionSuffix()?
- ;
-
-TernaryExpressionSuffix
- : TernaryExpressionOperator() (Type() | VariableLiteral())
- ::T_QMARK:: Type()
- ::T_COLON:: Type()
- ;
-
-TernaryExpressionOperator
- :
- |
- |
- |
- |
- |
- ;
diff --git a/libs/parser/src/Exception/ConstMaskException.php b/libs/parser/src/Exception/ConstMaskException.php
new file mode 100644
index 0000000..91e81db
--- /dev/null
+++ b/libs/parser/src/Exception/ConstMaskException.php
@@ -0,0 +1,29 @@
+ $offset
+ */
+ public static function becauseNothingFollowsAMask(
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ 'Constant mask is a type entire and cannot be followed by anything',
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
+ }
+}
diff --git a/libs/parser/src/Exception/FeatureNotAllowedException.php b/libs/parser/src/Exception/FeatureNotAllowedException.php
index e39e51b..61d63e9 100644
--- a/libs/parser/src/Exception/FeatureNotAllowedException.php
+++ b/libs/parser/src/Exception/FeatureNotAllowedException.php
@@ -4,6 +4,8 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class FeatureNotAllowedException extends SemanticException
{
/**
@@ -13,10 +15,18 @@ final class FeatureNotAllowedException extends SemanticException
* @param non-empty-string $name
* @param int<0, max> $offset
*/
- public static function becauseFeatureIsNotAllowed(string $name, int $offset = 0): self
- {
- $message = \sprintf('%s not allowed', \ucfirst($name));
-
- return new self($offset, $message);
+ public static function becauseFeatureIsNotAllowed(
+ string $name,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf('%s not allowed', \ucfirst($name)),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/Exception/Formatter.php b/libs/parser/src/Exception/Formatter.php
deleted file mode 100644
index 0a8539d..0000000
--- a/libs/parser/src/Exception/Formatter.php
+++ /dev/null
@@ -1,56 +0,0 @@
- 'end of input',
- '"' => 'double quote (")',
- default => \sprintf('"%s"', \addcslashes($expr, '"')),
- };
- }
-
- public static function source(string $statement): string
- {
- $statement = \trim($statement);
-
- if ($statement === '') {
- return '';
- }
-
- $renderer = new Renderer();
-
- return $renderer->value(new Token('', $statement, 0));
- }
-
- /**
- * @param int<0, max> $offset
- * @return non-empty-string
- */
- public static function suffix(string $statement, int $offset): string
- {
- if (\str_contains($statement, "\n")) {
- $pos = Position::fromOffset($statement, $offset);
-
- return \sprintf('on line %d at column %d', $pos->getLine(), $pos->getColumn());
- }
-
- return \sprintf('at column %d', $offset + 1);
- }
-}
diff --git a/libs/parser/src/Exception/InternalParseException.php b/libs/parser/src/Exception/InternalParseException.php
index 06a388f..2b63d30 100644
--- a/libs/parser/src/Exception/InternalParseException.php
+++ b/libs/parser/src/Exception/InternalParseException.php
@@ -4,33 +4,17 @@
namespace TypeLang\Parser\Exception;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
+use Phplrt\Contracts\Source\ReadableInterface;
-final class InternalParseException extends ParseException
+final class InternalParseException extends ParserException
{
/**
* Occurs when an unexpected error is raised while parsing a statement.
*/
- public static function becauseInternalErrorOccurs(string $statement, \Throwable $e): self
+ public static function becauseInternalErrorOccurs(ReadableInterface $source, \Throwable $e): self
{
- $message = \sprintf('An internal error occurred while parsing %s', Formatter::source($statement));
+ $message = \sprintf('An internal error occurred while parsing %s', self::printSource($source));
- return new self($message, self::ERROR_CODE_INTERNAL_ERROR, $e);
- }
-
- /**
- * Occurs when the parser produces no result for a readable statement.
- */
- public static function becauseTypeStatementIsUnreadable(): self
- {
- return new self('Could not read type statement', self::ERROR_CODE_INTERNAL_ERROR);
- }
-
- /**
- * Occurs when the source content cannot be read.
- */
- public static function becauseSourceIsUnreadable(SourceExceptionInterface $e): self
- {
- return new self($e->getMessage(), self::ERROR_CODE_INTERNAL_ERROR, $e);
+ return new self($message, $source, 0, $e);
}
}
diff --git a/libs/parser/src/Exception/InternalSemanticException.php b/libs/parser/src/Exception/InternalSemanticException.php
index 42d6264..7ec8d42 100644
--- a/libs/parser/src/Exception/InternalSemanticException.php
+++ b/libs/parser/src/Exception/InternalSemanticException.php
@@ -4,18 +4,27 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class InternalSemanticException extends SemanticException
{
/**
- * Occurs when an unexpected sub-node is encountered while building a
- * square bracket type and signals a bug in the parser itself.
+ * Occurs when the grammar builds a sub-node the reducer knows nothing of.
*
* @param int<0, max> $offset
*/
- public static function becauseSubNodeIsUnexpected(string $type, int $offset = 0): self
- {
- $message = \sprintf('Internal error, unexpected square bracket sub-node %s', $type);
-
- return new self($offset, $message);
+ public static function becauseSubNodeIsUnexpected(
+ string $type,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf('Internal error, unexpected square bracket sub-node %s', $type),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/Exception/InvalidConditionalOperatorException.php b/libs/parser/src/Exception/InvalidConditionalOperatorException.php
index dcc549f..8bb1f77 100644
--- a/libs/parser/src/Exception/InvalidConditionalOperatorException.php
+++ b/libs/parser/src/Exception/InvalidConditionalOperatorException.php
@@ -4,18 +4,28 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class InvalidConditionalOperatorException extends SemanticException
{
/**
- * Occurs when a conditional expression uses an unsupported operator.
+ * Occurs when a condition is written with an operator the grammar knows
+ * nothing of.
*
- * @param non-empty-string $operator
* @param int<0, max> $offset
*/
- public static function becauseConditionalOperatorIsInvalid(string $operator, int $offset = 0): self
- {
- $message = \sprintf('Invalid conditional operator "%s"', $operator);
-
- return new self($offset, $message, self::ERROR_CODE_INVALID_OPERATOR);
+ public static function becauseConditionalOperatorIsInvalid(
+ string $operator,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf('Invalid conditional operator "%s"', $operator),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/Exception/ParseException.php b/libs/parser/src/Exception/ParseException.php
deleted file mode 100644
index 82f8b03..0000000
--- a/libs/parser/src/Exception/ParseException.php
+++ /dev/null
@@ -1,29 +0,0 @@
-getMessage();
+ * // => Unexpected token end of input, a shape must be closed
+ * // with a brace "}" in "array{"
+ *
+ * (string) $e;
+ * // => Unexpected token end of input, a shape must be closed
+ * // with a brace "}" in "array{" on line 1 at column 7
+ * ```
+ */
+abstract class ParserException extends \LogicException implements
+ ParserExceptionInterface
+{
+ private static ?PrettyTokenPrinter $printer = null;
+
+ public function __construct(
+ string $message,
+ public readonly ReadableInterface $source,
+ int $code = 0,
+ ?\Throwable $previous = null,
+ ) {
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * ```
+ * "int" (T_NAME)
+ * end of input
+ * "%" (unknown token)
+ * ```
+ */
+ protected static function printToken(TokenInterface $token): string
+ {
+ return (self::$printer ??= new PrettyTokenPrinter())
+ ->print($token);
+ }
+
+ /**
+ * ```
+ * "array{a: int}"
+ * "array{a: int, b: str…" (12+)
+ *
+ * ```
+ */
+ protected static function printSource(ReadableInterface $source): string
+ {
+ $content = \trim($source->content);
+
+ if ($content === '') {
+ return '';
+ }
+
+ return self::printToken(new Token(
+ id: 0,
+ name: null,
+ channel: Channel::Default,
+ value: $content,
+ ));
+ }
+}
diff --git a/libs/parser/src/Exception/ParserExceptionInterface.php b/libs/parser/src/Exception/ParserExceptionInterface.php
index 658d9f7..537ef35 100644
--- a/libs/parser/src/Exception/ParserExceptionInterface.php
+++ b/libs/parser/src/Exception/ParserExceptionInterface.php
@@ -4,4 +4,9 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
+/**
+ * @property-read ReadableInterface $source
+ */
interface ParserExceptionInterface extends \Throwable {}
diff --git a/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php
new file mode 100644
index 0000000..d3f4706
--- /dev/null
+++ b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php
@@ -0,0 +1,15 @@
+getMessage();
+ * // => Unexpected token end of input, a shape must be closed
+ * // with a brace "}" in "array{"
+ *
+ * (string) $e;
+ * // => Unexpected token end of input, a shape must be closed
+ * // with a brace "}" in "array{" on line 1 at column 7
+ * ```
+ */
+abstract class ParsingException extends ParserException implements ParserRuntimeExceptionInterface
+{
+ public PositionInterface $position;
+
+ public function __construct(
+ string $message,
+ ReadableInterface $source,
+ public readonly TokenInterface $token,
+ int $code = 0,
+ ?\Throwable $previous = null,
+ ) {
+ $this->position = $this->createPosition($source, $token);
+
+ if ($source instanceof FileInterface) {
+ $pathname = \realpath($source->pathname);
+
+ if ($pathname === false) {
+ $pathname = \str_replace('\\', '/', $source->pathname);
+ }
+
+ $this->file = $pathname;
+ $this->line = $this->position->line;
+ }
+
+ parent::__construct($message, $source, $code, $previous);
+ }
+
+ private function createPosition(ReadableInterface $source, TokenInterface $token): PositionInterface
+ {
+ try {
+ return (new PositionFactory())
+ ->createFromOffset($source, $token->offset);
+ } catch (\Throwable) {
+ return new Position();
+ }
+ }
+
+ /**
+ * @param int<0, max> $offset
+ */
+ protected static function createToken(ReadableInterface $source, int $offset, int $length = 0): TokenInterface
+ {
+ return new Token(
+ id: 0,
+ name: null,
+ channel: Channel::Default,
+ value: \substr($source->content, $offset, $length),
+ offset: $offset,
+ );
+ }
+
+ private function printPosition(): string
+ {
+ return \sprintf('on line %d at column %d', $this->position->line, $this->position->column);
+ }
+
+ public function __toString(): string
+ {
+ $message = $this->message;
+ $this->message = \sprintf('%s %s', $message, $this->printPosition());
+
+ try {
+ return parent::__toString();
+ } finally {
+ $this->message = $message;
+ }
+ }
+}
diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php
index 575241f..1ebf95d 100644
--- a/libs/parser/src/Exception/SemanticException.php
+++ b/libs/parser/src/Exception/SemanticException.php
@@ -4,34 +4,17 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
/**
* Error occurring while validating the semantics of a syntactically correct
* type statement.
*/
-abstract class SemanticException extends \LogicException implements ParserExceptionInterface
+abstract class SemanticException extends ParsingException
{
- final public const int ERROR_CODE_SHAPE_KEY_DUPLICATION = 0x01;
-
- final public const int ERROR_CODE_SHAPE_KEY_MIX = 0x02;
-
- final public const int ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03;
-
- final public const int ERROR_CODE_VARIADIC_ALREADY_VARIADIC = 0x04;
-
- final public const int ERROR_CODE_INVALID_OPERATOR = 0x05;
-
- protected const int CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR;
-
- /**
- * @param int<0, max> $offset
- */
- final public function __construct(
- public readonly int $offset,
- string $message,
- int $code = 0,
- ?\Throwable $previous = null,
- ) {
- parent::__construct($message, $code, $previous);
+ protected static function describe(string $message, ReadableInterface $source): string
+ {
+ return \sprintf('%s in %s', $message, self::printSource($source));
}
/**
@@ -39,6 +22,6 @@ final public function __construct(
*/
public function getOffset(): int
{
- return $this->offset;
+ return $this->token->offset;
}
}
diff --git a/libs/parser/src/Exception/SemanticParseException.php b/libs/parser/src/Exception/SemanticParseException.php
deleted file mode 100644
index a1b86dd..0000000
--- a/libs/parser/src/Exception/SemanticParseException.php
+++ /dev/null
@@ -1,28 +0,0 @@
-getMessage()),
- Formatter::source($source->getContents()),
- Formatter::suffix($source->getContents(), $e->getOffset()),
- ]);
-
- return new self($message, self::ERROR_CODE_SEMANTIC_ERROR_BASE + $e->getCode());
- }
-}
diff --git a/libs/parser/src/Exception/ShapeFieldDuplicationException.php b/libs/parser/src/Exception/ShapeFieldDuplicationException.php
index 0381f6f..50fb0c1 100644
--- a/libs/parser/src/Exception/ShapeFieldDuplicationException.php
+++ b/libs/parser/src/Exception/ShapeFieldDuplicationException.php
@@ -4,6 +4,8 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class ShapeFieldDuplicationException extends SemanticException
{
/**
@@ -12,10 +14,18 @@ final class ShapeFieldDuplicationException extends SemanticException
* @param non-empty-string $key
* @param int<0, max> $offset
*/
- public static function becauseShapeFieldIsDuplicated(string $key, int $offset = 0): self
- {
- $message = \sprintf('Duplicate key "%s"', $key);
-
- return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_DUPLICATION);
+ public static function becauseShapeFieldIsDuplicated(
+ string $key,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf('Duplicate key "%s"', $key),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/Exception/ShapeKeyException.php b/libs/parser/src/Exception/ShapeKeyException.php
new file mode 100644
index 0000000..194d5bd
--- /dev/null
+++ b/libs/parser/src/Exception/ShapeKeyException.php
@@ -0,0 +1,29 @@
+ $offset
+ */
+ public static function becauseKeyIsNotAName(
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ 'Shape key must be a name, a number, a string or a reference to a constant',
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
+ }
+}
diff --git a/libs/parser/src/Exception/ShapeKeysMixingException.php b/libs/parser/src/Exception/ShapeKeysMixingException.php
index c416432..fd82349 100644
--- a/libs/parser/src/Exception/ShapeKeysMixingException.php
+++ b/libs/parser/src/Exception/ShapeKeysMixingException.php
@@ -4,17 +4,26 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class ShapeKeysMixingException extends SemanticException
{
/**
- * Occurs when a shape mixes explicit and implicit keys.
+ * Occurs when a shape declares both explicit and implicit keys.
*
* @param int<0, max> $offset
*/
- public static function becauseShapeKeysAreMixed(int $offset = 0): self
- {
- $message = 'Cannot mix explicit and implicit shape keys';
-
- return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_MIX);
+ public static function becauseShapeKeysAreMixed(
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ 'Cannot mix explicit and implicit shape keys',
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/Exception/TemplateBoundException.php b/libs/parser/src/Exception/TemplateBoundException.php
new file mode 100644
index 0000000..5620eb2
--- /dev/null
+++ b/libs/parser/src/Exception/TemplateBoundException.php
@@ -0,0 +1,74 @@
+ $offset
+ */
+ public static function becauseOperatorIsUnknown(
+ string $operator,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf(
+ 'Template parameter cannot be bounded with "%s", expected one of "of", "as" or "super"',
+ $operator,
+ ),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
+ }
+
+ /**
+ * Occurs when a template parameter carries the same limit twice.
+ *
+ * @param int<0, max> $offset
+ */
+ public static function becauseBoundIsDuplicated(
+ string $kind,
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ \sprintf('Template parameter cannot have more than one %s', $kind),
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
+ }
+
+ /**
+ * Occurs when a template parameter carries a bound behind its default.
+ *
+ * @param int<0, max> $offset
+ */
+ public static function becauseDefaultIsNotWrittenLast(
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ 'Template parameter default must be written last, since a bound '
+ . 'behind it reads as a bound of the default itself',
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
+ }
+}
diff --git a/libs/parser/src/Exception/UnexpectedTokenException.php b/libs/parser/src/Exception/UnexpectedTokenException.php
index bc1f932..ae0e2a4 100644
--- a/libs/parser/src/Exception/UnexpectedTokenException.php
+++ b/libs/parser/src/Exception/UnexpectedTokenException.php
@@ -4,24 +4,27 @@
namespace TypeLang\Parser\Exception;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
+use Phplrt\Contracts\Lexer\TokenInterface;
+use Phplrt\Contracts\Source\ReadableInterface;
-final class UnexpectedTokenException extends ParseException
+final class UnexpectedTokenException extends ParsingException
{
/**
* Occurs when a known token is found in an unexpected source location.
*
- * @param int<0, max> $offset
- * @throws SourceExceptionInterface
+ * The message is the one the grammar carries, whether it is written in
+ * an "@error" directive or worded by the parser itself.
*/
- public static function becauseTokenIsUnexpected(string $token, string $statement, int $offset): self
- {
- $message = \vsprintf('Syntax error, unexpected %s%s %s', [
- Formatter::token($token),
- $token === $statement ? '' : ' in ' . Formatter::source($statement),
- Formatter::suffix($statement, $offset),
+ public static function becauseTokenIsUnexpected(
+ string $message,
+ ReadableInterface $source,
+ TokenInterface $token,
+ ): self {
+ $message = \vsprintf('%s in %s', [
+ $message,
+ self::printSource($source),
]);
- return new self($message, self::ERROR_CODE_UNEXPECTED_TOKEN);
+ return new self($message, $source, $token);
}
}
diff --git a/libs/parser/src/Exception/UnreadableSourceException.php b/libs/parser/src/Exception/UnreadableSourceException.php
new file mode 100644
index 0000000..b646707
--- /dev/null
+++ b/libs/parser/src/Exception/UnreadableSourceException.php
@@ -0,0 +1,21 @@
+getMessage(), $source, 0, $e);
+ }
+}
diff --git a/libs/parser/src/Exception/UnrecognizedSyntaxException.php b/libs/parser/src/Exception/UnrecognizedSyntaxException.php
index a6b6e83..3ea50e8 100644
--- a/libs/parser/src/Exception/UnrecognizedSyntaxException.php
+++ b/libs/parser/src/Exception/UnrecognizedSyntaxException.php
@@ -4,24 +4,19 @@
namespace TypeLang\Parser\Exception;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
+use Phplrt\Contracts\Lexer\TokenInterface;
+use Phplrt\Contracts\Source\ReadableInterface;
-final class UnrecognizedSyntaxException extends ParseException
+final class UnrecognizedSyntaxException extends ParsingException
{
/**
* Occurs when the parser reaches a state that does not match any known
* grammar rule.
- *
- * @param int<0, max> $offset
- * @throws SourceExceptionInterface
*/
- public static function becauseSyntaxIsUnrecognized(string $statement, int $offset): self
+ public static function becauseSyntaxIsUnrecognized(ReadableInterface $source, TokenInterface $token): self
{
- $message = \vsprintf('Internal syntax error, in %s %s', [
- Formatter::source($statement),
- Formatter::suffix($statement, $offset),
- ]);
+ $message = \sprintf('Internal syntax error in %s', self::printSource($source));
- return new self($message, self::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR);
+ return new self($message, $source, $token);
}
}
diff --git a/libs/parser/src/Exception/UnrecognizedTokenException.php b/libs/parser/src/Exception/UnrecognizedTokenException.php
index 5973c13..ea9e04a 100644
--- a/libs/parser/src/Exception/UnrecognizedTokenException.php
+++ b/libs/parser/src/Exception/UnrecognizedTokenException.php
@@ -4,24 +4,21 @@
namespace TypeLang\Parser\Exception;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
+use Phplrt\Contracts\Lexer\TokenInterface;
+use Phplrt\Contracts\Source\ReadableInterface;
-final class UnrecognizedTokenException extends ParseException
+final class UnrecognizedTokenException extends ParsingException
{
/**
* Occurs when unable to recognize tokens in source code.
- *
- * @param int<0, max> $offset
- * @throws SourceExceptionInterface
*/
- public static function becauseTokenIsUnrecognized(string $token, string $statement, int $offset): self
+ public static function becauseTokenIsUnrecognized(ReadableInterface $source, TokenInterface $token): self
{
- $message = \vsprintf('Syntax error, unrecognized %s%s %s', [
- Formatter::token($token),
- $token === $statement ? '' : ' in ' . Formatter::source($statement),
- Formatter::suffix($statement, $offset),
+ $message = \vsprintf('Syntax error, unexpected %s in %s', [
+ self::printToken($token),
+ self::printSource($source),
]);
- return new self($message, self::ERROR_CODE_UNRECOGNIZED_TOKEN);
+ return new self($message, $source, $token);
}
}
diff --git a/libs/parser/src/Exception/VariadicRedefinitionException.php b/libs/parser/src/Exception/VariadicRedefinitionException.php
deleted file mode 100644
index 9f88f5e..0000000
--- a/libs/parser/src/Exception/VariadicRedefinitionException.php
+++ /dev/null
@@ -1,20 +0,0 @@
- $offset
- */
- public static function becauseVariadicIsRedefined(int $offset = 0): self
- {
- $message = 'Either prefix or postfix variadic syntax should be used, but not both';
-
- return new self($offset, $message, self::ERROR_CODE_VARIADIC_ALREADY_VARIADIC);
- }
-}
diff --git a/libs/parser/src/Exception/VariadicWithDefaultException.php b/libs/parser/src/Exception/VariadicWithDefaultException.php
index 9f85b44..11b9a99 100644
--- a/libs/parser/src/Exception/VariadicWithDefaultException.php
+++ b/libs/parser/src/Exception/VariadicWithDefaultException.php
@@ -4,17 +4,26 @@
namespace TypeLang\Parser\Exception;
+use Phplrt\Contracts\Source\ReadableInterface;
+
final class VariadicWithDefaultException extends SemanticException
{
/**
- * Occurs when a variadic parameter also declares a default value.
+ * Occurs when a variadic parameter is written with a default.
*
* @param int<0, max> $offset
*/
- public static function becauseVariadicHasDefault(int $offset = 0): self
- {
- $message = 'Cannot have variadic param with a default';
-
- return new self($offset, $message, self::ERROR_CODE_VARIADIC_WITH_DEFAULT);
+ public static function becauseVariadicHasDefault(
+ ReadableInterface $source,
+ int $offset = 0,
+ ): self {
+ return new self(
+ self::describe(
+ 'Cannot have variadic param with a default',
+ $source,
+ ),
+ $source,
+ self::createToken($source, $offset),
+ );
}
}
diff --git a/libs/parser/src/InMemoryTypeParser.php b/libs/parser/src/InMemoryTypeParser.php
index 05b10f3..5f87b1f 100644
--- a/libs/parser/src/InMemoryTypeParser.php
+++ b/libs/parser/src/InMemoryTypeParser.php
@@ -5,14 +5,22 @@
namespace TypeLang\Parser;
use JetBrains\PhpStorm\Language;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
+use Phplrt\Contracts\Source\Exception\SourceExceptionInterface;
+use Phplrt\Contracts\Source\ReadableInterface;
use Phplrt\Contracts\Source\SourceFactoryInterface;
use Phplrt\Source\SourceFactory;
use TypeLang\Parser\Exception\ParserExceptionInterface;
+use TypeLang\Parser\Partial\ParsedResult;
+use TypeLang\Parser\Validation\CheckResult;
use TypeLang\Type\TypeNode;
final class InMemoryTypeParser implements TypeParserInterface
{
+ /**
+ * @var non-empty-string
+ */
+ private const HASH_ALGORITHM = 'xxh128';
+
/**
* @var array
*/
@@ -23,10 +31,26 @@ final class InMemoryTypeParser implements TypeParserInterface
*/
private array $sequences = [];
+ /**
+ * @var array
+ */
+ private array $checks = [];
+
+ private readonly SourceFactoryInterface $sources;
+
public function __construct(
private readonly TypeParserInterface $parser = new TypeParser(),
- private readonly SourceFactoryInterface $sources = new SourceFactory(),
- ) {}
+ ?SourceFactoryInterface $sources = null,
+ ) {
+ $this->sources = $sources ?? SourceFactory::createDefault();
+ }
+
+ public function reset(): void
+ {
+ $this->types = [];
+ $this->sequences = [];
+ $this->checks = [];
+ }
/**
* @throws ParserExceptionInterface
@@ -37,7 +61,19 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode
{
$instance = $this->sources->create($source);
- return $this->types[$instance->getHash()] ??= $this->parser->parse($source);
+ return $this->types[$this->hash($instance)] ??= $this->parser->parse($source);
+ }
+
+ /**
+ * @throws ParserExceptionInterface
+ * @throws SourceExceptionInterface
+ * @throws \Throwable
+ */
+ public function partial(#[Language('PHP')] mixed $source): ParsedResult
+ {
+ $instance = $this->sources->create($source);
+
+ return $this->sequences[$this->hash($instance)] ??= $this->parser->partial($source);
}
/**
@@ -45,10 +81,18 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode
* @throws SourceExceptionInterface
* @throws \Throwable
*/
- public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult
+ public function validate(#[Language('PHP')] mixed $source): CheckResult
{
$instance = $this->sources->create($source);
- return $this->sequences[$instance->getHash()] ??= $this->parser->parseTolerant($source);
+ return $this->checks[$this->hash($instance)] ??= $this->parser->validate($source);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private function hash(ReadableInterface $source): string
+ {
+ return \hash(self::HASH_ALGORITHM, $source->content);
}
}
diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php
new file mode 100644
index 0000000..9782900
--- /dev/null
+++ b/libs/parser/src/Internal/CompiledExecutor.php
@@ -0,0 +1,2880 @@
+
+ */
+abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterface
+{
+ /** @var int */
+ public const T_WHITESPACE = 0;
+ /** @var int */
+ public const T_EQ = 1;
+ /** @var int */
+ public const T_NOT = 2;
+ /** @var int */
+ public const T_TRUE_LITERAL = 3;
+ /** @var int */
+ public const T_FALSE_LITERAL = 4;
+ /** @var int */
+ public const T_NULL_LITERAL = 5;
+ /** @var int */
+ public const T_NAME_WITH_SPACE = 6;
+ /** @var int */
+ public const T_NAME = 7;
+ /** @var int */
+ public const T_NS_DELIMITER = 8;
+ /** @var int */
+ public const T_OR = 9;
+ /** @var int */
+ public const T_COMMA = 10;
+ /** @var int */
+ public const T_DOUBLE_COLON = 11;
+ /** @var int */
+ public const T_COLON = 12;
+ /** @var int */
+ public const T_ANGLE_BRACKET_OPEN = 13;
+ /** @var int */
+ public const T_ANGLE_BRACKET_CLOSE = 14;
+ /** @var int */
+ public const T_QMARK = 15;
+ /** @var int */
+ public const T_BRACE_OPEN = 16;
+ /** @var int */
+ public const T_BRACE_CLOSE = 17;
+ /** @var int */
+ public const T_PARENTHESIS_OPEN = 18;
+ /** @var int */
+ public const T_PARENTHESIS_CLOSE = 19;
+ /** @var int */
+ public const T_SQUARE_BRACKET_OPEN = 20;
+ /** @var int */
+ public const T_SQUARE_BRACKET_CLOSE = 21;
+ /** @var int */
+ public const T_AMP = 22;
+ /** @var int */
+ public const T_ELLIPSIS = 23;
+ /** @var int */
+ public const T_ASSIGN = 24;
+ /** @var int */
+ public const T_ASTERISK = 25;
+ /** @var int */
+ public const T_THIS = 26;
+ /** @var int */
+ public const T_VARIABLE = 27;
+ /** @var int */
+ public const T_DQ_STRING_LITERAL = 28;
+ /** @var int */
+ public const T_SQ_STRING_LITERAL = 29;
+ /** @var int */
+ public const T_FLOAT_LITERAL = 30;
+ /** @var int */
+ public const T_BIN_INT_LITERAL = 31;
+ /** @var int */
+ public const T_OCT_INT_LITERAL = 32;
+ /** @var int */
+ public const T_HEX_INT_LITERAL = 33;
+ /** @var int */
+ public const T_DEC_INT_LITERAL = 34;
+ /** @var int */
+ public const T_COMMENT = 35;
+ /** @var int */
+ public const T_DOC_COMMENT = 36;
+
+ /** @var array */
+ private const PARSER_EXPECTATIONS = [
+ 'T_WHITESPACE',
+ 'T_EQ',
+ 'T_NOT',
+ 'T_TRUE_LITERAL',
+ 'T_FALSE_LITERAL',
+ 'T_NULL_LITERAL',
+ 'T_NAME_WITH_SPACE',
+ 'T_NAME',
+ 'T_NS_DELIMITER',
+ 'T_OR',
+ 'T_COMMA',
+ 'T_DOUBLE_COLON',
+ 'T_COLON',
+ 'T_ANGLE_BRACKET_OPEN',
+ 'T_ANGLE_BRACKET_CLOSE',
+ 'T_QMARK',
+ 'T_BRACE_OPEN',
+ 'T_BRACE_CLOSE',
+ 'T_PARENTHESIS_OPEN',
+ 'T_PARENTHESIS_CLOSE',
+ 'T_SQUARE_BRACKET_OPEN',
+ 'T_SQUARE_BRACKET_CLOSE',
+ 'T_AMP',
+ 'T_ELLIPSIS',
+ 'T_ASSIGN',
+ 'T_ASTERISK',
+ 'T_THIS',
+ 'T_VARIABLE',
+ 'T_DQ_STRING_LITERAL',
+ 'T_SQ_STRING_LITERAL',
+ 'T_FLOAT_LITERAL',
+ 'T_BIN_INT_LITERAL',
+ 'T_OCT_INT_LITERAL',
+ 'T_HEX_INT_LITERAL',
+ 'T_DEC_INT_LITERAL',
+ 'T_COMMENT',
+ 'T_DOC_COMMENT',
+ '/[^\\s]++/',
+ ];
+
+ /** @var array */
+ private const PARSER_MESSAGES = [
+ 5 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"',
+ 10 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"',
+ 41 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"',
+ 46 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"',
+ 72 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"',
+ 76 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"',
+ 80 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument',
+ 87 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"',
+ 91 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter',
+ 100 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"',
+ 115 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"',
+ 124 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"',
+ 131 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"',
+ 136 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"',
+ 138 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"',
+ 144 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of',
+ 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"',
+ 'Syntax error, unexpected {token}, a condition must carry the type it is false of',
+ ];
+
+ /** @var array */
+ private const PARSER_KEPT_TABLE = [
+ true,
+ true,
+ false,
+ true,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ false,
+ false,
+ true,
+ true,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ true,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ ];
+
+ /** @var array|null> */
+ private const PARSER_START_PREDICTION_TABLE = [
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 15 => true,
+ ],
+ [
+ 15 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 18 => true,
+ ],
+ [
+ 18 => true,
+ ],
+ [
+ 19 => true,
+ ],
+ [
+ 26 => true,
+ ],
+ [
+ 3 => true,
+ true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 3 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 28 => true,
+ true,
+ ],
+ [
+ 28 => true,
+ ],
+ [
+ 29 => true,
+ ],
+ [
+ 30 => true,
+ ],
+ [
+ 31 => true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 31 => true,
+ ],
+ [
+ 32 => true,
+ ],
+ [
+ 33 => true,
+ ],
+ [
+ 34 => true,
+ ],
+ [
+ 3 => true,
+ ],
+ [
+ 4 => true,
+ ],
+ [
+ 5 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 8 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 7 => true,
+ ],
+ [
+ 6 => true,
+ ],
+ [
+ 1 => true,
+ ],
+ [
+ 2 => true,
+ ],
+ [
+ 3 => true,
+ ],
+ [
+ 4 => true,
+ ],
+ [
+ 5 => true,
+ ],
+ null,
+ [
+ 8 => true,
+ ],
+ [
+ 8 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 8 => true,
+ 11 => true,
+ 13 => true,
+ 16 => true,
+ 18 => true,
+ 25 => true,
+ ],
+ [
+ 11 => true,
+ ],
+ [
+ 11 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 25 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 25 => true,
+ ],
+ [
+ 25 => true,
+ ],
+ null,
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 18 => true,
+ ],
+ null,
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 22 => true,
+ ],
+ null,
+ [
+ 23 => true,
+ ],
+ null,
+ [
+ 26 => true,
+ true,
+ ],
+ [
+ 27 => true,
+ ],
+ [
+ 26 => true,
+ ],
+ null,
+ [
+ 24 => true,
+ ],
+ null,
+ [
+ 10 => true,
+ ],
+ [
+ 10 => true,
+ ],
+ null,
+ [
+ 19 => true,
+ ],
+ null,
+ [
+ 12 => true,
+ ],
+ [
+ 12 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 13 => true,
+ ],
+ [
+ 13 => true,
+ ],
+ [
+ 13 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 6 => true,
+ ],
+ [
+ 6 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 10 => true,
+ ],
+ [
+ 14 => true,
+ ],
+ null,
+ [
+ 13 => true,
+ ],
+ [
+ 13 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 24 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 24 => true,
+ ],
+ [
+ 24 => true,
+ ],
+ null,
+ [
+ 10 => true,
+ ],
+ [
+ 14 => true,
+ ],
+ [
+ 16 => true,
+ ],
+ [
+ 16 => true,
+ ],
+ null,
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 12 => true,
+ 15 => true,
+ ],
+ null,
+ null,
+ [
+ 15 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 10 => true,
+ ],
+ null,
+ [
+ 10 => true,
+ ],
+ [
+ 23 => true,
+ ],
+ null,
+ null,
+ [
+ 17 => true,
+ ],
+ [
+ 8 => true,
+ ],
+ [
+ 25 => true,
+ ],
+ null,
+ [
+ 20 => true,
+ ],
+ [
+ 20 => true,
+ ],
+ null,
+ [
+ 21 => true,
+ ],
+ null,
+ [
+ 9 => true,
+ 22 => true,
+ ],
+ [
+ 9 => true,
+ ],
+ [
+ 9 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 22 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ 28 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ null,
+ [
+ 1 => true,
+ ],
+ [
+ 1 => true,
+ ],
+ null,
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 12 => true,
+ ],
+ [
+ 1 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ 15 => true,
+ 18 => true,
+ 25 => true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ ],
+ [
+ 26 => true,
+ true,
+ ],
+ ];
+
+ /** @var array>> */
+ private const PARSER_CHOICE_PREDICTION_TABLE = [
+ [
+ 1 => [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ 15 => [
+ 1,
+ ],
+ 18 => [
+ 1,
+ ],
+ 25 => [
+ 1,
+ ],
+ 28 => [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ [
+ 1,
+ ],
+ 27 => [
+ 147,
+ ],
+ ],
+ 2 => [
+ 15 => [
+ 3,
+ ],
+ 1 => [
+ 6,
+ ],
+ 2 => [
+ 6,
+ ],
+ 3 => [
+ 6,
+ ],
+ 4 => [
+ 6,
+ ],
+ 5 => [
+ 6,
+ ],
+ 6 => [
+ 6,
+ ],
+ 7 => [
+ 6,
+ ],
+ 8 => [
+ 6,
+ ],
+ 18 => [
+ 6,
+ ],
+ 25 => [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ 28 => [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ [
+ 6,
+ ],
+ ],
+ 7 => [
+ 18 => [
+ 8,
+ ],
+ 26 => [
+ -12,
+ ],
+ 3 => [
+ 12,
+ 26,
+ ],
+ 4 => [
+ 12,
+ 26,
+ ],
+ 5 => [
+ 12,
+ 26,
+ ],
+ 28 => [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ [
+ 12,
+ ],
+ 1 => [
+ 26,
+ ],
+ 2 => [
+ 26,
+ ],
+ 6 => [
+ 26,
+ ],
+ 7 => [
+ 26,
+ ],
+ 8 => [
+ 26,
+ ],
+ 25 => [
+ 126,
+ ],
+ ],
+ 12 => [
+ 3 => [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ 28 => [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ [
+ 13,
+ ],
+ 5 => [
+ -26,
+ ],
+ ],
+ [
+ 28 => [
+ 14,
+ ],
+ [
+ 14,
+ ],
+ [
+ -18,
+ ],
+ [
+ 18,
+ ],
+ [
+ 18,
+ ],
+ [
+ 18,
+ ],
+ [
+ 18,
+ ],
+ 3 => [
+ -24,
+ ],
+ 4 => [
+ -25,
+ ],
+ ],
+ [
+ 28 => [
+ -16,
+ ],
+ [
+ -17,
+ ],
+ ],
+ 18 => [
+ 31 => [
+ -20,
+ ],
+ [
+ -21,
+ ],
+ [
+ -22,
+ ],
+ [
+ -23,
+ ],
+ ],
+ 30 => [
+ 7 => [
+ -32,
+ ],
+ 6 => [
+ -33,
+ ],
+ 1 => [
+ -34,
+ ],
+ 2 => [
+ -35,
+ ],
+ 3 => [
+ -36,
+ ],
+ 4 => [
+ -37,
+ ],
+ 5 => [
+ -38,
+ ],
+ ],
+ 43 => [
+ 11 => [
+ 44,
+ ],
+ 18 => [
+ 54,
+ ],
+ 13 => [
+ 77,
+ 89,
+ ],
+ 16 => [
+ 101,
+ ],
+ 25 => [
+ 49,
+ ],
+ 8 => [
+ 125,
+ ],
+ ],
+ 46 => [
+ 1 => [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ [
+ 47,
+ ],
+ 25 => [
+ 49,
+ ],
+ ],
+ 63 => [
+ 27 => [
+ -65,
+ ],
+ 26 => [
+ -66,
+ ],
+ ],
+ 81 => [
+ 1 => [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ 7 => [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ 15 => [
+ 84,
+ ],
+ 18 => [
+ 84,
+ ],
+ 25 => [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ [
+ 84,
+ ],
+ ],
+ 84 => [
+ 25 => [
+ -51,
+ 0,
+ ],
+ 1 => [
+ 0,
+ ],
+ 2 => [
+ 0,
+ ],
+ 3 => [
+ 0,
+ ],
+ 4 => [
+ 0,
+ ],
+ 5 => [
+ 0,
+ ],
+ 6 => [
+ 0,
+ ],
+ 7 => [
+ 0,
+ ],
+ 8 => [
+ 0,
+ ],
+ 15 => [
+ 0,
+ ],
+ 18 => [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ ],
+ 94 => [
+ 1 => [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ [
+ 95,
+ ],
+ 24 => [
+ 96,
+ ],
+ ],
+ 106 => [
+ 15 => [
+ 116,
+ ],
+ 27 => [
+ 116,
+ ],
+ ],
+ 108 => [
+ 8 => [
+ 7,
+ ],
+ 18 => [
+ 7,
+ ],
+ 25 => [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ 28 => [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ [
+ 7,
+ ],
+ ],
+ 133 => [
+ 9 => [
+ 134,
+ ],
+ 22 => [
+ 137,
+ ],
+ ],
+ 143 => [
+ 1 => [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ 15 => [
+ 0,
+ ],
+ 18 => [
+ 0,
+ ],
+ 25 => [
+ 0,
+ ],
+ 28 => [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ [
+ 0,
+ ],
+ ],
+ ];
+
+ /** @var array> */
+ private const PARSER_SEQUENCE_PREDICTION_TABLE = [
+ 1 => [
+ 2,
+ 132,
+ -141,
+ ],
+ 26 => [
+ 27,
+ -44,
+ ],
+ [
+ -30,
+ 30,
+ 38,
+ ],
+ 47 => [
+ 30,
+ -50,
+ ],
+ 49 => [
+ 50,
+ 51,
+ -31,
+ ],
+ 54 => [
+ 9,
+ 55,
+ 72,
+ -75,
+ ],
+ 56 => [
+ 57,
+ 68,
+ -71,
+ ],
+ [
+ 0,
+ -60,
+ -62,
+ -64,
+ -68,
+ ],
+ 78 => [
+ 79,
+ 80,
+ 85,
+ -71,
+ 87,
+ ],
+ 90 => [
+ 79,
+ 91,
+ 98,
+ -71,
+ 100,
+ ],
+ 101 => [
+ 102,
+ 103,
+ -71,
+ 124,
+ ],
+ 104 => [
+ 105,
+ -121,
+ ],
+ 107 => [
+ 108,
+ -115,
+ 75,
+ 115,
+ ],
+ 111 => [
+ -5,
+ 75,
+ ],
+ 121 => [
+ 61,
+ -79,
+ ],
+ 126 => [
+ 50,
+ 30,
+ -50,
+ ],
+ 128 => [
+ 129,
+ -1,
+ 131,
+ ],
+ 141 => [
+ 33,
+ -35,
+ ],
+ ];
+
+ /**
+ * @var \Phplrt\Parser\Parser
+ */
+ protected readonly \Phplrt\Parser\Parser $parser;
+
+ protected readonly \Phplrt\Contracts\Lexer\LexerInterface $lexer;
+
+ public function __construct()
+ {
+ $this->lexer = new \Phplrt\Lexer\Lexer(
+ pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:is(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:not(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:6))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:7))|(?:(?:\\\\)(*MARK:8))|(?:(?:\\|)(*MARK:9))|(?:(?:,)(*MARK:10))|(?:(?:::)(*MARK:11))|(?:(?::)(*MARK:12))|(?:(?:<)(*MARK:13))|(?:(?:>)(*MARK:14))|(?:(?:\\?)(*MARK:15))|(?:(?:\\{)(*MARK:16))|(?:(?:\\})(*MARK:17))|(?:(?:\\()(*MARK:18))|(?:(?:\\))(*MARK:19))|(?:(?:\\[)(*MARK:20))|(?:(?:\\])(*MARK:21))|(?:(?:&)(*MARK:22))|(?:(?:\\.\\.\\.)(*MARK:23))|(?:(?:=)(*MARK:24))|(?:(?:\\*)(*MARK:25))|(?:(?:\\$this(?!(?:[a-zA-Z0-9_\\x80-\\xff])))(*MARK:26))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9_\\x80-\\xff])*+)(*MARK:27))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:28))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:29))|(?:(?:(?:[-+])?(?:(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+)\\.(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+))?|\\.(?:(?:[0-9])(?:_?(?:[0-9]))*+))(?:(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+)))?|(?:(?:[0-9])(?:_?(?:[0-9]))*+)(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+))))(*MARK:30))|(?:(?:(?:[-+])?0[bB](?:(?:[01])(?:_?(?:[01]))*+))(*MARK:31))|(?:(?:(?:[-+])?0(?:[oO](?:(?:[0-7])(?:_?(?:[0-7]))*+)|(?:_?(?:[0-7]))++))(*MARK:32))|(?:(?:(?:[-+])?0[xX](?:(?:[0-9a-fA-F])(?:_?(?:[0-9a-fA-F]))*+))(*MARK:33))|(?:(?:(?:[-+])?(?:(?:(?:[1-9])(?:_?(?:[0-9]))*+)|0))(*MARK:34))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:35))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:36))|(?:(?:[^\\s]++)(*MARK:37)))/Ssm',
+ channels: [
+ 'Hidden',
+ 35 => 'Hidden',
+ 'Hidden',
+ 'Unknown',
+ ],
+ names: [
+ 'T_WHITESPACE',
+ 'T_EQ',
+ 'T_NOT',
+ 'T_TRUE_LITERAL',
+ 'T_FALSE_LITERAL',
+ 'T_NULL_LITERAL',
+ 'T_NAME_WITH_SPACE',
+ 'T_NAME',
+ 'T_NS_DELIMITER',
+ 'T_OR',
+ 'T_COMMA',
+ 'T_DOUBLE_COLON',
+ 'T_COLON',
+ 'T_ANGLE_BRACKET_OPEN',
+ 'T_ANGLE_BRACKET_CLOSE',
+ 'T_QMARK',
+ 'T_BRACE_OPEN',
+ 'T_BRACE_CLOSE',
+ 'T_PARENTHESIS_OPEN',
+ 'T_PARENTHESIS_CLOSE',
+ 'T_SQUARE_BRACKET_OPEN',
+ 'T_SQUARE_BRACKET_CLOSE',
+ 'T_AMP',
+ 'T_ELLIPSIS',
+ 'T_ASSIGN',
+ 'T_ASTERISK',
+ 'T_THIS',
+ 'T_VARIABLE',
+ 'T_DQ_STRING_LITERAL',
+ 'T_SQ_STRING_LITERAL',
+ 'T_FLOAT_LITERAL',
+ 'T_BIN_INT_LITERAL',
+ 'T_OCT_INT_LITERAL',
+ 'T_HEX_INT_LITERAL',
+ 'T_DEC_INT_LITERAL',
+ 'T_COMMENT',
+ 'T_DOC_COMMENT',
+ ],
+ );
+
+ $this->parser = new \Phplrt\Parser\Parser(
+ lexer: $this->lexer,
+ grammar: [
+ new \Phplrt\Parser\Grammar\Alternation([1, 147]),
+ new \Phplrt\Parser\Grammar\Concatenation([2, 132, 139]),
+ new \Phplrt\Parser\Grammar\Alternation([3, 6]),
+ new \Phplrt\Parser\Grammar\Concatenation([4, 5]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false),
+ new \Phplrt\Parser\Grammar\Concatenation([6]),
+ new \Phplrt\Parser\Grammar\Concatenation([7, 127]),
+ new \Phplrt\Parser\Grammar\Alternation([8, 11, 12, 26, 126]),
+ new \Phplrt\Parser\Grammar\Concatenation([9, 0, 10]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true),
+ new \Phplrt\Parser\Grammar\Alternation([13, 25]),
+ new \Phplrt\Parser\Grammar\Alternation([14, 17, 18, 23, 24]),
+ new \Phplrt\Parser\Grammar\Alternation([15, 16]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Alternation([19, 20, 21, 22]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_DEC_INT_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Concatenation([27, 42]),
+ new \Phplrt\Parser\Grammar\Concatenation([28, 30, 38]),
+ new \Phplrt\Parser\Grammar\Optional(29),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, true),
+ new \Phplrt\Parser\Grammar\Alternation([31, 32, 33, 34, 35, 36, 37]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NOT, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true),
+ new \Phplrt\Parser\Grammar\Repetition(39, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([40, 41]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false),
+ new \Phplrt\Parser\Grammar\Concatenation([30]),
+ new \Phplrt\Parser\Grammar\Optional(43),
+ new \Phplrt\Parser\Grammar\Alternation([44, 54, 77, 89, 101, 49, 125]),
+ new \Phplrt\Parser\Grammar\Concatenation([45, 46]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true),
+ new \Phplrt\Parser\Grammar\Alternation([47, 49]),
+ new \Phplrt\Parser\Grammar\Concatenation([30, 48]),
+ new \Phplrt\Parser\Grammar\Optional(49),
+ new \Phplrt\Parser\Grammar\Concatenation([50, 51, 53]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true),
+ new \Phplrt\Parser\Grammar\Repetition(52, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([30, 50]),
+ new \Phplrt\Parser\Grammar\Optional(30),
+ new \Phplrt\Parser\Grammar\Concatenation([9, 55, 72, 73]),
+ new \Phplrt\Parser\Grammar\Optional(56),
+ new \Phplrt\Parser\Grammar\Concatenation([57, 68, 71]),
+ new \Phplrt\Parser\Grammar\Concatenation([0, 58, 60, 62, 66]),
+ new \Phplrt\Parser\Grammar\Optional(59),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true),
+ new \Phplrt\Parser\Grammar\Optional(61),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true),
+ new \Phplrt\Parser\Grammar\Optional(63),
+ new \Phplrt\Parser\Grammar\Alternation([64, 65]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true),
+ new \Phplrt\Parser\Grammar\Optional(67),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true),
+ new \Phplrt\Parser\Grammar\Repetition(69, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([70, 57]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false),
+ new \Phplrt\Parser\Grammar\Optional(70),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Optional(74),
+ new \Phplrt\Parser\Grammar\Concatenation([75, 76]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false),
+ new \Phplrt\Parser\Grammar\Concatenation([0]),
+ new \Phplrt\Parser\Grammar\Concatenation([78, 88]),
+ new \Phplrt\Parser\Grammar\Concatenation([79, 80, 85, 71, 87]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false),
+ new \Phplrt\Parser\Grammar\Concatenation([81]),
+ new \Phplrt\Parser\Grammar\Alternation([82, 84]),
+ new \Phplrt\Parser\Grammar\Concatenation([83, 84]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true),
+ new \Phplrt\Parser\Grammar\Alternation([50, 0]),
+ new \Phplrt\Parser\Grammar\Repetition(86, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([70, 81]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Predicate(9, false),
+ new \Phplrt\Parser\Grammar\Concatenation([90, 54]),
+ new \Phplrt\Parser\Grammar\Concatenation([79, 91, 98, 71, 100]),
+ new \Phplrt\Parser\Grammar\Concatenation([92]),
+ new \Phplrt\Parser\Grammar\Concatenation([30, 93]),
+ new \Phplrt\Parser\Grammar\Repetition(94, 0, \INF),
+ new \Phplrt\Parser\Grammar\Alternation([95, 96]),
+ new \Phplrt\Parser\Grammar\Concatenation([30, 0]),
+ new \Phplrt\Parser\Grammar\Concatenation([97, 0]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false),
+ new \Phplrt\Parser\Grammar\Repetition(99, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([70, 92]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Concatenation([102, 103, 71, 124]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false),
+ new \Phplrt\Parser\Grammar\Alternation([104, 123]),
+ new \Phplrt\Parser\Grammar\Concatenation([105, 119]),
+ new \Phplrt\Parser\Grammar\Concatenation([106, 117]),
+ new \Phplrt\Parser\Grammar\Alternation([107, 116]),
+ new \Phplrt\Parser\Grammar\Concatenation([108, 113, 75, 115]),
+ new \Phplrt\Parser\Grammar\Alternation([109, 7]),
+ new \Phplrt\Parser\Grammar\Concatenation([30, 110]),
+ new \Phplrt\Parser\Grammar\Predicate(111, true),
+ new \Phplrt\Parser\Grammar\Concatenation([112, 75]),
+ new \Phplrt\Parser\Grammar\Optional(4),
+ new \Phplrt\Parser\Grammar\Optional(114),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true),
+ new \Phplrt\Parser\Grammar\Concatenation([0]),
+ new \Phplrt\Parser\Grammar\Concatenation([0]),
+ new \Phplrt\Parser\Grammar\Repetition(118, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([70, 106]),
+ new \Phplrt\Parser\Grammar\Optional(120),
+ new \Phplrt\Parser\Grammar\Concatenation([70, 121]),
+ new \Phplrt\Parser\Grammar\Concatenation([61, 122]),
+ new \Phplrt\Parser\Grammar\Optional(78),
+ new \Phplrt\Parser\Grammar\Optional(121),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Concatenation([40, 49]),
+ new \Phplrt\Parser\Grammar\Concatenation([50, 30, 48]),
+ new \Phplrt\Parser\Grammar\Repetition(128, 0, \INF),
+ new \Phplrt\Parser\Grammar\Concatenation([129, 130, 131]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false),
+ new \Phplrt\Parser\Grammar\Optional(0),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false),
+ new \Phplrt\Parser\Grammar\Repetition(133, 0, \INF),
+ new \Phplrt\Parser\Grammar\Alternation([134, 137]),
+ new \Phplrt\Parser\Grammar\Concatenation([135, 136]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, true),
+ new \Phplrt\Parser\Grammar\Concatenation([2]),
+ new \Phplrt\Parser\Grammar\Concatenation([59, 138]),
+ new \Phplrt\Parser\Grammar\Concatenation([2]),
+ new \Phplrt\Parser\Grammar\Optional(140),
+ new \Phplrt\Parser\Grammar\Concatenation([141, 143, 4, 144, 145, 146]),
+ new \Phplrt\Parser\Grammar\Concatenation([33, 142]),
+ new \Phplrt\Parser\Grammar\Optional(34),
+ new \Phplrt\Parser\Grammar\Alternation([0, 63]),
+ new \Phplrt\Parser\Grammar\Concatenation([0]),
+ new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false),
+ new \Phplrt\Parser\Grammar\Concatenation([0]),
+ new \Phplrt\Parser\Grammar\Concatenation([63, 140]),
+ ],
+ initial: 0,
+ reducers: [
+ 0 => $this->reduceType(...),
+ 3 => self::reduceNullableType(...),
+ 6 => $this->reduceListOrOffsetType(...),
+ 11 => self::reduceThisType(...),
+ 12 => $this->reduceLiteralType(...),
+ 15 => self::reduceDoubleQuotedStringLiteralType(...),
+ 16 => self::reduceSingleQuotedStringLiteralType(...),
+ 17 => self::reduceFloatLiteralType(...),
+ 18 => self::reduceIntLiteralType(...),
+ 23 => self::reduceTrueLiteralType(...),
+ 24 => self::reduceFalseLiteralType(...),
+ 25 => self::reduceNullLiteralType(...),
+ 26 => $this->reduceNamedType(...),
+ 27 => self::reduceName(...),
+ 30 => self::reduceIdentifier(...),
+ 50 => self::reduceWildcardType(...),
+ 55 => self::reduceCallableParameters(...),
+ 57 => self::reduceCallableParameter(...),
+ 63 => self::reduceVariable(...),
+ 78 => $this->reduceTemplateArguments(...),
+ 81 => $this->reduceTemplateArgument(...),
+ 83 => self::reduceIdentifierWithExtraSpace(...),
+ 90 => $this->reduceTemplateParameters(...),
+ 92 => self::reduceTemplateParameter(...),
+ 95 => self::reduceTemplateBoundEdge(...),
+ 101 => $this->reduceShapeFields(...),
+ 105 => self::reduceShapeFieldsList(...),
+ 107 => self::reduceExplicitField(...),
+ 116 => self::reduceImplicitField(...),
+ 125 => self::reduceNamespacedMask(...),
+ 126 => self::reduceLeadingMaskType(...),
+ 128 => self::reduceListOrOffsetSuffix(...),
+ 141 => self::reduceTernaryExpressionOperator(...),
+ ],
+ expectations: self::PARSER_EXPECTATIONS,
+ messages: self::PARSER_MESSAGES,
+ kept: self::PARSER_KEPT_TABLE,
+ startPrediction: self::PARSER_START_PREDICTION_TABLE,
+ choicePrediction: self::PARSER_CHOICE_PREDICTION_TABLE,
+ sequencePrediction: self::PARSER_SEQUENCE_PREDICTION_TABLE,
+ );
+ }
+
+ public function parse(\Phplrt\Contracts\Source\ReadableInterface $source): mixed
+ {
+ return $this->parser->parse($source);
+ }
+
+ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ if (\count($children) === 1) {
+ return $children[0];
+ }
+
+ // The suffix of a condition is the only thing the rule reads that carries
+ // a flag of its own, which is what the operands are told from
+ $condition = null;
+
+ for ($i = 1, $count = \count($children); $i < $count; ++$i) {
+ if (\is_bool($children[$i])) {
+ $condition = \array_splice($children, $i);
+
+ break;
+ }
+ }
+
+ // The operands joined by an ampersand bind tighter, so they are collected
+ // into a group of their own, and the groups are what a union is made of
+ $groups = [];
+ $group = [$children[0]];
+ $isUnion = false;
+ $isIntersection = false;
+
+ for ($i = 1, $count = \count($children); $i < $count; $i += 2) {
+ if ($children[$i]->id === self::T_OR) {
+ $isUnion = true;
+ $groups[] = $group;
+ $group = [$children[$i + 1]];
+
+ continue;
+ }
+
+ $isIntersection = true;
+ $group[] = $children[$i + 1];
+ }
+
+ $groups[] = $group;
+
+ // The operand that binds tighter is the one reported first, the way the
+ // reading would have reached it
+ if ($isIntersection && $this->features->intersections === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset);
+ }
+
+ if ($isUnion && $this->features->unions === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset);
+ }
+
+ $statements = [];
+
+ foreach ($groups as $group) {
+ $statements[] = \count($group) === 1
+ ? $group[0]
+ : new Type\IntersectionTypeNode($group, $group[0]->offset);
+ }
+
+ $statement = \count($statements) === 1
+ ? $statements[0]
+ : new Type\UnionTypeNode($statements, $offset);
+
+ if ($condition === null) {
+ return $statement;
+ }
+
+ if ($this->features->conditions === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset);
+ }
+
+ $comparison = $condition[0]
+ ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset)
+ : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset);
+
+ return new Type\TernaryExpressionNode(
+ $comparison,
+ $condition[2],
+ $condition[3],
+ $offset,
+ );
+ }
+
+ private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\NullableTypeNode($children[0], $offset);
+ }
+
+ private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $statement = $children[0];
+ $count = \count($children);
+
+ for ($i = 1; $i < $count; ++$i) {
+ $child = $children[$i];
+
+ switch (true) {
+ case $child === true:
+ if ($this->features->lists === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset);
+ }
+
+ $statement = new Type\TypesListNode($statement, $offset);
+ break;
+ case $child instanceof Type\TypeNode:
+ if ($this->features->offsets === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset);
+ }
+
+ $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset);
+ break;
+ default:
+ throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected(
+ \get_debug_type($child),
+ $source,
+ $offset,
+ );
+ }
+ }
+
+ return $statement;
+ }
+
+ private static function reduceThisType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\ThisNode($offset);
+ }
+
+ private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ if ($this->features->literals === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset);
+ }
+
+ return $children;
+ }
+
+ private static function reduceDoubleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\StringLiteralNode(
+ StringDecoder::unpackAndDecode($children->value, true),
+ $children->value,
+ $offset,
+ );
+ }
+
+ private static function reduceSingleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\StringLiteralNode(
+ StringDecoder::unpackAndDecode($children->value, false),
+ $children->value,
+ $offset,
+ );
+ }
+
+ private static function reduceFloatLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\FloatLiteralNode(
+ FloatDecoder::decode($children->value),
+ $children->value,
+ $offset,
+ );
+ }
+
+ private static function reduceIntLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ $decoded = IntDecoder::decode($children->value);
+
+ return new Type\Literal\IntLiteralNode(
+ $decoded->value,
+ $children->value,
+ $decoded->decimal,
+ $offset,
+ );
+ }
+
+ private static function reduceTrueLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\BoolLiteralNode(true, $children->value, $offset);
+ }
+
+ private static function reduceFalseLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\BoolLiteralNode(false, $children->value, $offset);
+ }
+
+ private static function reduceNullLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Literal\NullLiteralNode(
+ $children->value,
+ $offset,
+ );
+ }
+
+ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $head = $children[0];
+ $suffix = $children[1] ?? null;
+
+ // "Some\Any\*" and "Some\Any\SOME_*"
+ //
+ // A mask closes the type it is written at the end of, so it is read as a
+ // suffix of a name and never carries a suffix of its own.
+ if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) {
+ $parts = $head->parts;
+
+ if ($suffix instanceof Type\MaskNode) {
+ // "Some\Any\*"
+ $mask = $suffix;
+ } else {
+ // "Some\Any\SOME_*"
+ $segment = \array_pop($parts);
+ $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset);
+ }
+
+ return new Type\ConstMaskNode(
+ $mask,
+ $parts === []
+ ? $head->isFullyQualified
+ : new Type\Name($parts, $head->isFullyQualified, $head->offset),
+ $offset,
+ );
+ }
+
+ switch (true) {
+ // Some\Any
+ case $suffix === null:
+ return new Type\NamedTypeNode($children[0], null, null, $offset);
+
+ // Some\Any
+ case $suffix instanceof Type\Template\TemplateArgumentListNode:
+ return new Type\NamedTypeNode($children[0], $suffix, null, $offset);
+
+ // Some\Any{name: T, ...}
+ case $suffix instanceof Type\Shape\FieldsListNode:
+ return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset);
+
+ // Some\Any(T, U): V
+ case $suffix instanceof Type\Callable\CallableParameterListNode:
+ if ($this->features->callables === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset);
+ }
+
+ return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset);
+
+ // Some\Any(T): U
+ case $suffix instanceof Type\Template\TemplateParameterListNode:
+ if ($this->features->callables === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset);
+ }
+
+ return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset);
+
+ // Some\Any::CONST, Some\Any::CONST_* and Some\Any::*
+ default:
+ if (\count($children) === 3 && $children[2] instanceof Type\Identifier) {
+ return new Type\ClassConstNode($children[0], $children[2], $offset);
+ }
+
+ return new Type\ClassConstMaskNode(
+ $children[0],
+ new Type\MaskNode(\array_slice($children, 2), $children[2]->offset),
+ $offset,
+ );
+ }
+ }
+
+ private static function reduceName(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ // "Some\Any" starts at a segment of its own
+ if ($children[0] instanceof Type\Identifier) {
+ return new Type\Name($children, false, $offset);
+ }
+
+ // "\Some\Any" carries the separator in front of its segments, and the
+ // name itself is the part that follows it
+ $segments = \array_slice($children, 1);
+
+ return new Type\Name($segments, true, $segments[0]->offset);
+ }
+
+ private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Identifier(
+ $children->id === self::T_NAME_WITH_SPACE
+ ? \rtrim($children->value)
+ : $children->value,
+ $offset,
+ );
+ }
+
+ private static function reduceWildcardType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\WildcardNode($offset);
+ }
+
+ private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Callable\CallableParameterListNode($children, $offset);
+ }
+
+ private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $parameter = new Type\Callable\CallableParameterNode(
+ $children[0],
+ null,
+ false,
+ false,
+ false,
+ $offset,
+ );
+
+ for ($i = 1, $count = \count($children); $i < $count; ++$i) {
+ $child = $children[$i];
+
+ if ($child instanceof Type\VariableNode) {
+ $parameter->name = $child;
+
+ continue;
+ }
+
+ switch ($child->id) {
+ case self::T_AMP:
+ $parameter->isOutput = true;
+ break;
+
+ case self::T_ELLIPSIS:
+ $parameter->isVariadic = true;
+ break;
+
+ default:
+ if ($parameter->isVariadic) {
+ throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset);
+ }
+
+ $parameter->isOptional = true;
+ }
+ }
+
+ return $parameter;
+ }
+
+ private static function reduceVariable(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\VariableNode(
+ new Type\Identifier(\substr($children->value, 1), $offset),
+ $offset,
+ );
+ }
+
+ private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ if ($this->features->generics === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset);
+ }
+
+ return new Type\Template\TemplateArgumentListNode($children, $offset);
+ }
+
+ private function reduceTemplateArgument(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ if (\is_array($children)) {
+ if ($this->features->hints === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset);
+ }
+
+ return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset);
+ }
+
+ return new Type\Template\TemplateArgumentNode($children, null, $offset);
+ }
+
+ private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Identifier(\rtrim($children->value), $offset);
+ }
+
+ private function reduceTemplateParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ if ($this->features->generics === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset);
+ }
+
+ return new Type\Template\TemplateParameterListNode($children, $offset);
+ }
+
+ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $upper = $lower = $default = null;
+
+ foreach (\array_slice($children, 1) as $limit) {
+ if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) {
+ if ($default !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset);
+ }
+
+ $default = $limit;
+
+ continue;
+ }
+
+ if ($default !== null) {
+ throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset);
+ }
+
+ switch ($limit->operator->value) {
+ case 'of':
+ case 'as':
+ if ($upper !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset);
+ }
+
+ $upper = $limit;
+ break;
+
+ case 'super':
+ if ($lower !== null) {
+ throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset);
+ }
+
+ $lower = $limit;
+ break;
+
+ default:
+ throw Exception\TemplateBoundException::becauseOperatorIsUnknown(
+ $limit->operator->value,
+ $source,
+ $limit->operator->offset,
+ );
+ }
+ }
+
+ return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset);
+ }
+
+ private static function reduceTemplateBoundEdge(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset);
+ }
+
+ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $count = \count($children);
+
+ if ($count === 0) {
+ return [new Type\Shape\FieldsListNode([], true, $offset)];
+ }
+
+ if ($this->features->shapes === false) {
+ throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset);
+ }
+
+ $parameters = null;
+
+ // The "..." suffix
+ if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) {
+ $parameters = $children[$count - 1];
+ --$count;
+ }
+
+ if ($children[0] instanceof Type\Shape\FieldsListNode) {
+ $fields = $children[0];
+ $unsealed = $count > 1;
+ } else {
+ $fields = new Type\Shape\FieldsListNode([], true, $offset);
+ $unsealed = $count > 0;
+ }
+
+ if ($unsealed) {
+ $fields->isSealed = false;
+ }
+
+ if ($parameters === null) {
+ return [$fields];
+ }
+
+ return [$fields, $parameters];
+ }
+
+ private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $explicit = false;
+ $implicit = false;
+ $indexed = [];
+
+ foreach ($children as $field) {
+ if (!$field instanceof Type\Shape\ExplicitFieldNode) {
+ $implicit = true;
+
+ continue;
+ }
+
+ $explicit = true;
+
+ if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) {
+ continue;
+ }
+
+ $key = $field->getIndex();
+
+ if (isset($indexed[$key])) {
+ throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset);
+ }
+
+ $indexed[$key] = true;
+ }
+
+ if ($explicit && $implicit) {
+ throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset);
+ }
+
+ return new Type\Shape\FieldsListNode($children, true, $offset);
+ }
+
+ private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $source = $ctx->source;
+ $offset = $ctx->begin;
+
+ $count = \count($children);
+ $name = $children[0];
+ $value = $children[$count - 1];
+
+ $optional = $count === 3;
+
+ return match (true) {
+ // "a", "true" and "null"
+ $name instanceof Type\Identifier
+ => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset),
+ $name instanceof Type\Literal\IntLiteralNode,
+ $name instanceof Type\Literal\StringLiteralNode
+ => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset),
+ // "Some::CONST", "Some::CONST_*" and "JSON_*"
+ $name instanceof Type\ClassConstNode,
+ $name instanceof Type\ClassConstMaskNode,
+ $name instanceof Type\ConstMaskNode
+ => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset),
+ default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset),
+ };
+ }
+
+ private static function reduceImplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\Shape\ImplicitFieldNode($children[0], false, $offset);
+ }
+
+ private static function reduceNamespacedMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\MaskNode($children, $offset);
+ }
+
+ private static function reduceLeadingMaskType(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ // The variables below are declared by the compiler
+ $offset = $ctx->begin;
+
+ return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset);
+ }
+
+ private static function reduceListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ return $children[0] ?? true;
+ }
+
+ private static function reduceTernaryExpressionOperator(\Phplrt\Parser\Context $ctx, mixed $children): mixed
+ {
+ return \count($children) === 2;
+ }
+}
diff --git a/libs/parser/src/Internal/ExecutionContext.php b/libs/parser/src/Internal/ExecutionContext.php
deleted file mode 100644
index a6a3358..0000000
--- a/libs/parser/src/Internal/ExecutionContext.php
+++ /dev/null
@@ -1,168 +0,0 @@
-,
- * ...
- * },
- * skip: list,
- * grammar: array,
- * reducers: array|non-empty-string, callable(Context, mixed): mixed>,
- * transitions?: array
- * }
- * @phpstan-type ParserConfigArrayType array
- *
- * @internal this is an internal library class, please do not use it in your code
- * @psalm-internal TypeLang\Parser
- */
-final readonly class ExecutionContext
-{
- /**
- * @var ParserRuntime
- */
- private ParserRuntime $parser;
-
- private LexerRuntime $lexer;
-
- /**
- * In-memory string literal pool.
- *
- * @api this property is accessible inside the grammar reducers
- *
- * @var \WeakMap
- */
- protected \WeakMap $stringPool;
-
- /**
- * In-memory integer literal pool.
- *
- * @api this property is accessible inside the grammar reducers
- *
- * @var \WeakMap
- */
- protected \WeakMap $integerPool;
-
- public function __construct(
- /**
- * @api this property is accessible inside the grammar reducers
- */
- protected TypeParserFeatures $features,
- /**
- * @api this property is accessible inside the grammar reducers
- */
- protected bool $tolerant,
- ) {
- /** @phpstan-var GrammarConfigArrayType $grammar */
- $grammar = require __DIR__ . '/../../resources/grammar.php';
-
- $this->stringPool = new \WeakMap();
- $this->integerPool = new \WeakMap();
-
- $this->lexer = $this->createLexer($grammar);
- $this->parser = $this->createParser($this->lexer, $grammar);
- }
-
- /**
- * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers
- */
- private function createBuilder(array $reducers): BuilderInterface
- {
- return new NodeBuilder($reducers);
- }
-
- /**
- * @param GrammarConfigArrayType $grammar
- * @return ParserRuntime
- */
- private function createParser(LexerInterface $lexer, array $grammar): ParserRuntime
- {
- /** @var ParserRuntime */
- return new ParserRuntime(
- lexer: $lexer,
- grammar: $grammar['grammar'],
- options: $this->createParserOptions($grammar),
- );
- }
-
- /**
- * @param GrammarConfigArrayType $grammar
- * @return ParserConfigArrayType
- */
- private function createParserOptions(array $grammar): array
- {
- return [
- ParserConfigsInterface::CONFIG_INITIAL_RULE => $grammar['initial'],
- ParserConfigsInterface::CONFIG_ALLOW_TRAILING_TOKENS => $this->tolerant,
- ParserConfigsInterface::CONFIG_AST_BUILDER => $this->createBuilder($grammar['reducers']),
- ];
- }
-
- /**
- * @param GrammarConfigArrayType $grammar
- */
- private function createLexer(array $grammar): LexerRuntime
- {
- return new LexerRuntime(
- tokens: $grammar['tokens']['default'],
- skip: $grammar['skip'],
- onUnknownToken: new PassthroughHandler(),
- );
- }
-
- /**
- * @return iterable
- * @throws LexerExceptionInterface
- * @throws LexerRuntimeExceptionInterface
- */
- public function lex(ReadableInterface $source): iterable
- {
- return $this->lexer->lex($source);
- }
-
- /**
- * @throws ParserRuntimeExceptionInterface
- * @throws ParserExceptionInterface
- */
- public function parse(ReadableInterface $source): ?ParsedResult
- {
- foreach ($this->parser->parse($source) as $stmt) {
- $offset = 0;
- $context = $this->parser->getLastExecutionContext();
-
- if ($context !== null) {
- $token = $context->buffer->current();
- $offset = $token->getOffset();
- }
-
- return new ParsedResult($stmt, $offset);
- }
-
- return null;
- }
-}
diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php
new file mode 100644
index 0000000..71fc2e0
--- /dev/null
+++ b/libs/parser/src/Internal/Executor.php
@@ -0,0 +1,246 @@
+
+ *
+ * @property-read ParserRuntime $parser
+ */
+final class Executor extends CompiledExecutor
+{
+ private readonly PositionFactory $positions;
+
+ public function __construct(
+ /**
+ * @api this property is accessible inside the grammar reducers
+ */
+ protected readonly TypeParserFeatures $features,
+ ) {
+ $this->positions = new PositionFactory();
+
+ parent::__construct();
+ }
+
+ /**
+ * @return iterable
+ */
+ public function lex(ReadableInterface $source): iterable
+ {
+ return $this->lexer->lex($source);
+ }
+
+ /**
+ * Reads the source whole and returns the type it is written of.
+ *
+ * @throws ParserException in case of the source is no type of its own
+ */
+ public function parse(ReadableInterface $source): TypeNode
+ {
+ $result = $this->build($source);
+
+ if (!$result instanceof SuccessfulResult || $result instanceof PartialResult) {
+ throw $this->createError($result, $source);
+ }
+
+ return $result->value;
+ }
+
+ /**
+ * Reads as much of the source as the grammar describes and returns the
+ * type that part is written of.
+ *
+ * @throws ParserException in case of an internal error occurs
+ */
+ public function partial(ReadableInterface $source): ParsedResult
+ {
+ $result = $this->build($source);
+
+ if ($result instanceof PartialResult) {
+ return new PartialParsedResult(
+ type: $result->value,
+ offset: $result->token->offset,
+ );
+ }
+
+ if ($result instanceof SuccessfulResult) {
+ return new SuccessfulParsedResult($result->value, $this->length($source));
+ }
+
+ return new FailureParsedResult(
+ message: $this->createError($result, $source)->getMessage(),
+ position: $this->createPosition($source, $result->token->offset),
+ offset: $result->token->offset,
+ );
+ }
+
+ /**
+ * Tells whether the source is a type the grammar describes, building
+ * nothing of it.
+ *
+ * @throws ParserException in case of an internal error occurs
+ */
+ public function validate(ReadableInterface $source): CheckResult
+ {
+ $result = $this->check($source);
+
+ if ($result instanceof SuccessfulResult && !$result instanceof PartialResult) {
+ return new SuccessfulCheckResult();
+ }
+
+ $error = $this->createError($result, $source);
+ $offset = $result->token->offset;
+ $position = $this->createPosition($source, $offset);
+
+ if ($result instanceof PartialResult) {
+ return new PartialCheckResult(
+ message: $error->getMessage(),
+ position: $position,
+ offset: $offset,
+ );
+ }
+
+ return new FailureCheckResult(
+ message: $error->getMessage(),
+ position: $position,
+ offset: $offset,
+ );
+ }
+
+ /**
+ * Reads the source into the type it describes.
+ *
+ * @return SuccessfulResult|FailureResult
+ * @throws ParserException in case of the grammar cannot be run
+ */
+ private function build(ReadableInterface $source): SuccessfulResult|FailureResult
+ {
+ assert($this->parser instanceof ParserRuntime);
+
+ try {
+ return $this->parser->analyze($source, Mode::Tolerant);
+ } catch (\Throwable $e) {
+ throw $this->raised($e, $source);
+ }
+ }
+
+ /**
+ * Reads the source without building anything of it.
+ *
+ * @return SuccessfulResult|FailureResult
+ * @throws ParserException in case of the grammar cannot be run
+ */
+ private function check(ReadableInterface $source): SuccessfulResult|FailureResult
+ {
+ assert($this->parser instanceof ParserRuntime);
+
+ try {
+ return $this->parser->analyze($source, Mode::SyntaxCheck);
+ } catch (\Throwable $e) {
+ throw $this->raised($e, $source);
+ }
+ }
+
+ /**
+ * Converts whatever the grammar raises while it reads into the error of
+ * this parser.
+ */
+ private function raised(\Throwable $e, ReadableInterface $source): ParserException
+ {
+ return match (true) {
+ $e instanceof ParserException => $e,
+ $e instanceof SourceExceptionInterface
+ => UnreadableSourceException::becauseSourceIsUnreadable($source, $e),
+ default => InternalParseException::becauseInternalErrorOccurs($source, $e),
+ };
+ }
+
+ /**
+ * Returns the length of the source, which is the offset a reading that
+ * has stopped at nothing ends at.
+ *
+ * @return int<0, max>
+ * @throws ParserExceptionInterface in case of the source cannot be read
+ */
+ private function length(ReadableInterface $source): int
+ {
+ try {
+ return \strlen($source->content);
+ } catch (\Throwable $e) {
+ throw $this->raised($e, $source);
+ }
+ }
+
+ /**
+ * @param int<0, max> $offset
+ * @throws SourceExceptionInterface
+ */
+ private function createPosition(ReadableInterface $source, int $offset): PositionInterface
+ {
+ return $this->positions->createFromOffset($source, $offset);
+ }
+
+ /**
+ * Converts the error of the grammar into the error of this parser.
+ *
+ * @param FailureResult|PartialResult $result
+ */
+ private function createError(FailureResult|PartialResult $result, ReadableInterface $source): ParserException
+ {
+ $error = $result->error;
+
+ if (!$error instanceof GrammarUnexpectedTokenException) {
+ return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized($source, $error->token);
+ }
+
+ // An input the lexer says nothing about is reported as an unrecognized
+ // one rather than as a token in a wrong place.
+ if ($error->token->channel === Channel::Unknown) {
+ return UnrecognizedTokenException::becauseTokenIsUnrecognized($source, $error->token);
+ }
+
+ return UnexpectedTokenException::becauseTokenIsUnexpected(
+ message: $error->getMessage(),
+ source: $source,
+ token: $error->token,
+ );
+ }
+}
diff --git a/libs/parser/src/Internal/FloatDecoder.php b/libs/parser/src/Internal/FloatDecoder.php
new file mode 100644
index 0000000..cd601b3
--- /dev/null
+++ b/libs/parser/src/Internal/FloatDecoder.php
@@ -0,0 +1,13 @@
+ \base_convert(\substr($value, 2), 16, 10),
+ // binary
+ 'b', 'B' => \base_convert(\substr($value, 2), 2, 10),
+ // octal
+ 'o', 'O' => \base_convert(\substr($value, 2), 8, 10),
+ // octal (legacy)
+ default => \base_convert($value, 8, 10),
+ };
+ }
+}
diff --git a/libs/parser/src/Internal/IntDecoder/BigIntValue.php b/libs/parser/src/Internal/IntDecoder/BigIntValue.php
new file mode 100644
index 0000000..3c2e2de
--- /dev/null
+++ b/libs/parser/src/Internal/IntDecoder/BigIntValue.php
@@ -0,0 +1,22 @@
+|non-empty-string, callable(Context, mixed):mixed> $reducers
- */
- public function __construct(
- private array $reducers,
- ) {}
-
- public function build(Context $context, mixed $result): mixed
- {
- if (!isset($this->reducers[$context->state])) {
- return $result;
- }
-
- $result = ($this->reducers[$context->state])($context, $result);
-
- if ($result instanceof Node && $result->offset === 0) {
- $result->offset = $context->lastProcessedToken->getOffset();
- }
-
- return $result;
- }
-}
diff --git a/libs/parser/src/Internal/StringDecoder.php b/libs/parser/src/Internal/StringDecoder.php
new file mode 100644
index 0000000..cc0e12e
--- /dev/null
+++ b/libs/parser/src/Internal/StringDecoder.php
@@ -0,0 +1,61 @@
+ '"']);
+ }
+
+ return \strtr(\substr($value, 1, -1), ["\'" => "'"]);
+ }
+
+ /**
+ * Decodes the sequences of an unpacked body: A `\\` in a single-quoted
+ * one, and special chars (like a `\n`), hexadecimal (like a `\xFF`),
+ * octal (like a `\101`) and utf-8 (like a `\u{FFFF}`) ones in a
+ * double-quoted one.
+ */
+ public static function decode(string $value, bool $isDoubleQuoted): string
+ {
+ if ($isDoubleQuoted === false) {
+ return \strtr($value, ['\\\\' => '\\']);
+ }
+
+ if (!\str_contains($value, '\\')) {
+ return $value;
+ }
+
+ return \strtr($value, StringSequencesFetcher::get($value));
+ }
+
+ /**
+ * Both of the above, in the order a literal is read in.
+ */
+ public static function unpackAndDecode(string $value, bool $isDoubleQuoted): string
+ {
+ return StringDecoder::decode(
+ StringDecoder::unpack($value, $isDoubleQuoted),
+ $isDoubleQuoted,
+ );
+ }
+}
diff --git a/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php
new file mode 100644
index 0000000..2c441a2
--- /dev/null
+++ b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php
@@ -0,0 +1,93 @@
+
+ */
+ private const ESCAPED_CHARS = [
+ '\n' => "\n",
+ '\r' => "\r",
+ '\t' => "\t",
+ '\v' => "\v",
+ '\e' => "\e",
+ '\f' => "\f",
+ '\$' => '$',
+ '\\\\' => '\\',
+ ];
+
+ /**
+ * @var non-empty-string
+ */
+ private const NUMERIC_PREFIX_PATTERN = '/\\\\[uxX0-7]/';
+
+ /**
+ * @var non-empty-string
+ */
+ private const NUMERIC_SEQUENCE_PATTERN = '/\\\\(?:u\{([0-9a-fA-F]+)}|[xX]([0-9a-fA-F]{1,2})|([0-7]{1,3}))/';
+
+ /**
+ * Returns a "sequence => replacement" map of all constant sequences along
+ * with each hexadecimal (like a "\xFF"), octal (like a "\101") and utf-8
+ * (like a "\u{FFFF}") sequence occurred in the past string.
+ *
+ * @link https://www.php.net/manual/en/language.types.string.php
+ *
+ * @return non-empty-array
+ */
+ public static function get(string $value): array
+ {
+ if (@\preg_match(self::NUMERIC_PREFIX_PATTERN, $value) !== 1) {
+ return self::ESCAPED_CHARS;
+ }
+
+ $count = @\preg_match_all(self::NUMERIC_SEQUENCE_PATTERN, $value, $matches, \PREG_SET_ORDER);
+
+ if ($count === false || $count === 0) {
+ return self::ESCAPED_CHARS;
+ }
+
+ $result = self::ESCAPED_CHARS;
+
+ /** @var list $matches */
+ foreach ($matches as $match) {
+ if (isset($result[$match[0]])) {
+ continue;
+ }
+
+ // A unicode sequence, like a "\u{FFFF}"
+ if ($match[1] !== '') {
+ /** @var int<0, max> $code */
+ $code = (int) \hexdec($match[1]);
+
+ $result[$match[0]] = UtfCharRenderer::render($code);
+
+ continue;
+ }
+
+ // A hexadecimal sequence, like a "\xFF"
+ if (($match[2] ?? '') !== '') {
+ // @phpstan-ignore-next-line : A hexdec returns int<0, 255>
+ $result[$match[0]] = \chr((int) \hexdec($match[2] ?? ''));
+
+ continue;
+ }
+
+ // An octal sequence, like a "\101". Overflowed sequences
+ // (greater than a "\377") are truncated, like in PHP itself.
+ $result[$match[0]] = \chr(((int) \octdec($match[3] ?? '')) & 0xFF);
+ }
+
+ return $result;
+ }
+}
diff --git a/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php
new file mode 100644
index 0000000..416e737
--- /dev/null
+++ b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php
@@ -0,0 +1,63 @@
+
+ */
+ private const MAX_CODE_POINT = 0x10FFFF;
+
+ /**
+ * @var non-empty-string
+ */
+ private const REPLACEMENT_CHAR = "\u{FFFD}";
+
+ /**
+ * Method for encoding an utf-8 character by its code.
+ *
+ * Codes above the last Unicode code point are encoded as a replacement
+ * char because such a sequence cannot be expressed in utf-8.
+ *
+ * @param int<0, max> $code
+ */
+ public static function render(int $code): string
+ {
+ if ($code > self::MAX_CODE_POINT) {
+ return self::REPLACEMENT_CHAR;
+ }
+
+ // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation
+ if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) {
+ return $result;
+ }
+
+ if (0x80 > $code) {
+ // @phpstan-ignore-next-line : Code is valid
+ return \chr($code);
+ }
+
+ if (0x800 > $code) {
+ return \chr(0xC0 | $code >> 6)
+ . \chr(0x80 | $code & 0x3F);
+ }
+
+ if (0x10000 > $code) {
+ return \chr(0xE0 | $code >> 12)
+ . \chr(0x80 | $code >> 6 & 0x3F)
+ . \chr(0x80 | $code & 0x3F);
+ }
+
+ return \chr(0xF0 | $code >> 18)
+ . \chr(0x80 | $code >> 12 & 0x3F)
+ . \chr(0x80 | $code >> 6 & 0x3F)
+ . \chr(0x80 | $code & 0x3F);
+ }
+}
diff --git a/libs/parser/src/ParsedResult.php b/libs/parser/src/ParsedResult.php
deleted file mode 100644
index 70862ff..0000000
--- a/libs/parser/src/ParsedResult.php
+++ /dev/null
@@ -1,20 +0,0 @@
-
- */
- public int $offset,
- ) {}
-}
diff --git a/libs/parser/src/Partial/FailureParsedResult.php b/libs/parser/src/Partial/FailureParsedResult.php
new file mode 100644
index 0000000..7e4b202
--- /dev/null
+++ b/libs/parser/src/Partial/FailureParsedResult.php
@@ -0,0 +1,32 @@
+ $offset
+ */
+ public function __construct(
+ /**
+ * What stands in the way of the reading.
+ */
+ public readonly string $message,
+ /**
+ * The line and the column the reading has stopped at.
+ */
+ public readonly PositionInterface $position,
+ /**
+ * The offset the reading has stopped at, which is the one the
+ * {@see $position} above is made of.
+ */
+ public readonly int $offset,
+ ) {}
+}
diff --git a/libs/parser/src/Partial/ParsedResult.php b/libs/parser/src/Partial/ParsedResult.php
new file mode 100644
index 0000000..3032bfa
--- /dev/null
+++ b/libs/parser/src/Partial/ParsedResult.php
@@ -0,0 +1,18 @@
+partial($source);
+ *
+ * echo \substr($source, $result->offset);
+ * // => " and more"
+ * ```
+ */
+final class PartialParsedResult extends SuccessfulParsedResult {}
diff --git a/libs/parser/src/Partial/SuccessfulParsedResult.php b/libs/parser/src/Partial/SuccessfulParsedResult.php
new file mode 100644
index 0000000..0e4c73b
--- /dev/null
+++ b/libs/parser/src/Partial/SuccessfulParsedResult.php
@@ -0,0 +1,33 @@
+ $offset
+ */
+ public function __construct(
+ /**
+ * The type the source is read into.
+ */
+ public readonly TypeNode $type,
+ /**
+ * The offset the reading has stopped at.
+ */
+ public readonly int $offset,
+ ) {}
+}
diff --git a/libs/parser/src/Traverser.php b/libs/parser/src/Traverser.php
index 6a728d3..755eb75 100644
--- a/libs/parser/src/Traverser.php
+++ b/libs/parser/src/Traverser.php
@@ -23,7 +23,9 @@ public function __construct(
iterable $visitors = [],
private PropertyAccessorInterface $propertyAccessor = new SimplePropertyAccessor(),
) {
- $this->visitors = \iterator_to_array($visitors, false);
+ $this->visitors = \is_array($visitors)
+ ? \array_values($visitors)
+ : \iterator_to_array($visitors, false);
}
/**
diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php
index 8d0131a..4af86b3 100644
--- a/libs/parser/src/Traverser/DumperVisitor.php
+++ b/libs/parser/src/Traverser/DumperVisitor.php
@@ -11,7 +11,7 @@ abstract class DumperVisitor extends Visitor
/**
* @var non-empty-string
*/
- public const string DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\';
+ public const DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\';
/**
* @var int<0, max>
@@ -35,7 +35,9 @@ public function enter(Node $node): ?Command
$suffix = \str_replace($this->simplifyNodeNamespace, '', $node::class);
if ($node instanceof \Stringable) {
- $suffix .= \sprintf('(%s)', (string) $node);
+ $suffix .= $this->printStringableNodeSuffix($node);
+ } else {
+ $suffix .= $this->printNodePropertiesSuffix($node);
}
$this->write($prefix . $suffix . "\n");
@@ -43,6 +45,47 @@ public function enter(Node $node): ?Command
return null;
}
+ /**
+ * @return non-empty-string
+ */
+ private function printStringableNodeSuffix(\Stringable $node): string
+ {
+ return \sprintf('(%s)', (string) $node);
+ }
+
+ /**
+ * Returns a "(prop=value, ...)" suffix built from the writable scalar
+ * properties of the node, or an empty string in case of there are none.
+ */
+ private function printNodePropertiesSuffix(Node $node): string
+ {
+ $result = [];
+
+ $reflection = new \ReflectionObject($node);
+
+ foreach ($reflection->getProperties() as $property) {
+ // Skip readonly + static and builtin "offset" properties
+ if ($property->isStatic() || $property->isReadOnly() || $property->getName() === 'offset') {
+ continue;
+ }
+
+ $value = $property->getValue($node);
+
+ // Skip non-scalar properties
+ if (!\is_scalar($value)) {
+ continue;
+ }
+
+ $result[] = \sprintf('%s=%s', $property->getName(), \var_export($value, true));
+ }
+
+ if ($result === []) {
+ return '';
+ }
+
+ return \sprintf('(%s)', \implode(', ', $result));
+ }
+
public function leave(Node $node): void
{
// @phpstan-ignore-next-line : $depth is always non-negative
diff --git a/libs/parser/src/Traverser/MatcherVisitor.php b/libs/parser/src/Traverser/MatcherVisitor.php
index 9dba466..37e7e5d 100644
--- a/libs/parser/src/Traverser/MatcherVisitor.php
+++ b/libs/parser/src/Traverser/MatcherVisitor.php
@@ -8,11 +8,7 @@
class MatcherVisitor extends Visitor
{
- public private(set) ?Node $node = null;
-
- public bool $isFound {
- get => $this->node !== null;
- }
+ public ?Node $node = null;
private bool $shouldContinue = false;
@@ -25,9 +21,18 @@ public function __construct(
private readonly ?\Closure $break = null,
) {}
+ /**
+ * Returns {@see true} in case of a node matching the criteria was found.
+ */
+ public function hasMatches(): bool
+ {
+ return $this->node !== null;
+ }
+
public function before(): void
{
$this->node = null;
+ $this->shouldContinue = false;
}
public function enter(Node $node): ?Command
diff --git a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php
index 685184e..e16081c 100644
--- a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php
+++ b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php
@@ -4,15 +4,20 @@
namespace TypeLang\Parser\Traverser\PropertyAccessor;
-final readonly class SimplePropertyAccessor implements PropertyAccessorInterface
+final class SimplePropertyAccessor implements PropertyAccessorInterface
{
/**
* Skips static and hooked properties
*/
private function shouldSkip(\ReflectionProperty $property): bool
{
- return $property->isStatic()
- || $property->hasHooks();
+ if ($property->isStatic() || $property->isReadOnly()) {
+ return true;
+ }
+
+ // Property hooks are available since PHP 8.4
+ return \method_exists($property, 'hasHooks')
+ && $property->hasHooks();
}
public function unwrap(object $object): iterable
diff --git a/libs/parser/src/Traverser/StreamDumperVisitor.php b/libs/parser/src/Traverser/StreamDumperVisitor.php
index 489ba98..1340265 100644
--- a/libs/parser/src/Traverser/StreamDumperVisitor.php
+++ b/libs/parser/src/Traverser/StreamDumperVisitor.php
@@ -6,7 +6,7 @@
final class StreamDumperVisitor extends DumperVisitor
{
- public const string DEFAULT_OUTPUT_STREAM = 'php://stderr';
+ public const DEFAULT_OUTPUT_STREAM = 'php://stderr';
/**
* @var resource
diff --git a/libs/parser/src/Traverser/StringDumperVisitor.php b/libs/parser/src/Traverser/StringDumperVisitor.php
index b336e74..4ca6951 100644
--- a/libs/parser/src/Traverser/StringDumperVisitor.php
+++ b/libs/parser/src/Traverser/StringDumperVisitor.php
@@ -6,7 +6,7 @@
final class StringDumperVisitor extends DumperVisitor
{
- public private(set) string $output = '';
+ public string $output = '';
public function before(): void
{
diff --git a/libs/parser/src/Traverser/TypeMapVisitor.php b/libs/parser/src/Traverser/TypeMapVisitor.php
index 76cc0ff..a15be2d 100644
--- a/libs/parser/src/Traverser/TypeMapVisitor.php
+++ b/libs/parser/src/Traverser/TypeMapVisitor.php
@@ -37,11 +37,19 @@ public function enter(Node $node): ?Command
switch (true) {
case $node instanceof NamedTypeNode:
case $node instanceof CallableTypeNode:
- case $node instanceof ConstMaskNode:
$node->name = $this->map($node->name);
return null;
+ case $node instanceof ConstMaskNode:
+ // A mask carries no namespace of its own in case of the
+ // constant is written with none, and nothing is mapped then
+ if ($node->namespaceOrFullyQualified instanceof Name) {
+ $node->namespaceOrFullyQualified = $this->map($node->namespaceOrFullyQualified);
+ }
+
+ return null;
+
case $node instanceof ClassConstNode:
case $node instanceof ClassConstMaskNode:
$node->class = $this->map($node->class);
diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php
index d17fbdc..dadb5fe 100644
--- a/libs/parser/src/TypeParser.php
+++ b/libs/parser/src/TypeParser.php
@@ -5,37 +5,34 @@
namespace TypeLang\Parser;
use JetBrains\PhpStorm\Language;
-use Phplrt\Contracts\Parser\ParserRuntimeExceptionInterface;
+use Phplrt\Contracts\Lexer\TokenInterface;
+use Phplrt\Contracts\Source\Exception\SourceExceptionInterface;
use Phplrt\Contracts\Source\ReadableInterface;
-use Phplrt\Contracts\Source\SourceExceptionInterface;
use Phplrt\Contracts\Source\SourceFactoryInterface;
-use Phplrt\Parser\Exception\UnexpectedTokenException as LexerUnexpectedTokenException;
-use Phplrt\Parser\Exception\UnrecognizedTokenException as LexerUnrecognizedTokenException;
use Phplrt\Source\SourceFactory;
-use TypeLang\Parser\Exception\InternalParseException;
-use TypeLang\Parser\Exception\ParseException;
-use TypeLang\Parser\Exception\SemanticException;
-use TypeLang\Parser\Exception\SemanticParseException;
-use TypeLang\Parser\Exception\UnexpectedTokenException;
-use TypeLang\Parser\Exception\UnrecognizedSyntaxException;
-use TypeLang\Parser\Exception\UnrecognizedTokenException;
-use TypeLang\Parser\Internal\ExecutionContext;
+use TypeLang\Parser\Exception\ParserExceptionInterface;
+use TypeLang\Parser\Internal\Executor;
+use TypeLang\Parser\Partial\ParsedResult;
+use TypeLang\Parser\Validation\CheckResult;
use TypeLang\Type\TypeNode;
+/**
+ * @template-contravariant TSource of mixed = mixed
+ *
+ * @template-implements TypeParserInterface
+ */
final class TypeParser implements TypeParserInterface
{
- private ExecutionContext $strict {
- get => $this->strict ??= new ExecutionContext($this->features, false);
- }
+ private readonly SourceFactoryInterface $sources;
- private ExecutionContext $tolerant {
- get => $this->tolerant ??= new ExecutionContext($this->features, true);
- }
+ private ?Executor $executor = null;
public function __construct(
public readonly TypeParserFeatures $features = new TypeParserFeatures(),
- private readonly SourceFactoryInterface $sources = new SourceFactory(),
- ) {}
+ ?SourceFactoryInterface $sources = null,
+ ) {
+ $this->sources = $sources ?? SourceFactory::createDefault();
+ }
/**
* Returns a new parser with an overridden parser feature flag.
@@ -55,99 +52,72 @@ public function withFeatures(bool ...$features): self
);
}
- public function parse(#[Language('PHP')] mixed $source): TypeNode
+ /**
+ * Reads the provided source code into the tokens it is written of,
+ * building nothing out of them.
+ *
+ * The tokens are what every other method of this parser reads the source
+ * through, so this is the source as the grammar sees it: what a token is
+ * called, what it carries, and where it stands.
+ *
+ * ```
+ * foreach ($parser->lex('array{ field: result }') as $token) {
+ * echo $token->name . ' ' . $token->value . \PHP_EOL;
+ * }
+ *
+ * // => T_NAME array
+ * // => T_BRACE_OPEN {
+ * // => ...
+ * ```
+ *
+ * @param TSource $source source code to read
+ * @return iterable the tokens the source is
+ * written of
+ * @throws \Throwable in case of internal error occurs
+ */
+ public function lex(#[Language('PHP')] mixed $source): iterable
{
- $result = $this->execute($this->strict, $source);
+ $executor = $this->getExecutor();
- return $result->type;
+ return $executor->lex($this->toSource($source));
}
- public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult
+ public function parse(#[Language('PHP')] mixed $source): TypeNode
{
- return $this->execute($this->tolerant, $source);
- }
+ $executor = $this->getExecutor();
- private function execute(ExecutionContext $context, mixed $source): ParsedResult
- {
- try {
- $instance = $this->sources->create($source);
-
- try {
- return $context->parse($instance)
- ?? throw InternalParseException::becauseTypeStatementIsUnreadable();
- } catch (LexerUnexpectedTokenException $e) {
- throw $this->unexpectedTokenError($e, $instance);
- } catch (LexerUnrecognizedTokenException $e) {
- throw $this->unrecognizedTokenError($e, $instance);
- } catch (ParserRuntimeExceptionInterface $e) {
- throw $this->parserRuntimeError($e, $instance);
- } catch (SemanticException $e) {
- throw $this->semanticError($e, $instance);
- } catch (\Throwable $e) {
- throw $this->internalError($e, $instance);
- }
- } catch (SourceExceptionInterface $e) {
- throw InternalParseException::becauseSourceIsUnreadable($e);
- }
+ return $executor->parse($this->toSource($source));
}
- /**
- * @throws SourceExceptionInterface in case of source content reading error
- */
- private function unexpectedTokenError(LexerUnexpectedTokenException $e, ReadableInterface $source): ParseException
+ public function partial(#[Language('PHP')] mixed $source): ParsedResult
{
- $token = $e->getToken();
+ $executor = $this->getExecutor();
- return UnexpectedTokenException::becauseTokenIsUnexpected(
- token: $token->getValue(),
- statement: $source->getContents(),
- offset: $token->getOffset(),
- );
+ return $executor->partial($this->toSource($source));
}
- /**
- * @throws SourceExceptionInterface in case of source content reading error
- */
- private function unrecognizedTokenError(LexerUnrecognizedTokenException $e, ReadableInterface $source): ParseException
+ public function validate(#[Language('PHP')] mixed $source): CheckResult
{
- $token = $e->getToken();
-
- return UnrecognizedTokenException::becauseTokenIsUnrecognized(
- token: $token->getValue(),
- statement: $source->getContents(),
- offset: $token->getOffset(),
- );
- }
+ $executor = $this->getExecutor();
- /**
- * @throws SourceExceptionInterface in case of source content reading error
- */
- private function semanticError(SemanticException $e, ReadableInterface $source): ParseException
- {
- return SemanticParseException::becauseSemanticErrorOccurs($e, $source);
+ return $executor->validate($this->toSource($source));
}
/**
- * @throws SourceExceptionInterface in case of source content reading error
+ * @throws SourceExceptionInterface in case of no source can be created out
+ * of the given value
*/
- private function parserRuntimeError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException
+ private function toSource(mixed $source): ReadableInterface
{
- $token = $e->getToken();
-
- return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized(
- statement: $source->getContents(),
- offset: $token->getOffset(),
- );
+ return $this->sources->create($source);
}
/**
- * @throws SourceExceptionInterface in case of source content reading error
+ * Returns a lazily created parser recognizing a source with the features
+ * of this one.
*/
- private function internalError(\Throwable $e, ReadableInterface $source): ParseException
+ private function getExecutor(): Executor
{
- return InternalParseException::becauseInternalErrorOccurs(
- statement: $source->getContents(),
- e: $e,
- );
+ return $this->executor ??= new Executor($this->features);
}
}
diff --git a/libs/parser/src/TypeParserFeatures.php b/libs/parser/src/TypeParserFeatures.php
index 2c3dc3d..8c87aca 100644
--- a/libs/parser/src/TypeParserFeatures.php
+++ b/libs/parser/src/TypeParserFeatures.php
@@ -20,65 +20,60 @@
* // => Error: Conditional expressions not allowed in ...
* ```
*/
-final readonly class TypeParserFeatures
+final class TypeParserFeatures
{
- public const bool CONDITIONAL_FEATURES_DEFAULT_VALUE = true;
- public const bool SHAPES_FEATURES_DEFAULT_VALUE = true;
- public const bool CALLABLES_FEATURES_DEFAULT_VALUE = true;
- public const bool LITERALS_FEATURES_DEFAULT_VALUE = true;
- public const bool GENERICS_FEATURES_DEFAULT_VALUE = true;
- public const bool UNION_FEATURES_DEFAULT_VALUE = true;
- public const bool INTERSECTION_FEATURES_DEFAULT_VALUE = true;
- public const bool LIST_FEATURES_DEFAULT_VALUE = true;
- public const bool OFFSETS_FEATURES_DEFAULT_VALUE = true;
- public const bool HINTS_FEATURES_DEFAULT_VALUE = true;
- public const bool ATTRIBUTES_FEATURES_DEFAULT_VALUE = true;
+ public const CONDITIONAL_FEATURES_DEFAULT_VALUE = true;
+ public const SHAPES_FEATURES_DEFAULT_VALUE = true;
+ public const CALLABLES_FEATURES_DEFAULT_VALUE = true;
+ public const LITERALS_FEATURES_DEFAULT_VALUE = true;
+ public const GENERICS_FEATURES_DEFAULT_VALUE = true;
+ public const UNION_FEATURES_DEFAULT_VALUE = true;
+ public const INTERSECTION_FEATURES_DEFAULT_VALUE = true;
+ public const LIST_FEATURES_DEFAULT_VALUE = true;
+ public const OFFSETS_FEATURES_DEFAULT_VALUE = true;
+ public const HINTS_FEATURES_DEFAULT_VALUE = true;
public function __construct(
/**
* Enables or disables support for conditional types such as `T ? U : V`
*/
- public bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE,
+ public readonly bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for shape types such as `T{key: U}`
*/
- public bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE,
+ public readonly bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for callable types such as `fn(T, U): V`
*/
- public bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE,
+ public readonly bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for literal types such as `42` or `'foo'`
*/
- public bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE,
+ public readonly bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for generic type arguments such as `T`
*/
- public bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE,
+ public readonly bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for union types such as `T | U`
*/
- public bool $unions = self::UNION_FEATURES_DEFAULT_VALUE,
+ public readonly bool $unions = self::UNION_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for intersection types such as `T & U`
*/
- public bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE,
+ public readonly bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for short list types such as `T[]`
*/
- public bool $lists = self::LIST_FEATURES_DEFAULT_VALUE,
+ public readonly bool $lists = self::LIST_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for offset access types such as `T[U]`
*/
- public bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE,
+ public readonly bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE,
/**
* Enables or disables support for generic variance hints such as `T`
*/
- public bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE,
- /**
- * Enables or disables support for attributes such as `#[attr]`
- */
- public bool $attributes = self::ATTRIBUTES_FEATURES_DEFAULT_VALUE,
+ public readonly bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE,
) {}
/**
diff --git a/libs/parser/src/TypeParserInterface.php b/libs/parser/src/TypeParserInterface.php
index b2163cc..2b9d99f 100644
--- a/libs/parser/src/TypeParserInterface.php
+++ b/libs/parser/src/TypeParserInterface.php
@@ -6,6 +6,8 @@
use JetBrains\PhpStorm\Language;
use TypeLang\Parser\Exception\ParserExceptionInterface;
+use TypeLang\Parser\Partial\ParsedResult;
+use TypeLang\Parser\Validation\CheckResult;
use TypeLang\Type\TypeNode;
/**
@@ -49,7 +51,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode;
* code.
*
* ```
- * $result = $parser->tolerant('array{ field: result } This is an example');
+ * $result = $parser->partial('array{ field: result } This is an example');
*
* $result->type;
* // => NamedTypeNode{ name: "array", ...
@@ -61,7 +63,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode;
* To get information about the remaining part, just apply `substr` to source:
* ```
* $source = 'array{ field: result } This is an example';
- * $result = $parser->tolerant($source);
+ * $result = $parser->partial($source);
*
* echo substr($source, $result->offset);
* // => "This is an example"
@@ -73,5 +75,31 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode;
* @throws ParserExceptionInterface in case of parsing exception occurs
* @throws \Throwable in case of internal error occurs
*/
- public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult;
+ public function partial(#[Language('PHP')] mixed $source): ParsedResult;
+
+ /**
+ * Tells whether the provided source code is a type, building nothing
+ * of it.
+ *
+ * Nothing is built while a source is checked, so this method is the
+ * cheaper one of the three whenever the type itself is of no use.
+ *
+ * ```
+ * $parser->validate('array{ field: result }');
+ * // => SuccessfulCheckResult
+ *
+ * $parser->validate('array{ field: result } and more');
+ * // => PartialCheckResult
+ *
+ * $parser->validate('array{');
+ * // => FailureCheckResult
+ * ```
+ *
+ * @param TSource $source source code to check
+ * @return CheckResult what stands in the way of the reading, in case of
+ * anything does
+ * @throws ParserExceptionInterface in case of parsing exception occurs
+ * @throws \Throwable in case of internal error occurs
+ */
+ public function validate(#[Language('PHP')] mixed $source): CheckResult;
}
diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php
index 687c994..13d0a40 100644
--- a/libs/parser/src/TypeResolver.php
+++ b/libs/parser/src/TypeResolver.php
@@ -9,13 +9,13 @@
use TypeLang\Parser\TypeResolver\PhpUseStatementsTransformer;
use TypeLang\Type\TypeNode;
-final readonly class TypeResolver
+final class TypeResolver
{
public function __construct(
/**
* @var array
*/
- private array $imports = [],
+ private readonly array $imports = [],
) {}
/**
@@ -116,8 +116,8 @@ public function withTypeImportAs(string $name, string $alias): self
*/
public function withTypeImportsFromClass(\ReflectionClass $class): self
{
- $statements = new PhpUseStatementsReader()
- ->getClassUseStatements($class);
+ $reader = new PhpUseStatementsReader();
+ $statements = $reader->getClassUseStatements($class);
return new self([...$this->imports, ...$statements]);
}
@@ -162,8 +162,8 @@ public function withTypeImportsFromFunction(\ReflectionFunctionAbstract $functio
return $this->withTypeImportsFromClass($function->getDeclaringClass());
}
- $statements = new PhpUseStatementsReader()
- ->getFunctionUseStatements($function);
+ $reader = new PhpUseStatementsReader();
+ $statements = $reader->getFunctionUseStatements($function);
return new self([...$this->imports, ...$statements]);
}
diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php
index 4b72cb2..1f82480 100644
--- a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php
+++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php
@@ -7,10 +7,10 @@
use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\NamespaceFinder;
use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\ReflectionSourcePrefixReader;
-final readonly class PhpUseStatementsReader
+final class PhpUseStatementsReader
{
- private ReflectionSourcePrefixReader $reader;
- private NamespaceFinder $namespace;
+ private readonly ReflectionSourcePrefixReader $reader;
+ private readonly NamespaceFinder $namespace;
public function __construct()
{
@@ -48,7 +48,23 @@ public function getFunctionUseStatements(\ReflectionFunctionAbstract $function):
$header = '';
}
- return [...$this->parse($function->getNamespaceName(), $header)];
+ return [...$this->parse($this->getFunctionNamespace($function, $header), $header)];
+ }
+
+ /**
+ * Returns the namespace the given function is written in.
+ */
+ private function getFunctionNamespace(\ReflectionFunctionAbstract $function, string $header): string
+ {
+ if (\PHP_VERSION_ID < 80400) {
+ return $function->getNamespaceName();
+ }
+
+ if ($function->isClosure()) {
+ return $this->namespace->findLast($this->lex($header));
+ }
+
+ return $function->getNamespaceName();
}
/**
diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php
index e4b7283..772cdd0 100644
--- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php
+++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php
@@ -4,8 +4,31 @@
namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader;
-final readonly class NamespaceFinder
+final class NamespaceFinder
{
+ /**
+ * Returns the last namespace the given source declares, which is the one
+ * whatever the source ends with is written in.
+ *
+ * @param \Iterator $tokens
+ */
+ public function findLast(\Iterator $tokens): string
+ {
+ $result = '';
+
+ while ($tokens->valid()) {
+ if ($tokens->current()->id === \T_NAMESPACE) {
+ $result = $this->readNamespace($tokens);
+
+ continue;
+ }
+
+ $tokens->next();
+ }
+
+ return $result;
+ }
+
/**
* @param \Iterator $tokens
* @return \Iterator
diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php
index dfce2d1..186d7d5 100644
--- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php
+++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php
@@ -4,7 +4,7 @@
namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader;
-final readonly class ReflectionSourcePrefixReader
+final class ReflectionSourcePrefixReader
{
/**
* @param \ReflectionClass