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
11 changes: 11 additions & 0 deletions cli/t27b/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ Each cell is the outcome of that test (`inc_max`, `shr_big`, `rem_neg`):
```
t27b test <file.t27> [--overflow trap|wrap] [--time] [--quiet] [--check] [--blockers]
t27b build <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--time]
t27b watch <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--then "<cmd>"]
t27b asm <file.t27> [--overflow trap|wrap]
t27b verilog <file.t27>
t27b ts <file.t27>
Expand All @@ -318,6 +319,16 @@ t27b corpus <dir> [--timeout-ms N] [--jobs N] [--overflow trap|wrap] [--list]
[--reference-timeout-ms N]]]
```

`t27b watch` builds the object once, then again after every settled save to the spec or to any
spec in its `use` closure (#10859). An idle look only stats the files the last build read; a
save is built after `HOT_DEBOUNCE_MS` of quiet or `HOT_MAX_WAIT_MS` after its first event, so a
save is rebuilt within `WATCH_POLL_MS` + `HOT_MAX_WAIT_MS` + the build. The object is written to
`<out>.watch-tmp` and renamed over `<out>` only when the build succeeded: a type error prints the
error and keeps the old object, and a hot-reloading app (`specs/t27os/hot_reload.t27`) never maps a
half-written one. `--then "<cmd>"` runs through `sh -c` once per successful build, never after a
failed one; its exit code is printed and never stops the watch. The decisions are
`specs/tri/t27b/watch_build.t27`, reusing `specs/verified/watch.t27` and `specs/t27f/hot.t27`.

`t27b verilog` writes a Verilog module for the spec to stdout, and after it, under
`` `ifdef T27B_TESTBENCH ``, a test bench that runs the spec's own tests against
that module (`iverilog -DT27B_TESTBENCH`). A construct it does not lower is
Expand Down
86 changes: 40 additions & 46 deletions cli/t27b/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
//! `t27b`: t27's own native backend (MVP), command line.
//!
//! ```text
//! t27b test <file.t27> [--overflow trap|wrap] [--time] [--quiet] [--check] [--blockers]
//! t27b build <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--time]
//! t27b asm <file.t27> [--overflow trap|wrap]
//! t27b corpus <dir> [--timeout-ms N] [--jobs N] [--overflow trap|wrap] [--list]
//! [--json <path>] [--runner "<cmd> [args]"]
//! [--blockers [--reference <t27c> [--reference-cache <file>]
//! [--reference-timeout-ms N]]]
//! ```
//!
//! Exit codes: 0 success; 1 a test failed; 2 unsupported construct;
//! 3 front-end (parse or typecheck) error; 4 JIT and interpreter disagree
//! (`--check`); 5 code generation limit; 64 usage; 74 I/O error.
//! Usage and exit codes: specs/tri/t27b/usage.t27 (`t27b help`).

use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};
Expand All @@ -28,6 +15,7 @@ use t27b::timing::Phases;
#[path = "../../../gen/rust/tri/t27b/check_budget.rs"]
#[allow(dead_code, unused_parens)]
mod check_budget; // --check fuel policy, t27c gen-rust of specs/tri/t27b/check_budget.t27 (#6664)
#[path = "../../../gen/rust/tri/t27b/watch_build.rs"] #[allow(dead_code, unused_parens)] mod watch_build; // `t27b watch`: when to build, the temp path, when --then runs (#10859)
use t27b::blockers::{self, Reference, Verdicts};
use t27b::{a64, front, lower, macho};

Expand Down Expand Up @@ -63,36 +51,11 @@ macro_rules! errln {
}};
}

