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);
+}