You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bundled Svelte grammar has cyclical includes — syntect consumers spin forever #4080
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.
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:
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.
Activity
aozmekik commentedon Oct 18, 2026
I checked this against bat itself: current master f748c1a (
bat 0.26.1 (v0.25.0-855-gf748c1a4), syntect 5.3.0), built withcargo build --releaseon macOS 26.6.2, Apple Silicon.In bat the line on its own is fine. It hangs when it follows an unclosed
{expression:The same
{a//**input with-l html,-l vue,-l jsor-l tsexits normally in 10-40 ms. One real file that hits this is Svelte's ownpackages/svelte/tests/parser-legacy/samples/javascript-comments/input.svelte, which ends with{ // .../a + b // ...//* ... *//}. bat prints it up to thea + bline and then hangs. I ran 5622.sveltefiles 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'sassets/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 sublimehqJavaScriptsyntax, which Svelte'sembed-scopeincludes for{...}.{a /*shows the same loop at column 2:line_ending_aheadmatches because the rest of the line is only whitespace plus a block comment, closed or not.line_continuation_lookaheadalso matches, because it reads the/of/*as a division operator. Both are zero-widthsets. 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-consumingsetcase described in trishume/syntect#709. It isn't specific to Svelte either.printf 'a /*\n' | bat -pp -l JavaScripthangs the same way, and so doesa /* x */..jsfiles aren't affected because bat maps them toJavaScript (Babel).bat already has
assets/patches/JavaScript.sublime-syntax.patch, so adding a second hunk there avoids the hang without waiting for syntect:I tested it like this: applied the patches, rebuilt with
bat cache --build --blank --source=assets --target=<dir>, then ran bat withBAT_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 intests/syntax-tests/sourceis byte-identical to assets built from the same tree without the hunk. The scope ops for 4773.jsfiles parsed as plainJavaScriptare unchanged. Continuations such asx = afollowed by/ bare highlighted the same as before.