const USAGE: &str = "usage:
t27b test <file.t27> [--overflow trap|wrap] [--time] [--quiet] [--check] [--blockers]
t27b build <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--time]
t27b asm <file.t27> [--overflow trap|wrap]
t27b verilog <file.t27> (the module, and its test bench under `ifdef T27B_TESTBENCH)
t27b ts <file.t27> (consts, scalar fns and tests as TypeScript; specs/tri/t27b/ts_emit.t27)
t27b corpus <dir> [--timeout-ms N] [--jobs N] [--overflow trap|wrap] [--list]
[--json <path>] [--runner \"<cmd> [args]\"]
[--blockers [--reference <t27c> [--reference-cache <file>]
[--reference-timeout-ms N]]]

--elf build: an ELF64 object for aarch64 Linux (specs/tri/t27b/elf.t27), the default on
Linux; --macho: a Mach-O object for arm64 macOS, the default elsewhere
--json corpus: also write per-file results and totals to <path> as JSON
(with --reference, the reference verdicts too)
--runner corpus: start each per-file `t27b test` as `<cmd> [args] <t27b> test ...`,
e.g. --runner \"qemu-aarch64 -L /usr/aarch64-linux-gnu\" when the corpus
itself runs under qemu-user without binfmt_misc
--blockers test: report every unsupported construct, not just the first.
corpus: per file, every construct t27b lacks; then the greedy
order in which supporting them unlocks the most whole files.
--reference corpus: also run the reference path (`<t27c> test-report`,
t27c's Zig backend) on every file, and leave the files it fails
out of the denominator. Where both sides ran a file's tests,
compare them test by test: `reference_disagree` counts files
where a test's verdict differs or only one side has it (#6441).
`mismatch` (= `jit_interp_mismatch`) is t27b's JIT against
t27b's own interpreter, not against the reference.";
#[path = "../../../gen/rust/tri/t27b/usage.rs"] #[allow(dead_code)] mod usage; use usage::T27B_USAGE as USAGE; // the usage text, t27c gen-rust of specs/tri/t27b/usage.t27

#[derive(Clone)]
struct Opts {
then: Option<String>,
cmd: String,
input: Option<String>,
out: Option<String>,
Expand Down Expand Up @@ -125,6 +88,7 @@ fn parse_args() -> Result<Opts, String> {
let cmd = args.next().ok_or("missing command")?;
let mut o = Opts {
cmd,
then: None,
input: None,
out: None,
mode: OverflowMode::Trap,
Expand Down Expand Up @@ -152,6 +116,7 @@ fn parse_args() -> Result<Opts, String> {
_ => return Err("--overflow takes trap or wrap".into()),
}
}
"--then" => o.then = Some(args.next().ok_or("--then needs a command")?),
"--time" => o.time = true,
"--quiet" | "-q" => o.quiet = true,
"--check" => o.check = true,
Expand Down Expand Up @@ -226,6 +191,7 @@ fn main() -> ExitCode {
Err(code) => ExitCode::from(code),
}
}
"watch" => cmd_watch(&o),
"corpus" => {
let Some(dir) = o.input.clone() else {
return usage("missing directory");
Expand Down Expand Up @@ -501,6 +467,37 @@ fn cmd_build(prog: &Program, ph: &mut Phases, o: &Opts) -> ExitCode {
ExitCode::SUCCESS
}

/// `t27b watch` (#10859): I/O only; when to build, the temp path and when --then runs are watch_build.t27.
fn cmd_watch(o: &Opts) -> ExitCode {
let (Some(input), Some(out)) = (o.input.clone(), o.out.clone()) else { return usage("watch needs <file.t27> -o <out.o>") };
let (tmp, ms) = (format!("{}{}", out, watch_build::WATCH_TMP_SUFFIX), |t: Instant| t.elapsed().as_millis().min(u32::MAX as u128) as u32);
let stamp = |p: &PathBuf| std::fs::metadata(p).and_then(|m| Ok((m.modified()?, m.len()))).ok();
let (mut files, mut seen, mut first, mut last, mut opened) = (Vec::<PathBuf>::new(), Vec::new(), true, Instant::now(), None::<Instant>);
loop {
let now: Vec<_> = files.iter().map(stamp).collect();
if now != seen { (seen, last) = (now, Instant::now()); opened.get_or_insert(last); }
let (quiet, waited) = (ms(last), opened.map_or(0, ms));
if watch_build::watch_build_now(first, opened.is_some(), quiet, waited) {
(first, opened, files) = (false, None, use_closure(Path::new(&input)));
seen = files.iter().map(stamp).collect(); // stats before the build: a save during it is seen next look
let _ = std::fs::remove_file(&tmp);
let ok = compile_front(&input, o).map(|(prog, mut ph)| cmd_build(&prog, &mut ph, &Opts { out: Some(tmp.clone()), ..o.clone() })).is_ok() && Path::new(&tmp).exists();
if watch_build::watch_publish(ok) && std::fs::rename(&tmp, &out).is_ok() { errln!("t27b watch: built {} ({} files)", out, files.len()); }
if watch_build::watch_run_then(ok, o.then.is_some()) { errln!("t27b watch: then exited {:?}", Command::new("sh").arg("-c").arg(o.then.as_deref().unwrap_or("")).status().ok().and_then(|s| s.code())); }
}
std::thread::sleep(Duration::from_millis(watch_build::watch_sleep_ms(opened.is_some(), quiet, waited) as u64));
}
}
fn use_closure(input: &Path) -> Vec<PathBuf> { // the spec and every spec its `use` lines reach (t27c's resolver)
let root = input.ancestors().find(|a| a.ends_with("specs")).map(Path::to_path_buf).unwrap_or_default();
let (mut files, mut i) = (vec![input.to_path_buf()], 0);
while let Some(f) = files.get(i).cloned() {
for t in t27b::use_resolve::use_edges(&std::fs::read_to_string(f).unwrap_or_default(), &root) { if !files.contains(&t) { files.push(t) } }
i += 1;
}
files
}

fn cmd_asm(prog: &Program) -> ExitCode {
let code = match codegen::compile(prog, TrapStyle::Brk, true) {
Ok(c) => c,
Expand Down Expand Up @@ -744,20 +741,17 @@ fn cmd_corpus(dir: &Path, o: &Opts) -> ExitCode {
cmd: String::new(),
input: None,
out: None,
mode: o.mode,
time: false,
quiet: true,
check: true,
list: false,
timeout_ms: o.timeout_ms,
jobs: 1,
json: None,
runner: o.runner.clone(),
blockers: o.blockers,
elf: false,
reference: None,
reference_cache: None,
reference_timeout_ms: 0,
..o.clone() // mode, timeout_ms, runner, blockers
};
handles.push(std::thread::spawn(move || loop {
let i = {
Expand Down
2 changes: 2 additions & 0 deletions docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1958,10 +1958,12 @@
{"path": "specs/tri/t27b/tuple_local_plan.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/type_text.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/undefined_arg_plan.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/usage.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/verdict_key.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/verilog_emit.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/verilog_plan.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/void_bind_plan.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/watch_build.t27", "t27b": "pass"},
{"path": "specs/tri/t27b/wide_plan.t27", "t27b": "pass"},
{"path": "specs/tri/test/report.t27", "t27b": "pass"},
{"path": "specs/tri/trees/avl_tree.t27", "t27b": "pass"},
Expand Down
16 changes: 16 additions & 0 deletions gen/rust/tri/t27b/usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Generated from .t27 spec
// DO NOT EDIT — generated by t27c
// NOT LOWERED BY THIS BACKEND: 2 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.

pub const T27B_USAGE: &'static str = "usage:\n t27b test <file.t27> [--overflow trap|wrap] [--time] [--quiet] [--check] [--blockers]\n t27b build <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--time]\n t27b watch <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--then '<cmd>']\n t27b asm <file.t27> [--overflow trap|wrap]\n t27b verilog <file.t27> (the module, and its test bench under `ifdef T27B_TESTBENCH)\n t27b ts <file.t27> (consts, scalar fns and tests as TypeScript; specs/tri/t27b/ts_emit.t27)\n t27b corpus <dir> [--timeout-ms N] [--jobs N] [--overflow trap|wrap] [--list]\n [--json <path>] [--runner '<cmd> [args]']\n [--blockers [--reference <t27c> [--reference-cache <file>]\n [--reference-timeout-ms N]]]\n\n --elf build: an ELF64 object for aarch64 Linux (specs/tri/t27b/elf.t27), the default on\n Linux; --macho: a Mach-O object for arm64 macOS, the default elsewhere\n --then watch: after every successful rebuild, run <cmd> with sh -c (never after a failed one)\n --json corpus: also write per-file results and totals to <path> as JSON\n (with --reference, the reference verdicts too)\n --runner corpus: start each per-file `t27b test` as `<cmd> [args] <t27b> test ...`,\n e.g. --runner 'qemu-aarch64 -L /usr/aarch64-linux-gnu' when the corpus\n itself runs under qemu-user without binfmt_misc\n --blockers test: report every unsupported construct, not just the first.\n corpus: per file, every construct t27b lacks; then the greedy\n order in which supporting them unlocks the most whole files.\n --reference corpus: also run the reference path (`<t27c> test-report`,\n t27c's Zig backend) on every file, and leave the files it fails\n out of the denominator. Where both sides ran a file's tests,\n compare them test by test: `reference_disagree` counts files\n where a test's verdict differs or only one side has it (#6441).\n `mismatch` (= `jit_interp_mismatch`) is t27b's JIT against\n t27b's own interpreter, not against the reference.";

pub fn usage_byte(i: usize) -> u8 {
return T27B_USAGE.as_bytes()[(i) as usize];
}

pub fn usage_len() -> usize {
return T27B_USAGE.len();
}

63 changes: 63 additions & 0 deletions gen/rust/tri/t27b/watch_build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Generated from .t27 spec
// DO NOT EDIT — generated by t27c
// NOT LOWERED BY THIS BACKEND: 6 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.

pub const WATCH_TMP_SUFFIX: &'static str = ".watch-tmp";

pub fn watch_build_now(first_look: bool, pending: bool, quiet_ms: u32, waited_ms: u32) -> bool {
return watch_walk(first_look, (pending && hot_ready(quiet_ms, waited_ms)));
}

pub fn watch_sleep_ms(pending: bool, quiet_ms: u32, waited_ms: u32) -> u32 {
if pending {
let left: u32 = hot_wait_ms(quiet_ms, waited_ms);
if (left > 0) {
return left;
}
return 1;
}
return WATCH_POLL_MS;
}

pub fn watch_publish(build_ok: bool) -> bool {
return build_ok;
}

pub fn watch_run_then(build_ok: bool, has_then: bool) -> bool {
return (build_ok && has_then);
}

pub const HOT_DEBOUNCE_MS: u32 = 20;

pub const HOT_MAX_WAIT_MS: u32 = 60;

pub fn hot_ready(quiet_ms: u32, waited_ms: u32) -> bool {
return (hot_wait_ms(quiet_ms, waited_ms) == 0);
}

pub fn hot_wait_ms(quiet_ms: u32, waited_ms: u32) -> u32 {
let mut quiet_left: u32 = 0;
if (quiet_ms < HOT_DEBOUNCE_MS) {
quiet_left = (HOT_DEBOUNCE_MS - quiet_ms);
}
let mut wait_left: u32 = 0;
if (waited_ms < HOT_MAX_WAIT_MS) {
wait_left = (HOT_MAX_WAIT_MS - waited_ms);
}
if (quiet_left < wait_left) {
return quiet_left;
}
return wait_left;
}

pub const WATCH_POLL_MS: u32 = 500;

pub fn watch_walk(first_look: bool, a_stat_moved: bool) -> bool {
if first_look {
return true;
}
return a_stat_moved;
}

35 changes: 35 additions & 0 deletions specs/tri/t27b/usage.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
; specs/tri/t27b/usage.t27 -- the text `t27b help` prints, and that every usage error prints after its
; message. Issue gHashTag/t27#10859 (moved here from a 29-line Rust literal in cli/t27b/src/main.rs, so
; adding `t27b watch` shrinks the hand-written Rust instead of growing it; owner rule 2026-10-09).
; --
; Exit codes, for the record (the values are consts in main.rs): 0 success; 1 a test failed; 2 unsupported
; construct; 3 front-end (parse or typecheck) error; 4 JIT and interpreter disagree (`--check`); 5 code
; generation limit; 64 usage; 74 I/O error.
; phi^2 + 1/phi^2 = 3 | TRINITY

