Skip to content

Bundled Svelte grammar has cyclical includes — syntect consumers spin forever #4080

Closed

Description

@yxxbc

Title: Bundled Svelte grammar has cyclical includes — syntect consumers spin forever

The Svelte syntax this crate bundles (02_Extra/Svelte, the
corneliusio/svelte-sublime submodule) contains cyclical includes:

style-common  → style-lang-attribute → style-lang-decider-type → style-common
style-common  → style-lang-attribute → style-lang-decider-lang → style-common
script-common → script-lang-attribute → script-lang-decider-lang → script-common
style-lang-decider-type → script-common → script-lang-attribute → …

Sublime Text / VS Code tolerate these (their include walk is cycle-safe), but
syntect's is not: MatchIter::next pushes the target context with no
"already on the stack?" check, so the context stack grows without bound and
parse_line never returns.

Concretely: highlighting a five-byte line — /** — inside a Svelte diff
pins a core at 100 % forever. html / javascript / typescript / vue parse
the same input in < 10 ms; only svelte loops. It is neither line length nor
regex backtracking (capping oniguruma's retry_limit_in_match to 20 000 still
spins).

This is filed here (rather than only upstream) because two-face ships the
grammar, so every two-face user who highlights a Svelte file hits it. A
second, non-consuming-set loop in parse_line compounds it.

Happy to send a PR here too if you'd rather adjust the bundled grammar (or pin
a fixed syntect) — just say which you prefer.

Activity

  1. aozmekik commented on Oct 18, 2026

    @aozmekik

    I checked this against bat itself: current master f748c1a (bat 0.26.1 (v0.25.0-855-gf748c1a4), syntect 5.3.0), built with cargo build --release on macOS 26.6.2, Apple Silicon.

    In bat the line on its own is fine. It hangs when it follows an unclosed { expression:

    $ printf '  /**\n' | bat -pp -l svelte --color=always      # exits normally in 10-20 ms
    $ printf '{a\n  /**\n' | bat -pp -l svelte --color=always  # prints the {a line, then hangs; killed after 60 s
    $ printf '{a /*\n' | bat -pp -l svelte --color=always      # no output, killed after 60 s
    

    The same {a / /** input with -l html, -l vue, -l js or -l ts exits normally in 10-40 ms. One real file that hits this is Svelte's own packages/svelte/tests/parser-legacy/samples/javascript-comments/input.svelte, which ends with { // ... / a + b // ... / /* ... */ / }. bat prints it up to the a + b line and then hangs. I ran 5622 .svelte files from sveltejs/svelte and sveltejs/kit through bat, and that was the only one that didn't finish within 5 s. The slowest of the rest took 273 ms.

    I don't think the Svelte include chains are what loops, though. Those chains go through set: into anonymous contexts, so every hop needs a match, and syntect's include walk ends. I traced the {a / /** input with a copy of syntect 5.3.0 that has tracing added, loading bat's assets/syntaxes.bin. The parser is stuck at column 0 of the second line. The stack stays at depth 5 while the parser switches back and forth between two contexts of the bundled sublimehq JavaScript syntax, which Svelte's embed-scope includes for {...}. {a /* shows the same loop at column 2:

    JavaScript#expression-statement-end   match '{{line_ending_ahead}}'          set -> anon_expression-statement-end_0
    anon_expression-statement-end_0       match '{{line_continuation_lookahead}}' set -> expression-statement-end
    

    line_ending_ahead matches because the rest of the line is only whitespace plus a block comment, closed or not. line_continuation_lookahead also matches, because it reads the / of /* as a division operator. Both are zero-width sets. syntect 5.3.0 only detects a zero-width push followed by a zero-width pop, so it never moves forward. This is the non-consuming set case described in trishume/syntect#709. It isn't specific to Svelte either. printf 'a /*\n' | bat -pp -l JavaScript hangs the same way, and so does a /* x */. .js files aren't affected because bat maps them to JavaScript (Babel).

    bat already has assets/patches/JavaScript.sublime-syntax.patch, so adding a second hunk there avoids the hang without waiting for syntect:

    @@ -93,7 +93,8 @@
           (?! \+\+ | -- )
           (?=
             != |
    -        [-+*/%><=&|^\[(;,.:?] |
    +        [-+*%><=&|^\[(;,.:?] |
    +        /(?!\*) |
             (?:in|instanceof){{identifier_break}}
           )
         ))

    I tested it like this: applied the patches, rebuilt with bat cache --build --blank --source=assets --target=<dir>, then ran bat with BAT_CACHE_PATH=<dir>. All the inputs above now exit normally, each in under 50 ms. All 5622 Svelte files complete, the slowest in 250 ms. The output for the 205 files in tests/syntax-tests/source is byte-identical to assets built from the same tree without the hunk. The scope ops for 4773 .js files parsed as plain JavaScript are unchanged. Continuations such as x = a followed by / b are highlighted the same as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions