runww: //ww:error arm asserts both stages reject (dual-stage)

A rejected program emits no .s, so the test-lang-byteid (T2) gate cannot
cover wwstage-reject -- yet the retired C twins asserted that BOTH stages
reject with the same diagnostic. runww's //ww:error arm was cstage-only,
so migrating reject rows onto it would silently drop the wwstage-reject
coverage the C twins carried.

Run w6c_ww (wwstage) alongside w6c (cstage) on each //ww:error case and
require both to fail with the shared diagnostic body present. The body is
identical across stages; only cstage's leading prefix differs, so the
substring matches the body alone (no file:line). An ERROR row now reports
PASS dual / FAIL cstage / FAIL wwstage. w6c_ww resolves off the same $BIN
as the C twins -- no new harness threading.

Two pilot reject cases (runww_dup_main_reject, runww_dup_type_reject)
exercise the dual-stage path; the wwstage leg is proven non-vacuous (a
cstage-rejects/wwstage-accepts case reports FAIL wwstage).

Prerequisite for migrating fold-3 reject rows onto runww.
This commit is contained in:
2026-06-22 15:34:36 +09:00
parent 214ced303f
commit d48b53d04d
3 changed files with 71 additions and 11 deletions

View File

@@ -5,14 +5,22 @@
// directive holds. This replaces the per-test C harnesses that embedded ww // directive holds. This replaces the per-test C harnesses that embedded ww
// source as C string literals and duplicated fork/exec/grep boilerplate. // source as C string literals and duplicated fork/exec/grep boilerplate.
// //
// This is T1: the routine behavioral-correctness gate, cstage-only. The // This is T1: the routine behavioral-correctness gate, cstage-only for the
// wwstage-behavior run is deliberately NOT here — per the rob/USER tier+stage // RUN/RUNEXIT/COMPILE arms. The wwstage-behavior run is deliberately NOT here
// split, a wwstage miscompile is already caught by T2 (cstage.s vs wwstage.s // — per the rob/USER tier+stage split, a wwstage miscompile is already caught
// byte-id over the same case.ww corpus, strictly more sensitive than re-running // by T2 (cstage.s vs wwstage.s byte-id over the same case.ww corpus, strictly
// behavior) or by T1's own cstage run (the both-wrong-identical blind spot is // more sensitive than re-running behavior) or by T1's own cstage run (the
// cstage being wrong, which only a behavioral check on cstage catches). So // both-wrong-identical blind spot is cstage being wrong, which only a
// rule-10 stage-symmetry is enforced by T2 (a separate pre-push tool), not by // behavioral check on cstage catches). So rule-10 stage-symmetry is enforced
// runww; routine runs stay cstage-only. // by T2 (a separate pre-push tool), not by runww; routine runs stay cstage-only.
//
// The ERROR arm is the one exception: it runs BOTH stages. A rejected program
// emits no `.s`, so T2's `.s`-cmp gate structurally CANNOT cover wwstage-reject
// (drew-fold3-spec §1a). The retired C reject twins (948/949/944) asserted both
// w6c AND w6c_ww reject with the same diagnostic body, so runww's ERROR arm runs
// w6c_ww directly on the same case.ww and asserts it ALSO fails with the
// substring present (#20 non-vacuity) — the coverage-equivalence safety net for
// reject-row migration.
// //
// Directives (first `//ww:` line of the case): // Directives (first `//ww:` line of the case):
// //ww:run compile + execute, expect exit 0 // //ww:run compile + execute, expect exit 0
@@ -244,6 +252,29 @@ fn runcase(drv: str, casepath: str, d: *dirv, errpath: str, tmpout: str) bool =
return false; return false;
}; };
// wwerror — the ERROR arm's wwstage leg: invoke w6c_ww directly on the same
// case.ww (mirroring the retired C twins' `$BIN/w6c_ww -o <out> <src>` direct
// invocation, not the `ww` driver — w6c_ww is the bare frontend compiler) and
// assert it ALSO hard-rejects with the shared diagnostic body present. #20
// non-vacuity: passes ONLY if w6c_ww failed AND the substring is on stderr.
fn wwerror(wdrv: str, casepath: str, d: *dirv, errpath: str, tmpout: str) bool = {
let argv: []*u8 = alloc([], 5u64)!;
argv.len = 5;
argv[0] = cstr(wdrv);
argv[1] = cstr("-o");
argv[2] = cstr(tmpout);
argv[3] = cstr(casepath);
argv[4] = nil: *u8;
let rc: i32 = runcap(wdrv, argv, errpath);
if (rc == 0) { return false; }; // w6c_ww accepted → wwstage-reject dropped
let eb: []u8 = alloc([], 65536u64)!;
eb.len = 65536;
let en: i64 = readfile(errpath, eb);
if (en < 0i64) { return false; };
return contains(eb.ptr, en: i32, mkstr(d.subp, d.subn));
};
export fn main() int = { export fn main() int = {
let a: []str = os.args(); let a: []str = os.args();
if (a.len < 2) { if (a.len < 2) {
@@ -257,6 +288,9 @@ export fn main() int = {
case void => { }; case void => { };
}; };
let cdrv: str = joinp(bin, "/ww"); let cdrv: str = joinp(bin, "/ww");
// ERROR arm goes dual-stage: w6c_ww (the bare wwstage frontend) is
// resolved off the SAME $BIN the C reject twins used ($BIN/w6c_ww).
let wdrv: str = joinp(bin, "/w6c_ww");
// Fixed scratch paths: the pilot drives cases sequentially in one // Fixed scratch paths: the pilot drives cases sequentially in one
// process, so reuse is safe; per-pid uniqueness for parallel invocation // process, so reuse is safe; per-pid uniqueness for parallel invocation
@@ -289,11 +323,20 @@ export fn main() int = {
}; };
total += 1; total += 1;
if (runcase(cdrv, casepath, &d, errpath, tmpout)) { let pass: bool = runcase(cdrv, casepath, &d, errpath, tmpout);
pr("PASS cstage "); pr(casepath); pr("\n"); if (!pass) {
} else {
pr("FAIL cstage "); pr(casepath); pr("\n"); pr("FAIL cstage "); pr(casepath); pr("\n");
fail += 1; fail += 1;
} else if (d.kind == dkind.ERROR) {
// cstage rejected; now demand wwstage rejects identically.
if (wwerror(wdrv, casepath, &d, errpath, tmpout)) {
pr("PASS dual "); pr(casepath); pr("\n");
} else {
pr("FAIL wwstage "); pr(casepath); pr("\n");
fail += 1;
};
} else {
pr("PASS cstage "); pr(casepath); pr("\n");
}; };
ci += 1; ci += 1;

View File

@@ -0,0 +1,8 @@
//ww:error "duplicate fn main"
// Pilot for #7a: two `fn main` definitions must be LOUD-rejected by BOTH
// stages. The substring is the shared diagnostic body (no file:line prefix,
// which differs cstage vs wwstage) — the #20 non-vacuity anchor and the
// dual-stage (w6c AND w6c_ww) reject-coverage proof.
package main;
fn main() i32 = { return 0; };
fn main() i32 = { return 1; };

View File

@@ -0,0 +1,9 @@
//ww:error "duplicate type t"
// Pilot for #7a: a redefined type alias must be LOUD-rejected by BOTH stages.
// The substring is the shared diagnostic body (no file:line prefix, which
// differs cstage vs wwstage) — the #20 non-vacuity anchor and the dual-stage
// (w6c AND w6c_ww) reject-coverage proof.
package main;
type t = i32;
type t = i64;
fn main() i32 = { return 0; };