feat(parsers): initialize language grammars lazily (#68)#802
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces lazy loading for tree-sitter language parsers and queries to optimize startup performance, ensuring grammars are loaded and cached only on first use. Feedback from the reviewer highlights that the current implementation is not thread-safe for concurrent environments. Specifically, the reviewer recommends importing the threading module and adding locking mechanisms to prevent race conditions during grammar loading in _ensure and when initializing the global _store in load_parsers().
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Greptile SummaryThis PR changes parser initialization so language grammars load only when needed. The main changes are:
Confidence Score: 5/5Safe to merge with minimal risk. The lazy-loading implementation is covered by focused tests for deferral, cache sharing, concurrent access, partial-load windows, failed-load cleanup, and full-view behavior. The previous concurrency and partial-load issues are addressed by requiring both parser and query entries before treating a language as loaded. The remaining changes are narrow type updates from No files require special attention.
What T-Rex did
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Consumer
participant Loader as load_parsers()
participant Store as _LazyGrammarStore
participant View as _LazyLanguageView
participant Grammar as tree_sitter grammar
Consumer->>Loader: request parsers, queries
Loader->>Store: create/reuse process-wide store
Loader-->>Consumer: return lazy Mapping views
Consumer->>View: access language key / membership
View->>Store: _ensure(language)
alt language not loaded
Store->>Grammar: import module and compile queries
Grammar-->>Store: parser + LanguageQueries
Store-->>View: cached language available
else already loaded
Store-->>View: reuse cached parser + queries
end
View-->>Consumer: parser/query value or absence
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Consumer
participant Loader as load_parsers()
participant Store as _LazyGrammarStore
participant View as _LazyLanguageView
participant Grammar as tree_sitter grammar
Consumer->>Loader: request parsers, queries
Loader->>Store: create/reuse process-wide store
Loader-->>Consumer: return lazy Mapping views
Consumer->>View: access language key / membership
View->>Store: _ensure(language)
alt language not loaded
Store->>Grammar: import module and compile queries
Grammar-->>Store: parser + LanguageQueries
Store-->>View: cached language available
else already loaded
Store-->>View: reuse cached parser + queries
end
View-->>Consumer: parser/query value or absence
Reviews (4): Last reviewed commit: "style(parsers): collapse the twin-entry ..." | Re-trigger Greptile |
|
@greptile review |
|
@greptile review |
…rop orphan parsers on failed loads
|
@greptile review |
|



Problem
Issue #68: a repo in one language paid for all 14 grammars anyway.
load_parsers()imported every grammar module and compiled every language's full query set (functions, classes, calls, imports, locals, highlights, plus two combined queries) eagerly, and it did all of that again on EVERY call: the CLI, the MCP server, the file editor, and each of the test suite's hundreds ofload_parsers()calls each recompiled the world.Fix
load_parsers()now returns two lazy views over a process-wide store. The views subclassdict, so everydict[SupportedLanguage, ...]-typed consumer signature keeps working: a missing key routes through__missing__and triggers a one-time grammar load for exactly that language;__contains__andget(which must be overridden becausedict.getbypasses__missing__in C) probe on demand. Full-view operations (iteration,len,keys/values/items) load every spec'd language first, so no consumer ever observes a partial availability picture, and an all-languages probe that finds nothing still raisesNO_LANGUAGES.parser_loaderinto a per-language cached resolver, so merely importing the module no longer touches anytree_sitter_*package.StructureProcessorcollectedpackage_indicatorsby iteratingqueries.values(), which would have forced every grammar to load; it now reads the staticLANGUAGE_SPECSdirectly (grammar-independent data). Side effect: package detection now covers all spec'd languages even when a grammar is unavailable, which is the more correct behavior.load_parsers()calls share loaded parsers instead of recompiling._reset_parser_cache()exists as a test hook.Validation
load_parsers()alone drops from roughly 0.6 to 0.9s (eager, main) to interpreter-startup cost (about 0.3 to 0.5s total wall, nothing loaded); with one language actually used it stays around 0.4s. Per-language cost is now paid only for languages present in the repo.load_parsers()call.Tests
RED -> GREEN:
test_lazy_parser_loading.py— deferral (nothing compiles until first use, touching python does not load rust), on-demand membership (lang in parsersloads that one language truthfully), process-level sharing acrossload_parsers()calls, unknown-language absence, and full-view completeness (iteration covers every available language).