Skip to content

SWG-INF-04: replace ProgramSpans with a full SourceMap - #194

Merged
PhysShell merged 5 commits into
mainfrom
claude/swang-inf04-source-map
Aug 29, 2026
Merged

SWG-INF-04: replace ProgramSpans with a full SourceMap#194
PhysShell merged 5 commits into
mainfrom
claude/swang-inf04-source-map

Conversation

@PhysShell

@PhysShell PhysShell commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Scope

SWG-INF-04 only: replace the level-1 ProgramSpans four-field special case with a general, span-free-AST SourceMap side table.

Base: main @ 722a70f549fdd37b123bfbe6e6e7cc29520459cb.
Head: ff802fe32156f339e04e9093e0903beb02763c52.

No level-2 parsing, dispatch, recovery, resource limits, token API, persistent selector identity, new dependency, or canonical-score-model change. LANGUAGE_LEVEL remains 1.

Commit evidence

Commit Role Evidence
fec2a17 RED tests only; fails with unresolved parse_with_source_map, AstId, FieldKind, FieldRef, and Parsed
2dbce7a GREEN SourceMap replaces ProgramSpans; one parser; evaluator migrates to the full map
ca91210 DOCS closes INF-04, records identity/prior-art decisions, moves next marker to INF-06
efaa80e review tests tests only; pins exact node extent and real end-to-end diagnostic ownership
ff802fe review docs corrects decision dates and records the full falsification history

The first three commits were not rewritten by review.

Shape

source
  -> parse_with_source_map
  -> Parsed<Program> {
       value: Program,
       source_map: SourceMap,
     }

SourceMap owns private BTreeMap<AstId, Span> and BTreeMap<FieldRef, Span> tables with read-only queries and deterministic iteration.

Two deliberate departures from the backlog sketch are recorded in-tree:

  • one AstId variant per construct instead of a shared PipelineStep, so field ownership is named rather than inferred;
  • no FieldRef::Node, because SourceMap::node_span already owns that relation.

FieldKind::Seed is shared by pruning and generation and disambiguated by the owning AstId. AstId and FieldKind are #[non_exhaustive].

AstId is explicitly parse-local structural identity. It is stable for equal AST topology across whitespace/legal word reordering, but is not persistent across AST-changing edits, not serialized, not a patch identity, and not a semantic-hash input. Phase 4C still owns persistent selector identity.

Acceptance evidence

The level-1 reference maps 7 nodes and 18 fields; without pruning/corpus it maps 15 fields. The contract suite pins:

  • exhaustive level-1 AST field classification with no .. destructuring;
  • exact author-value field slices;
  • exact node extents for all seven level-1 nodes;
  • optional fields having no phantom spans;
  • equal ASTs under legal word reordering having equal key sets but moved byte spans;
  • every span in-bounds and on a UTF-8 boundary;
  • parse and parse_with_source_map sharing acceptance and diagnostics;
  • formatter independence from source-map state;
  • frozen level-1 diagnostic ownership end to end through the evaluator;
  • deterministic map iteration.

ProgramSpans and parse_with_spans are removed rather than wrapped.

Falsification

Initial implementation round: 8 probes, 0 survivors.

Independent review found two evidence gaps. Three additional mutations were first run against the suite exactly as it stood at closure and survived:

  1. stretch every pipeline node span back to byte 0;
  2. swap unit and score/source ownership in flaw_to_diagnostic;
  3. point IncompleteFinalBar at unit instead of tail in lower_diagnostic.

efaa80e adds witnesses for those gaps. The same three mutations are then caught. Final accounting: 11 probes, 0 surviving after the review strengthening, without pretending the three review probes had been caught by the original suite.

No production line changed after 2dbce7a.

Validation

Locally verified on the head:

  • 1436 tests green across core, swang, pattern, cli, and ui-core;
  • cargo fmt --all --check clean;
  • cargo clippy --workspace --all-targets clean;
  • cargo check --workspace --all-targets clean, including cockpit/preview/plugin.

cargo test --workspace cannot link in the current development container because rust-lld cannot find -lasound. The same command fails identically in a clean worktree at the base commit 722a70f; this is a pre-existing environment limitation, not introduced by this branch. CI is the acceptance source for the full workspace test run and fuzz matrix.

State after merge

  • INF-04 closed;
  • Level 2 remains unfrozen;
  • Phase 4A remains open;
  • next parser-lane task: INF-06, then 4A-06.

