diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index c1285a1b7..a481fd84a 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -8248,3 +8248,60 @@ line. Neither instrument is complete; together they bracket the answer. **Where a deterministic instrument exists it is not a second opinion, it is the answer.** Use the fan-out to name constructs and raise hypotheses; use the compiler to decide. + +## 325. A name is a name: read all of it + +`module github::auth {` did not parse. The module-name reader took one +identifier and stopped, so the parser met a colon at module level and reported +something else entirely. Nine specs declared a path-qualified module and none +of them parsed. + +The repair is not to give `::` a meaning. It is to read the whole NAME -- a +loop over `::`-separated segments, each of which may still be hyphenated, +because `module tritype-base;` was already legal and had to stay so. + +**Both colons are required before either is consumed.** A single `:` after a +module name is not a path; swallowing it would turn a real error into a +stranger one further down the file. The existing path reader elsewhere in the +compiler consumes one colon and then optionally a second -- copying that idiom +verbatim would have inherited the looser rule. + +Controls that made this safe to land: 621 specs parsed before and 627 after +with **zero** regressions, and seal drift moved 537 to 543 -- exactly the six +new specs, proving no previously-parsing spec's output changed. + +## 326. The ritual on paper and the ritual in practice + +`FROZEN.md` ยง5 says a change to `bootstrap/src/compiler.rs` needs M1-M4 green, +a PR marked `[GOLD-RING]`, a milestone, or Architect approval. That reads like +a stop sign at 3am. + +The evidence says otherwise: the last 50 commits touching that file are eight +today alone, `FROZEN_HASH` is updated in **20 of the last 20**, and the phrase +GOLD-RING appears in **none** of them. The practice is: change the file, update +the seal in the same commit. + +**When a document and the commit history disagree about what is allowed, the +history is the measurement.** Check it before treating a document as a gate -- +and say which one you followed. + +## 327. Three readings of my own new census, three wrong + +`tri unparsed report` ranks the constructs that stop the compiler. Its first +three numbers were all wrong, and each was caught by asking a question the +output invited: + +1. **118 specs "the compiler cannot read"** -- 21 of them are under + `fixtures/`, broken ON PURPOSE as inputs to a detector. + `tools/specs_generate_baseline.txt` already omits all 21. A census that + disagrees with the repository's own ledger is wrong before it is useful. + Now they get their own line rather than being dropped. +2. **"not decided" was 30 too large** -- the abstention listed only TOP-LEVEL + keywords, so a failing `return x;` or `let y = 1;` inside a body fell + through as undecided when it is plainly upstream. +3. **36 + 27 + 30 came to 93 against a total of 97** -- four rows were leaving + through a bare `continue` where the error named no readable line. Counted + now, and the arithmetic closes. + +The rule that found all three: **make the printed numbers add up, out loud.** +A census whose parts do not sum to its total has a bucket you have not named. diff --git a/.trinity/seals/automation::wrapup.json b/.trinity/seals/automation::wrapup.json index 1ffebd1b9..448f3d4b0 100644 --- a/.trinity/seals/automation::wrapup.json +++ b/.trinity/seals/automation::wrapup.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:ce7911d950a54065c04f7d5833e64c8656b200b285bc1c6afc750ff2855af24a", + "gen_hash_rust": "sha256:d9b107adfd304c6453f4efd8fe375e031233dcd1e13b02ea60f708feff82b6af", + "gen_hash_verilog": "sha256:84c34ae050c5d6e0b5eec9a7fcdd4b7d8a8ca61c56753fe6e7abb6c7007a0dda", + "gen_hash_zig": "sha256:33cb5ad0b90b063d7521cfccaed966b3376e70f2d28a10bfccb756ce6b7ddf86", "module": "automation::wrapup", "ring": 12, "sealed_at": "2026-08-28T00:21:37Z", diff --git a/.trinity/seals/automation_automation::wrapup.json b/.trinity/seals/automation_automation::wrapup.json index 1ffebd1b9..40d47d9f8 100644 --- a/.trinity/seals/automation_automation::wrapup.json +++ b/.trinity/seals/automation_automation::wrapup.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:ce7911d950a54065c04f7d5833e64c8656b200b285bc1c6afc750ff2855af24a", + "gen_hash_rust": "sha256:d9b107adfd304c6453f4efd8fe375e031233dcd1e13b02ea60f708feff82b6af", + "gen_hash_verilog": "sha256:84c34ae050c5d6e0b5eec9a7fcdd4b7d8a8ca61c56753fe6e7abb6c7007a0dda", + "gen_hash_zig": "sha256:33cb5ad0b90b063d7521cfccaed966b3376e70f2d28a10bfccb756ce6b7ddf86", "module": "automation::wrapup", "ring": 12, - "sealed_at": "2026-08-28T00:21:37Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:ac2a7dd606abd2a289cfcf53e1362ad9e1d930066a8feaa18748f131db5971ca", "spec_path": "specs/automation/wrapup-auto.t27" -} +} \ No newline at end of file diff --git a/.trinity/seals/github::auth.json b/.trinity/seals/github::auth.json index 77d26a933..dc3e22e72 100644 --- a/.trinity/seals/github::auth.json +++ b/.trinity/seals/github::auth.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:d61996eea454c66dec1c9a71f8d03bfeef54dd13eef251c2a9b23745e7f9c794", + "gen_hash_rust": "sha256:0c8b63c1529edd98bcc8e68f0ab775bd86b0c5a3d681dfda27ac0f8d1f38f165", + "gen_hash_verilog": "sha256:9327e54360dc57ae9a86132842c6aa9a5de2cfbd31d1c22525593dcc7dbfcfc4", + "gen_hash_zig": "sha256:1caa1683e55da970a531003f0e3a8bbe153a6a28131adf4466be276f64774eb2", "module": "github::auth", "ring": 12, "sealed_at": "2026-08-28T00:22:25Z", diff --git a/.trinity/seals/github::comments.json b/.trinity/seals/github::comments.json index 951abf44d..62d99b730 100644 --- a/.trinity/seals/github::comments.json +++ b/.trinity/seals/github::comments.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:0cdf800714cf243b5ab43b7725de3241382a329f2830ada2e2573cbc4fc0eff5", + "gen_hash_rust": "sha256:6426bd4276c663bf1bd07fadf7505105abe79a82a4d072a9b8d8d2cbb2391647", + "gen_hash_verilog": "sha256:6798d2cc2799c3328949f44d004a526409d7e0ddc368e8f6fc72eda5deca703e", + "gen_hash_zig": "sha256:ab668814038be68338558a21908b9d7b61382002e179f9337dc59dee2f344249", "module": "github::comments", "ring": 12, "sealed_at": "2026-08-28T00:22:25Z", diff --git a/.trinity/seals/github::issues.json b/.trinity/seals/github::issues.json index 8ad1a55bf..a664f4eee 100644 --- a/.trinity/seals/github::issues.json +++ b/.trinity/seals/github::issues.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:1903479f04739705d7b4a711bccd6aa081df49a25475d016e83a633e005f33a4", + "gen_hash_rust": "sha256:3ff56561e934319e4dc34969c5635b517143ba701a4dea07c8f272b5873f1a67", + "gen_hash_verilog": "sha256:fdce888391c35bccd7585512d9d3753d654af54a1e0118c3b94dff36a0b41094", + "gen_hash_zig": "sha256:a156482ac0af843d0e411702e16e4a1a1e844c21ccbd4d01b07c0a0fca216515", "module": "github::issues", "ring": 12, "sealed_at": "2026-08-28T00:22:25Z", diff --git a/.trinity/seals/github::prs.json b/.trinity/seals/github::prs.json index a1bbd30dd..6b1fa62ed 100644 --- a/.trinity/seals/github::prs.json +++ b/.trinity/seals/github::prs.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:65b91c1216e41eb3d1d8b040a3ac6732e6e3f9ef13ac98d221b081a13f31e42a", + "gen_hash_rust": "sha256:3bc0749df78f07300bc0f1f1badb7ecec997a968068555106bb3df6bc337e7b0", + "gen_hash_verilog": "sha256:e98675e9c3f245707d48d1452980d65ad9ad9776e1823821a9d5e5df52965463", + "gen_hash_zig": "sha256:eb1721747e28fc0045ff612f4fa960ad3c044d0253e24aedeea3a3aea2a911d8", "module": "github::prs", "ring": 12, "sealed_at": "2026-08-28T00:22:25Z", diff --git a/.trinity/seals/github::tests::e2e_full_flow.json b/.trinity/seals/github::tests::e2e_full_flow.json index 0e5c140af..e7c01bf12 100644 --- a/.trinity/seals/github::tests::e2e_full_flow.json +++ b/.trinity/seals/github::tests::e2e_full_flow.json @@ -1,8 +1,8 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:0970c851fd35bc32976e4076cc67a582d9eadf54b60676c8cbab92859b4b7df2", + "gen_hash_rust": "sha256:0a3efa9f6b1b98d56f8b026a17df711b7ebad4d177bb1ae3cfa5a7aed1791661", + "gen_hash_verilog": "sha256:a5f40a1ce38a519d6f0b1be4bde6301b5943a15721445f4ad0b015fa8e298613", + "gen_hash_zig": "sha256:60a0f9bba9a5ad1c3161d13e8338e70fcf2d9a1b3573ddec4d833b617674cce4", "module": "github::tests::e2e_full_flow", "ring": 12, "sealed_at": "2026-08-28T00:21:37Z", diff --git a/.trinity/seals/github_github::auth.json b/.trinity/seals/github_github::auth.json index 77d26a933..b411c476b 100644 --- a/.trinity/seals/github_github::auth.json +++ b/.trinity/seals/github_github::auth.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:d61996eea454c66dec1c9a71f8d03bfeef54dd13eef251c2a9b23745e7f9c794", + "gen_hash_rust": "sha256:0c8b63c1529edd98bcc8e68f0ab775bd86b0c5a3d681dfda27ac0f8d1f38f165", + "gen_hash_verilog": "sha256:9327e54360dc57ae9a86132842c6aa9a5de2cfbd31d1c22525593dcc7dbfcfc4", + "gen_hash_zig": "sha256:1caa1683e55da970a531003f0e3a8bbe153a6a28131adf4466be276f64774eb2", "module": "github::auth", "ring": 12, - "sealed_at": "2026-08-28T00:22:25Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:4a02c3cd0fb34f6e9576d52e96910bc2937ffa3f921f315dd8887ec66f3776e5", "spec_path": "specs/github/auth.t27" -} +} \ No newline at end of file diff --git a/.trinity/seals/github_github::comments.json b/.trinity/seals/github_github::comments.json index 951abf44d..2cc3050fa 100644 --- a/.trinity/seals/github_github::comments.json +++ b/.trinity/seals/github_github::comments.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:0cdf800714cf243b5ab43b7725de3241382a329f2830ada2e2573cbc4fc0eff5", + "gen_hash_rust": "sha256:6426bd4276c663bf1bd07fadf7505105abe79a82a4d072a9b8d8d2cbb2391647", + "gen_hash_verilog": "sha256:6798d2cc2799c3328949f44d004a526409d7e0ddc368e8f6fc72eda5deca703e", + "gen_hash_zig": "sha256:ab668814038be68338558a21908b9d7b61382002e179f9337dc59dee2f344249", "module": "github::comments", "ring": 12, - "sealed_at": "2026-08-28T00:22:25Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:7ea2370c5b1c69d599af13bb3956130320593d3aa72db524671aa7ac35e35604", "spec_path": "specs/github/comments.t27" -} +} \ No newline at end of file diff --git a/.trinity/seals/github_github::issues.json b/.trinity/seals/github_github::issues.json index 8ad1a55bf..159af3511 100644 --- a/.trinity/seals/github_github::issues.json +++ b/.trinity/seals/github_github::issues.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:1903479f04739705d7b4a711bccd6aa081df49a25475d016e83a633e005f33a4", + "gen_hash_rust": "sha256:3ff56561e934319e4dc34969c5635b517143ba701a4dea07c8f272b5873f1a67", + "gen_hash_verilog": "sha256:fdce888391c35bccd7585512d9d3753d654af54a1e0118c3b94dff36a0b41094", + "gen_hash_zig": "sha256:a156482ac0af843d0e411702e16e4a1a1e844c21ccbd4d01b07c0a0fca216515", "module": "github::issues", "ring": 12, - "sealed_at": "2026-08-28T00:22:25Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:64d4d76ec19c214697c0f859f52391ebdd6f12947954308e51f49390b46d1ec4", "spec_path": "specs/github/issues.t27" -} +} \ No newline at end of file diff --git a/.trinity/seals/github_github::prs.json b/.trinity/seals/github_github::prs.json index a1bbd30dd..16a7cb642 100644 --- a/.trinity/seals/github_github::prs.json +++ b/.trinity/seals/github_github::prs.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:65b91c1216e41eb3d1d8b040a3ac6732e6e3f9ef13ac98d221b081a13f31e42a", + "gen_hash_rust": "sha256:3bc0749df78f07300bc0f1f1badb7ecec997a968068555106bb3df6bc337e7b0", + "gen_hash_verilog": "sha256:e98675e9c3f245707d48d1452980d65ad9ad9776e1823821a9d5e5df52965463", + "gen_hash_zig": "sha256:eb1721747e28fc0045ff612f4fa960ad3c044d0253e24aedeea3a3aea2a911d8", "module": "github::prs", "ring": 12, - "sealed_at": "2026-08-28T00:22:25Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:4e466319eaaca9d8dded516b01c4e044793adcf2dc0fd6b21baf29244e53b5f9", "spec_path": "specs/github/prs.t27" -} +} \ No newline at end of file diff --git a/.trinity/seals/tests_github::tests::e2e_full_flow.json b/.trinity/seals/tests_github::tests::e2e_full_flow.json index 0e5c140af..fea7d6828 100644 --- a/.trinity/seals/tests_github::tests::e2e_full_flow.json +++ b/.trinity/seals/tests_github::tests::e2e_full_flow.json @@ -1,11 +1,12 @@ { - "gen_hash_c": "none", - "gen_hash_rust": "none", - "gen_hash_verilog": "none", - "gen_hash_zig": "none", + "gen_hash_c": "sha256:0970c851fd35bc32976e4076cc67a582d9eadf54b60676c8cbab92859b4b7df2", + "gen_hash_rust": "sha256:0a3efa9f6b1b98d56f8b026a17df711b7ebad4d177bb1ae3cfa5a7aed1791661", + "gen_hash_verilog": "sha256:a5f40a1ce38a519d6f0b1be4bde6301b5943a15721445f4ad0b015fa8e298613", + "gen_hash_zig": "sha256:60a0f9bba9a5ad1c3161d13e8338e70fcf2d9a1b3573ddec4d833b617674cce4", "module": "github::tests::e2e_full_flow", "ring": 12, - "sealed_at": "2026-08-28T00:21:37Z", + "sealed_at": "2026-08-29T20:30:37Z", + "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:84a6ee5d56e617746da71f22a23e7c1db91639adfb29111a7ff4a84995d40569", "spec_path": "specs/github/tests/e2e_full_flow.t27" -} +} \ No newline at end of file diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index f58396eed..4d141073e 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -1546,17 +1546,47 @@ impl Parser { // Module name can contain hyphens: e.g. "tritype-base" let mut mod_name = String::new(); if self.current.kind == TokenKind::Ident { - mod_name.push_str(&self.current.lexeme); - self.advance(); - // Consume hyphenated parts: - ident - ident ... - while self.current.kind == TokenKind::Minus { - mod_name.push('-'); - self.advance(); // consume - - if self.current.kind == TokenKind::Ident - || self.current.kind == TokenKind::Number + // A module name is one or more `::`-separated SEGMENTS, each of + // which may itself be hyphenated: `module tritype-base;`, + // `module github::issues { }`, `module a::b::c;`. + // + // The path form was read up to its first segment and no + // further, so `module github::auth {` left the parser looking + // at a colon at module level. Nine specs on master declare a + // path-qualified module and none of them parsed. The name is a + // NAME -- the repair is to read all of it, not to give `::` a + // meaning. + // + // BOTH colons are required before either is consumed. A single + // `:` after a module name is not a path, and swallowing it + // would turn a real error into a stranger one further down. + loop { + mod_name.push_str(&self.current.lexeme); + self.advance(); + // Consume hyphenated parts: - ident - ident ... + while self.current.kind == TokenKind::Minus { + mod_name.push('-'); + self.advance(); // consume - + if self.current.kind == TokenKind::Ident + || self.current.kind == TokenKind::Number + { + mod_name.push_str(&self.current.lexeme); + self.advance(); + } + } + if self.current.kind != TokenKind::Colon + || self.peek.kind != TokenKind::Colon { - mod_name.push_str(&self.current.lexeme); - self.advance(); + break; + } + self.advance(); // consume first : + self.advance(); // consume second : + mod_name.push_str("::"); + if self.current.kind != TokenKind::Ident { + // `module a::` with nothing after it. The name keeps the + // separator so the shape is visible rather than tidied + // away, and the caller meets the real token next. + break; } } } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index c6ca0e377..e7e7cc96a 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -5958d7f24cb254e2149fb0c6ba265bfb11653ca1faef95f1f2bdbd4f7d49d35d +02ab350615e74f165346b62c205e6e34b6adcab73382fa2fd71280d50c5d9745 diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 749d42113..cc7c0cb88 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -16,6 +16,7 @@ mod fleet; mod fpga; mod gates; mod prose; +mod unparsed; mod hooks; mod mutate; mod nownote; @@ -170,6 +171,11 @@ enum Commands { #[command(subcommand)] action: prose::ProseCmd, }, + /// Specs the compiler cannot read, ranked by the construct that stops it. + Unparsed { + #[command(subcommand)] + action: unparsed::UnparsedCmd, + }, /// What `.trinity/seals` says about a spec, when it says it twice. Seals { #[command(subcommand)] @@ -806,6 +812,7 @@ fn main() -> Result<()> { Commands::Orphaned { action } => orphaned::run(action)?, Commands::Discard { action } => discard::run(action)?, Commands::Prose { action } => prose::run(action, std::env::current_dir()?)?, + Commands::Unparsed { action } => unparsed::run(action, std::env::current_dir()?)?, Commands::Seals { action } => seals::run(action)?, Commands::Hooks { action } => hooks::run(action)?, } diff --git a/cli/tri/src/unparsed.rs b/cli/tri/src/unparsed.rs new file mode 100644 index 000000000..bf99c72f3 --- /dev/null +++ b/cli/tri/src/unparsed.rs @@ -0,0 +1,325 @@ +//! Specs the compiler cannot read, ranked by the CONSTRUCT that stops it. +//! +//! WHY THIS EXISTS +//! --------------- +//! The obvious census groups by the compiler's message. That census is wrong, +//! and it was shipped once: `import x`, `algorithm y {`, `type T = T`, +//! `impl X {` and an English sentence all print +//! "unexpected token after expression statement: Ident". The message names the +//! state the parser recovered INTO, not what it choked on, so grouping by it +//! reported five different defects as one 23-strong "parser gap". +//! +//! Grouping by what the line CONTAINS gives a work queue instead: on the day +//! this was written the top rows were path-qualified module names (9) and +//! body-less function prototypes (9), and the first of those was one grammar +//! change worth six specs. +//! +//! WHAT IT ABSTAINS ON +//! ------------------- +//! When the failing line begins with a construct the top level ACCEPTS -- `fn`, +//! `pub`, `struct`, `const` -- the line is a symptom and the cause is upstream +//! of it. Roughly two thirds of the failures land there. Naming a construct in +//! that case would be inventing one, so the row says so and stops. +use anyhow::Result; +use clap::Subcommand; +use std::collections::BTreeMap; +use std::path::PathBuf; + +#[derive(Subcommand)] +pub enum UnparsedCmd { + /// Rank the constructs that stop the compiler, most specs first. + Report { + /// Name the specs under each construct. + #[arg(long)] + list: bool, + }, +} + +/// (name, matcher) in priority order -- first match wins. +/// +/// Ordered so that the more specific shape is tested first: a body-less `fn` +/// prototype must be recognised before the accepted-keyword abstention, or it +/// disappears into "cause is upstream" and the largest actionable row with it. +fn classify(line: &str) -> Option<&'static str> { + let t = line.trim(); + let head = t.strip_prefix("pub ").unwrap_or(t); + + // `fn f(a: T) -> U;` -- a signature with no body. + if head.starts_with("fn ") && t.ends_with(';') { + return Some("fn NAME(..) -> T; body-less prototype"); + } + // `struct Id(str);` -- a tuple/newtype struct. + if head.starts_with("struct ") && t.ends_with(");") { + return Some("struct NAME(T); tuple / newtype struct"); + } + // `module a::b { }` / `module a::b;` + if head.starts_with("module ") && head.contains("::") { + return Some("module a::b path-qualified module name"); + } + if head.starts_with("import ") { + return Some("import .. import statement"); + } + if head.starts_with("use ") { + return Some("use .. use declaration"); + } + if head.starts_with("trait ") { + return Some("trait NAME trait declaration"); + } + if head.starts_with("impl ") { + return Some("impl NAME impl block"); + } + if head.starts_with("algorithm ") { + return Some("algorithm NAME { algorithm block"); + } + if head.starts_with("type ") { + return Some("type T = U type alias"); + } + if t.starts_with("\\\\") { + return Some("\\\\ ... Zig multiline string block"); + } + // A macro call: an identifier immediately followed by `!(`. + if let Some(i) = t.find("!(") { + if t[..i] + .chars() + .rev() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .count() + > 0 + { + return Some("name!(..) Rust-style macro invocation"); + } + } + None +} + +/// Constructs the parser accepts -- at the top level or inside a body. A +/// failing line that starts with one of these is a symptom; the cause is +/// earlier in the file. +/// +/// The statement keywords were missing at first and 15 failures fell into +/// "not decided" that were plainly upstream: `return`, `let`, `}`. +const ACCEPTED: [&str; 17] = [ + "fn", + "struct", + "enum", + "const", + "var", + "test", + "invariant", + "bench", + "module", + "let", + "return", + "for", + "while", + "if", + "else", + "switch", + "try", +]; + +/// A file under `fixtures/` is BROKEN ON PURPOSE -- it is the reference input +/// for a detector, not debt. `tools/specs_generate_baseline.txt` already omits +/// all 21 of them; a census that counts them disagrees with the repository's +/// own ledger. They are printed on their own line rather than dropped, because +/// a number that silently excludes something is the defect this file exists to +/// avoid. +fn is_fixture(path: &str) -> bool { + path.contains("/fixtures/") +} + +fn accepted_head(line: &str) -> bool { + let t = line.trim(); + let t = t.strip_prefix("pub ").unwrap_or(t); + ACCEPTED.iter().any(|k| { + t.strip_prefix(k) + .is_some_and(|r| r.starts_with(|c: char| c.is_whitespace() || c == '(')) + }) +} + +fn line_of(text: &str) -> Option { + let i = text.find("line ")?; + let rest = &text[i + 5..]; + let end = rest + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(rest.len()); + rest[..end].parse().ok() +} + +pub fn run(cmd: &UnparsedCmd, root: PathBuf) -> Result<()> { + let UnparsedCmd::Report { list } = cmd; + let t27c = ["target/release/t27c", "target/debug/t27c"] + .iter() + .map(|p| root.join(p)) + .find(|p| p.is_file()); + let Some(t27c) = t27c else { + anyhow::bail!( + "no compiler -- the census asks it which line stops it, and its\n \ + absence is not a clean bill.\n cargo build --release -p t27c" + ); + }; + + let out = std::process::Command::new("git") + .args(["ls-files", "*.t27"]) + .current_dir(&root) + .output()?; + let specs: Vec = String::from_utf8_lossy(&out.stdout) + .lines() + .map(|s| s.to_string()) + .filter(|s| root.join(s).is_file()) + .collect(); + + let mut by: BTreeMap<&'static str, Vec> = BTreeMap::new(); + let mut upstream = 0usize; + let mut unnamed: Vec<(String, String)> = Vec::new(); + let mut failing = 0usize; + let mut fixtures = 0usize; + // The rows that fall out of every bucket. Counted, because 36 + 27 + 30 + // came to 93 against a total of 97 and the four were leaving through a + // bare `continue`. + let mut unlocated = 0usize; + + for spec in &specs { + let o = std::process::Command::new(&t27c) + .arg("check") + .arg(spec) + .current_dir(&root) + .output(); + let Ok(o) = o else { continue }; + if o.status.success() { + continue; + } + if is_fixture(spec) { + fixtures += 1; + continue; + } + failing += 1; + let text = String::from_utf8_lossy(&o.stderr) + String::from_utf8_lossy(&o.stdout); + let Some(n) = line_of(&text) else { + unlocated += 1; + continue; + }; + let Ok(src) = std::fs::read_to_string(root.join(spec)) else { + unlocated += 1; + continue; + }; + let lines: Vec<&str> = src.lines().collect(); + if n == 0 || n > lines.len() { + unlocated += 1; + continue; + } + let line = lines[n - 1]; + match classify(line) { + Some(k) => by.entry(k).or_default().push(spec.clone()), + None if accepted_head(line) => upstream += 1, + None => unnamed.push((spec.clone(), line.trim().chars().take(52).collect())), + } + } + + let named: usize = by.values().map(|v| v.len()).sum(); + println!(" specs tracked {}", specs.len()); + println!(" ... the compiler cannot read {failing}"); + println!(" ... construct NAMED on that line {named}"); + println!(" ... cause is UPSTREAM, not named {upstream}"); + println!(" ... not decided, nothing claimed {}", unnamed.len()); + if unlocated > 0 { + println!(" ... error names no readable line {unlocated}"); + } + if fixtures > 0 { + println!(" broken ON PURPOSE under fixtures/ {fixtures} (detector inputs, not debt)"); + } + + if by.is_empty() { + println!(); + println!(" No failing line carries a construct this census recognises."); + return Ok(()); + } + + let mut rows: Vec<(&&str, &Vec)> = by.iter().collect(); + rows.sort_by(|a, b| b.1.len().cmp(&a.1.len()).then(a.0.cmp(b.0))); + println!(); + println!(" work queue -- one grammar change per row, largest first"); + for (k, v) in &rows { + println!(" {:>4} {k}", v.len()); + if *list { + for s in v.iter() { + println!(" {s}"); + } + } + } + + println!(); + println!(" The UPSTREAM count is not a residue to be reduced: those lines"); + println!(" are `fn`, `struct`, `const` -- constructs the parser accepts. The"); + println!(" defect is earlier in the file and this census will not guess it."); + if !*list { + println!(); + println!(" --list names the specs under each row."); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_prototype_is_named_before_the_abstention_swallows_it() { + // `fn` is an ACCEPTED top-level keyword, so an ordering that tests the + // abstention first loses the largest actionable row entirely. + assert_eq!( + classify("pub fn poll(x: [str]) -> Result;"), + Some("fn NAME(..) -> T; body-less prototype") + ); + assert!(accepted_head("pub fn poll(x: [str]) -> Result;")); + } + + #[test] + fn a_function_with_a_body_is_not_a_prototype() { + assert_eq!(classify("pub fn poll(x: u32) -> bool {"), None); + } + + #[test] + fn a_plain_module_is_not_a_path() { + assert_eq!(classify("module Foo {"), None); + assert_eq!( + classify("module github::auth {"), + Some("module a::b path-qualified module name") + ); + } + + #[test] + fn a_sentence_containing_a_keyword_is_not_that_keyword() { + // "importantly" starts with "import"; "typed" starts with "type". + assert_eq!(classify("importantly, the bridge is read-only."), None); + assert_eq!(classify("typed values flow through the VM."), None); + assert!(!accepted_head("constant folding is described here")); + } + + #[test] + fn a_fixture_is_not_debt() { + assert!(is_fixture( + "bootstrap/tests/fixtures/damage/damage_class_01.t27" + )); + assert!(!is_fixture("specs/github/auth.t27")); + // The word must be a PATH SEGMENT, not a substring of a file name. + assert!(!is_fixture("specs/tools/fixtures_report.t27")); + } + + #[test] + fn a_statement_keyword_abstains_like_a_top_level_one() { + for l in ["return x;", "let y = 1;", " for (a) |b| {"] { + assert_eq!(classify(l), None, "{l}"); + assert!(accepted_head(l), "{l}"); + } + } + + #[test] + fn a_macro_needs_a_name_in_front_of_the_bang() { + assert_eq!( + classify("assert_eq!(a, b);"), + Some("name!(..) Rust-style macro invocation") + ); + assert_eq!(classify("if x != (a) {"), None); + } +} diff --git a/docs/now/2026-08-30-a-module-name-is-a-name-reading-all-of-it-unblocked-six-spec.md b/docs/now/2026-08-30-a-module-name-is-a-name-reading-all-of-it-unblocked-six-spec.md new file mode 100644 index 000000000..0f8966210 --- /dev/null +++ b/docs/now/2026-08-30-a-module-name-is-a-name-reading-all-of-it-unblocked-six-spec.md @@ -0,0 +1,9 @@ +# NOW -- A module name is a name: reading all of it unblocked six specs (2026-08-30) + +## A module name is a name: reading all of it unblocked six specs (Refs #2864) + +- The module-name parser read one segment and stopped, so 'module github::auth {' left the parser looking at a colon at module level. Nine specs declared a path-qualified module and none parsed. Rewritten as a loop over ::-separated segments, each of which may still be hyphenated. +- Both colons are required before either is consumed: a single ':' after a module name is not a path, and swallowing it would turn a real error into a stranger one further down. +- Controls: 621 specs parsed before, 627 after, ZERO regressions. Seal drift went 537 -> 543, exactly the six new specs, so no previously-parsing spec's output changed. Bootstrap test ratchet: no new failures. +- Hollow seals 187 -> 175, generate 626 -> 632, debt ledger 90 -> 84. FROZEN_HASH updated in the same commit per M5. +- New: tri unparsed report [--list] -- the work queue by construct. Its own first three readings were wrong: it counted 21 deliberately-broken fixtures as debt, lost 15 upstream statements into 'not decided', and dropped 4 rows through a bare continue. diff --git a/tools/specs_generate_baseline.txt b/tools/specs_generate_baseline.txt index 3b8ca26bb..c60602c35 100644 --- a/tools/specs_generate_baseline.txt +++ b/tools/specs_generate_baseline.txt @@ -18,7 +18,6 @@ specs/ar/composition.t27 | Error: Compile error: parse error in fn 'average' nea specs/ar/datalog_engine.t27 | Error: Compile error: parse error in fn 'eval_bottom_up' near line 152: Expected RBrace, got Semicolon (';') at line 152:38 specs/ar/ternary_logic.t27 | Error: Compile error: parse error at module level near line 7: unexpected token after expression statement: Ident specs/auth/config.t27 | Error: Compile error: Expected LBrace, got LParen ('(') at line 60:16 -specs/automation/wrapup-auto.t27 | Error: Compile error: parse error at module level near line 6: Unexpected token in expression: Colon (':') at line 6:18 specs/benchmarks/bench_nn.t27 | Error: Compile error: parse error at module level near line 6: unexpected token after expression statement: Ident specs/benchmarks/gf16_bfloat16_nmse.t27 | Error: Compile error: parse error at module level near line 6: unexpected token after expression statement: Ident specs/bus/pubsub.t27 | Error: Compile error: parse error in fn 'publish_result_to_string' near line 389: Unexpected token in expression: Semicolon (';') at line 389:6 @@ -27,16 +26,11 @@ specs/compiler/typechecker.t27 | Error: Compile error: parse error in fn 'check_ specs/config/load.t27 | Error: Compile error: Expected LBrace, got Minus ('-') at line 217:74 specs/config/migrate.t27 | Error: Compile error: parse error in fn 'migrate_detect_version' near line 396: Expected RBrace, got Colon (':') at line 396:59 specs/conformance/e2e_scenarios.t27 | Error: Compile error: parse error at module level near line 6: unexpected token after expression statement: Ident -specs/enrichment/audio_overview.t27 | Error: Compile error: parse error at module level near line 6: Unexpected token in expression: Colon (':') at line 6:18 -specs/enrichment/youtube_transcript.t27 | Error: Compile error: parse error at module level near line 6: Unexpected token in expression: Colon (':') at line 6:18 +specs/enrichment/audio_overview.t27 | Error: Compile error: Expected LBrace, got Semicolon (';') at line 76:102 +specs/enrichment/youtube_transcript.t27 | Error: Compile error: parse error in fn 'extract_video_id' near line 109: parse error near line 109: Unexpected token in expression: Semicolon (';') a specs/file/watcher.t27 | Error: Compile error: Expected LBrace, got LParen ('(') at line 14:21 specs/fpga/testbench/mac_tb.t27 | Error: Compile error: parse error in fn 'run_tests' near line 353: Unexpected token in expression: Semicolon (';') at line 353:69 specs/fpga/testbench/uart_tb.t27 | Error: Compile error: parse error in fn 'run_tests' near line 256: Unexpected token in expression: Semicolon (';') at line 256:39 -specs/github/auth.t27 | Error: Compile error: parse error at module level near line 7: Unexpected token in expression: Colon (':') at line 7:14 -specs/github/comments.t27 | Error: Compile error: parse error at module level near line 3: Unexpected token in expression: Colon (':') at line 3:14 -specs/github/issues.t27 | Error: Compile error: parse error at module level near line 3: Unexpected token in expression: Colon (':') at line 3:14 -specs/github/prs.t27 | Error: Compile error: parse error at module level near line 3: Unexpected token in expression: Colon (':') at line 3:14 -specs/github/tests/e2e_full_flow.t27 | Error: Compile error: parse error at module level near line 8: Unexpected token in expression: Colon (':') at line 8:14 specs/graph/knowledge_graph.t27 | Error: Compile error: Expected LBrace, got Semicolon (';') at line 101:59 specs/hslm/forward_pass.t27 | Error: Compile error: Expected LBrace, got Semicolon (';') at line 82:134 specs/interop/gf_cross_language.t27 | Error: Compile error: parse error at module level near line 7: unexpected token after expression statement: Ident @@ -57,7 +51,7 @@ specs/physics/hslm_benchmark.t27 | Error: Compile error: parse error at module l specs/physics/lqg_cs_bridge.t27 | Error: Compile error: unterminated string literal opened at line 74:86 specs/physics/p2_brain_physics.t27 | Error: Compile error: Expected LBrace, got Semicolon (';') at line 53:55 specs/pins/parser.t27 | Error: Compile error: unterminated string literal opened at line 596:24 -specs/portable/relay_observer.t27 | Error: Compile error: parse error at module level near line 6: Unexpected token in expression: Colon (':') at line 6:16 +specs/portable/relay_observer.t27 | Error: Compile error: parse error at module level near line 53: Unexpected token in expression: Semicolon (';') at line 53:2 specs/provider/adapters.t27 | Error: Compile error: parse error in fn 'adapter_config_create' near line 208: Unexpected token in expression: LBrace ('{') at line 208:55 specs/provider/schema.t27 | Error: Compile error: parse error in fn 'provider_config_create' near line 267: Unexpected token in expression: LBrace ('{') at line 267:55 specs/runtime/execute.t27 | Error: Compile error: Expected LBrace, got RParen (')') at line 238:77