Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .trinity/seals/t27o_T27oLedger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
22 changes: 11 additions & 11 deletions cli/tri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,8 @@ enum Commands {
/// The t27c binary; default target/release/t27c, then t27c on PATH.
#[arg(long)] t27c: Option<String>,
},
/// 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<String>, #[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 <dir>` 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<String>, #[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<String>, #[arg(long, default_value_t = t27o_ledger::SWEEP_TOP)] top: usize },
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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<u8> {
let t0 = std::time::Instant::now(); let seal = |f: &str| -> Result<String> { Ok(serde_json::from_slice::<serde_json::Value>(&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::<Vec<_>>()).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<u8> {
let t0 = std::time::Instant::now(); let seal = |f: &str| -> Result<String> { let p = std::env::current_dir()?.join(f); Ok(serde_json::from_slice::<serde_json::Value>(&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::<Vec<_>>()).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)));
Expand All @@ -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::<usize>(), 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.
Expand Down
74 changes: 73 additions & 1 deletion gen/rust/tools/t27o/ledger.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}

123 changes: 123 additions & 0 deletions specs/tools/t27o/ledger.t27
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -245,6 +256,39 @@ pub fn over_cap(size: usize, cap: usize) -> bool {
return size > cap;
}

; --- `tri t27o-sweep --repo <dir>` (#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-<name>.tsv with <name> 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 {
Expand Down Expand Up @@ -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 <dir>` (#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);
}
Loading