# 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** (`folding` branch) is at **rev 5, APPROVED, Stage 1 implementing** (head `40a820a`, clean worktree) — not framing-only. Its eventual touch set owns `semantic_render.rs`, `src/overlay.rs`, `src/syntax.rs` (the `ParseViewHandle` reads at `:696`/`:706`), `src/daemon.rs` (grid render + `dispatch_key` pre-edit unfold), the Lua command/dispatch layer + `src/lua_bindings/mod.rs`, **both frontends' gutter/render**, and `pmacs-gpu/src/main.rs` (fold mirror). - **gpu-invocation** (`gpu-invocation` branch) is **not framing-only either**: its worktree carries **uncommitted implementation right now, including edits to root `Cargo.toml`** — one of this lane's two shared files. It will own `src/main.rs`, `pmacs-gpu/src/main.rs` (argv/startup), `pmacs-gpu/src/attach.rs`, root `Cargo.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 the `yaml` entry's `},`, before `];` at `:1096`), disjoint from folding's `ParseViewHandle` reads at `:696`/`:706`. - root `Cargo.toml` — a **dependency line** in the grammar block (`:142`–`:219`). gpu-invocation's live edit is the `default-run` key in `[package]`; **no `default-run` key 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-latex` 0.1.0 (crates.io) **cannot link.** It is a third-party repackage (publisher `cijiugechu`) of an old latex-lsp grammar snapshot; its declared repo `github.com/tree-sitter/tree-sitter-latex` **does not exist** (404, and the crate's VCS sha is absent from latex-lsp history). The grammar declares external tokens but the tarball **ships no `scanner.c`**, and its `build.rs` compiles the scanner only "if it exists." Proven empirically: an offline path-dep build fails with undefined `tree_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-latex` 0.6.1** (26k downloads). See Q#LX1. - **F3 (severe).** Q#LX2's vendoring source did not exist: latex-lsp ships **no `queries/` directory** (its `include = ["queries/*"]` is vestigial), and no first-party LaTeX `highlights.scm` exists 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.toml` edits). §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. `LanguageEntry` has **six** fields. `math_parse` sorts **before** `mcp` in `src/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 the `LanguageFn` shim and renamed the crate), not a cut from an upstream `latex-lsp` commit. GitHub serves fork-network objects through the parent repo's API, which made the fork commit `948b89c` look upstream in rev 1/2. §2 fixed. - **R2-2.** The Q#LX1 provenance-diff gate is **discharged, not deferred**: the diff against `latex-lsp` master is byte-identical on `grammar.js`/`scanner.c` (460 node types in both; metadata-only drift in `grammar.json`/`node-types.json`); the sole residual is generated `parser.c`, covered by the parse smokes. §2 and Q#LX1 now record the result instead of the obligation. - **R2-3.** Relaxed §0's `Cargo.toml` note: the dep append and gpu-invocation's `default-run` key 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` / `.cls` buffers. 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.rs` recursive-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 - **`LanguageEntry` struct — `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 no `tags`/`filename`/`shebang` field — extensions are the only Rust-side detection signal. - **`BUILTIN_LANGUAGES` table — `src/syntax.rs:816` … `];` at `:1096`.** A new entry appends after the `yaml` entry's `},` at `:1095`. The "adding a grammar" checklist is documented in-place at `:803-815`. - Example entry, verbatim (`src/syntax.rs:985-992`): ```rust 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 `extensions` field drives `language_name_for_extension` (`src/syntax.rs:1223`) and `language_name_for_path` (`:1235`). - The full precedence chain (PR #132 modeline work) is Lua, `detect_buffer_language` at `builtin/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, adding `tex`/`latex`/`sty`/`cls` to the new entry's `extensions` **wires the whole chain with no Lua edit**. ### Query sourcing - **No in-repo `.scm` highlight overlays exist today.** Every grammar sources its queries from crate-exported `&'static str` constants (naming varies: `HIGHLIGHTS_QUERY`, `HIGHLIGHT_QUERY`, `HIGHLIGHT_QUERY_BLOCK`; documented at `src/syntax.rs:779-800`). The CUDA entry (`:919-923`) shows queries composed as a **slice of fragments** in the field. - The only `.scm` in the tree is `audit/audit-rules.scm`, pulled via `include_str!` at `src/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:189` already absorbs `@function.macro`, `@keyword.*`, `@string`, `@comment`, and `@punctuation.*`. But `@markup.*` (headings, italic, bold, math), `@module`, and `@label` **fall 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? local` property 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`) and `injection.language` (`:385-388`), plus `#set!` properties (`:397-404`). Child-language resolution is `resolve_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 mod` block at `src/lib.rs:42-142`; `mod math_parse;` sorts **before** `mcp` (`:100`). - Grammar test convention (CUDA template, `src/syntax.rs:2130-2221`): a table guard `builtin_languages_include_`, a smoke test `_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`/`tex` code anywhere in `src/`, `builtin/`, `pmacs-gpu/`, `pmacs-protocol/`. ### Crate facts (external, corrected + reviewer-verified 2026-07-23) - **Rejected — `tree-sitter-latex` 0.1.0.** Broken repackage; dead declared repo; **no `scanner.c`** for its declared external tokens; proven offline link failure (undefined `tree_sitter_latex_external_scanner_*`). Not linkable at any pin. (F1.) - **Chosen — `codebook-tree-sitter-latex` 0.6.1** (26k downloads, ~3.1 MB compressed). A republish **from the codebook project's fork of `latex-lsp/tree-sitter-latex`** (the real, maintained grammar — MIT, texlab's author). The fork **modernized the bindings to the `LanguageFn` shim and renamed the crate**; genuine `latex-lsp` master still carries old-style bindings (`tree-sitter = 0.24.1` as a *direct* dependency, no shim). (Rev 1/2 mis-attributed this to a Dec-2025 upstream `latex-lsp` commit — GitHub serves fork-network objects through the parent repo's commit/contents API, so the fork's commit `948b89c`, whose `lib.rs` already carries the `codebook_tree_sitter_latex` name, looked upstream.) It **ships `scanner.c`**, exports **`LANGUAGE: LanguageFn`** over `tree-sitter-language 0.1` with `tree-sitter` **dev-only** — the exact shim pattern every pmacs grammar uses. Verified end-to-end: builds against `tree-sitter 0.26` and parses a document containing a **verbatim environment** (which exercises the external scanner) → root kind `source_file`, no errors. The canonical name `tree-sitter-latex` is squatted by the broken crate, hence the `codebook-` prefix. - **Provenance diff — run, not deferred (R2-2).** The 0.6.1 crate was diffed against genuine `latex-lsp` master: `grammar.js` and `scanner.c` — the grammar definition and the hand-written C, the security-relevant surfaces — are **byte-identical**. `grammar.json` / `node-types.json` differ **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** is `parser.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 with `tree-sitter-cli` at 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: 1. **Capture reconciliation.** Rename/curate the query's `@markup.*`, `@module`, and `@label` captures 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). 2. **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. 3. **Node-name compatibility.** Acceptance test 3 (`Query::new` compiles 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 `MathNode` shape 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 1. **The `LanguageFn` shim makes crate-version skew a non-issue.** Every grammar already crosses the `tree-sitter 0.26` boundary through `tree-sitter-language`; `codebook-tree-sitter-latex` is 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 ships `scanner.c` (F1/F2). 2. **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`. 3. **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. 4. **A verified republish beats a canonical-but-broken name.** The squatted `tree-sitter-latex` name 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-fonts` or `ttf-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.rs` render path). The three-way hotspot; wait for folding and gpu-invocation to settle before opening it. - **Instance-side `(math_environment) @math` injection detection** + a `"math"` `BUILTIN_LANGUAGES` layer (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`/`\label` navigation, 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`/`minted` code injections. ## 6. Acceptance **Stage 1 (grammar).** Tests follow the CUDA convention (`src/syntax.rs:2130-2221`) + the paint template (`src/highlight.rs:1296`): 1. **Table guard** `builtin_languages_include_latex`: the entry exists, claims `tex`/`latex`/`sty`/`cls`, and carries the overlay highlights fragment. 2. **Load-and-parse smoke** `latex_grammar_loads_and_parses`: `reg.language("latex")`, parse a minimal `\documentclass{article}\begin{document}Hello $x$\end{document}` **plus a `verbatim` environment** (to exercise the external scanner), assert the root node kind `source_file` and `!has_error()`. 3. **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). 4. **Extension resolution** `language_for_path_resolves_latex_extensions`: `foo.tex` / `foo.latex` / `pkg.sty` / `cls.cls` all resolve to `"latex"`. 5. **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 — the `ParseTreeBundle` + `Layer` injection engine that a future `(math_environment) @math` capture rides (Q#LX4). - PR #132 (modeline detection) — the extension→language chain (`syntax.lua:452-466`) that a single `extensions` field 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 an `include_str!`-loaded `.scm`.