24 KiB
LaTeX grammar + math parser — framing (inline-math substrate)
Revision 3 — pre-implementation. Ground truth: canonical main @
96d0bae, protocol v19, 2026-07-23. Status: framing only; no
implementation. Rev 1's pmacs-side scouting passed review with every
in-repo anchor verified exactly; rev 2 corrected the external crate ground
truth (the originally named crate provably cannot link) and refreshed the
in-flight-lane snapshot; rev 3 folds in two provenance-wording corrections —
the fork-vs-upstream attribution and the now-discharged provenance diff — and
relaxes the Cargo.toml sequencing note. See §0.1 for the changelog.
Parent arc: docs/inline-math-framing.md. That note frames the full
four-tier inline-math renderer; its Tier 4 (GPU render) lands on
pmacs-gpu/src/main.rs, the render path that folding and any future math
draw pass also contend for. This lane deliberately carves out only the
frontend-agnostic, conflict-free substrate — the parts that can land now,
in a sibling worktree, without touching a single file the two in-flight
efforts will own.
0. Why this lane, why now (parallel-safety)
Two efforts are in flight; both share files with this lane, and the disjointness must be described against their current state, not a stale one (F5, verified 2026-07-23):
- folding (
foldingbranch) is at rev 5, APPROVED, Stage 1 implementing (head40a820a, clean worktree) — not framing-only. Its eventual touch set ownssemantic_render.rs,src/overlay.rs,src/syntax.rs(theParseViewHandlereads at:696/:706),src/daemon.rs(grid render +dispatch_keypre-edit unfold), the Lua command/dispatch layer +src/lua_bindings/mod.rs, both frontends' gutter/render, andpmacs-gpu/src/main.rs(fold mirror). - gpu-invocation (
gpu-invocationbranch) is not framing-only either: its worktree carries uncommitted implementation right now, including edits to rootCargo.toml— one of this lane's two shared files. It will ownsrc/main.rs,pmacs-gpu/src/main.rs(argv/startup),pmacs-gpu/src/attach.rs, rootCargo.toml(default-run), and the README. - inline-math Tier 4 (deferred, this lane's downstream) would own
pmacs-gpu/src/main.rs(the render path — the three-way hotspot).
This lane touches none of those code paths. Its entire footprint is: one
BUILTIN_LANGUAGES append, one Cargo.toml dependency line, one new in-repo
query overlay file, and one new pure module src/math_parse.rs. The only two
shared files, with disjointness re-verified:
src/syntax.rs— a localized append to the language table at:1095(after theyamlentry's},, before];at:1096), disjoint from folding'sParseViewHandlereads at:696/:706.- root
Cargo.toml— a dependency line in the grammar block (:142–:219). gpu-invocation's live edit is thedefault-runkey in[package]; nodefault-runkey exists yet, and the grammar dep block is a different region ~100 lines away. The two edits are disjoint hunks that git auto-merges cleanly — the same config-registry ∥ Vterm precedent this section cites — so no ordering is technically required. Flagging the overlap is coordination courtesy, not a merge risk.
This is the localized-single-region-append pattern that already rebased with
zero conflicts when config-registry and Vterm Stage 1 shared
editor.rs/mod.rs (docs/active-work.md, "Closed since the last
snapshot"). Its precondition — agree the file split before either lane starts,
keep each lane's footprint in shared files to a localized region — holds here
by construction; the now-active Cargo.toml overlap is disjoint (above) and
needs only coordination courtesy.
0.1 Revision history
Round 1 (rev 1 → rev 2)
- F1 (severe). Rev 1's
tree-sitter-latex0.1.0 (crates.io) cannot link. It is a third-party repackage (publishercijiugechu) of an old latex-lsp grammar snapshot; its declared repogithub.com/tree-sitter/tree-sitter-latexdoes not exist (404, and the crate's VCS sha is absent from latex-lsp history). The grammar declares external tokens but the tarball ships noscanner.c, and itsbuild.rscompiles the scanner only "if it exists." Proven empirically: an offline path-dep build fails with undefinedtree_sitter_latex_external_scanner_scan/_serialize/_deserialize. The rev-1 scouted facts (LANGUAGE: LanguageFn, shim ABI compatibility) were each individually true — docs.rs cannot surface a link failure, which is why the scout missed it. Fix: Q#LX1 re-based on a working vehicle (F2). - F2 (severe). A working, verified vehicle exists:
codebook-tree-sitter-latex0.6.1 (26k downloads). See Q#LX1. - F3 (severe). Q#LX2's vendoring source did not exist: latex-lsp ships
no
queries/directory (itsinclude = ["queries/*"]is vestigial), and no first-party LaTeXhighlights.scmexists anywhere. Fix: Q#LX2 re-pointed at nvim-treesitter's query. - F4 (major). Q#LX2's "real work" is broader than capture renaming — the fall-through classes and predicate-support gap are now named in Q#LX2.
- F5 (major). §0's in-flight snapshot was stale (folding is rev 5 /
approved / implementing; gpu-invocation has live uncommitted
Cargo.tomledits). §0 rewritten to reality; the disjointness argument survives with one added sequencing obligation. - F6 (minor). Parser is ~500 lines (parent framing, Tier 2 + the
integration table), not ~1,000.
LanguageEntryhas six fields.math_parsesorts beforemcpinsrc/lib.rs(alphabetical). The 0.6.1 crate is ~3.1 MB compressed. All corrected below.
Round 2 (rev 2 → rev 3)
- R2-1. Corrected the source attribution: the crate is a republish from
the codebook project's fork of
latex-lsp/tree-sitter-latex(the fork modernized the bindings to theLanguageFnshim and renamed the crate), not a cut from an upstreamlatex-lspcommit. GitHub serves fork-network objects through the parent repo's API, which made the fork commit948b89clook upstream in rev 1/2. §2 fixed. - R2-2. The Q#LX1 provenance-diff gate is discharged, not deferred: the
diff against
latex-lspmaster is byte-identical ongrammar.js/scanner.c(460 node types in both; metadata-only drift ingrammar.json/node-types.json); the sole residual is generatedparser.c, covered by the parse smokes. §2 and Q#LX1 now record the result instead of the obligation. - R2-3. Relaxed §0's
Cargo.tomlnote: the dep append and gpu-invocation'sdefault-runkey are disjoint hunks ~100 lines apart that git auto-merges cleanly (the cited config-registry ∥ Vterm precedent). Kept as coordination courtesy; dropped the inaccurate "can't assume a clean auto-merge" rationale.
1. What ships
- Stage 1 — LaTeX/TeX syntax highlighting. A bundled LaTeX grammar
(Q#LX1) plus a vendored highlights overlay (Q#LX2) lights up
.tex/.latex/.sty/.clsbuffers. Self-contained, user-visible, shippable as its own PR. This is the "basic LaTeX mode" foothold and the tier-1 detection substrate the inline-math arc needs. - Stage 2 — math parser: DEFERRED by decision (Q#LX5). The pure
src/math_parse.rsrecursive-descent parser is framed but not built in this lane — the reviewer and author concur it should land beside its Tier 3 layout consumer, not ahead of it. Recorded in §Deferred with its rationale so the decision is not re-litigated.
Also deferred to the inline-math arc proper (§Deferred): Tier 3 layout,
Tier 4 GPU render, and instance-side (math_environment) @math injection
detection.
2. Ground truth (scouted 2026-07-23, main @ 96d0bae; pmacs-side
anchors verified in review, external facts corrected per §0.1)
Grammar table
LanguageEntrystruct —src/syntax.rs:766-801, six fields:name(:769),extensions: &[&str](:774),loader: fn() -> tree_sitter::Language(:778),highlights_query: &[&str](:789),locals_query: &[&str](:793),injections_query: &[&str](:800). There is notags/filename/shebangfield — extensions are the only Rust-side detection signal.BUILTIN_LANGUAGEStable —src/syntax.rs:816…];at:1096. A new entry appends after theyamlentry's},at:1095. The "adding a grammar" checklist is documented in-place at:803-815.- Example entry, verbatim (
src/syntax.rs:985-992):LanguageEntry { name: "python", extensions: &["py", "pyi"], loader: || tree_sitter_python::LANGUAGE.into(), highlights_query: &[tree_sitter_python::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, - Grammar crate deps live in
Cargo.toml:136-219(tree-sitter = "0.26"at:136; grammar crates:142–:219).
Detection chain
- The
extensionsfield driveslanguage_name_for_extension(src/syntax.rs:1223) andlanguage_name_for_path(:1235). - The full precedence chain (PR #132 modeline work) is Lua,
detect_buffer_languageatbuiltin/runtime/syntax.lua:452-466: modeline → grammar extension → LSP filetype map → filename map → shebang. Because grammar-extension detection sits ahead of the LSP filetype map, addingtex/latex/sty/clsto the new entry'sextensionswires the whole chain with no Lua edit.
Query sourcing
- No in-repo
.scmhighlight overlays exist today. Every grammar sources its queries from crate-exported&'static strconstants (naming varies:HIGHLIGHTS_QUERY,HIGHLIGHT_QUERY,HIGHLIGHT_QUERY_BLOCK; documented atsrc/syntax.rs:779-800). The CUDA entry (:919-923) shows queries composed as a slice of fragments in the field. - The only
.scmin the tree isaudit/audit-rules.scm, pulled viainclude_str!atsrc/audit/mod.rs:76— the sole precedent for shipping static query text from a repo file. - Highlight-capture handling (F4): the prefix-walk at
src/highlight.rs:189already absorbs@function.macro,@keyword.*,@string,@comment, and@punctuation.*. But@markup.*(headings, italic, bold, math),@module, and@labelfall through to default style — i.e. the most LaTeX-distinctive content would paint as plain text unless the vendored query's captures are renamed onto the recognized set. - Predicate support (F4): pmacs evaluates only the
#is? localproperty predicate;#match?/#eq?/#any-of?/#lua-match?are silently ignored for every grammar (pre-existing, not a blocker). The nvim query has 8 such uses that must be curated out or accepted as benign over-matching.
Injection seam (future math detection)
collect_injection_matches(src/syntax.rs:375-427) is the capture- consumption point: it reads@injection.content(:381) andinjection.language(:385-388), plus#set!properties (:397-404). Child-language resolution isresolve_injected_language(:304). The markdown→rust fenced-code injection is the working precedent. A future(math_environment) @injection.content+#set! injection.language "math"hooks in here unchanged (Q#LX4, deferred).
Module registration & tests
- Crate modules are declared in the alphabetized
pub modblock atsrc/lib.rs:42-142;mod math_parse;sorts beforemcp(:100). - Grammar test convention (CUDA template,
src/syntax.rs:2130-2221): a table guardbuiltin_languages_include_<lang>, a smoke test<lang>_grammar_loads_and_parses_*(reg.language(...)→ parse → assert root kind +!has_error()), a highlight-resolve test, and an extension- resolution test. End-to-end paint template:src/highlight.rs:1296-1346. - Greenfield confirmed — no
math/latex/texcode anywhere insrc/,builtin/,pmacs-gpu/,pmacs-protocol/.
Crate facts (external, corrected + reviewer-verified 2026-07-23)
- Rejected —
tree-sitter-latex0.1.0. Broken repackage; dead declared repo; noscanner.cfor its declared external tokens; proven offline link failure (undefinedtree_sitter_latex_external_scanner_*). Not linkable at any pin. (F1.) - Chosen —
codebook-tree-sitter-latex0.6.1 (26k downloads, ~3.1 MB compressed). A republish from the codebook project's fork oflatex-lsp/tree-sitter-latex(the real, maintained grammar — MIT, texlab's author). The fork modernized the bindings to theLanguageFnshim and renamed the crate; genuinelatex-lspmaster still carries old-style bindings (tree-sitter = 0.24.1as a direct dependency, no shim). (Rev 1/2 mis-attributed this to a Dec-2025 upstreamlatex-lspcommit — GitHub serves fork-network objects through the parent repo's commit/contents API, so the fork's commit948b89c, whoselib.rsalready carries thecodebook_tree_sitter_latexname, looked upstream.) It shipsscanner.c, exportsLANGUAGE: LanguageFnovertree-sitter-language 0.1withtree-sitterdev-only — the exact shim pattern every pmacs grammar uses. Verified end-to-end: builds againsttree-sitter 0.26and parses a document containing a verbatim environment (which exercises the external scanner) → root kindsource_file, no errors. The canonical nametree-sitter-latexis squatted by the broken crate, hence thecodebook-prefix. - Provenance diff — run, not deferred (R2-2). The 0.6.1 crate was diffed
against genuine
latex-lspmaster:grammar.jsandscanner.c— the grammar definition and the hand-written C, the security-relevant surfaces — are byte-identical.grammar.json/node-types.jsondiffer only by newer tree-sitter-cli output metadata (supertype population, extra flags): 460 node types in both, with type sets, named flags, and rule counts all identical. The sole residual isparser.c, which is generated and not committed upstream, so it cannot be diffed — it is exercised by the parse smokes (acceptance 2), and an optional full discharge is regenerating it withtree-sitter-cliat implementation time.
3. Decisions
Q#LX1 — Bundle codebook-tree-sitter-latex 0.6.1
Add codebook-tree-sitter-latex = "0.6" to the Cargo.toml grammar block;
loader is || codebook_tree_sitter_latex::LANGUAGE.into() (confirmed against
the crate's [lib] name = "codebook_tree_sitter_latex"). This is the linkable
republish from the codebook project's fork of latex-lsp/tree-sitter-latex,
which modernized the bindings to the LanguageFn shim (§2); the
originally-named tree-sitter-latex 0.1.0 is rejected as unlinkable (§2, F1).
ABI is fine via the tree-sitter-language shim, exactly as every current
grammar. The provenance concern (dirty-tree publish, squatted name) is
discharged, not deferred: the diff against latex-lsp master is
byte-identical on grammar.js/scanner.c, with only tree-sitter-cli metadata
drift elsewhere and generated parser.c as the sole residual (§2, R2-2).
Rejected: vendoring generated parser.c/scanner.c directly (compile +
maintenance burden the crate absorbs) and re-pinning tree-sitter
(unnecessary — the shim decouples us).
Q#LX2 — Vendor nvim-treesitter's highlights query (no first-party source exists)
latex-lsp ships no queries/ directory and no first-party
highlights.scm exists anywhere (F3). Vendor nvim-treesitter's
queries/latex/highlights.scm (342 lines, Apache-2.0 — compatible with
pmacs' MIT OR Apache-2.0; Helix's MPL-2.0 version is the fallback), host it
at builtin/queries/latex/highlights.scm, and wire it into the entry's
highlights_query via include_str! (the audit-rules.scm precedent). This
establishes the first in-repo grammar-query overlay convention, which the
ruby/php/html/css backlog then reuses.
The real work (F4), beyond copying the file:
- Capture reconciliation. Rename/curate the query's
@markup.*,@module, and@labelcaptures onto pmacs' recognized set (src/highlight.rs:189) or they paint as default — and those are the LaTeX-distinctive captures (headings, emphasis, math, environment names). - Predicate curation. The 8
#match?/#eq?/#any-of?/#lua-match?uses are silently ignored (§2); curate them out, or accept the resulting over-matching, per rule. - Node-name compatibility. Acceptance test 3 (
Query::newcompiles the vendored query against the chosen grammar, §6) doubles as the grammar/query node-name compatibility check — it is the test that catches a grammar-version/query mismatch.
Prefer include_str! of a .scm file over an in-source &'static str const
so the query stays diffable and lintable as query source.
Q#LX3 — Extensions and detection
extensions: &["tex", "latex", "sty", "cls"]. A single BUILTIN_LANGUAGES
append at :1095 wires the full detection chain (grammar-extension precedes
the LSP filetype map in syntax.lua:457). No Lua edit; no
modeline/filename/shebang changes.
Q#LX4 — Locals and injections empty for v0
locals_query: &[], injections_query: &[]. LaTeX has no lexical-scope
locals worth shipping in v0. The (math_environment) @math injection — the
instance-side, wire-authoritative alternative to the inline-math framing's
frontend-local detection — is deferred to the inline-math arc; it hooks
collect_injection_matches (syntax.rs:375-427) unchanged when a "math"
layer exists. Verbatim/listings/minted code-block injections are a later
quality pass.
Q#LX5 — The math parser is deferred, not front-run (decided)
src/math_parse.rs (LaTeX math → the MathNode AST in
docs/inline-math-framing.md §Tier 2, ~500 lines + a ~200-entry symbol
map) is pure, dependency-free, and conflict-free. Reviewer and author concur:
defer it and land it beside its Tier 3 layout consumer. Rationale, beyond
the no-build-ahead discipline (folding refuses BlockAdornments;
gpu-invocation refuses FILE):
- No design pressure until layout exists. The parent framing places the
hard part in Tier 3 layout; the
MathNodeshape is only validated once layout consumes it. Freezing the AST now risks rework. - The parallelization argument is void. The parser is conflict-free by construction — it can land at any time without lane contention — so there is nothing to gain by front-running it. Front-running only trades that free optionality for speculative, unexercised code.
Stage 1 (grammar) therefore is this lane's shippable PR; the parser lands with the inline-math arc. Recorded here so it is not re-opened — the defer is ratified by the user's approval of this framing, per the workflow (author + reviewer concurrence proposes it; approval decides it).
Q#LX6 — No protocol, frontend, or GPU change in this lane
No wire type, no pmacs-protocol touch, no TUI/GPU change. Stage 1 is a pure
instance-side grammar-table addition. This is what keeps the lane orthogonal
to both in-flight efforts.
4. Categorical bets
- The
LanguageFnshim makes crate-version skew a non-issue. Every grammar already crosses thetree-sitter 0.26boundary throughtree-sitter-language;codebook-tree-sitter-latexis no different — confirmed by a real build. The linkability risk was in the scanner, not the shim, and is resolved by choosing a crate that shipsscanner.c(F1/F2). - LaTeX highlighting is worth ~3.1 MB of grammar. It is the largest
single grammar in the tree — a real binary-size and compile-time cost —
justified by being the LaTeX-mode foothold and a heavily-used authoring
format. A Cargo feature-gate is a v1 escape hatch if the size bites builds
that never edit
.tex. - An in-repo query overlay is a convention to establish, not a blocker. It generalizes to every grammar crate that ships no Rust query constant — directly unblocking the ruby/php/html/css backlog.
- A verified republish beats a canonical-but-broken name. The squatted
tree-sitter-latexname would have been the "obvious" pick and does not link; the provenance diff (Q#LX1, now discharged — grammar and scanner byte-identical to upstream) is the cost, already paid, of using the working republish safely.
5. Deferred (named)
- Math parser
src/math_parse.rs— decided defer (Q#LX5); lands with Tier 3 layout, not this lane. - Tier 3 math layout (
src/math_layout.rs, a font crate —read-fontsorttf-parser— + OpenType MATH-table box layout). New file, no contested touch, but pulls a dependency and has real complexity; belongs to the inline-math arc beside its GPU consumer. - Tier 4 GPU render (
pmacs-gpu/src/main.rsrender path). The three-way hotspot; wait for folding and gpu-invocation to settle before opening it. - Instance-side
(math_environment) @mathinjection detection + a"math"BUILTIN_LANGUAGESlayer (the wire-authoritative upgrade over the framing's frontend-local scan). - A real
latex-mode.lua: texlab LSP config, sectioning folds (rides Arc 6),\ref/\labelnavigation, environment/snippet completion, auto-\end{}. Full-document LaTeX, not math rendering. - Curated highlight refinements beyond the vendored query, including
re-instating the ignored predicates should pmacs' predicate support grow;
verbatim/
listings/mintedcode injections.
6. Acceptance
Stage 1 (grammar). Tests follow the CUDA convention
(src/syntax.rs:2130-2221) + the paint template (src/highlight.rs:1296):
- Table guard
builtin_languages_include_latex: the entry exists, claimstex/latex/sty/cls, and carries the overlay highlights fragment. - Load-and-parse smoke
latex_grammar_loads_and_parses:reg.language("latex"), parse a minimal\documentclass{article}\begin{document}Hello $x$\end{document}plus averbatimenvironment (to exercise the external scanner), assert the root node kindsource_fileand!has_error(). - Highlights resolve + node-name compatibility
latex_highlights_resolve:reg.highlights_query("latex")compiles the vendored query against the grammar (this is also the grammar/query compatibility gate, Q#LX2), and a\command, a{group}, and a sectioning/emphasis construct paint the reconciled capture classes (not default). - Extension resolution
language_for_path_resolves_latex_extensions:foo.tex/foo.latex/pkg.sty/cls.clsall resolve to"latex". - Full gate suite per
CLAUDE.md:cargo fmt --check;cargo clippy --workspace --all-targets -- -D warnings;cargo test --lib;cargo test --lib --features crdt; the touched acceptance suites;cargo test --test m4_acceptance -- --skip basedpyright;PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu;git diff --check.
(Stage 2 parser acceptance moves to the inline-math arc with the parser itself, per Q#LX5.)
7. Prior art in pmacs
docs/inline-math-framing.md— the parent arc; this lane is its Tier 1 substrate, with the Tier 2 parser and Tiers 3–4 deferred.docs/multi-language-injections-framing.md/ PR #122 — theParseTreeBundle+Layerinjection engine that a future(math_environment) @mathcapture rides (Q#LX4).- PR #132 (modeline detection) — the extension→language chain
(
syntax.lua:452-466) that a singleextensionsfield plugs into. - CUDA grammar (
src/syntax.rs:919-923, tests:2130-2221) — the closest precedent for a multi-fragment highlights query and the grammar test convention;audit/audit-rules.scm(src/audit/mod.rs:76) — the sole precedent for aninclude_str!-loaded.scm.