From 1cce4e54794f1f907f4afaafadcf3eb50858e543 Mon Sep 17 00:00:00 2001 From: OpaqueRock <305760598+OpaqueRock@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:17:59 +0300 Subject: [PATCH] Skip runs of horizontal whitespace in one scan in Lexer::positionAfterWhitespace --- benchmarks/LexerBench.php | 31 +++++++++++++++++++++++++++++++ src/Language/Lexer.php | 17 ++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/benchmarks/LexerBench.php b/benchmarks/LexerBench.php index e1f0ea5ff..b90018d7c 100644 --- a/benchmarks/LexerBench.php +++ b/benchmarks/LexerBench.php @@ -16,9 +16,24 @@ class LexerBench { private Source $introQuery; + private Source $deeplyIndentedQuery; + public function setUp(): void { $this->introQuery = new Source(Introspection::getIntrospectionQuery()); + $this->deeplyIndentedQuery = new Source($this->buildDeeplyIndentedQuery()); + } + + /** Query dominated by long runs of leading-tab indentation, to weigh the whitespace-skipping path. */ + private function buildDeeplyIndentedQuery(): string + { + $indent = str_repeat("\t", 16); + $fields = []; + for ($i = 0; $i < 200; ++$i) { + $fields[] = "{$indent}field{$i}(arg: \"{$i}\")"; + } + + return "query DeepIndent {\n" . implode("\n", $fields) . "\n}\n"; } /** @@ -36,4 +51,20 @@ public function benchIntrospectionQuery(): void $token = $lexer->advance(); } while ($token->kind !== Token::EOF); } + + /** + * @Warmup(2) + * + * @Revs(100) + * + * @Iterations(5) + */ + public function benchDeeplyIndentedQuery(): void + { + $lexer = new Lexer($this->deeplyIndentedQuery); + + do { + $token = $lexer->advance(); + } while ($token->kind !== Token::EOF); + } } diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index 928a49550..c5b8711a3 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -43,6 +43,9 @@ class Lexer */ private const STRING_STOP_BYTES = "\"\\\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"; + /** Ignored single-byte characters that never start or end a line, so runs of them can be skipped in bulk. */ + private const HORIZONTAL_WHITESPACE_BYTES = "\t ,"; + public Source $source; /** @phpstan-var ParserOptions */ @@ -640,12 +643,20 @@ private function assertValidBlockStringCharacterCode(int $code, int $position): */ private function positionAfterWhitespace(): void { + $body = $this->source->body; + while ($this->position < $this->source->length) { + // Tab, space, and comma runs carry no line/column bookkeeping, so + // an entire run can be consumed in one native scan instead of one + // readChar()/moveStringCursor() call per character. + $run = strspn($body, self::HORIZONTAL_WHITESPACE_BYTES, $this->byteStreamPosition); + if ($run > 0) { + $this->moveStringCursor($run, $run); + } + [, $code, $bytes] = $this->readChar(); - // Skip whitespace - // tab | space | comma | BOM - if (in_array($code, [9, 32, 44, 0xFEFF], true)) { + if ($code === 0xFEFF) { // BOM $this->moveStringCursor(1, $bytes); } elseif ($code === 10) { // new line $this->moveStringCursor(1, $bytes);