Skip to content

emit-portable: incremental tree reuse for portable targets (#57 S7a–S7c)#69

Merged
johnsoncodehk merged 3 commits into
masterfrom
portable-tree-reuse
Jul 12, 2026
Merged

emit-portable: incremental tree reuse for portable targets (#57 S7a–S7c)#69
johnsoncodehk merged 3 commits into
masterfrom
portable-tree-reuse

Conversation

@johnsoncodehk

Copy link
Copy Markdown
Owner

Completes the parsing side of incremental edit for portable targets (#57): top-level subtrees outside the damage window are now reused across edits instead of reparsing the whole document. Three commits, one per layer:

1. emit-portable: record token spans on CST nodes (S7a)

Every CST node now records its parse-consumption interval [tokStart, tokEnd) in token-stream indices. Consumption semantics (entry pos → finish pos) deliberately include trailing $punct that CST compression drops from children — byte spans alone cannot recover this (a let a=1; Stmt has byte span [0,7) but consumes the ; at 7). Pratt LED nodes take the leftmost operand's start, not the led trigger position.

  • Internal only: CST JSON is byte-identical to master (verified on calc/minijs/envspec/javascript).
  • New tok-spans runner mode + gate assertions: cross-target byte-identical output, top-level spans form a strictly increasing contiguous partition of the consumed stream.

2. emit-portable: reuse top-level subtrees across edits in TS target (S7b)

Doc.edit() on eligible grammars (entry = star(rule) / star(alt(rule…)), e.g. calc and javascript) keeps top-level statements outside the damage window:

  • Lookahead watermark (ext): peek() maintains maxLook; each top-level subtree records ext = max(tokEnd, maxLook). keep-prefix requires ext <= prefix — consumption span alone is unsound because backtracking (e.g. arrow-parameter probing) can peek far past what a statement ultimately consumed. A mutant using tokEnd <= prefix is caught by the gate's peek-trap scenario.
  • keep-suffix requires tokStart >= oldN - suffix; the damaged middle is reparsed via parseTopOne() and adoption happens only on an exact boundary hit (pos === candidate.tokStart + tokDelta). Any miss (mid reject, overshoot) falls back to a full parse with zero mutation of old nodes.
  • Adopted suffix subtrees get an O(subtree) shift (offset/end += byteDelta, token fields += tokDelta).
  • align gains reused (adopted top-level subtree count); validated mode gains a treeEq self-check (reused tree ≡ fresh parse, byte-compared), mirroring streamEq from emit-portable: make streamEq self-check opt-in (S6a, #57) #68. Ineligible grammars (envspec) keep the full-parse path with reused: 0.

3. emit-portable: arena-persistent tree reuse in Go/Rust targets (S7c)

Same algorithm on the flat-arena representation, which requires arenas to survive across edits:

  • Go: the reuse path appends to the package-level arena (old rows — including adopted subtrees — keep their ids); the full-parse path still resets. Rust: Doc now owns long-lived nodes/kids (no borrowed fields in Node); each edit builds a short-lived Parser borrowing the text, moving the arena in and out via mem::take. No unsafe.
  • Reclamation: append-model garbage (replaced mid rows, failed attempts) is bounded by a jsTarget-style policy — when len(nodes) > 4 × max(baseline, live), the edit rebuilds fresh. Probe: 500 mid-document edits stay at max 607 rows (live 151) vs unbounded 3151 with reclamation disabled.
  • Full-consumption guard (found by cross-target fuzzing during review): Go's parse wrapper returned partial trees (pos != len(toks)), unlike TS/Rust. After a rejecting edit, Doc kept a partial root and the next edit's reuse path resurrected it — Go accepted documents that fresh parses reject. parse now rejects partial consumption, matching TS, and the gate pins reject-then-edit / reject-then-repair scenarios.

Validation

  • Gate: all edit scenarios run per target in validated and fast modes; final CST byte-equal to a fresh parse of the final text; reused expectations (min/max/exact) per scenario; a 24-batch long session asserts per-batch treeEq; alignment fields cross-checked against the oracle lexer.
  • Review fuzzing: 30 random multi-batch calc sessions × 3 targets (exit codes, final CST bytes, per-batch align fields all cross-target identical) and 942 randomized single-doc edit steps on calc/javascript in fast mode, every step byte-compared against a fresh parse.
  • 8 mutation tests across the three commits (span semantics, ext check, suffix shift, exact boundary hit, token-field shift, kids placement, reclamation) all caught by the gate/self-checks.
  • Perf (fast mode, 200-stmt calc, reference): Go 22.5 vs 24.6 ms, Rust 15.7 vs 18.4 ms, TS ~1.37× on large docs. The structural win is node identity across edits plus bounded arenas, which later slices can build on.

johnsoncodehk and others added 3 commits July 12, 2026 16:31
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@johnsoncodehk
johnsoncodehk merged commit 51d8987 into master Jul 12, 2026
3 checks passed
@johnsoncodehk
johnsoncodehk deleted the portable-tree-reuse branch July 12, 2026 11:27
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.

1 participant