From e588449b0a02f7727ded335b12409f2b816b2aab Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 17:05:25 -0400 Subject: [PATCH 1/7] WIP: SG-2c --- src/v3/compiler/parse_tables.dag | 65 ++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/parse_tables.dag b/src/v3/compiler/parse_tables.dag index 0e015c970ff..d537ae8906e 100644 --- a/src/v3/compiler/parse_tables.dag +++ b/src/v3/compiler/parse_tables.dag @@ -16,14 +16,19 @@ // class (SG-2c-2), (3) type-RHS boundary keyword membership // (SG-2c-3), (4) bracket opener/closer role membership (SG-2c-4, // consumed by the parser's depth-tracking scan helpers `skip_where_clause` -// and `rhs_is_sum`), and (5) primary-expression prefix openers (SG-2c-6, -// consumed by `parse_primary`). The binary-operator table replaced hand-coded +// and `rhs_is_sum`), (5) primary-expression prefix openers (SG-2c-6, +// consumed by `parse_primary`), and (6) primary-expression atomic tail tokens +// after the prefix pass (SG-2c-7, same function). The binary-operator table replaced hand-coded // matches across five parser precedence functions in // `parse_parser_body.txt` // (`parse_logical_or`, `parse_logical_and`, `parse_comparison`, // `parse_additive`, `parse_term`). Those functions now call the // generated helper `binary_op_at_level` instead of open-coding the -// token → `OperatorKind` match. +// token → `OperatorKind` match. **SG-2c cluster (prefix + atom family):** after +// the `parse_primary` prefix pass (`PrimaryPrefixRow` → +// `primary_prefix_dispatch`, SG-2c-6), the non-prefix first token of a primary +// is classified by `PrimaryAtomRow` (SG-2c-7) via `primary_atom_class` so the +// closed set of `Int`/`true`/`false`/string/lambda/ident forms stays table-owned. // // **What this file is NOT.** Parser control flow. Cursor mechanics. // Error recovery. Item-kind dispatch. Recursive-descent itself. Those @@ -61,6 +66,11 @@ // `PrimaryPrefixDispatch` is emitted from `PrimaryPrefixRow` facts and // consumed by `parse_primary` in `parse_parser_body.txt`, replacing the // four inline `if matches!(peek, …)` prefix probes. +// +// **Same-PR consumer (E-6).** SG-2c-7 projection `primary_atom_class` / +// `PrimaryAtomClass` is emitted from `PrimaryAtomRow` facts and consumed +// in the same `parse_primary` function after the prefix pass, replacing the +// open `match` on the post-bump `TokenKind` for atomic primary forms. module v3.compiler.parse_tables @@ -447,3 +457,52 @@ data primary_prefix_lbrace: PrimaryPrefixRow = { data primary_prefix_lbracket: PrimaryPrefixRow = { token_variant: "LBracket" } + +// --- SG-2c-7: `parse_primary` atomic tail (after prefix pass). The bump that +// follows a non-prefix `peek` must be one of: integer literal, boolean +// keyword literals, string, pipe-(`|>`) lambda start, or identifier — the +// closed set that the parser accepts as a primary expression *atom*. Authored +// here once; `PrimaryAtomClass` and `primary_atom_class` are regen-only +// (projection over `token_variant` rows, same M7 shape as +// `PrimaryPrefixRow` → `PrimaryPrefixDispatch`). +// +// **Cluster boundary (SG-2c wave).** This table pairs with SG-2c-6: both are +// `parse_primary`-local dispatch; together they form one “parser dispatch +// family” extraction (prefix openers + atomic forms) under a single +// `parse_tables` authority surface. +// +// Dissolution classification: **🟡 YELLOW (scaffold)**. Same as other +// `parse_tables` row families. Full dissolution rides SG-2c proper (blocked +// on recursive `List` list-body emission). +type PrimaryAtomRow { + token_variant: String +} + +// Dispatch labels (Rust `PrimaryAtomClass` variants) derive at regen: see +// `primary_atom_class_label_from_token_variant` in `regen_parse_tables_emit.rs` +// (closed allowlist: `IntLit` → `IntLiteral`, `Kw{True,False}` → `{True,False}`, +// `StringLit` → `String`, `Pipe` → `Pipe`, `Ident` → `Ident`). + +data primary_atom_int_lit: PrimaryAtomRow = { + token_variant: "IntLit" +} + +data primary_atom_kw_true: PrimaryAtomRow = { + token_variant: "KwTrue" +} + +data primary_atom_kw_false: PrimaryAtomRow = { + token_variant: "KwFalse" +} + +data primary_atom_string_lit: PrimaryAtomRow = { + token_variant: "StringLit" +} + +data primary_atom_pipe: PrimaryAtomRow = { + token_variant: "Pipe" +} + +data primary_atom_ident: PrimaryAtomRow = { + token_variant: "Ident" +} From bc959b9f196a5055fd844986b0ca675050f8fc45 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 17:19:21 -0400 Subject: [PATCH 2/7] =?UTF-8?q?feat(SG-2c=20cluster):=20parse=5Fprimary=20?= =?UTF-8?q?prefix=20+=20atom=20family=20=E2=80=94=20full=20receipt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete same-PR extraction for PrimaryAtomRow (SG-2c-7) alongside SG-2c-6: - regen_parse_tables_emit: collect rows, emit primary_atom_class/PrimaryAtomClass - checked-in parse_tables_generated.rs + regen_parse parse_generated.rs - parse_parser_body: table-owned dispatch; remove open match on atomic TokenKinds - sg2c1 authority tests: row join + exact post-prefix token set - ROADMAP + compiler-std-consolidation: 7 exempted types, cluster note Cluster name: parse_primary prefix + atom family. Made-with: Cursor --- ROADMAP.md | 6 +- docs/thesis/compiler-std-consolidation.md | 6 +- src/v3/compiler/parse_parser_body.txt | 39 +++-- src/v3/compiler/parse_tables.dag | 8 ++ src/v3/compiler/src/bin/regen_parse_tables.rs | 5 +- src/v3/compiler/src/lib.rs | 3 +- src/v3/compiler/src/parse_generated.rs | 45 +++--- src/v3/compiler/src/parse_tables_generated.rs | 26 ++++ src/v3/compiler/src/regen_parse_emit.rs | 5 +- .../compiler/src/regen_parse_tables_emit.rs | 134 +++++++++++++++++- .../sg2c1_parse_tables_authority_test.rs | 93 +++++++++++- 11 files changed, 322 insertions(+), 48 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 3405455b5d5..92b6c29828f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -175,7 +175,7 @@ Findings from two reflective analyses (integration loop health, `main@b014746` a Tracked debt not dispatched with the 2026-04-21 lane wave currently in flight (Lane 1e P3.0, SG-3g-b, SG-2c-4, SG-4b-3, cascade cleanup, algebra inhabitance quartet). Opportunistic slots — no blocking dependencies, can be picked up whenever bandwidth frees. -- **SG-2c growth-discipline checkpoint (2026-04-22)**: SG-2c-1…5 have landed; SG-2c-6 is WIP. Decision doc [docs/briefs/sg-2c-proper-capability-gap.md](docs/briefs/sg-2c-proper-capability-gap.md) names the SG-2c-proper capability blocker concretely (recursive `.dag` function bodies over `List` with cursor threading + `Token` variant match + `Result<(Surface*, Int), Diagnostic>` short-circuit — load-bearing sub-gap is list-body emission per `src/v3/std/list.dag:13-15`), estimates cadence-vs-pivot scope, and recommends: **natural ceiling** — dispatch SG-2c-6 only under a sharper bar (named same-PR consumer block + non-derivable row family, per SG-2c-3 precedent), do not pre-queue SG-2c-7+, and begin capability-pivot design in parallel as a separate C1-scoped lane rather than serially behind more row lanes. Parallel-authority risk from the accumulated table is judged modest-not-accelerating (rows are data, trigger-gated, exempted in the std/-consolidation ratchet under the `parse_tables.dag` precedent-rule bullet). +- **SG-2c growth-discipline checkpoint (2026-04-22)**: SG-2c-1…5 have landed; the **parse_primary** cluster (SG-2c-6 + SG-2c-7) is closed. Decision doc [docs/briefs/sg-2c-proper-capability-gap.md](docs/briefs/sg-2c-proper-capability-gap.md) names the SG-2c-proper capability blocker concretely (recursive `.dag` function bodies over `List` with cursor threading + `Token` variant match + `Result<(Surface*, Int), Diagnostic>` short-circuit — load-bearing sub-gap is list-body emission per `src/v3/std/list.dag:13-15`); the **next** open question is the pivot-vs-tiny-extractions trade (see that doc) — do not pre-queue a long tail of one-row `parse_tables` extractions; land capability work for SG-2c proper in parallel. Parallel-authority risk from the accumulated `parse_tables` row families remains judged modest-not-accelerating (exempted under the std/-consolidation `parse_tables.dag` precedent-rule bullet). - **`algebra.dag` signature/comment reconciliation**: `dsl/std/algebra.dag:265-302` — `FreeMonoid`'s comments promise `index(FreeMonoid, Nat) -> T?`, `map(FreeMonoid, fn(T)->U) -> FreeMonoid`, `fold(FreeMonoid, init: U, fn(U,T)->U) -> U`; the declarations drop the carrier/source parameters (`index: fn(Int) -> T`, `map: fn(fn(T)->T) -> FreeMonoid`, `fold: fn(T, fn(T,T)->T) -> T`). `PartialFunction` at `:307-325` has the same issue — comment promises `merge(Map, Map, fn(V,V)->V) -> Map`, declaration is `merge: fn(PartialFunction) -> PartialFunction` (missing fn argument). One of the two (comments or declarations) is wrong; reconcile. P1 Modeling Faithfulness receipt. Owner: unassigned; small scope. - **`container_template_algebra_rows` string table is duplicate authority**: `dsl/std/types.dag:133-146` declares the renamed map (PR #651 collapsed both consumers onto a single `container_template_algebra` query); rows still duplicate the authoritative type aliases at `:211-213` (`type List = FreeMonoid`, etc.) across Pascal/snake spellings plus algebra self-identities. Dissolution trigger: when `.dag` gains alias reflection, the rows derive from the aliases directly and the hand-maintained table goes away. P2 Boundary Discipline / P1 receipt. Owner: unassigned; small scope. @@ -193,10 +193,10 @@ Tracked debt not dispatched with the 2026-04-21 lane wave currently in flight (L - **Lane E-M — `MethodSemantics` port-or-subsume (S–M)**: transitive carriers (`CollectionSizeEffect` / `CostShape` / `AlgebraFieldTemplate`) are already in `dsl/std/algebra.dag:408-430`, so M-a is a routine port of ~4 carriers (not a promotion-out-of-compiler-internal). M-b is structural subsumption via v3's `TransformTarget` model. Call is "does v3 structural resolution already carry these facts?" — queue after E-T/C/I land. - **CI ratchet architecture — exemption widening erodes the per-test timeout ratchet**: surfaced during the 2026-04-21 reflective analysis (`53b3110..ae8825a` range). Commits `37cd6128`, `4898983e`, `f84ed355` hardened the ratchet against CI-log instability, but `2d8396df` widened the exemption list. `scripts/slow-test-exemptions.txt` is 83 lines with no monotonic-shrink rule — `feedback_ratchet_only_down` violated at the meta level (the primary ratchet can drift upward through exemption growth). Dissolution: audit the exemption list, categorize each (stale / paydown / structural), delete stale entries, add a meta-ratchet that fails CI on exemption-count growth or per-exempt budget drift. Dispatch brief: `docs/briefs/ci-ratchet-architecture-audit.md`. Owner: unassigned; queue as infrastructure-principal lane, soon. - **Stale-receipt sweep in `docs/briefs/`**: surfaced during the 2026-04-21 reflective analysis. Briefs under `docs/briefs/` and prose references in adjacent docs still point at debt that the final code has partially or fully dissolved — the `DeclarationLookup` cleanup is the cited example where brief prose names debt the code has largely resolved. Integration drag, not code regression. Dissolution: sweep `docs/briefs/` for superseded claims post-wave, convert closed briefs to historical receipts under `docs/history/` or add a "Closed by #NNN" line, update inline prose in sibling docs that cite the resolved debt. Small scope, cosmetic. Owner: unassigned; do soon after the 2026-04-21 wave lands (most of its outputs will trigger more brief-staleness). **Include:** `docs/design-pure-bootstrap.md`'s hand-maintained count ("78 hand-maintained .rs files") is stale — live `EXPECTED_HAND_AUTHORED` in the SG-0 census has 89+ entries under `src/v3/compiler/`. Either refresh the number or delete it and point at the live census; detected during #642 review. -- **Compiler–`std/` consolidation program — specific migrations**: end-state defined in [docs/thesis/compiler-std-consolidation.md](docs/thesis/compiler-std-consolidation.md). Each of the migrations below is a standalone lane; together they collapse the compiler-specific type surface toward the positive definition (pipeline + regen + lens-specific return-type carriers + accessor). **Ratchet:** count of `type` declarations in `src/v3/compiler/*.dag` AND `src/v3/lenses/*.dag` that are NOT in the positive-def set AND NOT exempted → 0. Positive-def: pipeline/regen types + lens-API return carriers (`Origin`, `UnusedParameter`, `VariantPayloadShapeLookup` 3-variant, `TemplateArgumentBinding` semantic carrier, etc.). Exempted: `parse_tables.dag` (6 types, pending SG-2c-proper per-row classification). In-ratchet: **strict 2-variant Missing|Found Lookup-pattern carriers across lenses (3 today: `CostLookup`, `SymbolicCostLookup`, `TemplateArgumentLookup`)** + **workaround-shaped infer-helper coproducts with named dissolution triggers (`TemplateArgumentsMatch`, `TemplateArgumentCursor`)**. Baseline: 5. See thesis doc table for per-file disposition. +- **Compiler–`std/` consolidation program — specific migrations**: end-state defined in [docs/thesis/compiler-std-consolidation.md](docs/thesis/compiler-std-consolidation.md). Each of the migrations below is a standalone lane; together they collapse the compiler-specific type surface toward the positive definition (pipeline + regen + lens-specific return-type carriers + accessor). **Ratchet:** count of `type` declarations in `src/v3/compiler/*.dag` AND `src/v3/lenses/*.dag` that are NOT in the positive-def set AND NOT exempted → 0. Positive-def: pipeline/regen types + lens-API return carriers (`Origin`, `UnusedParameter`, `VariantPayloadShapeLookup` 3-variant, `TemplateArgumentBinding` semantic carrier, etc.). Exempted: `parse_tables.dag` (7 types, pending SG-2c-proper per-row classification). In-ratchet: **strict 2-variant Missing|Found Lookup-pattern carriers across lenses (3 today: `CostLookup`, `SymbolicCostLookup`, `TemplateArgumentLookup`)** + **workaround-shaped infer-helper coproducts with named dissolution triggers (`TemplateArgumentsMatch`, `TemplateArgumentCursor`)**. Baseline: 5. See thesis doc table for per-file disposition. - **`tokenize.dag` → `std/tokenize.dag`** — landed: `Token`, `TokenKind`, `KeywordTokenKind`, `PunctTokenKind`, `LocalPunctSpec`, `StringEscapeSpec` now live in `src/v3/std/tokenize.dag`; compiler-local duplicates deleted. Ratchet drop: 25 → 19. - **`runtime_mirrors.dag` → `src/v3/std/parse_surface.dag` (tranche 2)** — landed: `DagDifference`, `Surface*`, and related carriers (~14 types) now live in `v3.std.parse_surface`; `runtime_mirrors.dag` deleted. Ratchet drop: 19 → 5. Parse-rule/fragment dissolution triggers (`parse_parser_body.txt`, SG-2b/SG-3f) unchanged. - - **`parse_tables.dag`** — decide per-type whether `BinaryOpLevel` / `BinaryOpRow` / `TopLevelItemKwRow` / `SoftKeywordIdentRow` / `BracketRow` / `PrimaryPrefixRow` stay compiler-API (dispatch-row shapes) or move to `std/syntax.dag` (language-level precedence/dispatch facts). Owner: whoever picks up **SG-2c proper** parser cutover; migration-gate: SG-2c proper completion (the `.dag` parser reveals which rows it dispatches on vs which are pure data). Precedent rule: if any individual row needs to move before SG-2c proper lands, the mover-lane sets the classification and the rest follow the same rule when touched. + - **`parse_tables.dag`** — decide per-type whether `BinaryOpLevel` / `BinaryOpRow` / `TopLevelItemKwRow` / `SoftKeywordIdentRow` / `BracketRow` / `PrimaryPrefixRow` / `PrimaryAtomRow` stay compiler-API (dispatch-row shapes) or move to `std/syntax.dag` (language-level precedence/dispatch facts). Owner: whoever picks up **SG-2c proper** parser cutover; migration-gate: SG-2c proper completion (the `.dag` parser reveals which rows it dispatches on vs which are pure data). Precedent rule: if any individual row needs to move before SG-2c proper lands, the mover-lane sets the classification and the rest follow the same rule when touched. - **`src/v3/std/*.dag` → `dsl/std/*.dag`** — the whole v3-specific std tree collapses when the file-preference-scaffold dissolves (gated on v2 retirement or `dsl/std/` learning v3 grammar). Largest single consolidation; tracked separately in the **"File-preference rank is a ratified-parallel-authority scaffold"** row (earlier in the 2026-04-21 receipt-closure wave post-merge-debt section). - **`Node` → `std/node.dag`** — already captured in `project_node_to_std` memory as prior pattern. Relevant to this program as a precedent. - **Generic `Lookup` in `std/` — dissolve per-lens `CostLookup` / `SymbolicCostLookup` / `TemplateArgumentLookup` duplicates** — three lenses currently declare their own 2-variant `Missing | Found(T)` carrier. Collapse to a single `std/Lookup` generic consumed by every lens that returns a possibly-missing result. 3 types dissolve into 1. Scope is strict 2-variant shape only — carriers with additional semantic variants (e.g., `VariantPayloadShapeLookup`'s `NotPayloadProduct`, `TemplateArgumentBinding`'s `Conflict | NoOp | Append`) stay lens-API. Owner: unassigned; migration-gate: **ready-to-dispatch** (no capability gap — pure type-declaration work; generics already work per the earlier ParseStep discussion). diff --git a/docs/thesis/compiler-std-consolidation.md b/docs/thesis/compiler-std-consolidation.md index 3685a8436d6..cad55d24721 100644 --- a/docs/thesis/compiler-std-consolidation.md +++ b/docs/thesis/compiler-std-consolidation.md @@ -125,7 +125,7 @@ Positive-definition set (NOT counted against the ratchet): - Substrate reflection accessor declarations Exempted (pending named trigger, not counted either direction): -- `parse_tables.dag` — 6 types, exempted pending SG-2c-proper per-row classification +- `parse_tables.dag` — 7 types, exempted pending SG-2c-proper per-row classification Ratchet-tracked (**migrates to `std/`, counted against the baseline**): - Compiler-side types that duplicate user-facing concepts (tokenize types moved; parse-surface family moved — remaining debt is lens Lookup/coproduct scaffolds listed in the inventory) @@ -136,7 +136,7 @@ Baseline (2026-04-22, measured via `grep -cE "^type [A-Z]"` after the tokenizer | File | Type decls | Disposition | |---|---|---| -| `src/v3/compiler/parse_tables.dag` | 6 | exempted pending SG-2c-proper | +| `src/v3/compiler/parse_tables.dag` | 7 | exempted pending SG-2c-proper | | `src/v3/compiler/operators.dag` | 0 | — | | `src/v3/compiler/pipeline.dag` | 3 | positive-def | | `src/v3/compiler/regen.dag` | 1 | positive-def | @@ -155,7 +155,7 @@ Baseline (2026-04-22, measured via `grep -cE "^type [A-Z]"` after the tokenizer End state: 0. Each migration lane reduces the count; positive-definition types track growth separately and are not bounded downward by this ratchet. -**`parse_tables.dag` exemption protocol.** The gap analysis above (§"From `src/v3/compiler/parse_tables.dag` → unclear") says some row *shapes* (`BinaryOpRow`, `TopLevelItemKwRow`, `SoftKeywordIdentRow`, `PrimaryPrefixRow`, etc.) may legitimately stay compiler-API as parser-dispatch shapes, while the *data* they carry (operator precedence levels, keyword-to-item mappings) moves to `std/syntax.dag`. That per-row classification is deferred until SG-2c-proper parser cutover. While deferred, `parse_tables.dag` is **exempted from the primary ratchet count** — its 6 types neither count toward the migration target nor against it. When SG-2c-proper classifies: +**`parse_tables.dag` exemption protocol.** The gap analysis above (§"From `src/v3/compiler/parse_tables.dag` → unclear") says some row *shapes* (`BinaryOpRow`, `TopLevelItemKwRow`, `SoftKeywordIdentRow`, `PrimaryPrefixRow`, `PrimaryAtomRow`, etc.) may legitimately stay compiler-API as parser-dispatch shapes, while the *data* they carry (operator precedence levels, keyword-to-item mappings) moves to `std/syntax.dag`. That per-row classification is deferred until SG-2c-proper parser cutover. While deferred, `parse_tables.dag` is **exempted from the primary ratchet count** — its 7 types neither count toward the migration target nor against it. When SG-2c-proper classifies: - Rows classified as **compiler-API dispatch shapes** → formally added to the positive-definition set (same status as `pipeline.dag` / `regen.dag` types). - Rows classified as **language-level data** → become ratchet-tracked migrations to `std/syntax.dag`. diff --git a/src/v3/compiler/parse_parser_body.txt b/src/v3/compiler/parse_parser_body.txt index 75784de1d11..091c7f65f10 100644 --- a/src/v3/compiler/parse_parser_body.txt +++ b/src/v3/compiler/parse_parser_body.txt @@ -8,11 +8,12 @@ // (`parse_logical_or`, `parse_logical_and`, `parse_comparison`, `parse_additive`, // `parse_term`) used to open-code the token → `OperatorKind` mapping in five separate // match blocks. That mapping is now authored as `binary_op_*` data rows in -// `parse_tables.dag` and consumed here via `binary_op_at_level`. SG-2c-6 moves the -// `parse_primary` prefix opener set (`if` / `match` / `{` / `[`) into `PrimaryPrefixRow` -// rows, consumed via `primary_prefix_dispatch`. The parser's control flow — cursor -// mechanics, recursive descent, error recovery, span fusion — remains semantic -// authority in this file. +// `parse_tables.dag` and consumed here via `binary_op_at_level`. The SG-2c +// `parse_primary` cluster: SG-2c-6 `PrimaryPrefixRow` + `primary_prefix_dispatch` +// (prefix openers) and SG-2c-7 `PrimaryAtomRow` + `primary_atom_class` (int/bool/ +// string/lambda/ident atomic forms). The parser's control flow — cursor mechanics, +// recursive descent, error recovery, span fusion — remains semantic authority in +// this file. // // Dissolution trigger (SG-2c proper): delete this file when parse **logic** is // structurally `.dag`-owned (SELF_HOSTING §6 Phase 4a direct port or Phase 4b @@ -1086,27 +1087,37 @@ impl<'a> Parser<'a> { None => {} } let token = self.bump().clone(); - match token.kind { - TokenKind::IntLit(value) => Ok(SurfaceExpr::Literal { + let cls = primary_atom_class(&token.kind).ok_or_else(|| Diagnostic::ParseError { + message: format!("expected primary expression, got {:?}", token.kind), + span: token.span.clone(), + fixes: Vec::new(), + })?; + match (cls, token.kind) { + (PrimaryAtomClass::IntLiteral, TokenKind::IntLit(value)) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Int(value), span: token.span, }), - TokenKind::KwTrue => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::True, TokenKind::KwTrue) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Bool(true), span: token.span, }), - TokenKind::KwFalse => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::False, TokenKind::KwFalse) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Bool(false), span: token.span, }), - TokenKind::StringLit(value) => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::String, TokenKind::StringLit(value)) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::String(value), span: token.span, }), - TokenKind::Pipe => self.parse_lambda(token.span), - TokenKind::Ident(name) => self.parse_ident_expr(name, token.span), - other => Err(Diagnostic::ParseError { - message: format!("expected primary expression, got {other:?}"), + (PrimaryAtomClass::Pipe, TokenKind::Pipe) => self.parse_lambda(token.span), + (PrimaryAtomClass::Ident, TokenKind::Ident(name)) => { + self.parse_ident_expr(name, token.span) + } + (class, other) => Err(Diagnostic::ParseError { + message: format!( + "internal: primary-atom class {class:?} does not match `TokenKind` {other:?} \ + (fix `PrimaryAtomRow` in `parse_tables.dag` or `regen_parse_tables` output)" + ), span: token.span, fixes: Vec::new(), }), diff --git a/src/v3/compiler/parse_tables.dag b/src/v3/compiler/parse_tables.dag index d537ae8906e..2d2bbb3d921 100644 --- a/src/v3/compiler/parse_tables.dag +++ b/src/v3/compiler/parse_tables.dag @@ -1,5 +1,13 @@ // v3.compiler.parse_tables — SG-2c-1 grammar-tables prototype authority. // +// **SG-2c cluster (structural lane) — `parse_primary` prefix + atom family.** +// Two table surfaces in one function: (A) `PrimaryPrefixRow` / `primary_prefix_dispatch` +// (SG-2c-6), (B) `PrimaryAtomRow` / `primary_atom_class` (SG-2c-7). Same-PR receipt: +// authority in this file; `regen_parse_tables` to `parse_tables_generated.rs`; +// `regen_parse` of `parse_parser_body.txt` to `parse_generated.rs`; +// `sg2c1_parse_tables_authority_test` ratchet; open-coded `TokenKind` dispatch in +// `parse_primary` removed in favor of those projections. +// // **Scope.** SG-2c proper (parser authority proper — retiring // `parse_parser_body.txt` as parse logic) is blocked on a named // substrate capability: recursive list-body emission over `List` diff --git a/src/v3/compiler/src/bin/regen_parse_tables.rs b/src/v3/compiler/src/bin/regen_parse_tables.rs index d2fbe4beecd..2367b603edf 100644 --- a/src/v3/compiler/src/bin/regen_parse_tables.rs +++ b/src/v3/compiler/src/bin/regen_parse_tables.rs @@ -8,8 +8,9 @@ //! Emitted projection includes **SG-2c-1** `binary_op_at_level` / `BinaryOpLevel`, //! **SG-2c-2** `top_level_item_dispatch` / `ItemDispatchKind`, **SG-2c-3** //! `is_type_rhs_boundary_keyword`, **SG-2c-4** `bracket_role` / `BracketRole`, -//! **SG-2c-5** `soft_keyword_ident_spelling`, and **SG-2c-6** `primary_prefix_dispatch` / -//! `PrimaryPrefixDispatch` (see `regen_parse_tables_emit.rs`; not in this file). +//! **SG-2c-5** `soft_keyword_ident_spelling`, **SG-2c-6** `primary_prefix_dispatch` / +//! `PrimaryPrefixDispatch`, and **SG-2c-7** `primary_atom_class` / `PrimaryAtomClass` +//! (see `regen_parse_tables_emit.rs`; not in this file). //! //! Scope (grammar-tables prototype, NOT SG-2c proper) — see //! `src/v3/compiler/parse_tables.dag` for the full scope note and diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index b379fb98d2c..acb925be115 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -85,7 +85,8 @@ pub mod operators { } /// SG-2c grammar-tables prototype (SG-2c-1 binary ops, SG-2c-2 item-keyword -/// dispatch, SG-2c-3 type-RHS boundary keywords). +/// dispatch, SG-2c-3 type-RHS boundary keywords, SG-2c-4 brackets, SG-2c-5 soft +/// keyword idents, SG-2c-6/7 `parse_primary` prefix + atom cluster). /// Authority: `src/v3/compiler/parse_tables.dag`. /// The generated Rust projection is emitted by `regen_parse_tables` and consumed /// from `parse_parser_body.txt` so the parser no longer open-codes token → operator diff --git a/src/v3/compiler/src/parse_generated.rs b/src/v3/compiler/src/parse_generated.rs index 61eb3320a9e..ed9beb2b9e0 100644 --- a/src/v3/compiler/src/parse_generated.rs +++ b/src/v3/compiler/src/parse_generated.rs @@ -8,9 +8,9 @@ pub use crate::parse_surface::{ SurfaceVariant, VariantPayload, }; use crate::parse_tables::{ - binary_op_at_level, bracket_role, is_type_rhs_boundary_keyword, primary_prefix_dispatch, - soft_keyword_ident_spelling, top_level_item_dispatch, BinaryOpLevel, BracketRole, - ItemDispatchKind, PrimaryPrefixDispatch, + binary_op_at_level, bracket_role, is_type_rhs_boundary_keyword, primary_atom_class, + primary_prefix_dispatch, soft_keyword_ident_spelling, top_level_item_dispatch, BinaryOpLevel, + BracketRole, ItemDispatchKind, PrimaryAtomClass, PrimaryPrefixDispatch, }; use crate::tokenize::{Token, TokenKind}; @@ -35,11 +35,12 @@ impl SurfaceType { // (`parse_logical_or`, `parse_logical_and`, `parse_comparison`, `parse_additive`, // `parse_term`) used to open-code the token → `OperatorKind` mapping in five separate // match blocks. That mapping is now authored as `binary_op_*` data rows in -// `parse_tables.dag` and consumed here via `binary_op_at_level`. SG-2c-6 moves the -// `parse_primary` prefix opener set (`if` / `match` / `{` / `[`) into `PrimaryPrefixRow` -// rows, consumed via `primary_prefix_dispatch`. The parser's control flow — cursor -// mechanics, recursive descent, error recovery, span fusion — remains semantic -// authority in this file. +// `parse_tables.dag` and consumed here via `binary_op_at_level`. The SG-2c +// `parse_primary` cluster: SG-2c-6 `PrimaryPrefixRow` + `primary_prefix_dispatch` +// (prefix openers) and SG-2c-7 `PrimaryAtomRow` + `primary_atom_class` (int/bool/ +// string/lambda/ident atomic forms). The parser's control flow — cursor mechanics, +// recursive descent, error recovery, span fusion — remains semantic authority in +// this file. // // Dissolution trigger (SG-2c proper): delete this file when parse **logic** is // structurally `.dag`-owned (SELF_HOSTING §6 Phase 4a direct port or Phase 4b @@ -1115,27 +1116,37 @@ impl<'a> Parser<'a> { None => {} } let token = self.bump().clone(); - match token.kind { - TokenKind::IntLit(value) => Ok(SurfaceExpr::Literal { + let cls = primary_atom_class(&token.kind).ok_or_else(|| Diagnostic::ParseError { + message: format!("expected primary expression, got {:?}", token.kind), + span: token.span.clone(), + fixes: Vec::new(), + })?; + match (cls, token.kind) { + (PrimaryAtomClass::IntLiteral, TokenKind::IntLit(value)) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Int(value), span: token.span, }), - TokenKind::KwTrue => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::True, TokenKind::KwTrue) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Bool(true), span: token.span, }), - TokenKind::KwFalse => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::False, TokenKind::KwFalse) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::Bool(false), span: token.span, }), - TokenKind::StringLit(value) => Ok(SurfaceExpr::Literal { + (PrimaryAtomClass::String, TokenKind::StringLit(value)) => Ok(SurfaceExpr::Literal { value: SurfaceLiteral::String(value), span: token.span, }), - TokenKind::Pipe => self.parse_lambda(token.span), - TokenKind::Ident(name) => self.parse_ident_expr(name, token.span), - other => Err(Diagnostic::ParseError { - message: format!("expected primary expression, got {other:?}"), + (PrimaryAtomClass::Pipe, TokenKind::Pipe) => self.parse_lambda(token.span), + (PrimaryAtomClass::Ident, TokenKind::Ident(name)) => { + self.parse_ident_expr(name, token.span) + } + (class, other) => Err(Diagnostic::ParseError { + message: format!( + "internal: primary-atom class {class:?} does not match `TokenKind` {other:?} \ + (fix `PrimaryAtomRow` in `parse_tables.dag` or `regen_parse_tables` output)" + ), span: token.span, fixes: Vec::new(), }), diff --git a/src/v3/compiler/src/parse_tables_generated.rs b/src/v3/compiler/src/parse_tables_generated.rs index 441f87fa439..563afea4b17 100644 --- a/src/v3/compiler/src/parse_tables_generated.rs +++ b/src/v3/compiler/src/parse_tables_generated.rs @@ -148,3 +148,29 @@ pub fn primary_prefix_dispatch(kind: &TokenKind) -> Option None, } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrimaryAtomClass { + False, + Ident, + IntLiteral, + Pipe, + String, + True, +} + +/// After the `parse_primary` prefix pass, the bumped token is one of the +/// atomic primary forms. Authored as `primary_atom_*: PrimaryAtomRow` rows in +/// `src/v3/compiler/parse_tables.dag`; regenerated by `regen_parse_tables`. +/// `PrimaryAtomClass` is emitted from those rows — not a substrate coproduct. +pub fn primary_atom_class(kind: &TokenKind) -> Option { + match kind { + TokenKind::Ident(_) => Some(PrimaryAtomClass::Ident), + TokenKind::IntLit(_) => Some(PrimaryAtomClass::IntLiteral), + TokenKind::KwFalse => Some(PrimaryAtomClass::False), + TokenKind::KwTrue => Some(PrimaryAtomClass::True), + TokenKind::Pipe => Some(PrimaryAtomClass::Pipe), + TokenKind::StringLit(_) => Some(PrimaryAtomClass::String), + _ => None, + } +} diff --git a/src/v3/compiler/src/regen_parse_emit.rs b/src/v3/compiler/src/regen_parse_emit.rs index f18ce05d850..f4ae0c9d237 100644 --- a/src/v3/compiler/src/regen_parse_emit.rs +++ b/src/v3/compiler/src/regen_parse_emit.rs @@ -88,8 +88,9 @@ fn emit_parse_module(parser_body: &str) -> String { ); out.push_str( "use crate::parse_tables::{binary_op_at_level, bracket_role, is_type_rhs_boundary_keyword, \ - primary_prefix_dispatch, soft_keyword_ident_spelling, top_level_item_dispatch, \ - BinaryOpLevel, BracketRole, ItemDispatchKind, PrimaryPrefixDispatch};\n", + primary_atom_class, primary_prefix_dispatch, soft_keyword_ident_spelling, \ + top_level_item_dispatch, \ + BinaryOpLevel, BracketRole, ItemDispatchKind, PrimaryAtomClass, PrimaryPrefixDispatch};\n", ); out.push_str("use crate::tokenize::{Token, TokenKind};\n\n"); out.push_str( diff --git a/src/v3/compiler/src/regen_parse_tables_emit.rs b/src/v3/compiler/src/regen_parse_tables_emit.rs index 9c184511fbc..a966eb81da7 100644 --- a/src/v3/compiler/src/regen_parse_tables_emit.rs +++ b/src/v3/compiler/src/regen_parse_tables_emit.rs @@ -9,8 +9,9 @@ //! **SG-2c-3:** existing `TopLevelItemKwRow.token_variant` rows also project //! `is_type_rhs_boundary_keyword` for the parser's shared top-level item //! boundary set. **SG-2c-5:** `SoftKeywordIdentRow` → `soft_keyword_ident_spelling`. -//! **SG-2c-6:** `PrimaryPrefixRow` → `PrimaryPrefixDispatch` + -//! `primary_prefix_dispatch` for `parse_primary` prefix openers. +//! **SG-2c-6 / SG-2c-7 (parse_primary cluster):** `PrimaryPrefixRow` → +//! `primary_prefix_dispatch`; `PrimaryAtomRow` → `primary_atom_class` for the +//! post-prefix atomic token set in the same `parse_primary` function. //! //! Used by the `regen_parse_tables` binary (writes the file) and by the //! hermetic integration snapshot test (compare in-memory only — avoids a @@ -82,7 +83,8 @@ impl fmt::Display for RenderParseTablesGeneratedError { /// Validation: `BinaryOpRow` vs `tokenize.dag` + `operators.dag` + shared-syntax /// `dag_operators`; `TopLevelItemKwRow` vs `tokenize.dag` + `Kw`-strip dispatch /// labels (SG-2c-2/SG-2c-3); `SoftKeywordIdentRow` vs `tokenize.dag` + shared keyword set -/// (SG-2c-5); `PrimaryPrefixRow` vs `tokenize.dag` + prefix dispatch labels (SG-2c-6). +/// (SG-2c-5); `PrimaryPrefixRow` / `PrimaryAtomRow` vs `tokenize.dag` + regen +/// dispatch labels (SG-2c-6/SG-2c-7). /// Does not read or write workspace paths. pub fn render_parse_tables_generated_rs( parse_tables_source: &str, @@ -129,6 +131,13 @@ pub fn render_parse_tables_generated_rs( .collect(); primary_prefix_variants.sort_unstable(); primary_prefix_variants.dedup(); + let primary_atom_rows = collect_primary_atom_rows(&tables_dag, &token_variants); + let mut primary_atom_variants: Vec = primary_atom_rows + .iter() + .map(|r| r.dispatch.clone()) + .collect(); + primary_atom_variants.sort_unstable(); + primary_atom_variants.dedup(); let module = EmitParseTablesModule { levels: &levels, binary_op_rows: &rows, @@ -138,6 +147,8 @@ pub fn render_parse_tables_generated_rs( bracket_rows: &bracket_rows, primary_prefix_variants: &primary_prefix_variants, primary_prefix_rows: &primary_prefix_rows, + primary_atom_variants: &primary_atom_variants, + primary_atom_rows: &primary_atom_rows, }; let rust = emit_module(&module); let combined = format!("{HEADER}{rust}"); @@ -387,6 +398,11 @@ struct PrimaryPrefixRow { dispatch: String, } +struct PrimaryAtomRow { + token_variant: String, + dispatch: String, +} + /// `BracketRow.token_variant` is the sole authored field; role is derived /// from the `L`/`R` prefix (same one-authority shape as `TopLevelItemKwRow` → /// `ItemDispatchKind`, where dispatch is `strip_prefix("Kw")`). No separate @@ -436,6 +452,88 @@ fn collect_bracket_rows(dag: &Dag, token_variants: &[String]) -> Vec rows } +/// Collect `PrimaryAtomRow` facts for SG-2c-7; `PrimaryAtomClass` labels are +/// derived at regen (see `primary_atom_class_label_from_token_variant`). +fn collect_primary_atom_rows(dag: &Dag, token_variants: &[String]) -> Vec { + let token_variant_set: BTreeSet<&str> = token_variants.iter().map(String::as_str).collect(); + + let row_type_id = dag + .declarations() + .iter() + .find(|d| d.name.as_deref() == Some("PrimaryAtomRow")) + .map(|d| d.id) + .expect("PrimaryAtomRow declaration"); + + let mut rows: Vec = Vec::new(); + let mut seen_token_variants: BTreeSet = BTreeSet::new(); + for decl in dag.declarations() { + if decl.meta_tag != Some(row_type_id) { + continue; + } + let name = decl.name.as_deref().unwrap_or(""); + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + panic!( + "`parse_tables.dag::{name}`: `PrimaryAtomRow` binding must carry a structural value body" + ); + }; + let token_variant = string_field(fields, "token_variant", name); + + assert!( + token_variant_set.contains(token_variant.as_str()), + "`parse_tables.dag::{name}`: token_variant `{token_variant}` is not a \ + `TokenKind` variant in `tokenize.dag`" + ); + + let dispatch = primary_atom_class_label_from_token_variant(&token_variant, name); + + assert!( + seen_token_variants.insert(token_variant.clone()), + "`parse_tables.dag`: duplicate primary-atom row for `TokenKind::{token_variant}`" + ); + + rows.push(PrimaryAtomRow { + token_variant, + dispatch, + }); + } + rows.sort_by(|a, b| a.token_variant.cmp(&b.token_variant)); + rows +} + +/// `PrimaryAtomRow.token_variant` is the sole authored key; the Rust +/// `PrimaryAtomClass` label is a closed allowlist at regen — not a second +/// field in `parse_tables.dag`. +fn primary_atom_class_label_from_token_variant( + token_variant: &str, + decl_name: &str, +) -> String { + let label = match token_variant { + "IntLit" => "IntLiteral", + "KwTrue" => "True", + "KwFalse" => "False", + "StringLit" => "String", + "Pipe" => "Pipe", + "Ident" => "Ident", + other => panic!( + "`parse_tables.dag::{decl_name}`: token_variant `{other}` is not in the SG-2c-7 \ + primary-atom allowlist (IntLit, KwTrue, KwFalse, StringLit, Pipe, Ident)" + ), + }; + assert_dispatch_label_is_rust_identifier(label, decl_name); + label.to_string() +} + +/// Rust `match` pattern on `&TokenKind` for payload-carrying variants; nullary +/// `TokenKind` use the identifier alone. +fn token_kind_pattern_ref(token_variant: &str) -> String { + match token_variant { + "Ident" => "Ident(_)".to_string(), + "IntLit" => "IntLit(_)".to_string(), + "StringLit" => "StringLit(_)".to_string(), + other => other.to_string(), + } +} + /// `SoftKeywordIdentRow.token_variant` is authoritative; spelling is the /// lowercased `Kw` suffix, not a second authored string field. fn collect_soft_keyword_ident_rows( @@ -741,6 +839,8 @@ struct EmitParseTablesModule<'a> { bracket_rows: &'a [BracketRow], primary_prefix_variants: &'a [String], primary_prefix_rows: &'a [PrimaryPrefixRow], + primary_atom_variants: &'a [String], + primary_atom_rows: &'a [PrimaryAtomRow], } fn emit_module(ctx: &EmitParseTablesModule<'_>) -> String { @@ -753,6 +853,8 @@ fn emit_module(ctx: &EmitParseTablesModule<'_>) -> String { bracket_rows, primary_prefix_variants, primary_prefix_rows, + primary_atom_variants, + primary_atom_rows, } = *ctx; // `BracketRole` variants are the set of distinct roles derived from the // authored rows (projection-only). Matches the `ItemDispatchKind` pattern: @@ -901,6 +1003,32 @@ fn emit_module(ctx: &EmitParseTablesModule<'_>) -> String { } s.push_str(" _ => None,\n"); s.push_str(" }\n"); + s.push_str("}\n\n"); + + s.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n"); + s.push_str("pub enum PrimaryAtomClass {\n"); + for v in primary_atom_variants { + s.push_str(&format!(" {v},\n")); + } + s.push_str("}\n\n"); + + s.push_str( + "/// After the `parse_primary` prefix pass, the bumped token is one of the\n\ + /// atomic primary forms. Authored as `primary_atom_*: PrimaryAtomRow` rows in\n\ + /// `src/v3/compiler/parse_tables.dag`; regenerated by `regen_parse_tables`.\n\ + /// `PrimaryAtomClass` is emitted from those rows — not a substrate coproduct.\n", + ); + s.push_str("pub fn primary_atom_class(kind: &TokenKind) -> Option {\n"); + s.push_str(" match kind {\n"); + for row in primary_atom_rows { + let pat = token_kind_pattern_ref(&row.token_variant); + s.push_str(&format!( + " TokenKind::{} => Some(PrimaryAtomClass::{}),\n", + pat, row.dispatch + )); + } + s.push_str(" _ => None,\n"); + s.push_str(" }\n"); s.push_str("}\n"); s } diff --git a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs index 1eb6dc48898..ace22e20d45 100644 --- a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs @@ -2,13 +2,14 @@ //! authority for parser-owned tables projected into `parse_tables_generated.rs`: //! SG-2c-1 binary-operator precedence rows, SG-2c-2 top-level item keyword //! dispatch, SG-2c-3 type-RHS boundary keywords, SG-2c-4 bracket opener/closer -//! roles, SG-2c-5 soft-keyword ident aliases, SG-2c-6 `parse_primary` prefix opener -//! dispatch. `parse_tables_generated.rs` +//! roles, SG-2c-5 soft-keyword ident aliases, SG-2c-6/SG-2c-7 `parse_primary` +//! prefix + atomic-tail dispatch (cluster). `parse_tables_generated.rs` //! must stay in sync with the authoring `.dag`; `binary_op_*` rows cover every //! binary-operator token the parser dispatches on, `top_level_kw_*` rows cover //! every keyword `parse_item` accepts, and the same rows also project the shared //! top-level item-boundary helper used by type-RHS lookahead. `primary_prefix_*` -//! rows cover the peek tokens `parse_primary` dispatches before its atomic match. +//! rows cover prefix openers; `primary_atom_*` rows cover the post-bump atomic +//! `TokenKind` set (see `primary_atom_rows_cover_exactly_the_tokens_parse_primary_atomic_arm`). //! //! This lane is explicitly NOT SG-2c proper (parser authority proper). //! Full parser-algorithm dissolution is blocked on recursive list-body @@ -527,3 +528,89 @@ fn primary_prefix_rows_cover_exactly_the_tokens_parse_primary_prefix_dispatches_ `parse_primary` prefix-dispatches on" ); } + +#[test] +fn every_primary_atom_row_token_variant_is_a_token_kind_variant() { + let tables_dag = compile_to_dag(PARSE_TABLES_DAG, "src/v3/compiler/parse_tables.dag") + .unwrap_or_else(|e| panic!("parse_tables.dag should compile: {e:?}")); + let tokenize_dag = compile_to_dag(TOKENIZE_DAG, "src/v3/compiler/tokenize.dag") + .unwrap_or_else(|e| panic!("tokenize.dag should compile: {e:?}")); + + let token_kind_decl = tokenize_dag + .declaration_by_name("TokenKind") + .expect("TokenKind declaration in tokenize.dag"); + let TypeConnective::Disj { + variants: token_variants, + } = &token_kind_decl.connective + else { + panic!("TokenKind should lower to a Disj"); + }; + let token_variant_names: std::collections::BTreeSet = + token_variants.iter().map(|v| v.label.clone()).collect(); + + let row_type_id = tables_dag + .declaration_by_name("PrimaryAtomRow") + .expect("PrimaryAtomRow declaration") + .id; + for decl in tables_dag.declarations() { + if decl.meta_tag != Some(row_type_id) { + continue; + } + let name = decl.name.as_deref().unwrap_or(""); + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + continue; + }; + let token_variant = fields + .iter() + .find_map(|(k, v)| (k == "token_variant").then_some(v)) + .and_then(|v| match v { + FieldValue::Literal(LiteralBits::String(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_else(|| panic!("`{name}`: missing string `token_variant` field")); + assert!( + token_variant_names.contains(&token_variant), + "`parse_tables.dag::{name}`: token_variant `{token_variant}` is not a \ + `TokenKind` variant in `tokenize.dag`" + ); + } +} + +#[test] +fn primary_atom_rows_cover_exactly_the_tokens_parse_primary_atomic_arm() { + // Post-prefix bump in `parse_primary` must be one of these + // `TokenKind` variants; `primary_atom_class` is the fail-closed table. + let expected: std::collections::BTreeSet<&'static str> = [ + "IntLit", "KwTrue", "KwFalse", "StringLit", "Pipe", "Ident", + ] + .into_iter() + .collect(); + + let tables_dag = compile_to_dag(PARSE_TABLES_DAG, "src/v3/compiler/parse_tables.dag") + .unwrap_or_else(|e| panic!("parse_tables.dag should compile: {e:?}")); + let mut got: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let row_type_id = tables_dag + .declaration_by_name("PrimaryAtomRow") + .expect("PrimaryAtomRow declaration") + .id; + for decl in tables_dag.declarations() { + if decl.meta_tag != Some(row_type_id) { + continue; + } + let Some(ValueBody::Structural { fields }) = &decl.value_body else { + continue; + }; + if let Some(FieldValue::Literal(LiteralBits::String(s))) = fields + .iter() + .find_map(|(k, v)| (k == "token_variant").then_some(v)) + { + got.insert(s.clone()); + } + } + let got_ref: std::collections::BTreeSet<&str> = got.iter().map(String::as_str).collect(); + assert_eq!( + expected, got_ref, + "primary_atom_* rows in parse_tables.dag do not cover exactly the `TokenKind` set for \ + `parse_primary` after the prefix pass" + ); +} From ee3c3d0671400a5c804fb152832b9a1aeb6f93f6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 17:19:28 -0400 Subject: [PATCH 3/7] chore: apply cargo fmt --- src/v3/compiler/src/regen_parse_tables_emit.rs | 5 +---- .../integration/sg2c1_parse_tables_authority_test.rs | 9 ++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/v3/compiler/src/regen_parse_tables_emit.rs b/src/v3/compiler/src/regen_parse_tables_emit.rs index a966eb81da7..c36e98aa2cf 100644 --- a/src/v3/compiler/src/regen_parse_tables_emit.rs +++ b/src/v3/compiler/src/regen_parse_tables_emit.rs @@ -503,10 +503,7 @@ fn collect_primary_atom_rows(dag: &Dag, token_variants: &[String]) -> Vec String { +fn primary_atom_class_label_from_token_variant(token_variant: &str, decl_name: &str) -> String { let label = match token_variant { "IntLit" => "IntLiteral", "KwTrue" => "True", diff --git a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs index ace22e20d45..22b7b01c645 100644 --- a/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs +++ b/src/v3/compiler/tests/integration/sg2c1_parse_tables_authority_test.rs @@ -580,11 +580,10 @@ fn every_primary_atom_row_token_variant_is_a_token_kind_variant() { fn primary_atom_rows_cover_exactly_the_tokens_parse_primary_atomic_arm() { // Post-prefix bump in `parse_primary` must be one of these // `TokenKind` variants; `primary_atom_class` is the fail-closed table. - let expected: std::collections::BTreeSet<&'static str> = [ - "IntLit", "KwTrue", "KwFalse", "StringLit", "Pipe", "Ident", - ] - .into_iter() - .collect(); + let expected: std::collections::BTreeSet<&'static str> = + ["IntLit", "KwTrue", "KwFalse", "StringLit", "Pipe", "Ident"] + .into_iter() + .collect(); let tables_dag = compile_to_dag(PARSE_TABLES_DAG, "src/v3/compiler/parse_tables.dag") .unwrap_or_else(|e| panic!("parse_tables.dag should compile: {e:?}")); From 29c81d5742cb88f0ee7ef6cbf71904da2e8f82c0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 19:44:20 -0400 Subject: [PATCH 4/7] WIP: SG-2c --- src/v3/compiler/tests/integration/parse_corpus_manifest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt index e4b31fff175..c9798624b9b 100644 --- a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt +++ b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt @@ -9,7 +9,7 @@ dsl/std/logic.dag 5 5614 bb0fc43f94c4374f dsl/std/string_type.dag 4 1845 3810f615d6874bcd dsl/std/types.dag 108 137844 1fa72880c2e639cf src/v3/compiler/operators.dag 5 47160 b75a91c65bf8a242 -src/v3/compiler/parse_tables.dag 36 80759 0b5768e50a18f286 +src/v3/compiler/parse_tables.dag 43 92946 e2f49cbffb6846e6 src/v3/compiler/pipeline.dag 26 47032 a13b41aa379fd167 src/v3/compiler/regen.dag 11 30786 49f47c59c626d776 src/v3/compiler/tests/four_fixture_pressure/drop.v3 2 2517 00953bc6b6d8c811 From befc1245f4e4ee90855fe6bf1b8699d81db41dba Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 17:45:30 -0400 Subject: [PATCH 5/7] docs(parse_tables): fix Pipe vs PipeArrow comment (review feedback) SG-2c-7 atomic tail uses TokenKind::Pipe (|), not PipeArrow (|>). Made-with: Cursor --- src/v3/compiler/parse_tables.dag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/parse_tables.dag b/src/v3/compiler/parse_tables.dag index 2d2bbb3d921..5da24c3312c 100644 --- a/src/v3/compiler/parse_tables.dag +++ b/src/v3/compiler/parse_tables.dag @@ -468,7 +468,8 @@ data primary_prefix_lbracket: PrimaryPrefixRow = { // --- SG-2c-7: `parse_primary` atomic tail (after prefix pass). The bump that // follows a non-prefix `peek` must be one of: integer literal, boolean -// keyword literals, string, pipe-(`|>`) lambda start, or identifier — the +// keyword literals, string, `Pipe` (single `|`, not `PipeArrow` / `|>`) for +// lambda syntax, or identifier — the // closed set that the parser accepts as a primary expression *atom*. Authored // here once; `PrimaryAtomClass` and `primary_atom_class` are regen-only // (projection over `token_variant` rows, same M7 shape as From f4c8c737611ad76a3a123d27d26ad1afc90baafb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 17:56:21 -0400 Subject: [PATCH 6/7] fix: refresh parse_corpus_manifest after parse_tables.dag comment edit The Pipe vs PipeArrow doc fix changed source spans; fnv1a64 for the parse surface snapshot moved (items/byte len unchanged). Unblocks v3 CI handwritten_parse_snapshot_matches_manifest. Made-with: Cursor --- src/v3/compiler/tests/integration/parse_corpus_manifest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt index c9798624b9b..7d61a4f3f0d 100644 --- a/src/v3/compiler/tests/integration/parse_corpus_manifest.txt +++ b/src/v3/compiler/tests/integration/parse_corpus_manifest.txt @@ -9,7 +9,7 @@ dsl/std/logic.dag 5 5614 bb0fc43f94c4374f dsl/std/string_type.dag 4 1845 3810f615d6874bcd dsl/std/types.dag 108 137844 1fa72880c2e639cf src/v3/compiler/operators.dag 5 47160 b75a91c65bf8a242 -src/v3/compiler/parse_tables.dag 43 92946 e2f49cbffb6846e6 +src/v3/compiler/parse_tables.dag 43 92946 2364c4858f6b9e3d src/v3/compiler/pipeline.dag 26 47032 a13b41aa379fd167 src/v3/compiler/regen.dag 11 30786 49f47c59c626d776 src/v3/compiler/tests/four_fixture_pressure/drop.v3 2 2517 00953bc6b6d8c811 From f3a40aa595d8c456800f5933af3ee3610bea8f1d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 22 Apr 2026 18:06:13 -0400 Subject: [PATCH 7/7] fix(ci): exempt PrimaryAtomRow in compiler-std consolidation ratchet SG-2c-7 added `type PrimaryAtomRow` to parse_tables.dag; the #642 script must list it next to PrimaryPrefixRow and the other parse_tables row shapes. Unblocks: Compiler-std consolidation ratchet (v3 workflow). Made-with: Cursor --- scripts/check-compiler-std-ratchet.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-compiler-std-ratchet.sh b/scripts/check-compiler-std-ratchet.sh index 3bb26d024e2..4287c4c5b8a 100755 --- a/scripts/check-compiler-std-ratchet.sh +++ b/scripts/check-compiler-std-ratchet.sh @@ -49,6 +49,7 @@ EXEMPT_ROWS=( "src/v3/compiler/parse_tables.dag:SoftKeywordIdentRow" "src/v3/compiler/parse_tables.dag:BracketRow" "src/v3/compiler/parse_tables.dag:PrimaryPrefixRow" + "src/v3/compiler/parse_tables.dag:PrimaryAtomRow" ) TRACKED_ROWS=(