diff --git a/.trinity/seals/t27o_T27oLedger.json b/.trinity/seals/t27o_T27oLedger.json index 2fb9cdacc7..9dea130d86 100644 --- a/.trinity/seals/t27o_T27oLedger.json +++ b/.trinity/seals/t27o_T27oLedger.json @@ -8,30 +8,34 @@ { "spec": "specs/tri/reseal/seal.t27", "spec_hash": "sha256:58eed39e2ed5b40cca6c35c5f951e867a7f92cb260972ee0305278f1e0028a47" + }, + { + "spec": "specs/tri/t27b/macho.t27", + "spec_hash": "sha256:3d7b5211967649508eff9770e966bc7d8522d1f3d964e30cfc76a5ae75978af3" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:70b876438caf032fd5539af93b210cec8f661ab519bb418ffdfb33d7c9f55eac", - "gen_hash_rust": "sha256:81513af17b39ada3c99c5b4340270456c67cfb1ff592ff82c35e032541210e0a", - "gen_hash_verilog": "sha256:42e96be374322dd1a6a2dfd10d9e598eb6c670d338f6c80f6c3bc8403ce84b87", - "gen_hash_zig": "sha256:27cc3b986f364b51e0a64ec4ceb5334b57bb9eca2a1d698ca08f6c4a8ffb10a8", + "gen_hash_c": "sha256:d75c3b7403359012744ee9791d5f24603c538fbc993cd059275cbce85ca04633", + "gen_hash_rust": "sha256:02f9f45cec23ed1149a09edd668dfc48aef3b4d83d0ad7a1f0709a564fd6d3c5", + "gen_hash_verilog": "sha256:7d461b24959b4ee95bcb3617f2a41e5eaac048bf7640f8a467f7e60b6db1c3b8", + "gen_hash_zig": "sha256:362faa94a4b8c45abc3a5485b5eaf1bdcf30b0e70fe327c231eb745bbede4a69", "module": "T27oLedger", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T18:07:43Z", + "sealed_at": "2026-10-18T20:26:56Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:c8ced20f9d10b20c8cfe366b991b34d2053775d9ea26fb656aeacf70d8004574", + "spec_hash": "sha256:93762f5c3e0dc493071e3850a933960a657b651964ca1da095afa6472ef10432", "spec_path": "specs/tools/t27o/ledger.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 23, - "total": 23, + "passed": 29, + "total": 29, "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 889e37da92..1a567106a5 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -439,8 +439,8 @@ enum Commands { /// The t27c binary; default target/release/t27c, then t27c on PATH. #[arg(long)] t27c: Option, }, - /// t27o over a tree (#10543): every tracked .rs 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); counts, coverage and the ranked first-refusal kinds (rules: specs/tools/t27o/ledger.t27). - T27oSweep { paths: Vec, #[arg(long, default_value_t = t27o_ledger::SWEEP_MAX_BYTES)] max_bytes: usize, #[arg(long, default_value = t27o_ledger::SWEEP_CACHE)] cache: String, #[arg(long, default_value_t = t27o_ledger::SWEEP_TOP)] top: usize }, + /// t27o over a tree (#10543): every tracked .rs 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, 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). + 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 }, } #[derive(Subcommand)] @@ -1331,7 +1331,7 @@ fn main() -> Result<()> { Commands::Hot { t27c, action, rest } => std::process::exit(cmd_hot(action, rest, t27c.as_deref()) as i32), Commands::Payout { epoch, pool, json } => std::process::exit(payout::cmd(*epoch, *pool, *json)? as i32), Commands::T27o { file, name, oracle, check, t27c } => std::process::exit(cmd_t27o(file, name.as_deref(), *oracle, *check, t27c.as_deref())? as i32), - Commands::T27oSweep { paths, max_bytes, cache, top } => std::process::exit(cmd_t27o_sweep(paths, *max_bytes, cache, *top)? as i32), + Commands::T27oSweep { paths, repo, max_bytes, cache, top } => std::process::exit(cmd_t27o_sweep(paths, repo, *max_bytes, cache.as_deref(), *top)? as i32), } Ok(()) @@ -1370,14 +1370,15 @@ fn cmd_t27o(file: &str, name: Option<&str>, oracle: bool, check: bool, t27c: Opt } /// `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. -fn cmd_t27o_sweep(paths: &[String], max: usize, cache: &str, top: usize) -> Result { - let t0 = std::time::Instant::now(); let seal = |f: &str| -> Result { Ok(serde_json::from_slice::(&fs::read(f)?)?["spec_hash"].as_str().with_context(|| format!("{f}: no spec_hash"))?.to_string()) }; - let (conv, std) = (seal(".trinity/seals/t27o_T27oRust.json")?, seal(".trinity/seals/t27o_T27oRustStd.json")?); - 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 listing: &'static str = String::from_utf8(Command::new("git").args(["ls-files", "--"]).args(paths).output()?.stdout)?.leak(); +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) = (seal(t27o_ledger::SEAL_CONVERTER)?, seal(t27o_ledger::SEAL_STD)?); 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 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]); for p in listing.lines().filter(|p| t27o_ledger::sweep_path(p)) { - let Ok(src) = fs::read(p) else { continue }; + 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; } let (sha, r) = (format!("{:x}", Sha256::digest(&src)), rows.get(p)); let reuse = t27o_ledger::reuse_row(t27o_ledger::key_check(r.is_some(), r.is_some_and(|r| r[1] == sha), r.is_some_and(|r| r[2] == conv), r.is_some_and(|r| r[3] == std))); @@ -1396,8 +1397,7 @@ fn cmd_t27o_sweep(paths: &[String], max: usize, cache: &str, top: usize) -> Resu 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()); - if let Some(d) = Path::new(cache).parent() { fs::create_dir_all(d)?; } - fs::write(cache, tab)?; Ok(0) + if let Some(d) = Path::new(&cache).parent() { fs::create_dir_all(d)?; } fs::write(&cache, tab)?; Ok(0) } /// `tri lanes`: gh's open pull requests into the base, the clone's worktrees and branch times, and what specs/tri/lanes/lanes.t27 reads in them. diff --git a/gen/rust/tools/t27o/ledger.rs b/gen/rust/tools/t27o/ledger.rs index f8ddc90f16..7bd930bc18 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: 23 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 29 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. @@ -226,6 +226,18 @@ pub const SWEEP_TOP: usize = 15; pub const SWEEP_CACHE: &'static str = "target/t27o-sweep.tsv"; +pub const SWEEP_CACHE_HEAD: &'static str = "target/t27o-sweep-"; + +pub const SWEEP_CACHE_TAIL: &'static str = ".tsv"; + +pub const SWEEP_HERE: &'static str = "."; + +pub const SEAL_CONVERTER: &'static str = ".trinity/seals/t27o_T27oRust.json"; + +pub const SEAL_STD: &'static str = ".trinity/seals/t27o_T27oRustStd.json"; + +pub const SEAL_HINT: &'static str = "run from a t27 checkout"; + pub const COVERAGE_UNKNOWN: usize = 1001; pub const KIND_CASCADE: &'static str = "uses refused"; @@ -262,6 +274,42 @@ pub fn over_cap(size: usize, cap: usize) -> bool { return (size > cap); } +pub fn repo_is_here(repo: &'static str) -> bool { + if (repo.len() == 0) { + return true; + } + let mut to: usize = repo.len(); + while ((to > 0) && (repo.as_bytes()[((to - 1)) as usize] == 47)) { + to = (to - 1); + } + return ((to == 1) && (repo.as_bytes()[0] == 46)); +} + +pub fn cache_path(repo: &'static str, out: &mut [u8]) -> usize { + if repo_is_here(repo) { + return put_text(out, 0, SWEEP_CACHE); + } + let mut to: usize = repo.len(); + while ((to > 0) && (repo.as_bytes()[((to - 1)) as usize] == 47)) { + to = (to - 1); + } + let mut from: usize = to; + while ((from > 0) && (repo.as_bytes()[((from - 1)) as usize] != 47)) { + from = (from - 1); + } + if (((SWEEP_CACHE_HEAD.len() + (to - from)) + SWEEP_CACHE_TAIL.len()) > out.len()) { + return 0; + } + let mut w: usize = put_text(out, 0, SWEEP_CACHE_HEAD); + let mut k: usize = from; + while (k < to) { + out[(w) as usize] = repo.as_bytes()[(k) as usize]; + w = (w + 1); + k = (k + 1); + } + return put_text(out, w, SWEEP_CACHE_TAIL); +} + pub fn coverage_permille(seen: usize, converted: usize) -> usize { if (seen == 0) { return COVERAGE_UNKNOWN; @@ -350,6 +398,21 @@ 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 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())) { + return false; + } + let mut k: usize = 0; + while (k < n) { + if (out[(k) as usize] != want.as_bytes()[(k) as usize]) { + return false; + } + k = (k + 1); + } + return true; +} + pub const V_BEHIND: u8 = 2; pub const V_BLOCKED: u8 = 3; @@ -616,3 +679,12 @@ pub fn verdict(conflicted: bool, matches: bool, write: bool) -> u8 { return (base + 2); } +pub fn put_text(out: &mut [u8], at: usize, w: &'static str) -> usize { + let mut i: usize = 0; + while (i < w.len()) { + out[((at + i)) as usize] = w.as_bytes()[(i) as usize]; + i = (i + 1); + } + return (at + w.len()); +} + diff --git a/specs/tools/t27o/ledger.t27 b/specs/tools/t27o/ledger.t27 index 1b93b39c29..9e33079b12 100644 --- a/specs/tools/t27o/ledger.t27 +++ b/specs/tools/t27o/ledger.t27 @@ -207,10 +207,21 @@ pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_ use tri::lab::receipt::span_is; use tri::reseal::seal::starts_at; +use tri::t27b::macho::put_text; pub const SWEEP_MAX_BYTES : usize = 300000; pub const SWEEP_TOP : usize = 15; +; The cache of the repository the command runs in, and the pieces of the cache of any other (cache_path). pub const SWEEP_CACHE : str = "target/t27o-sweep.tsv"; +pub const SWEEP_CACHE_HEAD : str = "target/t27o-sweep-"; +pub const SWEEP_CACHE_TAIL : str = ".tsv"; +; --repo when it is not given: the checkout the command runs in. +pub const SWEEP_HERE : str = "."; +; The two seals the keys come from, relative to the t27 checkout the command runs in (never to the +; swept repository), and what a missing one is told to do. +pub const SEAL_CONVERTER : str = ".trinity/seals/t27o_T27oRust.json"; +pub const SEAL_STD : str = ".trinity/seals/t27o_T27oRustStd.json"; +pub const SEAL_HINT : str = "run from a t27 checkout"; ; seen == 0 has no share: unknown, one above 100.0 percent, never a number inside the scale. pub const COVERAGE_UNKNOWN : usize = 1001; ; The kind (see kind_to) of a refusal whose reason is another refusal. @@ -245,6 +256,39 @@ pub fn over_cap(size: usize, cap: usize) -> bool { return size > cap; } +; --- `tri t27o-sweep --repo ` (#10638): one cache per swept repository --------------------- +; A row is keyed by its path, so a cache shared by two repositories would evict the other's rows on +; every run. The default cache of a repository is named after its directory. + +; Is --repo this checkout: not given, empty, or "." (with any trailing slashes). Anything else is +; another directory, "/" and ".." included. +pub fn repo_is_here(repo: string) -> bool { + if (repo.len == 0) { return true; } + var to : usize = repo.len; + while (to > 0 and repo[to - 1] == 47) { to = to - 1; } + return to == 1 and repo[0] == 46; +} + +; The default cache of a repository written into out, its length: SWEEP_CACHE for this checkout, else +; target/t27o-sweep-.tsv with the last component of the directory, trailing slashes +; ignored. 0 when the path would not fit out: nothing is written then. +pub fn cache_path(repo: string, out: []u8) -> usize { + if (repo_is_here(repo)) { return put_text(out, 0, SWEEP_CACHE); } + var to : usize = repo.len; + while (to > 0 and repo[to - 1] == 47) { to = to - 1; } + var from : usize = to; + while (from > 0 and repo[from - 1] != 47) { from = from - 1; } + if (SWEEP_CACHE_HEAD.len + (to - from) + SWEEP_CACHE_TAIL.len > out.len) { return 0; } + var w : usize = put_text(out, 0, SWEEP_CACHE_HEAD); + var k : usize = from; + while (k < to) { + out[w] = repo[k]; + w = w + 1; + k = k + 1; + } + return put_text(out, w, SWEEP_CACHE_TAIL); +} + ; The share of functions converted, per mille. No function seen is unknown, never 1000; a count of ; converted above the count seen does not add up and is unknown too. pub fn coverage_permille(seen: usize, converted: usize) -> usize { @@ -515,3 +559,82 @@ test the_ranking_puts_more_refusals_first_and_equal_counts_in_text_order { assert(text_order("type", "types") == 0 - 1); assert(text_order("types", "type") == 1); } + +; --- Tests: `tri t27o-sweep --repo ` (#10638) ------------------------------------------------- + +; out[0..n) holds exactly the bytes of want. +pub fn out_is(out: []u8, n: usize, want: str) -> bool { + if (n != want.len or n > out.len) { return false; } + var k : usize = 0; + while (k < n) { + if (out[k] != want[k]) { return false; } + k = k + 1; + } + return true; +} + +test this_checkout_is_not_given_empty_or_a_dot_and_nothing_else_is { + assert(repo_is_here(SWEEP_HERE)); + assert(repo_is_here("")); + assert(repo_is_here("./")); + assert(repo_is_here(".//")); + assert(repo_is_here("/Users/playom/trios") == false); + assert(repo_is_here("trios") == false); + assert(repo_is_here("..") == false); + assert(repo_is_here("../") == false); + assert(repo_is_here("/") == false); + assert(repo_is_here("./trios") == false); + assert(repo_is_here("trios/.") == false); +} + +test the_cache_of_this_checkout_is_the_path_it_always_had { + var out : [128]u8 = undefined; + assert(cache_path(SWEEP_HERE, out[0 .. 128]) == SWEEP_CACHE.len); + assert(out_is(out[0 .. 128], SWEEP_CACHE.len, "target/t27o-sweep.tsv")); + assert(cache_path("", out[0 .. 128]) == SWEEP_CACHE.len); + assert(out_is(out[0 .. 128], SWEEP_CACHE.len, SWEEP_CACHE)); +} + +test the_cache_of_another_repository_is_named_after_its_directory { + var out : [128]u8 = undefined; + assert(cache_path("/Users/playom/trios", out[0 .. 128]) == 27); + assert(out_is(out[0 .. 128], 27, "target/t27o-sweep-trios.tsv")); + assert(cache_path("/Users/playom/999-multibots-telegraf", out[0 .. 128]) == 44); + assert(out_is(out[0 .. 128], 44, "target/t27o-sweep-999-multibots-telegraf.tsv")); + assert(cache_path("trinity-fpga", out[0 .. 128]) == 34); + assert(out_is(out[0 .. 128], 34, "target/t27o-sweep-trinity-fpga.tsv")); +} + +test negative_control_two_repositories_never_share_a_cache_and_a_slash_changes_nothing { + var a : [128]u8 = undefined; + var b : [128]u8 = undefined; + var na : usize = cache_path("/Users/playom/trios", a[0 .. 128]); + var nb : usize = cache_path("/Users/playom/trinity", b[0 .. 128]); + assert(out_is(a[0 .. 128], na, "target/t27o-sweep-trinity.tsv") == false); + assert(out_is(b[0 .. 128], nb, "target/t27o-sweep-trios.tsv") == false); + assert(out_is(a[0 .. 128], na, SWEEP_CACHE) == false); + assert(cache_path("/Users/playom/trios/", b[0 .. 128]) == na); + assert(out_is(b[0 .. 128], na, "target/t27o-sweep-trios.tsv")); + assert(cache_path("/Users/playom/trios//", b[0 .. 128]) == na); + assert(cache_path("/", b[0 .. 128]) == 22); + assert(out_is(b[0 .. 128], 22, "target/t27o-sweep-.tsv")); +} + +test a_cache_path_that_does_not_fit_is_not_written { + var out : [24]u8 = undefined; + out[0] = 0; + assert(cache_path("/Users/playom/trios", out[0 .. 24]) == 0); + assert(out[0] == 0); + var fits : [27]u8 = undefined; + assert(cache_path("/Users/playom/trios", fits[0 .. 27]) == 27); + assert(cache_path(SWEEP_HERE, out[0 .. 24]) == SWEEP_CACHE.len); +} + +test the_seals_are_read_from_the_checkout_and_a_missing_one_says_so { + assert(span_is(SEAL_CONVERTER, 0, SEAL_CONVERTER.len, ".trinity/seals/t27o_T27oRust.json")); + assert(span_is(SEAL_STD, 0, SEAL_STD.len, ".trinity/seals/t27o_T27oRustStd.json")); + assert(starts_at(SEAL_CONVERTER, 0, ".trinity/seals/")); + assert(starts_at(SEAL_STD, 0, ".trinity/seals/")); + assert(span_is(SEAL_HINT, 0, SEAL_HINT.len, "run from a t27 checkout")); + assert(SEAL_CONVERTER.len != SEAL_STD.len); +}