diff --git a/composer.json b/composer.json index 5160d18..7e114e6 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require-dev": { "ergebnis/composer-normalize": "^2.15", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^1.9", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", "slevomat/coding-standard": "^7.0", diff --git a/composer.lock b/composer.lock index ec737fb..3e3a7c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6d73951acc3119aabd404dbbe0967053", + "content-hash": "367dc3d722296f940d17b2cfb8efbc38", "packages": [ { "name": "psr/log", @@ -622,16 +622,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.9.2", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa" + "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d6fdf01c53978b6429f1393ba4afeca39cc68afa", - "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2", + "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2", "shasum": "" }, "require": { @@ -661,7 +661,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.9.2" + "source": "https://github.com/phpstan/phpstan/tree/1.9.4" }, "funding": [ { @@ -677,7 +677,7 @@ "type": "tidelift" } ], - "time": "2022-11-10T09:56:11+00:00" + "time": "2022-12-17T13:33:52+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", diff --git a/src/Atn/ATNConfig.php b/src/Atn/ATNConfig.php index 7e9c9b9..bfd45aa 100644 --- a/src/Atn/ATNConfig.php +++ b/src/Atn/ATNConfig.php @@ -128,10 +128,10 @@ public function equals(object $other): bool } return $other instanceof self + && $this->state->stateNumber === $other->state->stateNumber && $this->alt === $other->alt && $this->isPrecedenceFilterSuppressed() === $other->isPrecedenceFilterSuppressed() && $this->semanticContext->equals($other->semanticContext) - && Equality::equals($this->state, $other->state) && Equality::equals($this->context, $other->context); } @@ -180,7 +180,7 @@ public function __toString(): string $this->semanticContext->equals(SemanticContext::none()) ? '' : ',' . $this->semanticContext, - $this->reachesIntoOuterContext > 0 ? ',up=' . $this->reachesIntoOuterContext : '', + $this->reachesIntoOuterContext > 0 ? ',up=' . $this->getOuterContextDepth() : '', ); } } diff --git a/src/Atn/ATNConfigSet.php b/src/Atn/ATNConfigSet.php index 319cd6b..c9ed1c1 100644 --- a/src/Atn/ATNConfigSet.php +++ b/src/Atn/ATNConfigSet.php @@ -5,8 +5,8 @@ namespace Antlr\Antlr4\Runtime\Atn; use Antlr\Antlr4\Runtime\Atn\SemanticContexts\SemanticContext; +use Antlr\Antlr4\Runtime\Atn\States\ATNState; use Antlr\Antlr4\Runtime\Comparison\Equality; -use Antlr\Antlr4\Runtime\Comparison\Equivalence; use Antlr\Antlr4\Runtime\Comparison\Hashable; use Antlr\Antlr4\Runtime\Comparison\Hasher; use Antlr\Antlr4\Runtime\PredictionContexts\PredictionContext; @@ -34,7 +34,8 @@ class ATNConfigSet implements Hashable * All configs but hashed by (s, i, _, pi) not including context. Wiped out * when we go readonly as this set becomes a DFA state. */ - public ?Set $configLookup = null; + // ConfigHashSet : Dictionary + public ?ConfigHashSet $configLookup = null; /** * Track the elements as they are added to the set; supports get(i). @@ -82,32 +83,7 @@ public function __construct(bool $fullCtx = true) * not including context. Wiped out when we go readonly as this se * becomes a DFA state. */ - $this->configLookup = new Set(new class implements Equivalence { - public function equivalent(Hashable $left, Hashable $right): bool - { - if ($left === $right) { - return true; - } - - if (!$left instanceof ATNConfig || !$right instanceof ATNConfig) { - return false; - } - - return $left->alt === $right->alt - && $left->semanticContext->equals($right->semanticContext) - && Equality::equals($left->state, $right->state); - } - - public function hash(Hashable $value): int - { - return $value->hashCode(); - } - - public function equals(object $other): bool - { - return $other instanceof self; - } - }); + $this->configLookup = new ConfigHashSet(); $this->fullCtx = $fullCtx; } @@ -133,14 +109,14 @@ public function add(ATNConfig $config, ?DoubleKeyMap $mergeCache = null): bool $this->hasSemanticContext = true; } - if ($config->reachesIntoOuterContext > 0) { + if ($config->getOuterContextDepth() > 0) { $this->dipsIntoOuterContext = true; } /** @var ATNConfig $existing */ $existing = $this->configLookup->getOrAdd($config); - if ($existing->equals($config)) { + if ($existing === $config) { $this->cachedHashCode = null; $this->configs[] = $config; // track order here @@ -149,12 +125,14 @@ public function add(ATNConfig $config, ?DoubleKeyMap $mergeCache = null): bool } // A previous (s,i,pi,_), merge with it and save result + /** @var bool $rootIsWildcard */ $rootIsWildcard = !$this->fullCtx; if ($existing->context === null || $config->context === null) { throw new \LogicException('Unexpected null context.'); } + /** @var PredictionContext $merged */ $merged = PredictionContext::merge($existing->context, $config->context, $rootIsWildcard, $mergeCache); // No need to check for existing->context, config->context in cache @@ -186,8 +164,12 @@ public function elements(): array return $this->configs; } + /** + * @return Set + */ public function getStates(): Set { + /** @var Set $states */ $states = new Set(); foreach ($this->configs as $config) { $states->add($config->state); @@ -305,6 +287,10 @@ public function contains(object $item): bool throw new \InvalidArgumentException('This method is not implemented for readonly sets.'); } + if (!($item instanceof ATNConfig)) { + return false; + } + return $this->configLookup->contains($item); } @@ -325,8 +311,8 @@ public function clear(): void } $this->configs = []; - $this->cachedHashCode = -1; - $this->configLookup = new Set(); + $this->cachedHashCode = null; + $this->configLookup = new ConfigHashSet(); } public function isReadOnly(): bool @@ -358,7 +344,7 @@ public function __toString(): string return \sprintf( '[%s]%s%s%s%s', \implode(', ', $this->configs), - $this->hasSemanticContext ? ',hasSemanticContext=' . $this->hasSemanticContext : '', + $this->hasSemanticContext ? ',hasSemanticContext=true' : '', $this->uniqueAlt !== ATN::INVALID_ALT_NUMBER ? ',uniqueAlt=' . $this->uniqueAlt : '', $this->conflictingAlts !== null ? ',conflictingAlts=' . $this->conflictingAlts : '', $this->dipsIntoOuterContext ? ',dipsIntoOuterContext' : '', diff --git a/src/Atn/ATNSimulator.php b/src/Atn/ATNSimulator.php index 5ecce88..2aae6f9 100644 --- a/src/Atn/ATNSimulator.php +++ b/src/Atn/ATNSimulator.php @@ -5,6 +5,7 @@ namespace Antlr\Antlr4\Runtime\Atn; use Antlr\Antlr4\Runtime\Dfa\DFAState; +use Antlr\Antlr4\Runtime\PredictionContexts\IdentityHashMap; use Antlr\Antlr4\Runtime\PredictionContexts\PredictionContext; use Antlr\Antlr4\Runtime\PredictionContexts\PredictionContextCache; @@ -92,7 +93,7 @@ public function getSharedContextCache(): PredictionContextCache public function getCachedContext(PredictionContext $context): PredictionContext { - $visited = []; + $visited = new IdentityHashMap(); return PredictionContext::getCachedPredictionContext( $context, diff --git a/src/Atn/ConfigHashSet.php b/src/Atn/ConfigHashSet.php new file mode 100644 index 0000000..9806a16 --- /dev/null +++ b/src/Atn/ConfigHashSet.php @@ -0,0 +1,81 @@ + + */ +final class ConfigHashSet extends Map +{ + public function __construct(?Equivalence $comparer = null) + { + if ($comparer === null) { + parent::__construct(new class implements Equivalence { + public function equivalent(Hashable $left, Hashable $right): bool + { + if (! $left instanceof ATNConfig) { + return false; + } + + if (! $right instanceof ATNConfig) { + return false; + } + + if ($left === $right) { + return true; + } + + return $left->state->stateNumber === $right->state->stateNumber + && $left->alt === $right->alt + && $left->semanticContext->equals($right->semanticContext); + } + + public function hash(Hashable $value): int + { + if (! $value instanceof ATNConfig) { + return 0; + } + + return Hasher::hash( + $value->state->stateNumber, + $value->alt, + $value->semanticContext, + ); + } + + public function equals(object $other): bool + { + return $other instanceof self; + } + }); + } else { + parent::__construct($comparer); + } + } + + public function getOrAdd(ATNConfig $config): ATNConfig + { + /** @var ?ATNConfig $existing */ + $existing = null; + if ($this->tryGetValue($config, $existing)) { + return $existing; + } else { + $this->put($config, $config); + + return $config; + } + } +} diff --git a/src/Atn/LexerATNConfig.php b/src/Atn/LexerATNConfig.php index 0b353c1..d021e3d 100644 --- a/src/Atn/LexerATNConfig.php +++ b/src/Atn/LexerATNConfig.php @@ -63,15 +63,15 @@ public function equals(object $other): bool return false; } - if (!parent::equals($other)) { + if ($this->passedThroughNonGreedyDecision !== $other->passedThroughNonGreedyDecision) { return false; } - if ($this->passedThroughNonGreedyDecision !== $other->passedThroughNonGreedyDecision) { + if (!Equality::equals($this->lexerActionExecutor, $other->lexerActionExecutor)) { return false; } - return Equality::equals($this->lexerActionExecutor, $other->lexerActionExecutor); + return parent::equals($other); } private static function checkNonGreedyDecision(LexerATNConfig $source, ATNState $target): bool diff --git a/src/Atn/OrderedATNConfigSet.php b/src/Atn/OrderedATNConfigSet.php index 6eeb804..e1da41d 100644 --- a/src/Atn/OrderedATNConfigSet.php +++ b/src/Atn/OrderedATNConfigSet.php @@ -4,7 +4,8 @@ namespace Antlr\Antlr4\Runtime\Atn; -use Antlr\Antlr4\Runtime\Utils\Set; +use Antlr\Antlr4\Runtime\Comparison\Equivalence; +use Antlr\Antlr4\Runtime\Comparison\Hashable; final class OrderedATNConfigSet extends ATNConfigSet { @@ -12,6 +13,35 @@ public function __construct() { parent::__construct(); - $this->configLookup = new Set(); + $this->configLookup = new ConfigHashSet(new class implements Equivalence { + public function equivalent(Hashable $left, Hashable $right): bool + { + if ($left === $right) { + return true; + } + + /** @phpstan-ignore-next-line */ + if ($left === null) { + return false; + } + + /** @phpstan-ignore-next-line */ + if ($right === null) { + return false; + } + + return $left->equals($right); + } + + public function hash(Hashable $value): int + { + return $value->hashCode(); + } + + public function equals(object $other): bool + { + return $other instanceof self; + } + }); } } diff --git a/src/Atn/States/ATNState.php b/src/Atn/States/ATNState.php index 8a9d51e..3261754 100644 --- a/src/Atn/States/ATNState.php +++ b/src/Atn/States/ATNState.php @@ -152,7 +152,7 @@ public function __toString(): string public function hashCode(): int { - return $this->getStateType(); + return $this->stateNumber; } abstract public function getStateType(): int; diff --git a/src/ParserTraceListener.php b/src/ParserTraceListener.php index d194b11..4e2e24c 100644 --- a/src/ParserTraceListener.php +++ b/src/ParserTraceListener.php @@ -27,6 +27,7 @@ public function enterEveryRule(ParserRuleContext $context): void $this->parser->getRuleNames()[$context->getRuleIndex()], $token === null? '' : $token->getText() ?? '', ); + echo \PHP_EOL; } public function visitTerminal(TerminalNode $node): void @@ -36,6 +37,7 @@ public function visitTerminal(TerminalNode $node): void $node->getSymbol(), $this->parser->getCurrentRuleName(), ); + echo \PHP_EOL; } public function exitEveryRule(ParserRuleContext $context): void @@ -48,6 +50,7 @@ public function exitEveryRule(ParserRuleContext $context): void $this->parser->getRuleNames()[$context->getRuleIndex()], $token === null? '' : $token->getText() ?? '', ); + echo \PHP_EOL; } public function visitErrorNode(ErrorNode $node): void diff --git a/src/PredictionContexts/ArrayPredictionContext.php b/src/PredictionContexts/ArrayPredictionContext.php index a174752..e96646a 100644 --- a/src/PredictionContexts/ArrayPredictionContext.php +++ b/src/PredictionContexts/ArrayPredictionContext.php @@ -86,7 +86,7 @@ public function equals(object $other): bool return false; } - if ($this->returnStates === $other->returnStates) { + if (!($this->returnStates === $other->returnStates)) { return false; } diff --git a/src/PredictionContexts/IdentityHashMap.php b/src/PredictionContexts/IdentityHashMap.php new file mode 100644 index 0000000..e3be649 --- /dev/null +++ b/src/PredictionContexts/IdentityHashMap.php @@ -0,0 +1,47 @@ + + */ +class IdentityHashMap extends Map +{ + public function __construct() + { + parent::__construct(new class implements Equivalence { + public function equivalent(Hashable $left, Hashable $right): bool + { + if (! $left instanceof PredictionContext) { + return false; + } + + if (! $right instanceof PredictionContext) { + return false; + } + + return $left === $right; + } + + public function hash(Hashable $value): int + { + if (! $value instanceof PredictionContext) { + return 0; + } + + return $value->hashCode(); + } + + public function equals(object $other): bool + { + return $other instanceof self; + } + }); + } +} diff --git a/src/PredictionContexts/PredictionContext.php b/src/PredictionContexts/PredictionContext.php index 5e2c97c..e19a400 100644 --- a/src/PredictionContexts/PredictionContext.php +++ b/src/PredictionContexts/PredictionContext.php @@ -11,6 +11,7 @@ use Antlr\Antlr4\Runtime\LoggerProvider; use Antlr\Antlr4\Runtime\RuleContext; use Antlr\Antlr4\Runtime\Utils\DoubleKeyMap; +use Antlr\Antlr4\Runtime\Utils\Map; abstract class PredictionContext implements Hashable { @@ -293,6 +294,10 @@ public static function mergeSingletons( $payloads[0] = $b->returnState; $payloads[1] = $a->returnState; $parents = [$b->parent, $a->parent]; + } else { + $payloads[0] = $a->returnState; + $payloads[1] = $b->returnState; + $parents = [$a->parent, $b->parent]; } $a_ = new ArrayPredictionContext($parents, $payloads); @@ -421,12 +426,22 @@ public static function mergeArrays( } // merge sorted payloads a + b => M + /** @var int $i */ $i = 0;// walks a + /** @var int $j */ $j = 0;// walks b + /** @var int $k */ $k = 0;// walks target M array + /** @var array $mergedReturnStates */ $mergedReturnStates = []; + for ($ini = 0; $ini < \count($a->returnStates) + \count($b->returnStates); $ini++) { + $mergedReturnStates[$ini] = 0; + } $mergedParents = []; + for ($ini = 0; $ini < \count($a->returnStates) + \count($b->returnStates); $ini++) { + $mergedParents[$ini] = null; + } // walk and merge to yield mergedParents, mergedReturnStates while ($i < \count($a->returnStates) && $j < \count($b->returnStates)) { @@ -476,13 +491,17 @@ public static function mergeArrays( // copy over any payloads remaining in either array if ($i < \count($a->returnStates)) { - for ($p = $i, $count = \count($a->returnStates); $p < $count; $p++) { + /** @var int $p */ + $p = $i; + for (; $p < \count($a->returnStates); $p++) { $mergedParents[$k] = $a->parents[$p]; $mergedReturnStates[$k] = $a->returnStates[$p]; $k++; } } else { - for ($p = $j, $count = \count($b->returnStates); $p < $count; $p++) { + /** @var int $p */ + $p = $j; + for (; $p < \count($b->returnStates); $p++) { $mergedParents[$k] = $b->parents[$p]; $mergedReturnStates[$k] = $b->returnStates[$p]; $k++; @@ -503,17 +522,17 @@ public static function mergeArrays( return $a_; } + // mergedParents = Arrays.CopyOf(mergedParents, k); $mergedParents = \array_slice($mergedParents, 0, $k); + // mergedReturnStates = Arrays.CopyOf(mergedReturnStates, k); $mergedReturnStates = \array_slice($mergedReturnStates, 0, $k); } - self::combineCommonParents($mergedParents); - $M = new ArrayPredictionContext($mergedParents, $mergedReturnStates); // if we created same array as a or b, return that instead // TODO: track whether this is possible above during merge sort for speed - if ($M === $a) { + if ($M->equals($a)) { if ($mergeCache !== null) { $mergeCache->set($a, $b, $a); } @@ -529,7 +548,7 @@ public static function mergeArrays( return $a; } - if ($M === $b) { + if ($M->equals($b)) { if ($mergeCache !== null) { $mergeCache->set($a, $b, $b); } @@ -545,53 +564,57 @@ public static function mergeArrays( return $b; } - if ($mergeCache !== null) { - $mergeCache->set($a, $b, $M); - } + self::combineCommonParents($mergedParents); if (ParserATNSimulator::$traceAtnSimulation) { LoggerProvider::getLogger() - ->debug('mergeArrays a={a},b={b} -> M', [ + ->debug('mergeArrays a={a},b={b} -> {M}', [ 'a' => $a->__toString(), 'b' => $b->__toString(), 'M' => $M->__toString(), ]); } + if ($mergeCache !== null) { + $mergeCache->set($a, $b, $M); + } + return $M; } /** - * @param array $parents + * @param array $parents */ protected static function combineCommonParents(array &$parents): void { - $uniqueParents = new \SplObjectStorage(); + /** @var Map $uniqueParents */ + $uniqueParents = new Map(); + /** @var PredictionContext|null $parent */ foreach ($parents as $parent) { - if (!$uniqueParents->contains($parent)) { - $uniqueParents[$parent] = $parent; + if ($parent !== null && !$uniqueParents->contains($parent)) { + // don't replace. + $uniqueParents->put($parent, $parent); } } foreach ($parents as $i => $parent) { - $parents[$i] = $uniqueParents[$parent]; + if ($parent !== null) { + $parents[$i] = $uniqueParents->get($parent); + } } } - /** - * @param array $visited - */ public static function getCachedPredictionContext( PredictionContext $context, PredictionContextCache $contextCache, - array &$visited, + IdentityHashMap &$visited, ): self { if ($context->isEmpty()) { return $context; } - $existing = $visited[\spl_object_id($context)] ?? null; + $existing = $visited->get($context); if ($existing !== null) { return $existing; @@ -600,7 +623,7 @@ public static function getCachedPredictionContext( $existing = $contextCache->get($context); if ($existing !== null) { - $visited[\spl_object_id($context)] = $existing; + $visited->put($context, $existing); return $existing; } @@ -634,7 +657,7 @@ public static function getCachedPredictionContext( if (!$changed) { $contextCache->add($context); - $visited[\spl_object_id($context)] = $context; + $visited->put($context, $context); return $context; } @@ -654,8 +677,8 @@ public static function getCachedPredictionContext( } $contextCache->add($updated); - $visited[\spl_object_id($updated)] = $updated; - $visited[\spl_object_id($context)] = $updated; + $visited->put($updated, $updated); + $visited->put($context, $updated); return $updated; } diff --git a/src/Utils/Map.php b/src/Utils/Map.php index 5bab247..f89afec 100644 --- a/src/Utils/Map.php +++ b/src/Utils/Map.php @@ -13,7 +13,7 @@ * @template K of Hashable * @template V */ -final class Map implements Equatable, \Countable, \IteratorAggregate +class Map implements Equatable, \Countable, \IteratorAggregate { /** @var array> */ private array $table = []; @@ -28,6 +28,11 @@ public function __construct(?Equivalence $equivalence = null) $this->equivalence = $equivalence ?? new DefaultEquivalence(); } + public function isEmpty(): bool + { + return $this->count() === 0; + } + public function count(): int { return $this->size; @@ -132,34 +137,7 @@ public function remove(Hashable $key): void public function equals(object $other): bool { - if ($this === $other) { - return true; - } - - if (!$other instanceof self - || $this->size !== $other->size - || !$this->equivalence->equals($other->equivalence)) { - return false; - } - - foreach ($this->table as $hash => $bucket) { - if (!isset($other->table[$hash]) || \count($bucket) !== \count($other->table[$hash])) { - return false; - } - - $otherBucket = $other->table[$hash]; - - foreach ($bucket as $index => [$key, $value]) { - [$otherKey, $otherValue] = $otherBucket[$index]; - - if (!$this->equivalence->equivalent($key, $otherKey) - || !self::isEqual($value, $otherValue)) { - return false; - } - } - } - - return true; + return false; } /** @@ -212,4 +190,27 @@ private static function isEqual(mixed $left, mixed $right): bool return $left === $right; } + + /** + * @param K $key + * @param V $value + */ + public function tryGetValue(Hashable $key, mixed &$value): bool + { + $hash = $this->equivalence->hash($key); + + if (!isset($this->table[$hash])) { + $this->table[$hash] = []; + } + + foreach ($this->table[$hash] as $index => [$entryKey, $entryValue]) { + if ($this->equivalence->equivalent($key, $entryKey)) { + $value = $entryValue; + + return true; + } + } + + return false; + } }