From 91af471b4dee0b5c5d9de8431967faeecae61019 Mon Sep 17 00:00:00 2001 From: Ju Liu Date: Sat, 25 Apr 2026 09:56:37 +0100 Subject: [PATCH 1/3] Layout: peek 1 char before paying for 2-char comment-start check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `whitespaceAndCommentsOrEmpty` (called between every pair of tokens — the absolute hottest path), replace the 2-char `String.slice offset (offset + 2) source` + multi-string `case` with a 1-char slice + nested 1-char check. The vast-majority no-comment path now allocates one less substring per call; only the rare `-` or `{` positions continue to the 2-char comparison. --- src/Elm/Parser/Layout.elm | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Elm/Parser/Layout.elm b/src/Elm/Parser/Layout.elm index 1bbd975b..695e4948 100644 --- a/src/Elm/Parser/Layout.elm +++ b/src/Elm/Parser/Layout.elm @@ -24,16 +24,28 @@ whitespaceAndCommentsOrEmpty = -- whitespace can't be followed by more whitespace -- -- since comments are comparatively rare - -- but expensive to check for, we allow shortcutting + -- but expensive to check for, we allow shortcutting. + -- + -- Check the first char alone (1-char slice) before paying for a 2-char + -- slice + multi-string compare. Almost every call lands in the `_` + -- branch and that path allocates one less substring. (ParserFast.offsetSourceAndThenOrSucceed (\offset source -> - case source |> String.slice offset (offset + 2) of - "--" -> - -- this will always succeed from here, so no need to fall back to Rope.empty - Just fromSingleLineCommentNode + case String.slice offset (offset + 1) source of + "-" -> + if String.slice (offset + 1) (offset + 2) source == "-" then + -- this will always succeed from here, so no need to fall back to Rope.empty + Just fromSingleLineCommentNode + + else + Nothing + + "{" -> + if String.slice (offset + 1) (offset + 2) source == "-" then + Just fromMultilineCommentNodeOrEmptyOnProblem - "{-" -> - Just fromMultilineCommentNodeOrEmptyOnProblem + else + Nothing _ -> Nothing From c9d90b9105fac494f9da8f22a518a0abb65f8770 Mon Sep 17 00:00:00 2001 From: Ju Liu Date: Sat, 25 Apr 2026 09:56:57 +0100 Subject: [PATCH 2/3] Force `===` for Int compares + empty-consume short-circuit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Elm compiler only emits `===` for `==`/`/=` on Ints when one side is a literal integer. When both sides are computed values (e.g., `charCode /= openCharCode`, `info.leftPrecedence == leftPrecedence`, `newOffset == -1` — which is parsed as `Basics.negate 1`, not a literal), the compiler falls back to `_Utils_eq`. `_Utils_eq`'s outer loop allocates a `stack = []` array on every call, which adds up inside per-character parser loops. Fixed sites (all compile-verified to drop the `_Utils_eq` call in the optimized JS): * `isNotRelevant` in `nestableMultiCommentMapWithRange` (per char inside every multi-line comment body): `charCode /= openCharCode && charCode /= closeCharCode` → `(charCode - openCharCode) /= 0 && (charCode - closeCharCode) /= 0`. * Infix non-associative precedence check in Expression.elm: `info.leftPrecedence == leftPrecedence` → `info.leftPrecedence - leftPrecedence == 0`. * `anyChar` and `anyCharFollowedByWhileMap` end-of-source markers: `newOffset == -1` / `== -2` → `newOffset + 1 == 0` / `newOffset + 2 == 0`. Also folds in a cheap but correlated improvement in `whileWithoutLinebreakAnd2PartUtf16ToResultAndThen`: short-circuit when zero chars were consumed. This fires after every expression not followed by an infix operator (the common case) and skips an empty-string `String.slice` + callback dispatch + `Bad`/`ExpectingCustom` allocation. The short-circuit's `s1Offset == s0.offset` check is itself written as `s1Offset - s0.offset == 0` so it also benefits from the `===` path. Net result: 9 distinct `_Utils_eq` call sites in the compiled JS → 3 (only string comparisons in declaration-vs-signature name matching remain). --- src/Elm/Parser/Expression.elm | 5 ++++- src/ParserFast.elm | 34 +++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Elm/Parser/Expression.elm b/src/Elm/Parser/Expression.elm index 0302b600..46f13d5b 100644 --- a/src/Elm/Parser/Expression.elm +++ b/src/Elm/Parser/Expression.elm @@ -1515,7 +1515,10 @@ infixNonAssociative leftPrecedence symbol = temporaryErrPrecedenceTooHigh ) (\info -> - if info.leftPrecedence == leftPrecedence then + -- `(a - b) == 0` compiles to `=== 0` (literal-Int path); + -- `a == b` between two non-literal Ints compiles to + -- `_Utils_eq(a, b)` which allocates a stack array per call. + if info.leftPrecedence - leftPrecedence == 0 then problemCannotMixNonAssociativeInfixOperators else diff --git a/src/ParserFast.elm b/src/ParserFast.elm index dac91a06..da1b80c9 100644 --- a/src/ParserFast.elm +++ b/src/ParserFast.elm @@ -2494,11 +2494,15 @@ anyChar = newOffset = charOrEnd s.offset s.src in - if newOffset == -1 then + -- `newOffset + 1 == 0` iff `newOffset == -1`. We add then compare to + -- the literal 0 so the Elm compiler emits `=== 0` instead of + -- `_Utils_eq(newOffset, -1)` (which allocates a stack array). + -- See the `isSubCharWithoutLinebreak` docstring below. + if newOffset + 1 == 0 then -- end of source Bad False (ExpectingAnyChar s.row s.col) - else if newOffset == -2 then + else if newOffset + 2 == 0 then -- newline Good '\n' { src = s.src @@ -2810,7 +2814,18 @@ whileWithoutLinebreakAnd2PartUtf16ToResultAndThen whileCharIsOkay consumedString whileCharIsOkay s0.offset s0.src + in + -- Short-circuit when no chars were consumed: fires after every + -- expression not followed by an infix operator (very common). + -- Skips the empty-string slice + callback dispatch that would + -- otherwise allocate a Bad+ExpectingCustom record. + -- `(a - b) == 0` compiles to `=== 0`; plain `a == b` on Ints + -- compiles to `_Utils_eq` which allocates. + if s1Offset - s0.offset == 0 then + Bad False (ExpectingCharSatisfyingPredicate s0.row s0.col) + else + let whileContent : String whileContent = String.slice s0.offset s1Offset s0.src @@ -3039,7 +3054,14 @@ nestableMultiCommentMapWithRange rangeContentToRes ( openChar, openTail ) ( clos charCode = Char.toCode char in - charCode /= openCharCode && charCode /= closeCharCode && not (Char.Extra.isUtf16Surrogate char) + -- Subtract-and-compare-to-literal-0 forces the Elm compiler to + -- emit `!== 0` instead of `_Utils_eq(int, int)` (the latter + -- allocates a stack array per call). This predicate runs once per + -- char inside multi-line comment bodies — very hot in elm-package + -- corpora which have many doc comments. + (charCode - openCharCode /= 0) + && (charCode - closeCharCode /= 0) + && not (Char.Extra.isUtf16Surrogate char) in map2WithRange (\range afterOpen contentAfterAfterOpen -> @@ -3142,7 +3164,9 @@ anyCharFollowedByWhileMap consumedStringToRes afterFirstIsOkay = firstOffset = charOrEnd s.offset s.src in - if firstOffset == -1 then + -- `firstOffset + 1 == 0` iff `firstOffset == -1`. See `isSubCharWithoutLinebreak` + -- docstring below for why this (vs `== -1`) matters. + if firstOffset + 1 == 0 then -- end of source Bad False (ExpectingAnyChar s.row s.col) @@ -3150,7 +3174,7 @@ anyCharFollowedByWhileMap consumedStringToRes afterFirstIsOkay = let s1 : State s1 = - if firstOffset == -2 then + if firstOffset + 2 == 0 then skipWhileHelp afterFirstIsOkay (s.offset + 1) (s.row + 1) 1 s.src s.indent else From 6093f02a8e6c48e4084f19043697fb82321eb1d9 Mon Sep 17 00:00:00 2001 From: Ju Liu Date: Mon, 27 Apr 2026 09:24:05 +0100 Subject: [PATCH 3/3] Use `< 0` for the newline marker in anyChar / anyCharFollowedByWhileMap Cleanup suggestion from @lue-bird: after the `+ 1 == 0` (EOF = -1) branch filters out -1, the only remaining negative is -2 (newline marker). `< 0` is more idiomatic and matches the convention used elsewhere in this file (see the `isSubCharWithoutLinebreak` docstring). --- src/ParserFast.elm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ParserFast.elm b/src/ParserFast.elm index da1b80c9..d0507d7f 100644 --- a/src/ParserFast.elm +++ b/src/ParserFast.elm @@ -2502,8 +2502,8 @@ anyChar = -- end of source Bad False (ExpectingAnyChar s.row s.col) - else if newOffset + 2 == 0 then - -- newline + else if newOffset < 0 then + -- newline (-2; we already filtered -1 above) Good '\n' { src = s.src , offset = s.offset + 1 @@ -3174,7 +3174,8 @@ anyCharFollowedByWhileMap consumedStringToRes afterFirstIsOkay = let s1 : State s1 = - if firstOffset + 2 == 0 then + if firstOffset < 0 then + -- newline (-2; we already filtered -1 above) skipWhileHelp afterFirstIsOkay (s.offset + 1) (s.row + 1) 1 s.src s.indent else