Summary by CodeRabbit

  • New Features

    • Added source-map support for locating parsed constructs and fields in source text.
    • Added APIs for retrieving construct locations and iterating through mapped source ranges.
    • Compilation results now expose source-location data for improved diagnostic handling.
  • Bug Fixes

    • Diagnostics now identify the correct source sections for kernels, units, sources, tails, and generation failures.
    • Improved UTF-8 boundary handling and span accuracy.
  • Documentation

    • Documented source-map behavior, identifier guarantees, coverage, and design decisions.
  • Tests

    • Added comprehensive coverage for source mapping, diagnostic ownership, ordering, and compatibility.

claude added 5 commits August 29, 2026 11:09
Tests only; no `src/` change. `swang/tests/source_map_contract.rs` states the
contract against the public surface, and cannot compile:

    error[E0432]: unresolved imports `griff_swang::syntax::parse_with_source_map`,
      `griff_swang::syntax::AstId`, `griff_swang::syntax::FieldKind`,
      `griff_swang::syntax::FieldRef`
    error[E0425]: cannot find type `Parsed` in module `griff_swang::syntax`
    error: could not compile `griff-swang` (test "source_map_contract")

`ProgramSpans` locates four words because four were all the expansion
frontend needed. That is a special case wearing a struct: every other value
in a program is unlocatable, and each new one means another field. What
replaces it:

    source -> parse_with_source_map -> Parsed<Program> { value, source_map }

The witnesses, and what each is for:

- **the reference census** — seven nodes, eighteen fields, each slicing back
  to its exact author value. `classify` destructures every level-1 AST
  struct exhaustively with no `..`, so a new AST field stops this compiling
  and forces the choice — field span, or located by a child's node span —
  rather than letting it default to unlocatable;
- **node containment** — every field span lies inside its owner's node span,
  which catches a node span built from the wrong construct;
- **optionality** — no prune and no corpus is fifteen fields, and `density`,
  the pruning `seed` and `corpus` have no location at all. No phantom span
  for syntax nobody wrote. The generation `seed` survives, because the
  owning `AstId` is what tells the two seeds apart;
- **legal reordering** — two sources whose word order differs and whose ASTs
  are equal must produce the same node and field key sets, with *different*
  bytes for the words that moved. A map built from canonical field order
  instead of the author's tokens gives identical spans and fails here;
- **UTF-8** — multibyte literals, every span checked in-bounds and on a
  `char` boundary. A map computed in `chars` slices mid-character;
- **one parser** — `parse` and `parse_with_source_map` accept the same
  programs and refuse with the same codes, messages, spans and order, over
  five flawed sources. Two implementations could drift; there must be one;
- **formatter independence** — a `Program` built directly in Rust, with no
  source text anywhere, still formats and reparses. If the formatter ever
  required a map this would not compile;
- **frozen ownership** — the four locations §3.5 already released still
  slice to the same bytes. The map now knows `bars`, `candidates` and
  `strategy` too; that is editor capability, not permission to move a
  diagnostic someone's tooling already parses;
- **determinism** — two walks agree and both come out in key order, which is
  what a `BTreeMap` is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
A general side table in place of a four-field special case.

    source -> parse_with_source_map -> Parsed<Program> { value, source_map }

`SourceMap` holds two `BTreeMap`s — `AstId -> Span` and `FieldRef -> Span` —
both private, with read-only queries and key-ordered iteration. `AstId`
carries an occurrence ordinal (always `0` at level 1, because level 1 has one
of each construct) so that level 2's repeated nodes need no new identity
model. `FieldKind` is deliberately not unique on its own: `Seed` names both
the pruning and the generation seed, and the owning `AstId` tells them apart.
A variant per (construct, word) pair would grow quadratically and say nothing
the pair does not.

Both enums are `#[non_exhaustive]`, so level 2 appends variants without
breaking a caller that matches today's set. There is no `FieldRef::Node`
variant: `node_span` already owns that relation, and two ways to ask one
question is one too many.

**The AST did not change.** No spans, no ids, no `Parsed` in `Program`,
`PatternDef` or `ExactScoreDocument`; `format` still takes `&Program` alone.
A lifter that builds a program in memory formats and reparses it with no
source text anywhere, which the contract suite proves by doing exactly that.

**One parser, not two.** `parse` is `parse_with_source_map` with the map
dropped, so acceptance and diagnostics cannot drift. `ProgramSpans` and
`parse_with_spans` are removed rather than wrapped — a compatibility shim
would have kept the four-field model alive indefinitely, which is the thing
this task exists to end.

`CompiledProgram` now carries the whole map instead of a four-word
projection, and `source_map()` exposes it. What that does **not** license is
moving a diagnostic: `flaw_to_diagnostic` and `lower_diagnostic` resolve
exactly the four locations §3.5 released — kernel, unit, tail, source — and
`released()` says so at the call site. The map now knows `bars`,
`candidates` and `strategy` too; that is editor capability, not permission
to relocate something a frontend already parses.

