diff --git a/.trinity/seals/t27o_T27oLedger.json b/.trinity/seals/t27o_T27oLedger.json index 7da7822664..2fb9cdacc7 100644 --- a/.trinity/seals/t27o_T27oLedger.json +++ b/.trinity/seals/t27o_T27oLedger.json @@ -1,24 +1,33 @@ { "built_by": "t27c-bootstrap@0.5.2+9b556b7c3", - "closure": [], + "closure": [ + { + "spec": "specs/tri/lab/receipt.t27", + "spec_hash": "sha256:00d9eec8d7be84dde596c9990b3a5574de24aff9f7a94579fc620eb585712441" + }, + { + "spec": "specs/tri/reseal/seal.t27", + "spec_hash": "sha256:58eed39e2ed5b40cca6c35c5f951e867a7f92cb260972ee0305278f1e0028a47" + } + ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:7718109194bbe93a643a14dc387c9a9ea965f5762a1a61273fb9e25f5a598488", - "gen_hash_rust": "sha256:c1b340dbe2e18469610447bce3edc48acde9c30b06544ad97038b5ddeb436793", - "gen_hash_verilog": "sha256:4463b9a8fcef3622b4e17271e74d188615cde707c432fa11efbcacfcce5612ae", - "gen_hash_zig": "sha256:edb095d112d0f61f6ab296e112409cab0fc978dc21acd18eeca6e7a3fc70d00f", + "gen_hash_c": "sha256:70b876438caf032fd5539af93b210cec8f661ab519bb418ffdfb33d7c9f55eac", + "gen_hash_rust": "sha256:81513af17b39ada3c99c5b4340270456c67cfb1ff592ff82c35e032541210e0a", + "gen_hash_verilog": "sha256:42e96be374322dd1a6a2dfd10d9e598eb6c670d338f6c80f6c3bc8403ce84b87", + "gen_hash_zig": "sha256:27cc3b986f364b51e0a64ec4ceb5334b57bb9eca2a1d698ca08f6c4a8ffb10a8", "module": "T27oLedger", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T16:37:38Z", + "sealed_at": "2026-10-18T18:07:43Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:03a101483be4dbd1be9f5e1dde9681ab01c513256138a0de37241b58db6b32d4", + "spec_hash": "sha256:c8ced20f9d10b20c8cfe366b991b34d2053775d9ea26fb656aeacf70d8004574", "spec_path": "specs/tools/t27o/ledger.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 12, - "total": 12, + "passed": 23, + "total": 23, "vacuous": 0 }, "toolchain": { diff --git a/cli/dlc10/src/lib.rs b/cli/dlc10/src/lib.rs index 433000a73c..78523a3050 100644 --- a/cli/dlc10/src/lib.rs +++ b/cli/dlc10/src/lib.rs @@ -1813,20 +1813,6 @@ fn extract_rx(resp: &[u8], total_bits: usize, rx_start_bits: usize, rx_len_bits: out } -/// Default JTAG-bit latency between TDI presentation and the corresponding -/// TDO bit for the Migen JTAG2SPI bridge. The Verilog has a 2-stage MISO -/// flop (`negedge`/`miso_capture` then `tdo`) plus the JTAG host's own -/// 1-bit Capture-DR delay — so a starting guess of 3 is reasonable. Can -/// be overridden by `T27_DLC10_MIGEN_LATENCY` for empirical tuning. -const MIGEN_TDO_LATENCY_BITS_DEFAULT: usize = 3; - -fn migen_latency() -> usize { - std::env::var("T27_DLC10_MIGEN_LATENCY") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(MIGEN_TDO_LATENCY_BITS_DEFAULT) -} - /// Index into the DLC10 16-bit-LE packed response by absolute Shift-DR /// bit position (LSB-first within each 16-bit word). fn bit_at(resp: &[u8], bit_idx: usize) -> bool { diff --git a/cli/tri/src/depin/types.rs b/cli/tri/src/depin/types.rs index a2e33851af..01f7c401c3 100644 --- a/cli/tri/src/depin/types.rs +++ b/cli/tri/src/depin/types.rs @@ -42,7 +42,6 @@ pub struct EpochChallengeResponse { pub struct MiningEpoch { pub epoch_id: u64, pub phi_seed: [u8; 16], - pub start_ts: u64, pub block_reward: u64, } @@ -51,22 +50,9 @@ impl MiningEpoch { Self { epoch_id: 0, phi_seed: [0u8; 16], - start_ts: 0, block_reward: 50_000_000, } } - - pub fn next(&self) -> Self { - let mut seed = [0u8; 16]; - let hash = sha2_hash(&[b"EPOCH_SEED", &self.epoch_id.to_le_bytes(), &self.phi_seed]); - seed.copy_from_slice(&hash[..16]); - Self { - epoch_id: self.epoch_id + 1, - phi_seed: seed, - start_ts: 0, - block_reward: self.block_reward, - } - } } pub fn sha2_hash(inputs: &[&[u8]]) -> [u8; 32] { diff --git a/cli/tri/src/gates.rs b/cli/tri/src/gates.rs index 33f43bd0c8..711e2e23d5 100644 --- a/cli/tri/src/gates.rs +++ b/cli/tri/src/gates.rs @@ -5775,20 +5775,6 @@ fn preview(base: &str) -> Result<()> { Ok(()) } -// Its only caller was the `check-now-freshness` row of `preview`, removed in #5935. -#[allow(dead_code)] -fn rev(root: &std::path::Path, r: &str) -> Result { - let out = std::process::Command::new("git") - .args(["rev-parse", r]) - .current_dir(root) - .output() - .context("failed to invoke git rev-parse")?; - if !out.status.success() { - anyhow::bail!("git rev-parse {r} failed -- the revision is wrong, not the tree"); - } - Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) -} - /// The pull request's title and body, if this branch has one open. fn pr_text(root: &std::path::Path) -> Option { let out = std::process::Command::new("gh") diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 97bd9b43a6..77771d0e49 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -437,6 +437,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 }, } #[derive(Subcommand)] @@ -1327,6 +1329,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), } Ok(()) @@ -1358,6 +1361,37 @@ 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. +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(); + 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 }; + 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))); + 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 << 16], std::collections::BTreeMap::<&'static str, usize>::new()); + let m = t27o::convert(&src, Path::new(p).file_stem().map_or("Converted".into(), |s| s.to_string_lossy().into_owned()).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{conv}\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() + }; + 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)); + 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()); + 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. fn cmd_lanes(repo: &str, base: &str, clone: &str, dir: &str, stale: u64) -> Result { fn run(c: &str, a: &[&str]) -> Result<&'static str> { Ok(String::from_utf8_lossy(&Command::new(c).args(a).output()?.stdout).into_owned().leak()) } diff --git a/gen/rust/tools/t27o/ledger.rs b/gen/rust/tools/t27o/ledger.rs index 6dd46f2b37..f8ddc90f16 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: 12 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 23 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. @@ -200,19 +200,419 @@ pub fn ledger_status(total: u32, out_of_scope: u32, blocked: u32, contradicted: return L_OPEN; } -pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_table_version: u32) -> u32 { +pub fn key_check(recorded: bool, same_source: bool, same_converter: bool, same_std: bool) -> u32 { if (recorded == false) { return K_NONE; } if (same_source == false) { return K_STALE_SOURCE; } - if (converter_version != CONVERTER_VERSION) { + if (same_converter == false) { return K_STALE_CONVERTER; } - if (std_table_version != STD_TABLE_VERSION) { + if (same_std == false) { return K_STALE_STD; } return K_FRESH; } +pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_table_version: u32) -> u32 { + return key_check(recorded, same_source, (converter_version == CONVERTER_VERSION), (std_table_version == STD_TABLE_VERSION)); +} + +pub const SWEEP_MAX_BYTES: usize = 300000; + +pub const SWEEP_TOP: usize = 15; + +pub const SWEEP_CACHE: &'static str = "target/t27o-sweep.tsv"; + +pub const COVERAGE_UNKNOWN: usize = 1001; + +pub const KIND_CASCADE: &'static str = "uses refused"; + +pub fn reuse_row(state: u32) -> bool { + return (state == K_FRESH); +} + +pub fn sweep_path(path: &'static str) -> bool { + if (path.len() < 3) { + return false; + } + if (span_is(path, (path.len() - 3), path.len(), ".rs") == false) { + return false; + } + let mut from: usize = 0; + let mut k: usize = 0; + while (k < path.len()) { + if (path.as_bytes()[(k) as usize] == 47) { + if span_is(path, from, k, "gen") { + return false; + } + if span_is(path, from, k, "target") { + return false; + } + from = (k + 1); + } + k = (k + 1); + } + return true; +} + +pub fn over_cap(size: usize, cap: usize) -> bool { + return (size > cap); +} + +pub fn coverage_permille(seen: usize, converted: usize) -> usize { + if (seen == 0) { + return COVERAGE_UNKNOWN; + } + if (converted > seen) { + return COVERAGE_UNKNOWN; + } + return ((converted * 1000) / seen); +} + +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 reason_from(line: &'static str) -> usize { + let mut seen_line: bool = false; + let mut k: usize = 0; + while (k < line.len()) { + if starts_at(line, k, "(line ") { + seen_line = true; + } + if (seen_line && starts_at(line, k, "): ")) { + return (k + 3); + } + k = (k + 1); + } + return line.len(); +} + +pub fn kind_from(line: &'static str) -> usize { + if (reason_from(line) == line.len()) { + return 0; + } + return reason_from(line); +} + +pub fn kind_to(line: &'static str) -> usize { + if (reason_from(line) == line.len()) { + return line.len(); + } + let mut k: usize = reason_from(line); + while ((k < line.len()) && (line.as_bytes()[(k) as usize] != 96)) { + k = (k + 1); + } + if ((k > reason_from(line)) && (line.as_bytes()[((k - 1)) as usize] == 32)) { + return (k - 1); + } + return k; +} + +pub fn is_cascade(kind: &'static str) -> bool { + return starts_at(kind, 0, KIND_CASCADE); +} + +pub fn text_order(a: &'static str, b: &'static str) -> i32 { + let mut k: usize = 0; + while ((k < a.len()) && (k < b.len())) { + if (a.as_bytes()[(k) as usize] < b.as_bytes()[(k) as usize]) { + return (0 - 1); + } + if (a.as_bytes()[(k) as usize] > b.as_bytes()[(k) as usize]) { + return 1; + } + k = (k + 1); + } + if (a.len() < b.len()) { + return (0 - 1); + } + if (a.len() > b.len()) { + return 1; + } + return 0; +} + +pub fn rank_cmp(count_a: usize, kind_a: &'static str, count_b: usize, kind_b: &'static str) -> i32 { + if (count_a > count_b) { + return (0 - 1); + } + if (count_a < count_b) { + return 1; + } + return text_order(kind_a, kind_b); +} + +pub const V_BEHIND: u8 = 2; + +pub const V_BLOCKED: u8 = 3; + +pub const V_NOT_RUN: u8 = 4; + +pub const V_RED: u8 = 5; + +pub const V_VACUOUS: u8 = 1; + +pub const V_VERIFIED: u8 = 0; + +pub fn cell_from(text: &'static str, o: usize, e: usize, k: u8) -> usize { + return value_from(text, key_at(text, o, e, 3, column(k))); +} + +pub fn cell_to(text: &'static str, o: usize, e: usize, k: u8) -> usize { + return value_to(text, key_at(text, o, e, 3, column(k))); +} + +pub fn column(k: u8) -> &'static str { + if (k == 0) { + return "file"; + } + if (k == 1) { + return "reference"; + } + if (k == 2) { + return "t27b"; + } + if (k == 3) { + return "tests"; + } + return "asserts"; +} + +pub fn glob(pat: &'static str, s: &'static str, from: usize, to: usize) -> bool { + let mut p: usize = 0; + let mut i: usize = from; + let mut star: usize = pat.len(); + let mut mark: usize = from; + while (i < to) { + if ((p < pat.len()) && ((pat.as_bytes()[(p) as usize] == 63) || (pat.as_bytes()[(p) as usize] == s.as_bytes()[(i) as usize]))) { + p = (p + 1); + i = (i + 1); + } else { + if ((p < pat.len()) && (pat.as_bytes()[(p) as usize] == 42)) { + star = p; + mark = i; + p = (p + 1); + } else { + if (star == pat.len()) { + return false; + } + p = (star + 1); + mark = (mark + 1); + i = mark; + } + } + } + while ((p < pat.len()) && (pat.as_bytes()[(p) as usize] == 42)) { + p = (p + 1); + } + return (p == pat.len()); +} + +pub fn key_at(text: &'static str, from: usize, stop: usize, ind: usize, key: &'static str) -> usize { + let mut at: usize = from; + while ((at < stop) && (at < text.len())) { + let end: usize = line_end(text, at); + let v: usize = key_value(text, at, end, ind, key); + if (v <= end) { + return v; + } + at = (end + 1); + } + return (text.len() + 1); +} + +pub fn key_value(text: &'static str, at: usize, end: usize, ind: usize, key: &'static str) -> usize { + let v: usize = (((at + ind) + key.len()) + 4); + if (v > end) { + return (end + 1); + } + let mut k: usize = 0; + while (k < ind) { + if (text.as_bytes()[((at + k)) as usize] != 32) { + return (end + 1); + } + k = (k + 1); + } + if (text.as_bytes()[((at + ind)) as usize] != 34) { + return (end + 1); + } + if (span_is(text, ((at + ind) + 1), (((at + ind) + 1) + key.len()), key) == false) { + return (end + 1); + } + if (((text.as_bytes()[((v - 3)) as usize] != 34) || (text.as_bytes()[((v - 2)) as usize] != 58)) || (text.as_bytes()[((v - 1)) as usize] != 32)) { + return (end + 1); + } + return v; +} + +pub fn line_end(text: &'static str, at: usize) -> usize { + let mut k: usize = at; + while ((k < text.len()) && (text.as_bytes()[(k) as usize] != 10)) { + k = (k + 1); + } + return k; +} + +pub fn matches(text: &'static str, o: usize, e: usize, pat: &'static str) -> bool { + return glob(pat, text, cell_from(text, o, e, 0), cell_to(text, o, e, 0)); +} + +pub fn name(v: u8) -> &'static str { + if (v == V_VERIFIED) { + return "verified"; + } + if (v == V_VACUOUS) { + return "vacuous"; + } + if (v == V_BEHIND) { + return "behind"; + } + if (v == V_BLOCKED) { + return "blocked"; + } + if (v == V_NOT_RUN) { + return "not-run"; + } + if (v == V_RED) { + return "RED"; + } + return "UNREAD"; +} + +pub fn span_is(text: &'static str, from: usize, to: usize, lit: &'static str) -> bool { + if ((to < from) || ((to - from) != lit.len())) { + return false; + } + let mut k: usize = 0; + while (k < lit.len()) { + if (text.as_bytes()[((from + k)) as usize] != lit.as_bytes()[(k) as usize]) { + return false; + } + k = (k + 1); + } + return true; +} + +pub fn value_from(text: &'static str, v: usize) -> usize { + if (v >= text.len()) { + return text.len(); + } + if (text.as_bytes()[(v) as usize] == 34) { + return (v + 1); + } + return v; +} + +pub fn value_to(text: &'static str, v: usize) -> usize { + if (v >= text.len()) { + return text.len(); + } + let mut e: usize = line_end(text, v); + if ((e > v) && (text.as_bytes()[((e - 1)) as usize] == 44)) { + e = (e - 1); + } + if (((text.as_bytes()[(v) as usize] == 34) && (e > (v + 1))) && (text.as_bytes()[((e - 1)) as usize] == 34)) { + e = (e - 1); + } + if (e < value_from(text, v)) { + return value_from(text, v); + } + return e; +} + +pub const HAVE: &'static str = "$HAVE"; + +pub const V_CHECK_CONFLICT: u8 = 0; + +pub const V_CHECK_DRIFT: u8 = 2; + +pub const V_CHECK_MATCH: u8 = 1; + +pub const V_WRITE_CONFLICT: u8 = 3; + +pub const V_WRITE_MATCH: u8 = 4; + +pub const WANT: &'static str = "$WANT"; + +pub fn line(v: u8, k: u8) -> &'static str { + if (v == V_CHECK_CONFLICT) { + if (k == 0) { + return "bootstrap/stage0/FROZEN_HASH still holds conflict markers."; + } + if (k == 1) { + return "Neither side is right after a merge: each hash describes its own"; + } + return "side's bootstrap/src/compiler.rs, not the merged bytes. Run `tri reseal write`."; + } + if (v == V_CHECK_MATCH) { + return "seal matches: $WANT"; + } + if (v == V_CHECK_DRIFT) { + if (k == 0) { + return "seal: $HAVE"; + } + if (k == 1) { + return "actual: $WANT"; + } + if (k == 2) { + return ""; + } + if (k == 3) { + return "`cargo build` in bootstrap/ reads the first token and will refuse"; + } + if (k == 4) { + return "until these agree."; + } + return "If the change to bootstrap/src/compiler.rs is deliberate: tri reseal write"; + } + if (v == V_WRITE_MATCH) { + return "seal already matches ($WANT); nothing written."; + } + if (k == 0) { + if (v == V_WRITE_CONFLICT) { + return "bootstrap/stage0/FROZEN_HASH held conflict markers; both candidates discarded."; + } + return "was: $HAVE"; + } + if (k == 1) { + return "now: $WANT"; + } + return "sealed bootstrap/src/compiler.rs"; +} + +pub fn starts_at(raw: &'static str, at: usize, lit: &'static str) -> bool { + if ((at + lit.len()) > raw.len()) { + return false; + } + let mut k: usize = 0; + while (k < lit.len()) { + if (raw.as_bytes()[((at + k)) as usize] != lit.as_bytes()[(k) as usize]) { + return false; + } + k = (k + 1); + } + return true; +} + +pub fn verdict(conflicted: bool, matches: bool, write: bool) -> u8 { + let mut base: u8 = V_CHECK_CONFLICT; + if write { + base = V_WRITE_CONFLICT; + } + if conflicted { + return base; + } + if matches { + return (base + 1); + } + return (base + 2); +} + diff --git a/specs/tools/t27o/ledger.t27 b/specs/tools/t27o/ledger.t27 index 4c79ad7d2e..1b93b39c29 100644 --- a/specs/tools/t27o/ledger.t27 +++ b/specs/tools/t27o/ledger.t27 @@ -180,16 +180,141 @@ pub fn ledger_status(total: u32, out_of_scope: u32, blocked: u32, contradicted: return L_OPEN; } -; Whether a recorded verdict still holds. same_source is the caller's comparison of the full source -; digests; a truncated digest is not a digest and must be passed as recorded == false. -pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_table_version: u32) -> u32 { +; Whether a recorded verdict still holds: THE rule of staleness, from four booleans. same_source is +; the caller's comparison of the full source digests; a truncated digest is not a digest and must be +; passed as recorded == false. same_converter and same_std say the converter and the std table the +; row was made with are the ones in force: a version number compared with CONVERTER_VERSION below, +; or, for `tri t27o-sweep` (#10543), the spec_hash of rust_convert.t27's and rust_std.t27's seals +; compared with the recorded ones, so a changed converter makes every row stale with no constant to bump. +pub fn key_check(recorded: bool, same_source: bool, same_converter: bool, same_std: bool) -> u32 { if (recorded == false) { return K_NONE; } if (same_source == false) { return K_STALE_SOURCE; } - if (converter_version != CONVERTER_VERSION) { return K_STALE_CONVERTER; } - if (std_table_version != STD_TABLE_VERSION) { return K_STALE_STD; } + if (same_converter == false) { return K_STALE_CONVERTER; } + if (same_std == false) { return K_STALE_STD; } return K_FRESH; } +; The same rule keyed by the two version constants. +pub fn key_state(recorded: bool, same_source: bool, converter_version: u32, std_table_version: u32) -> u32 { + return key_check(recorded, same_source, converter_version == CONVERTER_VERSION, std_table_version == STD_TABLE_VERSION); +} + +; --- `tri t27o-sweep` (#10543): t27o over a tree, with a cache of every file's result ------------ +; One row per file. A row is reused only when its key holds; the glue computes the booleans by string +; equality of the SHA-256 of the file and of the two seals' spec_hash, and this spec decides. Every +; other decision of the command is below too: which files, which are too big, the coverage share, +; 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::starts_at; + +pub const SWEEP_MAX_BYTES : usize = 300000; +pub const SWEEP_TOP : usize = 15; +pub const SWEEP_CACHE : str = "target/t27o-sweep.tsv"; +; 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. +pub const KIND_CASCADE : str = "uses refused"; + + +; A row whose key holds is reused; any other state, including none, converts again. +pub fn reuse_row(state: u32) -> bool { + return state == K_FRESH; +} + +; Is this tracked path one the sweep converts: a .rs file with no gen/ or target/ directory above it +; (generated Rust is gen-rust of a .t27 spec, build output is neither). +pub fn sweep_path(path: string) -> bool { + if (path.len < 3) { return false; } + if (span_is(path, path.len - 3, path.len, ".rs") == false) { return false; } + var from : usize = 0; + var k : usize = 0; + while (k < path.len) { + if (path[k] == 47) { + if (span_is(path, from, k, "gen")) { return false; } + if (span_is(path, from, k, "target")) { return false; } + from = k + 1; + } + k = k + 1; + } + return true; +} + +; A file over the cap is reported as skipped, not converted: the converter has no time cap. +pub fn over_cap(size: usize, cap: usize) -> bool { + return size > cap; +} + +; 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 { + if (seen == 0) { return COVERAGE_UNKNOWN; } + if (converted > seen) { return COVERAGE_UNKNOWN; } + return converted * 1000 / seen; +} + +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 "); +} + +; Where the reason starts: after the first "): " that follows "(line "; line.len when there is none. +pub fn reason_from(line: string) -> usize { + 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; } + k = k + 1; + } + return line.len; +} + +; 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). 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_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) { k = k + 1; } + if (k > reason_from(line) and line[k - 1] == 32) { return k - 1; } + return k; +} + +; A cascade refusal is an item refused because it uses a refused one: fixing it is fixing the +; refusal it names, so it is counted apart and never ranked among the root kinds. +pub fn is_cascade(kind: string) -> bool { + return starts_at(kind, 0, KIND_CASCADE); +} + +; The order of two texts, byte by byte, a shorter prefix first: -1, 0 or 1. +pub fn text_order(a: string, b: string) -> i32 { + var k : usize = 0; + while (k < a.len and k < b.len) { + if (a[k] < b[k]) { return 0 - 1; } + if (a[k] > b[k]) { return 1; } + k = k + 1; + } + if (a.len < b.len) { return 0 - 1; } + if (a.len > b.len) { return 1; } + return 0; +} + +; The ranking of kinds: more refusals first, equal counts by the kind's text. -1 puts a before b. +pub fn rank_cmp(count_a: usize, kind_a: string, count_b: usize, kind_b: string) -> i32 { + if (count_a > count_b) { return 0 - 1; } + if (count_a < count_b) { return 1; } + return text_order(kind_a, kind_b); +} + ; --- Tests ---------------------------------------------------------------------------------- test an_oracle_run_passes_only_when_every_sample_agreed { @@ -284,3 +409,109 @@ test negative_control_a_code_outside_t27b_table_is_unknown_never_a_pass { assert(needs_reference(t27b_outcome(0 - 1))); assert(ORC_NAMES[t27b_outcome(T27B_DISAGREE)] == "unknown"); } + +test the_version_rule_and_the_hash_rule_are_one_rule { + assert(key_check(true, true, true, true) == K_FRESH); + assert(key_check(false, true, true, true) == K_NONE); + assert(key_check(true, false, true, true) == K_STALE_SOURCE); + assert(key_check(true, true, false, true) == K_STALE_CONVERTER); + assert(key_check(true, true, true, false) == K_STALE_STD); + assert(key_check(true, false, false, false) == K_STALE_SOURCE); + assert(key_check(true, true, false, false) == K_STALE_CONVERTER); + assert(key_state(true, true, CONVERTER_VERSION, STD_TABLE_VERSION) == key_check(true, true, true, true)); + assert(key_state(true, true, CONVERTER_VERSION + 1, STD_TABLE_VERSION) == key_check(true, true, false, true)); +} + +test negative_control_a_stale_or_missing_key_is_never_reused { + assert(reuse_row(key_check(true, true, true, true))); + assert(reuse_row(key_check(false, true, true, true)) == false); + assert(reuse_row(key_check(true, false, true, true)) == false); + assert(reuse_row(key_check(true, true, false, true)) == false); + assert(reuse_row(key_check(true, true, true, false)) == false); + assert(reuse_row(key_check(true, false, false, false)) == false); + assert(reuse_row(K_NONE) == false); + assert(reuse_row(K_STALE_SOURCE) == false); + assert(reuse_row(K_STALE_CONVERTER) == false); + assert(reuse_row(K_STALE_STD) == false); +} + +test the_sweep_takes_rust_files_outside_gen_and_target { + assert(sweep_path("cli/tri/src/main.rs")); + assert(sweep_path("bootstrap/src/compiler.rs")); + assert(sweep_path("build.rs")); + assert(sweep_path("gen.rs")); + assert(sweep_path("src/generator/x.rs")); + assert(sweep_path("src/targets/x.rs")); + assert(sweep_path("src/gen/x.rs") == false); + assert(sweep_path("gen/rust/tools/t27o/ledger.rs") == false); + assert(sweep_path("target/release/build/x/out.rs") == false); + assert(sweep_path("a/b/target/c.rs") == false); +} + +test negative_control_a_path_that_is_not_rust_or_is_empty_is_not_swept { + assert(sweep_path("") == false); + assert(sweep_path("rs") == false); + assert(sweep_path("main.rs.bak") == false); + assert(sweep_path("specs/tools/t27o/ledger.t27") == false); + assert(sweep_path("README.md") == false); +} + +test a_file_over_the_cap_is_skipped_and_one_at_the_cap_is_converted { + assert(over_cap(300001, SWEEP_MAX_BYTES)); + assert(over_cap(300000, SWEEP_MAX_BYTES) == false); + assert(over_cap(0, SWEEP_MAX_BYTES) == false); +} + +test coverage_is_per_mille_and_a_tree_with_no_fn_is_unknown_not_complete { + assert(coverage_permille(7108, 627) == 88); + assert(coverage_permille(3, 3) == 1000); + assert(coverage_permille(5, 0) == 0); + assert(coverage_known(coverage_permille(5, 0))); + assert(coverage_known(coverage_permille(3, 3))); +} + +test negative_control_no_fn_seen_is_never_one_hundred_percent { + assert(coverage_permille(0, 0) == COVERAGE_UNKNOWN); + assert(coverage_known(coverage_permille(0, 0)) == false); + assert(coverage_permille(0, 0) != 1000); + assert(coverage_permille(2, 3) == COVERAGE_UNKNOWN); +} + +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")); + 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")); + 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")); + 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")); +} + +test only_a_reason_that_names_a_refused_item_is_a_cascade { + assert(is_cascade("uses refused")); + assert(is_cascade("type") == false); + assert(is_cascade("uses external") == false); + assert(is_cascade("") == false); + assert(is_cascade("uses") == false); +} + +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); + 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)); +} + +test the_ranking_puts_more_refusals_first_and_equal_counts_in_text_order { + assert(rank_cmp(9, "type", 3, "method") == 0 - 1); + assert(rank_cmp(3, "type", 9, "method") == 1); + assert(rank_cmp(5, "method", 5, "type") == 0 - 1); + assert(rank_cmp(5, "type", 5, "method") == 1); + assert(rank_cmp(5, "type", 5, "type") == 0); + assert(text_order("type", "types") == 0 - 1); + assert(text_order("types", "type") == 1); +}