Skip to content

Make tokenization thread-safe when a grammar is shared by several threads - #133

Merged
danipen merged 2 commits into
masterfrom
fix-grammar-concurrent-compilation
Oct 13, 2026
Merged

danipen merged 2 commits into
masterfrom
fix-grammar-concurrent-compilation

Conversation

@danipen

@danipen danipen commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Related to #131 (follow-up to #132).

Problem

Grammar is not thread-safe, but the same IGrammar instance is often used from several threads: for example, several AvaloniaEdit editors with the same language, or the old and new TMModel tokenizer threads when the editor document is replaced (the scenario in #131).

  • Lazy compilation: Tokenize compiles the grammar on first use (if (_rootId == null) GenerateRootId()). Two threads can compile at the same time, both registering rules into a plain Dictionary and calling SetId on the shared raw rules. A thread can then get a root id whose rule is not registered yet, and TokenizeLine returns null.
  • Mutable pattern caches: rules build their RegExpSourceList / CompiledRule caches lazily, and BeginEndRule / BeginWhileRule call SetSource on the shared list on every scan with a back-referenced end pattern. Concurrent use throws InvalidOperationException: Collection was modified.

So a lock around GenerateRootId alone is not enough: with only that lock, the new test still fails with the second error.

Fix

Serialize Grammar.Tokenize per grammar instance with a private lock. The uncontended cost is negligible compared to tokenizing a line. The grammar never calls back into the caller while holding the lock (TMModel calls the tokenizer outside its own lock), so this does not introduce lock-ordering issues.

IsCompiling is kept for API compatibility; TMModel still uses it (with the polling fix from #132).

Tests

  • First commit adds Concurrent_First_Tokenization_Should_Compile_Grammar_Once_And_Generate_Same_Tokens: 8 threads behind a Barrier tokenize the same line with a fresh C# grammar, 20 iterations, comparing against single-threaded tokens. It fails on master on the first iteration (null results).
  • Second commit is the fix; the full suite passes.

🤖 Generated with Claude Code

danipen and others added 2 commits October 13, 2026 10:27
When several threads tokenize with a grammar that is not compiled yet,
all of them compile it at the same time and some get null results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grammar is compiled lazily on the first tokenization, and its rules
cache and mutate their compiled patterns (RegExpSourceList.SetSource for
back-referenced end patterns) while tokenizing. When several threads used
the same grammar, they compiled it concurrently, got null results or hit
'Collection was modified' exceptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The locking scope covers both tokenization paths, and the regression test exercises the reported race.

0 open findings

What changed in this PR

Serializes tokenization per grammar instance to prevent races during lazy compilation and mutable rule-cache access.

Changes:

  • Adds a private tokenization lock around both tokenization APIs.
  • Adds a concurrent first-tokenization regression test.
File Description
src/​TextMateSharp/​Internal/​Grammars/​Grammar.cs Serializes tokenization through TokenizeCore.
src/​TextMateSharp.Tests/​Internal/​Grammars/​GrammarTests.cs Verifies concurrent tokenization produces consistent results without errors.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@danipen
danipen merged commit adfd844 into master Oct 13, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants