diff --git a/.trinity/seals/t27o_T27oLedger.json b/.trinity/seals/t27o_T27oLedger.json index 2e0bdbe576..a30585a5da 100644 --- a/.trinity/seals/t27o_T27oLedger.json +++ b/.trinity/seals/t27o_T27oLedger.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+ea35d9b72", + "built_by": "t27c-bootstrap@0.5.2+b258b9007", "closure": [ { "spec": "specs/tri/lab/receipt.t27", @@ -15,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:9d516eccbc6e4404953f1f5c85c3fcce8c3e0abe08bcca15cfa0e5abb70a5185", - "gen_hash_rust": "sha256:1b30b3432b64262d5c3e42e5fbec57937afbc0c0b31fbc2ccf27a11513431955", - "gen_hash_verilog": "sha256:744dab1dd12a361c179b15417e0feebebaef4cd8b3c7044c0452458c5ba43cba", - "gen_hash_zig": "sha256:274bbd97674fbb7f4d3e100a400e64e10861c1651dd961a31ad9335690a124c9", + "gen_hash_c": "sha256:852ac5df69c82e3105afd569588b91ea3f8f523ff0f7735998d966444bd0c0bb", + "gen_hash_rust": "sha256:dbdec23bddbd792eb02e1dccf030ae97d857632a2d0db30f72da1ae26b889fed", + "gen_hash_verilog": "sha256:0d09f99c7b82ebac2ebede0f14057a37fea2b0adf4da781375bad836a3912e05", + "gen_hash_zig": "sha256:d5ed66af09cfbff648cbd88b9ebb2f1330edf91e9b5915a41acfe39c029f539d", "module": "T27oLedger", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T05:41:02Z", + "sealed_at": "2026-10-19T18:13:45Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:1ca252b0187eb7b996e2e1f0c5331fe53c0db65b3f4aa78f85a3eb5d0704b023", + "spec_hash": "sha256:5eca91dc4bdcb3feb648c3cd11cf86982409390f63f174391affe7e45415614e", "spec_path": "specs/tools/t27o/ledger.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 38, - "total": 38, + "passed": 51, + "total": 51, "vacuous": 0 }, "toolchain": { diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 7f6d0e0db0..d62dd70e8d 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -449,7 +449,7 @@ enum Commands { /// A netlist: print the M1 cell constants (CELL_N, CELL_D, CELL_NV) folded from it, or the refusal, instead of the inventory. #[arg(long)] emit_cell: bool, }, - /// t27o over a tree (#10543): every tracked .rs and .zig file converted in-process; a file's row is reused while its key holds (its SHA-256 and the seals of rust_convert.t27 and rust_std.t27, and of zig_convert.t27 for a .zig file, read from the t27 checkout this runs in); `--repo ` sweeps another checkout, with a cache of its own (#10638); counts, coverage and the ranked first-refusal kinds (rules: specs/tools/t27o/ledger.t27). + /// t27o over a tree (#10543): every tracked .rs and .zig file converted in-process; a file's row is reused while its key holds (its SHA-256 and the seals of rust_convert.t27 and rust_std.t27, and of zig_convert.t27 for a .zig file, read from the t27 checkout this runs in); `--repo ` sweeps another checkout, with a cache of its own (#10638); counts, coverage, the files that convert whole, and the ranked first-refusal kinds, a cascade counted under its root (#11122; rules: specs/tools/t27o/ledger.t27). T27oSweep { paths: Vec, #[arg(long, default_value = t27o_ledger::SWEEP_HERE)] repo: String, #[arg(long, default_value_t = t27o_ledger::SWEEP_MAX_BYTES)] max_bytes: usize, #[arg(long)] cache: Option, #[arg(long, default_value_t = t27o_ledger::SWEEP_TOP)] top: usize }, /// Land a lane branch (default: the current one): merge master, then tests with no vacuous pass, seals, types ratchet, dupe subset, push, pull request, auto-merge; the first failing gate stops it with a named code (CONFLICT, TESTS_NOT_CLEAN, SEAL_NOT_CLEAN, TYPES_NOT_CLEAN, DUPE_NOT_SUBSET, PUSH_NOT_CONFIRMED) and a second run prints ALREADY_LANDED; --plan prints the steps and runs none, --check stops before the commit; T27C names the t27c program (default t27c on PATH) (rules: specs/tri/land/land.t27, #11068). Land { branch: Option, #[arg(long, default_value_t = 0)] issue: u64, #[arg(long)] plan: bool, #[arg(long)] check: bool }, @@ -1417,14 +1417,14 @@ fn cmd_t27o(file: &str, name: Option<&str>, oracle: bool, check: bool, t27c: Opt Ok(Command::new(mutate::resolve_t27c(t27c)).arg("test-report").arg(&path).status()?.code().unwrap_or(1) as u8) } -/// `tri t27o-sweep` (#10543): every rule is ledger.t27 (which files, whether a row is reusable, coverage, root vs cascade, the ranking); this lists, hashes, converts in-process and keeps the cache. +/// `tri t27o-sweep` (#10543): every rule is ledger.t27 (which files, whether a row is reusable, coverage, whole files, a cascade's root, the ranking); this lists, hashes, converts in-process and keeps the cache. fn cmd_t27o_sweep(paths: &[String], repo: &str, max: usize, cache: Option<&str>, top: usize) -> Result { let t0 = std::time::Instant::now(); let seal = |f: &str| -> Result { let p = std::env::current_dir()?.join(f); Ok(serde_json::from_slice::(&fs::read(&p).with_context(|| format!("{}: cannot read the seal; {}", p.display(), t27o_ledger::SEAL_HINT))?)?["spec_hash"].as_str().with_context(|| format!("{}: no spec_hash", p.display()))?.to_string()) }; let (conv, std, zs) = (seal(t27o_ledger::SEAL_CONVERTER)?, seal(t27o_ledger::SEAL_STD)?, seal(t27o_ledger::SEAL_ZIG)?); let zk = format!("{conv}+{zs}"); let repo: &'static str = repo.to_string().leak(); // the keys come from this checkout; the listing, the files and the cache belong to `root` let root: &'static str = if t27o_ledger::repo_is_here(repo) { t27o_ledger::SWEEP_HERE } else { fs::canonicalize(repo).with_context(|| format!("{repo}: cannot open the repository"))?.to_string_lossy().into_owned().leak() }; let cache = cache.map_or_else(|| { let mut b = vec![0u8; 4096]; let n = t27o_ledger::cache_path(root, &mut b); String::from_utf8_lossy(&b[..n]).into_owned() }, str::to_string); - let old: &'static str = fs::read_to_string(&cache).unwrap_or_default().leak(); let rows: std::collections::HashMap<&'static str, Vec<&'static str>> = old.lines().map(|l| l.split('\t').collect::>()).filter(|f| f.len() == 10).map(|f| (f[0], f)).collect(); + let old: &'static str = fs::read_to_string(&cache).unwrap_or_default().leak(); let rows: std::collections::HashMap<&'static str, Vec<&'static str>> = old.lines().map(|l| l.split('\t').collect::>()).filter(|f| f.len() == 11).map(|f| (f[0], f)).collect(); let ls = Command::new("git").args(["-C", root, "ls-files", "--"]).args(paths).output()?; anyhow::ensure!(ls.status.success(), "{root}: git ls-files failed: {}", String::from_utf8_lossy(&ls.stderr).trim()); let listing: &'static str = String::from_utf8(ls.stdout)?.leak(); - let (mut tab, mut kinds, mut tot, mut n) = (String::new(), std::collections::BTreeMap::<&'static str, usize>::new(), [0usize; 5], [0usize; 3]); + let (mut tab, mut kinds, mut tot, mut n) = (String::new(), std::collections::BTreeMap::<&'static str, usize>::new(), [0usize; 6], [0usize; 4]); for p in listing.lines().filter(|p| t27o_ledger::sweep_path(p)) { let Ok(src) = fs::read(Path::new(root).join(p)) else { continue }; if t27o_ledger::over_cap(src.len(), max) { n[2] += 1; println!("{p} {} bytes skipped", src.len()); continue; } @@ -1434,18 +1434,18 @@ fn cmd_t27o_sweep(paths: &[String], repo: &str, max: usize, cache: Option<&str>, let f: Vec<&'static str> = if reuse { r.unwrap().clone() } else { let (mut out, mut st, mut k) = (vec![0u8; 1 << 24], vec![0usize; 1 << 18], std::collections::BTreeMap::<&'static str, usize>::new()); let stem = Path::new(p).file_stem().map_or("Converted".into(), |s| s.to_string_lossy().into_owned()); let m = if z { zig::to_t27(&src, stem.as_bytes(), &mut out, &mut st) } else { t27o::convert(&src, stem.as_bytes(), &mut out, &mut st) }; - let o: &'static str = String::from_utf8_lossy(&out[..m.min(out.len())]).into_owned().leak(); for l in o.lines().filter(|l| t27o_ledger::is_refusal(l)) { *k.entry(&l[t27o_ledger::kind_from(l)..t27o_ledger::kind_to(l)]).or_default() += 1; } - format!("{p}\t{sha}\t{ck}\t{std}\t{}\t{}\t{}\t{}\t{}\t{}", st[t27o::NFN], st[t27o::NOK], st[t27o::NTEST], st[t27o::NTS], st[t27o::NSTUB], k.iter().map(|(a, b)| format!("{a}={b}")).collect::>().join(";")).leak().split('\t').collect() + let o: &'static str = String::from_utf8_lossy(&out[..m.min(out.len())]).into_owned().leak(); let (mut at, mut via) = (t27o_ledger::next_refusal(o, 0), 0usize); while at < o.len() { let r = t27o_ledger::root_of(o, at); *k.entry(&o[t27o_ledger::kind_from(o, r)..t27o_ledger::kind_to(o, r)]).or_default() += 1; via += t27o_ledger::attributed(o, at, r) as usize; at = t27o_ledger::next_refusal(o, at + 1); } + format!("{p}\t{sha}\t{ck}\t{std}\t{}\t{}\t{}\t{}\t{}\t{via}\t{}", st[t27o::NFN], st[t27o::NOK], st[t27o::NTEST], st[t27o::NTS], st[t27o::NSTUB], k.iter().map(|(a, b)| format!("{a}={b}")).collect::>().join(";")).leak().split('\t').collect() }; n[reuse as usize] += 1; tab += &format!("{}\n", f.join("\t")); - (0..5).for_each(|i| tot[i] += f[4 + i].parse::().unwrap_or(0)); - f[9].split(';').filter_map(|e| e.rsplit_once('=')).for_each(|(k, c)| *kinds.entry(k).or_default() += c.parse::().unwrap_or(0)); + (0..6).for_each(|i| tot[i] += f[4 + i].parse::().unwrap_or(0)); n[3] += t27o_ledger::converts_whole(f[4].parse().unwrap_or(0), f[5].parse().unwrap_or(0)) as usize; + f[10].split(';').filter_map(|e| e.rsplit_once('=')).for_each(|(k, c)| *kinds.entry(k).or_default() += c.parse::().unwrap_or(0)); println!("{} {} {} {}/{} {} {}", f[0], f[4], f[5], f[6], f[7], f[8], if reuse { "reused" } else { "fresh" }); } let cov = t27o_ledger::coverage_permille(tot[0], tot[1]); println!("total {} files {} fn seen {} converted tests {}/{} stubs {} coverage {}", n[0] + n[1], tot[0], tot[1], tot[2], tot[3], tot[4], if t27o_ledger::coverage_known(cov) { format!("{}.{}%", cov / 10, cov % 10) } else { "unknown".into() }); let (casc, mut root): (Vec<_>, Vec<_>) = kinds.into_iter().partition(|(k, _)| t27o_ledger::is_cascade(*k)); root.sort_by(|a, b| t27o_ledger::rank_cmp(a.1, a.0, b.1, b.0).cmp(&0)); root.iter().take(top).for_each(|(k, c)| println!("{c:>7} {k}")); - println!("{:>7} cascade ({})\ncold {} / reused {} / skipped {} wall {:.1} s", casc.iter().map(|k| k.1).sum::(), t27o_ledger::KIND_CASCADE, n[0], n[1], n[2], t0.elapsed().as_secs_f64()); + let ext = casc.iter().map(|k| k.1).sum::(); println!("{:>7} files convert whole (every fn converted)\n{:>7} refused only through a cascade: {} counted under their root above, {ext} {}\ncold {} / reused {} / skipped {} wall {:.1} s", n[3], tot[5] + ext, tot[5], t27o_ledger::KIND_EXTERNAL, n[0], n[1], n[2], t0.elapsed().as_secs_f64()); if let Some(d) = Path::new(&cache).parent() { fs::create_dir_all(d)?; } fs::write(&cache, tab)?; Ok(0) } diff --git a/gen/rust/tools/t27o/ledger.rs b/gen/rust/tools/t27o/ledger.rs index 1f94ee3b14..e1d34bc1c1 100644 --- a/gen/rust/tools/t27o/ledger.rs +++ b/gen/rust/tools/t27o/ledger.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 38 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 51 test(s), 0 invariant(s), 0 function(s) with no body. // This backend emits declarations only. The spec's checks live in // the Zig and Verilog outputs; do not read this file as verified. @@ -271,6 +271,14 @@ pub const COVERAGE_UNKNOWN: usize = 1001; pub const KIND_CASCADE: &'static str = "uses refused"; +pub const KIND_EXTERNAL: &'static str = "uses refused (external)"; + +pub const ROOT_HOPS: usize = 64; + +pub const REFUSED: &'static str = "// t27o: refused "; + +pub const REF_LINE: &'static str = " (line "; + pub fn reuse_row(state: u32) -> bool { return (state == K_FRESH); } @@ -353,41 +361,43 @@ pub fn coverage_known(permille: usize) -> bool { return (permille < COVERAGE_UNKNOWN); } -pub fn is_refusal(line: &'static str) -> bool { - return starts_at(line, 0, "// t27o: refused "); +pub fn is_refusal(text: &'static str, at: usize) -> bool { + return starts_at(text, at, REFUSED); } -pub fn reason_from(line: &'static str) -> usize { +pub fn reason_from(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); let mut seen_line: bool = false; - let mut k: usize = 0; - while (k < line.len()) { - if starts_at(line, k, "(line ") { + let mut k: usize = at; + while (k < e) { + if starts_at(text, k, "(line ") { seen_line = true; } - if (seen_line && starts_at(line, k, "): ")) { + if (seen_line && starts_at(text, k, "): ")) { return (k + 3); } k = (k + 1); } - return line.len(); + return e; } -pub fn kind_from(line: &'static str) -> usize { - if (reason_from(line) == line.len()) { - return 0; +pub fn kind_from(text: &'static str, at: usize) -> usize { + if (reason_from(text, at) == line_stop(text, at)) { + return at; } - return reason_from(line); + return reason_from(text, at); } -pub fn kind_to(line: &'static str) -> usize { - if (reason_from(line) == line.len()) { - return line.len(); +pub fn kind_to(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); + if (reason_from(text, at) == e) { + return e; } - let mut k: usize = reason_from(line); - while (((k < line.len()) && (line.as_bytes()[(k) as usize] != 96)) && (line.as_bytes()[(k) as usize] != 59)) { + let mut k: usize = reason_from(text, at); + while (((k < e) && (text.as_bytes()[(k) as usize] != 96)) && (text.as_bytes()[(k) as usize] != 59)) { k = (k + 1); } - if ((k > reason_from(line)) && (line.as_bytes()[((k - 1)) as usize] == 32)) { + if ((k > reason_from(text, at)) && (text.as_bytes()[((k - 1)) as usize] == 32)) { return (k - 1); } return k; @@ -427,6 +437,119 @@ pub fn rank_cmp(count_a: usize, kind_a: &'static str, count_b: usize, kind_b: &' return text_order(kind_a, kind_b); } +pub fn next_refusal(text: &'static str, from: usize) -> usize { + let mut k: usize = from; + if (((k > 0) && (k <= text.len())) && (text.as_bytes()[((k - 1)) as usize] != 10)) { + k = (line_stop(text, k) + 1); + } + while (k < text.len()) { + if is_refusal(text, k) { + return k; + } + k = (line_stop(text, k) + 1); + } + return text.len(); +} + +pub fn item_from(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); + let mut k: usize = (at + REFUSED.len()); + while ((k < e) && (text.as_bytes()[(k) as usize] != 32)) { + k = (k + 1); + } + if ((k >= e) || starts_at(text, k, REF_LINE)) { + return k; + } + return (k + 1); +} + +pub fn item_to(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); + let mut k: usize = item_from(text, at); + while ((k < e) && (starts_at(text, k, REF_LINE) == false)) { + k = (k + 1); + } + if (k >= e) { + return item_from(text, at); + } + return k; +} + +pub fn target_from(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); + let mut k: usize = reason_from(text, at); + while ((k < e) && (text.as_bytes()[(k) as usize] != 96)) { + k = (k + 1); + } + if (k < e) { + return (k + 1); + } + return e; +} + +pub fn target_to(text: &'static str, at: usize) -> usize { + let e: usize = line_stop(text, at); + let mut k: usize = target_from(text, at); + while ((k < e) && (text.as_bytes()[(k) as usize] != 96)) { + k = (k + 1); + } + return k; +} + +pub fn quoted_refusal(text: &'static str, at: usize) -> usize { + let from: usize = target_from(text, at); + let n: usize = (target_to(text, at) - from); + if (n == 0) { + return text.len(); + } + let mut k: usize = next_refusal(text, 0); + while (k < text.len()) { + if ((item_to(text, k) - item_from(text, k)) == n) { + let mut i: usize = 0; + while ((i < n) && (text.as_bytes()[((item_from(text, k) + i)) as usize] == text.as_bytes()[((from + i)) as usize])) { + i = (i + 1); + } + if (i == n) { + return k; + } + } + k = next_refusal(text, (k + 1)); + } + return text.len(); +} + +pub fn is_cascade_at(text: &'static str, at: usize) -> bool { + if ((kind_to(text, at) - kind_from(text, at)) < KIND_CASCADE.len()) { + return false; + } + return starts_at(text, kind_from(text, at), KIND_CASCADE); +} + +pub fn root_of(text: &'static str, at: usize) -> usize { + let mut cur: usize = at; + let mut hops: usize = 0; + while (hops < ROOT_HOPS) { + if (is_cascade_at(text, cur) == false) { + return cur; + } + let next: usize = quoted_refusal(text, cur); + if (next >= text.len()) { + return cur; + } + cur = next; + hops = (hops + 1); + } + return at; +} + +pub fn attributed(text: &'static str, at: usize, root: usize) -> bool { + return (is_cascade_at(text, at) && (is_cascade_at(text, root) == false)); +} + +pub fn converts_whole(seen: usize, converted: usize) -> bool { + return ((seen > 0) && (converted == seen)); +} + pub fn out_is(out: impl AsRef<[u8]>, n: usize, want: &'static str) -> bool { let out: &[u8] = out.as_ref(); if ((n != want.len()) || (n > out.len())) { @@ -680,6 +803,14 @@ pub fn line(v: u8, k: u8) -> &'static str { return "sealed bootstrap/src/compiler.rs"; } +pub fn line_stop(raw: &'static str, at: usize) -> usize { + let mut k: usize = at; + while ((k < raw.len()) && (raw.as_bytes()[(k) as usize] != 10)) { + k = (k + 1); + } + return k; +} + pub fn starts_at(raw: &'static str, at: usize, lit: &'static str) -> bool { if ((at + lit.len()) > raw.len()) { return false; diff --git a/specs/tools/t27o/ledger.t27 b/specs/tools/t27o/ledger.t27 index 278cabadd9..cc328ea091 100644 --- a/specs/tools/t27o/ledger.t27 +++ b/specs/tools/t27o/ledger.t27 @@ -237,6 +237,7 @@ pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_ ; the kind of a refusal and whether it is a cascade, and the order of the ranking. use tri::lab::receipt::span_is; +use tri::reseal::seal::line_stop; use tri::reseal::seal::starts_at; use tri::t27b::macho::put_text; @@ -260,6 +261,14 @@ pub const SEAL_HINT : str = "run from a t27 checkout"; pub const COVERAGE_UNKNOWN : usize = 1001; ; The kind (see kind_to) of a refusal whose reason is another refusal. pub const KIND_CASCADE : str = "uses refused"; +; A cascade whose callee has no refusal in the file (a fn of another file, or a receiver: `self`, `Self`) stays a +; cascade; this is its row, apart from the root kinds (see root_of, #11122). +pub const KIND_EXTERNAL : str = "uses refused (external)"; +; How many callees root_of follows before it gives up: a run of callees longer than this, or a cycle, is not attributed. +pub const ROOT_HOPS : usize = 64; +; Where a refusal line starts and where its item's line number begins: " (line N): ". +pub const REFUSED : str = "// t27o: refused "; +pub const REF_LINE : str = " (line "; ; A row whose key holds is reused; any other state, including none, converts again. @@ -335,37 +344,41 @@ pub fn coverage_known(permille: usize) -> bool { return permille < COVERAGE_UNKNOWN; } -; Is this output line a refusal: `// t27o: refused (line N): `` at line M`. -pub fn is_refusal(line: string) -> bool { - return starts_at(line, 0, "// t27o: refused "); +; Is the line that starts at text[at] a refusal: `// t27o: refused (line N): `` at line M`. +; The functions below read a line of a text by the offset it starts at; a text that is one line is at 0. +pub fn is_refusal(text: string, at: usize) -> bool { + return starts_at(text, at, REFUSED); } -; Where the reason starts: after the first "): " that follows "(line "; line.len when there is none. -pub fn reason_from(line: string) -> usize { +; Where the reason of the line starts: after the first "): " that follows "(line "; the line's end when there is +; none. +pub fn reason_from(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); var seen_line : bool = false; - var k : usize = 0; - while (k < line.len) { - if (starts_at(line, k, "(line ")) { seen_line = true; } - if (seen_line and starts_at(line, k, "): ")) { return k + 3; } + var k : usize = at; + while (k < e) { + if (starts_at(text, k, "(line ")) { seen_line = true; } + if (seen_line and starts_at(text, k, "): ")) { return k + 3; } k = k + 1; } - return line.len; + return e; } ; The kind of a refusal is the reason's words before the first backtick (the quoted text that follows ; them differs from one refusal to the next) or the first semicolon: a row of the sweep's cache separates its ; kinds with ';', and the Zig front end's "generated; convert its source spec instead" was cut in two by it and ; counted as the second half (#10823). A line with no reason is its own kind, whole. -pub fn kind_from(line: string) -> usize { - if (reason_from(line) == line.len) { return 0; } - return reason_from(line); +pub fn kind_from(text: string, at: usize) -> usize { + if (reason_from(text, at) == line_stop(text, at)) { return at; } + return reason_from(text, at); } -pub fn kind_to(line: string) -> usize { - if (reason_from(line) == line.len) { return line.len; } - var k : usize = reason_from(line); - while (k < line.len and line[k] != 96 and line[k] != 59) { k = k + 1; } - if (k > reason_from(line) and line[k - 1] == 32) { return k - 1; } +pub fn kind_to(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); + if (reason_from(text, at) == e) { return e; } + var k : usize = reason_from(text, at); + while (k < e and text[k] != 96 and text[k] != 59) { k = k + 1; } + if (k > reason_from(text, at) and text[k - 1] == 32) { return k - 1; } return k; } @@ -395,6 +408,119 @@ pub fn rank_cmp(count_a: usize, kind_a: string, count_b: usize, kind_b: string) return text_order(kind_a, kind_b); } +; --- A cascade is counted under its root (#11122) --------------------------------------------------------- +; "uses refused `a`" is a consequence: the fn b is refused because the item a is, and what to remove next is +; a's reason. Ranked under its own kind it hid 4,468 of the 16,057 first refusals of the five repositories +; (2026-10-19), so the converted share could not show a root blocker removed while its callers stayed refused +; for another reason. The converted text of one file holds one line per refused item, +; // t27o: refused (line N): `` at line M +; and a cascade quotes the name of the item it uses, so the root is a lookup in the same text: the first +; refusal line whose item is named so, followed while that one is a cascade too. A callee with no refusal in +; the file (a fn of another file, a receiver `self` or `Self`) ends the run at the cascade, counted apart as +; KIND_EXTERNAL; so does a cycle or a run over ROOT_HOPS. Every function below reads the line that starts +; at text[at], and `at` is the start of a refusal line (next_refusal). + +; The start of the first refusal line at or after `from`; text.len when there is none. A `from` inside a line +; means the next line, so next_refusal(text, at + 1) is the refusal after the one at `at`. +pub fn next_refusal(text: string, from: usize) -> usize { + var k : usize = from; + if (k > 0 and k <= text.len and text[k - 1] != 10) { k = line_stop(text, k) + 1; } + while (k < text.len) { + if (is_refusal(text, k)) { return k; } + k = line_stop(text, k) + 1; + } + return text.len; +} + +; Where the item's name starts in the refusal line at `at`: after " ". Items without a name +; (`unsafe`, `extern`, `std::fmt::Display`: a trait impl) have and no name; the name is empty then. +pub fn item_from(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); + var k : usize = at + REFUSED.len; + while (k < e and text[k] != 32) { k = k + 1; } + if (k >= e or starts_at(text, k, REF_LINE)) { return k; } + return k + 1; +} + +; Where it ends: before " (line ". The name is empty (item_to == item_from) when the line has none. +pub fn item_to(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); + var k : usize = item_from(text, at); + while (k < e and starts_at(text, k, REF_LINE) == false) { k = k + 1; } + if (k >= e) { return item_from(text, at); } + return k; +} + +; The name a refusal quotes: the text between the first two backticks of its reason; empty when it has none. +pub fn target_from(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); + var k : usize = reason_from(text, at); + while (k < e and text[k] != 96) { k = k + 1; } + if (k < e) { return k + 1; } + return e; +} + +pub fn target_to(text: string, at: usize) -> usize { + const e : usize = line_stop(text, at); + var k : usize = target_from(text, at); + while (k < e and text[k] != 96) { k = k + 1; } + return k; +} + +; The first refusal line whose item is named what the refusal at `at` quotes, byte for byte; text.len when none is, +; and when it quotes nothing (an empty name matches no item). Compared here, not by a span helper of another spec, +; none of which this spec can import as it is (measured 2026-10-20): tri::loop::tick makes `t27b test` stop on an +; unresolved name of lanes.t27, tri::network::payout pulls its network modules (a SHA-512) into the generated Rust, +; and the widgets' take []const u8 where the text here is a string (their Rust is &[u8]). +pub fn quoted_refusal(text: string, at: usize) -> usize { + const from : usize = target_from(text, at); + const n : usize = target_to(text, at) - from; + if (n == 0) { return text.len; } + var k : usize = next_refusal(text, 0); + while (k < text.len) { + if (item_to(text, k) - item_from(text, k) == n) { + var i : usize = 0; + while (i < n and text[item_from(text, k) + i] == text[from + i]) { i = i + 1; } + if (i == n) { return k; } + } + k = next_refusal(text, k + 1); + } + return text.len; +} + +; Is the refusal at `at` a cascade: its kind (kind_from .. kind_to) starts with KIND_CASCADE. +pub fn is_cascade_at(text: string, at: usize) -> bool { + if (kind_to(text, at) - kind_from(text, at) < KIND_CASCADE.len) { return false; } + return starts_at(text, kind_from(text, at), KIND_CASCADE); +} + +; The refusal the one at `at` comes down to: itself when it is not a cascade, else the first refusal of the item it +; quotes, followed while that is a cascade. The line it stops at when the callee has no refusal in the file, when +; the run is a cycle or longer than ROOT_HOPS, is a cascade still (is_cascade_at): nothing was attributed. +pub fn root_of(text: string, at: usize) -> usize { + var cur : usize = at; + var hops : usize = 0; + while (hops < ROOT_HOPS) { + if (is_cascade_at(text, cur) == false) { return cur; } + const next : usize = quoted_refusal(text, cur); + if (next >= text.len) { return cur; } + cur = next; + hops = hops + 1; + } + return at; +} + +; Was the refusal at `at` a cascade that root_of attributed: it is one, and the line it came down to is not. +pub fn attributed(text: string, at: usize, root: usize) -> bool { + return is_cascade_at(text, at) and is_cascade_at(text, root) == false; +} + +; A file converts whole when it has fn and every one converted. No fn is unknown, never whole (as ledger_status +; refuses an empty set); a count of converted above the count seen does not add up and is not whole either. +pub fn converts_whole(seen: usize, converted: usize) -> bool { + return seen > 0 and converted == seen; +} + ; --- Tests ---------------------------------------------------------------------------------- test an_oracle_run_passes_only_when_every_sample_agreed { @@ -559,14 +685,14 @@ test negative_control_no_fn_seen_is_never_one_hundred_percent { test a_refusal_kind_is_the_words_before_the_first_backtick { const a : string = "// t27o: refused fn ids (line 65): type `Vec` at line 65"; - assert(is_refusal(a)); - assert(span_is(a, kind_from(a), kind_to(a), "type")); + assert(is_refusal(a, 0)); + assert(span_is(a, kind_from(a, 0), kind_to(a, 0), "type")); const b : string = "// t27o: refused enum JumpsCmd (line 47): enum variant with data `Census` at line 50"; - assert(span_is(b, kind_from(b), kind_to(b), "enum variant with data")); + assert(span_is(b, kind_from(b, 0), kind_to(b, 0), "enum variant with data")); const c : string = "// t27o: refused fn run (line 58): uses refused `JumpsCmd::Census` at line 60; 3 fn inside"; - assert(span_is(c, kind_from(c), kind_to(c), "uses refused")); + assert(span_is(c, kind_from(c, 0), kind_to(c, 0), "uses refused")); const d : string = "// t27o: refused fn eq (line 9): == of structs `a == b` at line 11"; - assert(span_is(d, kind_from(d), kind_to(d), "== of structs")); + assert(span_is(d, kind_from(d, 0), kind_to(d, 0), "== of structs")); } test only_a_reason_that_names_a_refused_item_is_a_cascade { @@ -578,12 +704,12 @@ test only_a_reason_that_names_a_refused_item_is_a_cascade { } test negative_control_a_line_that_is_not_a_refusal_or_has_no_reason_is_not_misread { - assert(is_refusal("// t27o: 11 fn seen, 0 converted") == false); - assert(is_refusal("") == false); - assert(is_refusal("pub fn run() -> void {") == false); + assert(is_refusal("// t27o: 11 fn seen, 0 converted", 0) == false); + assert(is_refusal("", 0) == false); + assert(is_refusal("pub fn run() -> void {", 0) == false); const e : string = "// t27o: refused fn x (line 1) no reason"; - assert(is_refusal(e)); - assert(span_is(e, kind_from(e), kind_to(e), e)); + assert(is_refusal(e, 0)); + assert(span_is(e, kind_from(e, 0), kind_to(e, 0), e)); } test the_ranking_puts_more_refusals_first_and_equal_counts_in_text_order { @@ -746,16 +872,153 @@ test negative_control_a_generated_or_target_zig_path_and_a_near_miss_are_not_swe test a_refusal_kind_stops_at_a_semicolon_because_the_cache_row_separates_kinds_with_one { const g : string = "// t27o: refused file (line 1): generated; convert its source spec instead"; - assert(is_refusal(g)); - assert(span_is(g, kind_from(g), kind_to(g), "generated")); + assert(is_refusal(g, 0)); + assert(span_is(g, kind_from(g, 0), kind_to(g, 0), "generated")); const h : string = "// t27o: refused fn f (line 3): pointer `*` at line 3; 2 fn inside"; - assert(span_is(h, kind_from(h), kind_to(h), "pointer")); + assert(span_is(h, kind_from(h, 0), kind_to(h, 0), "pointer")); } test negative_control_a_kind_with_no_semicolon_or_backtick_is_still_the_whole_reason { const k : string = "// t27o: refused fn f (line 3): name table full"; - assert(span_is(k, kind_from(k), kind_to(k), "name table full")); + assert(span_is(k, kind_from(k, 0), kind_to(k, 0), "name table full")); const c : string = "// t27o: refused fn f (line 3): uses refused `g` at line 4; 1 fn inside"; - assert(span_is(c, kind_from(c), kind_to(c), "uses refused")); + assert(span_is(c, kind_from(c, 0), kind_to(c, 0), "uses refused")); assert(is_cascade("uses refused")); } + +; --- Tests: a cascade is counted under its root (#11122) ------------------------------------------------- + +test a_cascade_is_attributed_to_the_first_refusal_at_its_root { + const t : string = "// t27o: refused fn b (line 3): uses refused `a` at line 3\n\nfn b() -> u32 {\n}\n\n// t27o: refused fn a (line 7): type `Vec` at line 7\n\nfn a() -> u32 {\n}\n\n"; + const b : usize = next_refusal(t, 0); + const a : usize = next_refusal(t, b + 1); + assert(b == 0); + assert(a > b); + assert(next_refusal(t, a + 1) == t.len); + assert(root_of(t, b) == a); + assert(span_is(t, kind_from(t, root_of(t, b)), kind_to(t, root_of(t, b)), "type")); + assert(attributed(t, b, root_of(t, b))); + assert(root_of(t, a) == a); + assert(attributed(t, a, a) == false); +} + +test a_run_of_cascades_is_followed_to_the_refusal_that_is_not_one { + const t : string = "// t27o: refused fn c (line 1): uses refused `b` at line 1\n\n// t27o: refused fn b (line 2): uses refused `a` at line 2; 1 fn inside\n\n// t27o: refused fn a (line 3): macro `vec` at line 3\n\n"; + const c : usize = next_refusal(t, 0); + const b : usize = next_refusal(t, c + 1); + const a : usize = next_refusal(t, b + 1); + assert(root_of(t, c) == a); + assert(root_of(t, b) == a); + assert(span_is(t, kind_from(t, a), kind_to(t, a), "macro")); + assert(attributed(t, c, root_of(t, c))); + assert(attributed(t, b, root_of(t, b))); +} + +test a_cascade_may_end_in_any_root_reason_including_uses_external { + const t : string = "// t27o: refused fn b (line 1): uses refused `a` at line 1\n\n// t27o: refused fn a (line 2): uses external `Thing` at line 2\n\n"; + const a : usize = next_refusal(t, 1); + assert(root_of(t, 0) == a); + assert(span_is(t, kind_from(t, a), kind_to(t, a), "uses external")); + assert(attributed(t, 0, a)); +} + +test a_callee_in_another_file_stays_a_cascade_and_is_not_attributed { + const t : string = "// t27o: refused fn b (line 1): uses refused `elsewhere` at line 1\n\n// t27o: refused fn c (line 2): uses refused `b` at line 2\n\nfn d() -> u32 {\n}\n\n"; + const c : usize = next_refusal(t, 1); + assert(root_of(t, 0) == 0); + assert(is_cascade_at(t, 0)); + assert(attributed(t, 0, root_of(t, 0)) == false); + assert(root_of(t, c) == 0); + assert(is_cascade_at(t, root_of(t, c))); + assert(attributed(t, c, root_of(t, c)) == false); + assert(span_is(t, kind_from(t, 0), kind_to(t, 0), "uses refused")); +} + +test negative_control_a_cycle_or_a_self_call_ends_and_is_not_attributed { + const t : string = "// t27o: refused fn a (line 1): uses refused `b` at line 1\n\n// t27o: refused fn b (line 2): uses refused `a` at line 2\n\n// t27o: refused fn r (line 3): uses refused `r` at line 3\n\n"; + const b : usize = next_refusal(t, 1); + const r : usize = next_refusal(t, b + 1); + assert(attributed(t, 0, root_of(t, 0)) == false); + assert(attributed(t, b, root_of(t, b)) == false); + assert(attributed(t, r, root_of(t, r)) == false); + assert(is_cascade_at(t, root_of(t, 0))); + assert(is_cascade_at(t, root_of(t, r))); +} + +test negative_control_a_name_is_matched_whole_and_by_case_and_self_is_nobodys { + const t : string = "// t27o: refused fn x (line 1): uses refused `ab` at line 1\n\n// t27o: refused fn y (line 2): uses refused `A` at line 2\n\n// t27o: refused fn z (line 3): uses refused `self` at line 3\n\n// t27o: refused fn a (line 4): type `Vec` at line 4\n\n// t27o: refused fn abc (line 5): type `Vec` at line 5\n\n// t27o: refused struct Self (line 6): type `Vec` at line 6\n\n"; + const y : usize = next_refusal(t, 1); + const z : usize = next_refusal(t, y + 1); + assert(attributed(t, 0, root_of(t, 0)) == false); + assert(attributed(t, y, root_of(t, y)) == false); + assert(attributed(t, z, root_of(t, z)) == false); + assert(quoted_refusal(t, 0) == t.len); +} + +test negative_control_a_refusal_with_no_quoted_name_and_a_nameless_item_resolve_nothing { + const t : string = "// t27o: refused unsafe (line 1): item `unsafe` at line 1; 1 fn inside\n\n// t27o: refused fn f (line 2): uses refused `unsafe` at line 2\n\n// t27o: refused fn g (line 3): uses refused\n\n"; + const f : usize = next_refusal(t, 1); + const g : usize = next_refusal(t, f + 1); + assert(target_to(t, g) == target_from(t, g)); + assert(quoted_refusal(t, g) == t.len); + assert(quoted_refusal(t, f) == t.len); + assert(attributed(t, f, root_of(t, f)) == false); + assert(attributed(t, g, root_of(t, g)) == false); +} + +test the_item_a_refusal_names_is_the_word_after_its_kind_and_some_have_none { + const t : string = "// t27o: refused struct Gen (line 1): type `Vec` at line 1\n// t27o: refused std::fmt::Display (line 2): trait impl `std::fmt::Display` at line 2; 1 fn inside\n// t27o: refused test \"calls bad\" (line 3): uses refused `bad` at line 5\n// t27o: refused unsafe (line 4): item `unsafe` at line 4\n"; + const s : usize = next_refusal(t, 0); + const d : usize = next_refusal(t, s + 1); + const q : usize = next_refusal(t, d + 1); + const u : usize = next_refusal(t, q + 1); + assert(span_is(t, item_from(t, s), item_to(t, s), "Gen")); + assert(item_to(t, d) == item_from(t, d)); + assert(span_is(t, item_from(t, q), item_to(t, q), "\"calls bad\"")); + assert(item_to(t, u) == item_from(t, u)); + assert(span_is(t, target_from(t, q), target_to(t, q), "bad")); + assert(next_refusal(t, u + 1) == t.len); +} + +test the_first_refusal_of_a_name_is_the_one_a_cascade_follows { + const t : string = "// t27o: refused fn go (line 1): uses refused `new` at line 1\n\n// t27o: refused fn new (line 2): type `Vec` at line 2\n\n// t27o: refused fn new (line 3): macro `vec` at line 3\n\n"; + const first : usize = next_refusal(t, 1); + assert(root_of(t, 0) == first); + assert(span_is(t, kind_from(t, first), kind_to(t, first), "type")); +} + +test the_next_refusal_skips_other_lines_and_a_start_inside_a_line_means_the_next_line { + const t : string = "pub fn ok() -> u32 {\n}\n\n// t27o: refused fn f (line 4): type `Vec` at line 4\n\n// t27o: 3 fn seen, 1 converted\n// t27o: refused fn g (line 5): type `Vec` at line 5\n"; + const f : usize = next_refusal(t, 0); + const g : usize = next_refusal(t, f + 1); + assert(f == 24); + assert(g > f); + assert(next_refusal(t, f) == f); + assert(next_refusal(t, g + 1) == t.len); + assert(next_refusal(t, t.len) == t.len); + assert(next_refusal("", 0) == 0); +} + +test the_kind_of_a_line_inside_a_text_is_the_kind_of_the_line_alone_at_offset_zero { + const l : string = "// t27o: refused fn a (line 7): type `Vec` at line 7"; + const t : string = "x\n// t27o: refused fn a (line 7): type `Vec` at line 7\ny\n"; + assert(kind_from(t, 2) - 2 == kind_from(l, 0)); + assert(kind_to(t, 2) - 2 == kind_to(l, 0)); + assert(reason_from(t, 2) - 2 == reason_from(l, 0)); + const n : string = "// t27o: refused fn a (line 7) no reason\nnext\n"; + assert(kind_from(n, 0) == 0); + assert(kind_to(n, 0) == line_stop(n, 0)); +} + +test a_file_converts_whole_when_it_has_fn_and_every_one_converted { + assert(converts_whole(3, 3)); + assert(converts_whole(1, 1)); + assert(converts_whole(21, 20) == false); + assert(converts_whole(5, 0) == false); +} + +test negative_control_a_file_with_no_fn_or_a_count_that_does_not_add_up_is_not_whole { + assert(converts_whole(0, 0) == false); + assert(converts_whole(2, 3) == false); + assert(coverage_known(coverage_permille(0, 0)) == false); +}