module T27bUsage;

/// The whole usage text, one line per `\n`.
pub const T27B_USAGE : str = "usage:\n t27b test <file.t27> [--overflow trap|wrap] [--time] [--quiet] [--check] [--blockers]\n t27b build <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--time]\n t27b watch <file.t27> -o <out.o> [--elf | --macho] [--overflow trap|wrap] [--then '<cmd>']\n t27b asm <file.t27> [--overflow trap|wrap]\n t27b verilog <file.t27> (the module, and its test bench under `ifdef T27B_TESTBENCH)\n t27b ts <file.t27> (consts, scalar fns and tests as TypeScript; specs/tri/t27b/ts_emit.t27)\n t27b corpus <dir> [--timeout-ms N] [--jobs N] [--overflow trap|wrap] [--list]\n [--json <path>] [--runner '<cmd> [args]']\n [--blockers [--reference <t27c> [--reference-cache <file>]\n [--reference-timeout-ms N]]]\n\n --elf build: an ELF64 object for aarch64 Linux (specs/tri/t27b/elf.t27), the default on\n Linux; --macho: a Mach-O object for arm64 macOS, the default elsewhere\n --then watch: after every successful rebuild, run <cmd> with sh -c (never after a failed one)\n --json corpus: also write per-file results and totals to <path> as JSON\n (with --reference, the reference verdicts too)\n --runner corpus: start each per-file `t27b test` as `<cmd> [args] <t27b> test ...`,\n e.g. --runner 'qemu-aarch64 -L /usr/aarch64-linux-gnu' when the corpus\n itself runs under qemu-user without binfmt_misc\n --blockers test: report every unsupported construct, not just the first.\n corpus: per file, every construct t27b lacks; then the greedy\n order in which supporting them unlocks the most whole files.\n --reference corpus: also run the reference path (`<t27c> test-report`,\n t27c's Zig backend) on every file, and leave the files it fails\n out of the denominator. Where both sides ran a file's tests,\n compare them test by test: `reference_disagree` counts files\n where a test's verdict differs or only one side has it (#6441).\n `mismatch` (= `jit_interp_mismatch`) is t27b's JIT against\n t27b's own interpreter, not against the reference.";

/// Byte i of the usage text (the tests read it at run time, not folded at compile time).
pub fn usage_byte(i: usize) -> u8 {
return T27B_USAGE[i];
}

/// Length of the usage text in bytes.
pub fn usage_len() -> usize {
return T27B_USAGE.len;
}

test the_usage_starts_with_its_heading {
// 'u' then ':' at index 5
assert usage_byte(0) == 117;
assert usage_byte(5) == 58;
}

test the_usage_is_the_whole_text_not_a_stub {
assert usage_len() > 2000;
assert usage_len() < 4000;
}
Loading
Loading