From 543d20950b417bce70ffba27c4453f320f7c74b8 Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Sat, 20 Jun 2026 21:08:23 +0000 Subject: [PATCH] perf(expression): fast-path identifier characters and short-circuit keyword scans Speed up expression parsing by skipping work that provably cannot match: - Add an identifier/number-character fast path to the expression loop. Such a character is never whitespace, never a terminator (no `shouldTerminate` implementation matches a word character), and is not one of the switch's cases, so it can short-circuit the termination checks and the switch dispatch entirely and just advance the position. - Bail out of the unary/binary operator keyword scans immediately when the surrounding character cannot start or end a keyword (every keyword is lowercase ASCII letters). No behavior change: the full test suite passes and parser output is identical. --- .changeset/fast-expressions-skip-scans.md | 11 +++++++++++ src/states/EXPRESSION.ts | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .changeset/fast-expressions-skip-scans.md diff --git a/.changeset/fast-expressions-skip-scans.md b/.changeset/fast-expressions-skip-scans.md new file mode 100644 index 00000000..2893b99c --- /dev/null +++ b/.changeset/fast-expressions-skip-scans.md @@ -0,0 +1,11 @@ +--- +"htmljs-parser": patch +--- + +Speed up expression parsing by skipping work that provably cannot match. An +identifier/number character is never whitespace, never a terminator (no +`shouldTerminate` implementation matches a word character), and is not handled +by the expression switch, so it now takes a fast path that just advances the +position. The unary/binary operator keyword scans also bail out immediately when +the surrounding character cannot start or end a keyword. This improves +steady-state parsing throughput with no behavior change. diff --git a/src/states/EXPRESSION.ts b/src/states/EXPRESSION.ts index 1e50fb4e..6df0e367 100644 --- a/src/states/EXPRESSION.ts +++ b/src/states/EXPRESSION.ts @@ -120,6 +120,16 @@ export const EXPRESSION: StateDefinition = { continue; } + // Fast path: an identifier/number character is never whitespace, never + // a terminator (no `shouldTerminate` implementation matches a word + // character), and is not handled by the switch below, so it just + // advances. Short-circuiting here skips the termination checks and the + // switch dispatch for the bulk of expression content. + if (isWordCode(code)) { + this.pos++; + continue; + } + // Termination checks (no groupStack) if (!expression.groupStack.length) { if (expression.terminatedByWhitespace && isWhitespaceCode(code)) { @@ -461,6 +471,10 @@ function lookBehindForOperator( } default: { + // Every unary keyword ends in a lowercase letter; if the character + // before `pos` is not one, no keyword can match. + if (code < CODE.LOWER_A || code > CODE.LOWER_Z) return -1; + for (const keyword of expression.inType ? tsUnaryKeywords : unaryKeywords) { @@ -510,6 +524,11 @@ function lookAheadForOperator( } default: { + // Every binary keyword starts with a lowercase letter; if the character + // at `pos` is not one, no keyword can match. + const startCode = data.charCodeAt(pos); + if (startCode < CODE.LOWER_A || startCode > CODE.LOWER_Z) return -1; + for (const keyword of binaryKeywords) { const keywordPos = lookAheadFor(data, pos, keyword); if (keywordPos === -1) continue;