`released()` falls back rather than panicking. The map is total over a
well-formed program — the contract suite proves all eighteen level-1 fields
are present — so the fallback is unreachable; it exists because a location
bug should degrade to a worse message, not to a crash in someone's editor.

The two frozen level-1 span tests migrate to the new API with their
assertions intact, as characterization rather than rewrite.

Prior art, and the departure worth naming: byte offsets with no line/column
state is rustc's; the structural-identity/position split is
rust-analyzer's `AstIdMap`; a lossless CST is rowan's and is *not* adopted,
because SWG-UI-07 owns that gate. Unlike rust-analyzer, these ids are
parse-local — deterministic for an AST topology, blind to whitespace and
legal word reordering, and explicitly not stable across edits. Persistent
identity is Phase 4C's question and is not pre-decided here.

`LANGUAGE_LEVEL` stays 1. No dispatch, no level-2 parsing, no recovery, no
resource limits, no token API, no new dependency.

Verified: 1430 tests green across core, swang, pattern, cli, ui-core;
`cargo fmt --all --check`, `cargo clippy --workspace --all-targets` and
`cargo check --workspace --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
Marks INF-04 done, records the acceptance actually met, and moves the "next"
marker to INF-06 — which is a hard prerequisite for 4A-06, not a neighbour:
§5.11 requires level 2's input bounds to be declared before its first
accepted program.

Two departures from this entry's own sketch, recorded rather than quietly
implemented differently. `AstId` gets one variant per construct instead of a
shared `PipelineStep`, so a field's owner is named rather than inferred. And
`FieldRef` has no `Node` variant, because `SourceMap::node_span` already
owns that relation.

One correction in the direction of claiming less: the entry said "the four
existing span tests pass through the new model". There were **two**. They
are carried across as characterization, and the count is fixed rather than
left flattering.

The decision log gains the two records this task owes:

- **what an `AstId` promises.** It is parse-local. Equal ASTs give equal
  node-key and field-key sets with differing spans — deterministic under
  whitespace and under §3.2's legal word reordering, which is the stability
  an editor needs to re-anchor after a reformat. It is *not* stable across
  AST-changing edits, and is not a patch identity, a serialized id, or a
  semantic-hash input. Phase 4C owns persistent identity; a side table that
  never addressed that problem should not look like it solved it.
- **the prior art, adopted and refused.** Byte offsets with no line/column
  state, from `rustc`. The structural-identity/position split, from
  `rust-analyzer`'s `AstIdMap`. A lossless CST is refused: `rowan`'s
  `SyntaxNodePtr` is sound prior art, but SWG-UI-07 owns that gate and its
  three demonstrated needs have not been demonstrated.

Level 2 is not released or frozen, Phase 4A is not closed, v2 parsing does
not exist, and neither do multi-error recovery, resource limits, or
persistent selector identity.

Verified: 16 census witnesses still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
…ownership

Tests only. No production line changes: independent review found two gaps in
the *evidence*, and the behaviour they describe was already correct.

**Gap 1 — frozen ownership was checked on the map, not end to end.**
`the_four_released_diagnostic_locations_are_unchanged` proves the source map
holds the right kernel/unit/tail/source spans. Between that map and a
rendered diagnostic sits `flaw_to_diagnostic`, which decides *which* span
each flaw class gets, and no source-map test can see a mistake there. Swap
two arms of that match and every existing witness stays green while a unit
error starts underlining the seed path.

`swang/tests/diagnostic_ownership.rs` drives the evaluator instead: it
builds seed scores that trigger each of the four released classes, takes the
`EvalDiagnostic` actually produced, and slices the source with the span it
actually chose. A meter change gives `SWG0304` at `"seed.gp5"`; a 1/4 unit
against a 1680-tick 7/8 bar gives `SWG0301` at `1/4`; nine cells into
sixteen-slot bars under `tail reject` gives `SWG0302` at `reject`; a
seventeen-cell kernel with one onset and `bars 1` gives `SWG0306` at the
quoted literal. A fifth test asserts the four resolve to four *distinct*
words, which is what makes an arm swap visible rather than merely possible.

**Gap 2 — node spans were checked for containment, not extent.**
`SourceMap` documents a node span as the *smallest* contiguous range
covering the construct. The only structural witness asked that field spans
lie inside their node span — and `0..source.len()` contains every field it
owns while being wrong about all of them.
`every_node_span_covers_exactly_its_construct` slices all seven level-1
nodes and states what each must be.

Both gaps are confirmed rather than argued. The three mutations were run
against the suite as it stood **before** this commit and against it after:

    stretch every pipeline node back to byte 0   SURVIVED -> CAUGHT
    swap the unit and source diagnostic owners   SURVIVED -> CAUGHT
    point an incomplete final bar at the unit    SURVIVED -> CAUGHT

each now caught by exactly the witness written for it. A guard that would
have passed anyway is decoration; these were not.

That takes the falsification round to eleven probes, none surviving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
…ount

Docs only.

**The dates were wrong by three days.** Both new decision-log entries were
written `2026-08-26`; the whole INF-04 run is `2026-08-29` — RED at
11:09:01Z, closure at 11:20:42Z. In an append-only log the date is most of
what a later reader has to order events by, so a three-day drift for no
reason at all is worth one commit to remove. Corrected in place rather than
appended to: the entries themselves are unchanged, and a correction note
about a typo would be longer than the typo.

**The falsification count moves from eight to eleven.** The closure said
"eight mutations of the implementation, none survived", which was true of
the round as run and stopped being the whole story when independent review
found two evidence gaps. The three probes added since — a node span
stretched back to byte 0, and two swapped arms of the evaluator's
flaw-to-location match — all survived the suite as it stood at closure. The
backlog now says so, because "eleven caught" reads like a stronger round
than it was, and what actually happened is that three holes were found and
closed.

Verified: 16 census witnesses still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 858c405f-0b3e-46dc-a1ac-fc37769e65ed

📥 Commits

Reviewing files that changed from the base of the PR and between 722a70f and ff802fe.

📒 Files selected for processing (9)
  • docs/decisions.log.md
  • docs/swang/foundation-backlog.md
  • swang/src/eval.rs
  • swang/src/syntax.rs
  • swang/src/syntax/parser/v1.rs
  • swang/src/syntax/source_map.rs
  • swang/src/syntax/tests.rs
  • swang/tests/diagnostic_ownership.rs
  • swang/tests/source_map_contract.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The parser now returns a SourceMap with parse-local AST and field identifiers. CompiledProgram uses the map to resolve diagnostic spans. Tests verify coverage, ownership, UTF-8 boundaries, deterministic iteration, parser parity, and unchanged diagnostic locations.

Changes

SourceMap contract and public API

Layer / File(s) Summary
SourceMap contract and public API
swang/src/syntax/source_map.rs, swang/src/syntax.rs, docs/decisions.log.md
Adds Parsed, AstId, FieldKind, FieldRef, and SourceMap. Replaces ProgramSpans and parse_with_spans exports. Documents parse-local identity and byte-offset locations.

Parser source-map population

Layer / File(s) Summary
Parser source-map population
swang/src/syntax/parser/v1.rs
Renames the mapped parser entry point and records program, pattern, pipeline-node, and field spans in a side table. Step parsers now return AST values directly.

Evaluator diagnostic resolution

Layer / File(s) Summary
Evaluator diagnostic resolution
swang/src/eval.rs
Stores SourceMap in CompiledProgram and exposes it through source_map(). Diagnostic lowering resolves kernel, unit, tail, and source spans through mapped fields with fallback locations.

Source-map and diagnostic validation

Layer / File(s) Summary
Source-map and diagnostic validation
swang/src/syntax/tests.rs, swang/tests/source_map_contract.rs, swang/tests/diagnostic_ownership.rs, docs/swang/foundation-backlog.md
Adds tests for map coverage, field ownership, node containment, UTF-8 boundaries, deterministic handles, parser parity, formatting independence, and diagnostic ownership. Marks SWG-INF-04 complete.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to ff802

The PR replaces specialized source spans with a read-only source map for parser and diagnostic locations without changing accepted language behavior or evaluation authority. No actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Source as Source text
  participant Parser as parse_with_source_map
  participant Map as SourceMap
  participant Evaluator as compile_program
  participant Diagnostic as diagnostic lowering

  Source->>Parser: parse level-1 program
  Parser->>Map: record node and field spans
  Parser-->>Evaluator: return Parsed<Program>
  Evaluator->>Diagnostic: pass compiled program and SourceMap
  Diagnostic->>Map: resolve FieldRef span
  Map-->>Diagnostic: return diagnostic location
Loading

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing ProgramSpans with a full SourceMap for SWG-INF-04.
Docstring Coverage ✅ Passed Docstring coverage is 83.61% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 7 files. (2 skipped: 2 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 83.61% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 7 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/swang-inf04-source-map

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: ff802fe321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@PhysShell
PhysShell merged commit c44313c into main Aug 29, 2026
15 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