diff --git a/Makefile b/Makefile index b7d244cf..977e1718 100644 --- a/Makefile +++ b/Makefile @@ -343,24 +343,18 @@ $(BIN) $(LIB) $(OBJ)/wcc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/w6c $(OBJ)/w6a $(OBJ)/w6 # per-case Make rule. UNIT_SOURCES = test/wcc/000_smoke.c test/wcc/100_lex.c \ test/wcc/200_parse.c test/wcc/300_check.c test/wcc/400_w6c.c -# Byte/artifact residuals that the blanket test-data-byteid comparator -# cannot subsume: asm-pattern/symbol/frame observers, .wwi round-trips, -# inline sources with no corpus twin, and the wwstage-driver-leg gates. -# 61 pure data-fixture byte-id carriers were retired into test-data-byteid -# (fold-4 migrate-and-retire; audit 2026-08-07). -BYTEID_WRAPPER_SOURCES = test/wcc/753_convwrap_audit.c \ - test/wcc/754_slice_of_slice_index.c test/wcc/755_amp_dot_idx.c \ - test/wcc/758_cgalloc_str_field.c \ - test/wcc/930_free_noop_run.c \ - test/wcc/946_structparam_run.c \ - test/wcc/946_structret_run.c \ - test/wcc/989_m1mangle_sym.c - # Ww-native byte/artifact observers: single-file ww tests under -# test/byteid/ on the test/testenv helper package. Each drives the -# stage tools it observes itself, so the build needs only cstage plus -# the wwstage binaries it launches. -BYTEID_WW_TESTS = test/byteid/libbyteid_test.ww test/byteid/wwi_test.ww +# test/byteid/ on the test/testenv helper package (asm-pattern/symbol +# windows, .wwi round-trips, inline sources with no corpus twin, the +# wwstage-driver-leg gates). The native byteid carrier partition is +# EMPTY: 61 pure data-fixture carriers retired into test-data-byteid +# (fold-4, 2026-08-07) and the last 11 observers ported here +# (2026-08-08). Each test drives the stage tools it observes itself, +# so the build needs only cstage plus the wwstage binaries it +# launches. +BYTEID_WW_TESTS = test/byteid/libbyteid_test.ww test/byteid/wwi_test.ww \ + test/byteid/asmwindow_test.ww test/byteid/freenoop_test.ww \ + test/byteid/structabi_test.ww test/byteid/mangle_test.ww BYTEID_WW_TARGETS = $(BYTEID_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ @@ -369,11 +363,10 @@ BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ PLATFORM_WRAPPER_SOURCES = test/wcc/996_dyn_ww.c ALL_WRAPPER_SOURCES = $(wildcard test/wcc/*.c) COMPILER_WRAPPER_SOURCES = $(filter-out $(UNIT_SOURCES) \ - $(BYTEID_WRAPPER_SOURCES) $(BOOTSTRAP_WRAPPER_SOURCES) \ + $(BOOTSTRAP_WRAPPER_SOURCES) \ $(PLATFORM_WRAPPER_SOURCES),$(ALL_WRAPPER_SOURCES)) UNIT_BINS = $(patsubst test/wcc/%.c,$(BIN)/test_%,$(UNIT_SOURCES)) -BYTEID_WRAPPER_BINS = $(patsubst test/wcc/%.c,$(BIN)/test_%,$(BYTEID_WRAPPER_SOURCES)) BOOTSTRAP_WRAPPER_BINS = $(patsubst test/wcc/%.c,$(BIN)/test_%,$(BOOTSTRAP_WRAPPER_SOURCES)) PLATFORM_WRAPPER_BINS = $(patsubst test/wcc/%.c,$(BIN)/test_%,$(PLATFORM_WRAPPER_SOURCES)) COMPILER_WRAPPER_BINS = $(patsubst test/wcc/%.c,$(BIN)/test_%,$(COMPILER_WRAPPER_SOURCES)) @@ -480,11 +473,6 @@ test-library: $(LIBRARY_TEST_TARGETS) $(BIN)/w6c $(BIN)/w6c_ww \ $(CURDIR)/$(BIN)/w6c_ww $(CURDIR)/$$f > /dev/null; \ done -test-native-byteid: $(WRAPPER_TOOLS) $(BYTEID_WRAPPER_BINS) - @set -e; for t in $(BYTEID_WRAPPER_BINS); do \ - echo "byte-id $$t"; BIN=$(CURDIR)/$(BIN) $(CURDIR)/$$t; \ - done - test-bootstrap-native: $(WRAPPER_TOOLS) $(BOOTSTRAP_WRAPPER_BINS) @set -e; for t in $(BOOTSTRAP_WRAPPER_BINS); do \ echo "bootstrap $$t"; BIN=$(CURDIR)/$(BIN) $(CURDIR)/$$t; \ @@ -631,8 +619,7 @@ test-data-byteid: $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww fi; \ exit $$rc -test-byteid: test-lang-byteid test-data-byteid test-native-byteid \ - $(BYTEID_WW_TARGETS) +test-byteid: test-lang-byteid test-data-byteid $(BYTEID_WW_TARGETS) # Incremental == clean. The -w reuse path must be byte-identical to a # from-scratch build, or it is not a build-graph optimization but a @@ -837,6 +824,6 @@ nocc: test test-unit test-compiler-smoke test-compiler test-package \ test-lang test-library test-commit test-lang-byteid \ $(LANG_TEST_TARGETS) $(LIBRARY_TEST_TARGETS) $(BYTEID_WW_TARGETS) \ - test-data-byteid test-native-byteid test-byteid \ + test-data-byteid test-byteid \ test-bootstrap-native test-bootstrap test-incremental \ test-platform test-all install clean bootstrap nocc bootstrap-snapshot diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index cded0423..bef7dd7b 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -36,27 +36,28 @@ The live declarative compiler corpus has 1,495 fixtures and 2,990 C/WW cells: 338 expected rejections (314 shared and 24 stage-specific), 17 compile-only successes, 198 exit-zero programs, and 942 explicit-exit programs. -131 native C carriers remain. They are partitioned exactly once as five -in-process units, 8 byte/artifact gates, six bootstrap gates, one platform -gate, and 111 residual compiler, package-layout, ABI, diagnostic-observer, -driver, linker, or FFI gates. The `.wwi` round-trip observers are -ww-native (`test/byteid/wwi_test.ww`, replacing `989_m2wwi_run` and -`989_wwileaf_run`). Rows migrated to fixtures or native `@test` +123 native C carriers remain. They are partitioned exactly once as five +in-process units, six bootstrap gates, one platform gate, and 111 +residual compiler, package-layout, ABI, diagnostic-observer, driver, +linker, or FFI gates. The byte/artifact partition is EMPTY: its last +eleven observers are ww-native under `test/byteid/` on the +`test/testenv` helper package (`libbyteid`, `wwi`, `asmwindow`, +`freenoop`, `structabi`, `mangle`), each retired with its port in the +same commit. Rows migrated to fixtures or native `@test` owners were removed from those carriers; there is no compatibility execution path for retired rows. 74 former byte/artifact carriers whose only assertion was a cstage-vs-wwstage `.s` byte-compare of sources now living in the corpus were retired into the blanket `test-data-byteid` comparator (the last 13 had their remaining inline sources added as fixtures first); the -survivors observe something the blanket cannot: assembly patterns, symbol -tables, `.wwi` round-trips, or the wwstage-driver leg. Wwstage-driver-leg -byte identity (`ww_ww` versus `ww` over the emitted `.s` set) has one -owner, now ww-native: `test/byteid/libbyteid_test.ww` on the -`test/testenv` helper package, whose unit sweep spans the 44-entry lib +observations the blanket cannot subsume — assembly-pattern windows, +symbol tables, `.wwi` round-trips, the wwstage-driver leg — are +ww-native tests under `test/byteid/`. Wwstage-driver-leg byte identity +(`ww_ww` versus `ww` over the emitted `.s` set) has one owner, +`test/byteid/libbyteid_test.ww`, whose unit sweep spans the 44-entry lib roster (test fixtures, sentinel-guarded import probes, a zero-dep root-only build) plus the lib/ completeness scan; the former 815/940/951 driver-parity carriers were folded into it, their content identity -already owned by their corpus twins, and the native `989_lib_byteid.c` -carrier retired with the port. +already owned by their corpus twins. ## Carrier endgame diff --git a/test/byteid/asmwindow_test.ww b/test/byteid/asmwindow_test.ww new file mode 100644 index 00000000..fbc7c35d --- /dev/null +++ b/test/byteid/asmwindow_test.ww @@ -0,0 +1,364 @@ +package asmwindow_test; + +// Direct-w6c asm-window gate. Port of the retired native carriers +// test/wcc/753_convwrap_audit.c, 754_slice_of_slice_index.c, +// 755_amp_dot_idx.c and 758_cgalloc_str_field.c; every assertion +// preserved. +// +// Every row compiles an inline single-file source DIRECTLY through +// w6c and w6c_ww (`w6c -o out.s src.ww`), never `ww build` — the 758 +// noscope rows exist precisely because `ww build` combines +// lib/rt/malloc.ww into the fixture and always supplies the @symbol +// decl, masking the hardcoded-rt_malloc regression they pin. +// +// Windowed search: a pattern must fall between "TEXT " and the +// first RET needle after it — positional index arithmetic, never a +// whole-file contains (753's want/bad pair discriminates the cgmlet +// wrong-module shape probe; main's own code must not satisfy a row). +// 754 row ptr_to_byte and 755 row byte_field_1 are NEGATIVE rows: no +// complete line `MOVQ $imm, CX` (a stride scale) may appear inside +// the window — a per-line scan, exactly the retired carriers' shape. +// +// Byte-identity legs (testenv.same over both .s bodies) ride every +// 754 and 755 row as in the C. 753 carries NONE by design (the +// `let a, b = call()` cs-vs-ws PUSHQ/POPQ DX cosmetic divergence is +// out of #17 scope — see the retired carrier); 758 carries NONE by +// design (task #24 part B history — its retained coverage is the +// CALL-line equality plus the exact-framing disp-line asserts). +// +// 753's single-file THREE-package source order beta/main/alpha is +// load-bearing (alpha registers last → head of c.fnrets) and is +// copied verbatim. +// +// Dropped C machinery, not assertions: the w6c_ww-absent skip gates +// (the Make target declares both tools), getpid()-keyed /tmp names +// and unlink-errno accounting (testenv.fresh/clean own the per-row +// scratch and assert cleanup), and the 16KB/1MB slurp caps +// (testenv.readfile is unbounded). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("asmwindow FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +fn emitstage(td: str, label: str, stage: str, drv: str, + outname: str) void = { + let av: []str = []; + append(av, drv); + append(av, "-o"); + append(av, outname); + append(av, "src.ww"); + let co: testenv.commandout; + testenv.runcommand(td, td, stage, av, tmo(), &co); + let ok: bool = co.termination == exec.termination.EXIT && co.code == 0; + if (!ok) { fail(label, strings.concat(stage, " compile failed")); }; +}; + +// [first `sym` .. first `retneedle` after it). A missing retneedle +// fails when mustret != 0 (753) and widens the window to EOF +// otherwise (754/755). +fn asmwindow(label: str, stage: str, s: str, sym: str, retneedle: str, + mustret: i32) str = { + let fp: i32 = testenv.pos(s, sym); + if (fp < 0) { + fail(label, strings.concat(stage, ": no ", sym, " in .s")); + }; + let tail: str = strings.sub(s, fp, s.len); + let rp: i32 = testenv.pos(tail, retneedle); + if (rp < 0) { + if (mustret != 0) { + fail(label, strings.concat(stage, ": no RET inside ", + sym)); + }; + return tail; + }; + return strings.sub(tail, 0, rp); +}; + +// Stride-1 rows must ELIDE the scale multiply: no complete line in the +// window may end `MOVQ $imm, CX`. An unterminated trailing chunk is +// skipped exactly as the carriers' eol-bounded scan skipped it. +fn nostridescale(label: str, stage: str, w: str) void = { + let i: i32 = 0; + for (i < w.len) { + let rest: str = strings.sub(w, i, w.len); + let e: i32 = testenv.pos(rest, "\n"); + if (e < 0) { break; }; + let line: str = strings.sub(w, i, i + e); + if (testenv.has(line, "\tMOVQ\t$") + && strings.hassuffix(line, ", CX")) { + fail(label, strings.concat(stage, + ": unexpected MOVQ $imm, CX scale inside the window")); + }; + i = i + e + 1; + }; +}; + +fn convcheck(label: str, stage: str, s: str) void = { + let w: str = asmwindow(label, stage, s, "TEXT main.run", "\tRET", 1); + if (!testenv.has(w, "MOVQ\tAX,")) { + fail(label, strings.concat(stage, + ": want_imm MOVQ AX, missing inside TEXT main.run")); + }; + if (testenv.has(w, "MOVQ\tCX,")) { + fail(label, strings.concat(stage, + ": bad_imm MOVQ CX, present inside TEXT main.run -- ", + "wrong-module shape probe fired")); + }; +}; + +// 753 row cgmlet_tuple_modshadow: beta.foo returns (i64, i64); a +// wrong-module probe grabbing alpha.foo's (i64, str) fires the +// (scalar, str) emit branch whose str.len store (MOVQ CX,) has no +// defined source on this ABI. No byte-id leg (see header). +@test fn convwrap() void = { + let label: str = "cgmlet_tuple_modshadow"; + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), strings.concat( + "package beta;\n", + "export fn foo() (i64, i64) = { return (10i64, 20i64); };\n", + "package main;\n", + "import alpha;\n", + "import beta;\n", + "export fn run() i32 = {\n", + "\tlet a, b = beta.foo();\n", + "\tif (a != 10i64) { return 1; };\n", + "\tif (b != 20i64) { return 2; };\n", + "\treturn 0;\n", + "};\n", + "export fn main() i32 = { return run(); };\n", + "package alpha;\n", + "export fn foo() (i64, str) = { return (10i64, \"hi\"); };\n")); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + convcheck(label, "cstage", + testenv.readfile(strings.concat(td, "/cs.s"))); + convcheck(label, "wwstage", + testenv.readfile(strings.concat(td, "/ws.s"))); + testenv.clean(td); +}; + +fn stridecheck(label: str, stage: str, s: str, stride: str) void = { + let w: str = asmwindow(label, stage, s, "TEXT main.probe", "\tRET\n", + 0); + if (stride.len != 0) { + if (!testenv.has(w, stride)) { + fail(label, strings.concat(stage, ": expected `", + stride, "` inside TEXT main.probe")); + }; + } else { + nostridescale(label, stage, w); + }; +}; + +// One 754/755 row: emit both stages, window-check both, then rule-10 +// byte-id. stride == "" selects the negative stride-scale scan. +fn striderow(label: str, src: str, stride: str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cs: str = testenv.readfile(strings.concat(td, "/cs.s")); + let ws: str = testenv.readfile(strings.concat(td, "/ws.s")); + stridecheck(label, "cstage", cs, stride); + stridecheck(label, "wwstage", ws, stride); + if (!testenv.same(cs, ws)) { + fail(label, "cstage vs wwstage asm differs"); + }; + testenv.clean(td); +}; + +// 754: `p[i]` for p: *[]T is []T (stride 24, slice header); p: *u8 +// stays on the default `*T -> T` path with no scale at all. The +// `package main;` line reproduces the carrier's wwtest_fputs prefix. +@test fn slicestride() void = { + striderow("ptr_to_slice", strings.concat( + "package main;\n", + "fn probe(p: *[]u8) i32 = {\n", + "\tlet r: []u8 = p[0];\n", + "\treturn r.len;\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVQ\t$24, CX"); + striderow("ptr_to_byte", strings.concat( + "package main;\n", + "fn probe(p: *u8) i32 = {\n", + "\tlet x: u8 = p[0];\n", + "\treturn x: i32;\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + ""); +}; + +// 755: `&N_DOT[N_INDEX]` strides — 24 (slice header via the `.ptr` +// pseudo-field arm), 16 (struct slot), elided (u8, the Shape-B +// `.ptr`-arm overreach row), 8 (generic struct-field arm). +@test fn ampdotidx() void = { + striderow("slice_elem_24", strings.concat( + "package main;\n", + "fn probe(s: *[][]u8, i: i32) *[]u8 = {\n", + "\treturn &s.ptr[i];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVQ\t$24, CX"); + striderow("struct_field_16", strings.concat( + "package main;\n", + "type box16 = struct { a: i64, b: i64 };\n", + "type holder = struct { items: *box16 };\n", + "fn probe(p: *holder, i: i32) *box16 = {\n", + "\treturn &p.items[i];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVQ\t$16, CX"); + striderow("byte_field_1", strings.concat( + "package main;\n", + "type holder = struct { ptr: *u8 };\n", + "fn probe(p: *holder, i: i32) *u8 = {\n", + "\treturn &p.ptr[i];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + ""); + striderow("i64_field_8", strings.concat( + "package main;\n", + "type holder = struct { qs: *i64 };\n", + "fn probe(p: *holder, i: i32) *i64 = {\n", + "\treturn &p.qs[i];\n", + "};\n", + "export fn main() i32 = { return 0; };\n"), + "MOVQ\t$8, CX"); +}; + +// FIRST line carrying a CALL to alloc(SB)/rt_malloc(SB), newline +// included (fgets semantics); "" when none. +fn firstcallline(s: str) str = { + let i: i32 = 0; + for (i < s.len) { + let rest: str = strings.sub(s, i, s.len); + let e: i32 = testenv.pos(rest, "\n"); + let lend: i32 = s.len; + if (e >= 0) { lend = i + e + 1; }; + let line: str = strings.sub(s, i, lend); + if (testenv.has(line, "\tCALL\t")) { + if (testenv.has(line, "alloc(SB)") + || testenv.has(line, "rt_malloc(SB)")) { + return line; + }; + }; + i = lend; + }; + return ""; +}; + +// One 758 asm_row: both stages' first alloc-CALL lines must exist, be +// byte-equal to each other, and equal the exact expected line. +fn callrow(label: str, src: str, wantsym: str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cline: str = firstcallline( + testenv.readfile(strings.concat(td, "/cs.s"))); + let wline: str = firstcallline( + testenv.readfile(strings.concat(td, "/ws.s"))); + if (cline.len == 0 || wline.len == 0) { + fail(label, "no CALL alloc line found"); + }; + if (!testenv.same(cline, wline)) { + fail(label, "cstage vs wwstage CALL line differs"); + }; + let want: str = strings.concat("\tCALL\t", wantsym, "(SB)\n"); + if (!testenv.same(cline, want)) { + fail(label, strings.concat("CALL line is not CALL ", wantsym, + "(SB)")); + }; + testenv.clean(td); +}; + +// 758 ffiresolve CALL-line parity: without an @symbol decl in scope +// ffiresolve("malloc") misses and both stages must say CALL +// malloc(SB); with the decl both must say CALL rt_malloc(SB). +@test fn calllines() void = { + callrow("noscope_intfield", strings.concat( + "package main;\n", + "type holder = struct { n: i32 };\n", + "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n"), + "malloc"); + callrow("noscope_strfield", strings.concat( + "package main;\n", + "type holder = struct { s: str };\n", + "fn dummy() *holder = { return alloc(holder { s = \"hi\" })!; };\n"), + "malloc"); + callrow("noscope_let_then_assign", strings.concat( + "package main;\n", + "type holder = struct { n: i32 };\n", + "fn dummy() *holder = {\n", + " let p: *holder = alloc(holder { n = 0 })!;\n", + " p.n = 7;\n", + " return p;\n", + "};\n"), + "malloc"); + callrow("withsym_intfield", strings.concat( + "package main;\n", + "@symbol(\"rt_malloc\") export fn malloc(n: u64) *void;\n", + "type holder = struct { n: i32 };\n", + "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n"), + "rt_malloc"); +}; + +// One 758 asm_disp_row: the literal line (exact tab/newline framing, +// so `(DX)` never collides with `0(DX)`) must appear in BOTH .s. +fn disprow(label: str, src: str, wantline: str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + if (!testenv.has(testenv.readfile(strings.concat(td, "/cs.s")), + wantline)) { + fail(label, "want_line missing from the cstage .s"); + }; + if (!testenv.has(testenv.readfile(strings.concat(td, "/ws.s")), + wantline)) { + fail(label, "want_line missing from the wwstage .s"); + }; + testenv.clean(td); +}; + +// 758 foff displacement formatting (#24 part B): zero displacement +// prints `(REG)`, non-zero keeps its offset. str fields route the +// heap base through DX (str IS []u8, cap in CX); int/f64 use BX. +@test fn displines() void = { + disprow("alloc_str_at_offset0", strings.concat( + "package main;\n", + "type holder = struct { s: str };\n", + "fn dummy() *holder = { return alloc(holder { s = \"x\" })!; };\n"), + "\tMOVQ\tAX, (DX)\n"); + disprow("alloc_str_at_nonzero_offset", strings.concat( + "package main;\n", + "type holder = struct { pad: i64, s: str };\n", + "fn dummy() *holder = {\n", + " return alloc(holder { pad = 0, s = \"x\" })!;\n", + "};\n"), + "\tMOVQ\tAX, 8(DX)\n"); + disprow("alloc_int_at_offset0", strings.concat( + "package main;\n", + "type holder = struct { n: i64 };\n", + "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n"), + "\tMOVQ\tAX, (BX)\n"); + disprow("alloc_f64_at_offset0", strings.concat( + "package main;\n", + "type holder = struct { f: f64 };\n", + "fn dummy() *holder = { return alloc(holder { f = 1.0f64 })!; };\n"), + "\tMOVSD\tX0, (BX)\n"); +}; diff --git a/test/byteid/freenoop_test.ww b/test/byteid/freenoop_test.ww new file mode 100644 index 00000000..f9faa5bc --- /dev/null +++ b/test/byteid/freenoop_test.ww @@ -0,0 +1,144 @@ +package freenoop_test; + +// free()-is-a-no-op byte-identity gate. Port of the retired native +// carrier test/wcc/930_free_noop_run.c; every assertion preserved. +// +// ww has no free by design (rt/alloc.s:30 — drop-amalloc); post-#27 +// both stages evaluate free's operand for side effects and emit +// NOTHING else. Per row the inline source (prefixed exactly as the +// carrier wrote it, `package main;` + blank line) compiles through +// w6c AND w6c_ww with the asm on stdout; the stages must agree +// byte-for-byte and the NEGATIVE gate holds: no substring 'free' +// anywhere in the cstage output — no CALL free, no symbol reference +// (byte identity extends the proof to wwstage, as in the carrier). +// +// The carrier captured stdout+stderr MERGED (`> cs.s 2>&1`) and +// compared/grepped that one file; here the streams land separately +// and EACH is byte-compared and 'free'-grepped — at least as strict +// on both axes. +// +// Path discipline (the carrier's #8 note): no scratch path may carry +// the substring 'free' or the whole-output grep false-fails. +// testenv.fresh() is /tmp/<16 lowercase hex> — 'free' needs an 'r', +// which the hex alphabet cannot produce — and the source basename +// stays noopfr.ww, mirroring the carrier's naming. +// +// Dead data preserved as dead: the carrier's row table carried a +// `want` exit-code field its main() never read (no run leg existed); +// none is invented here. Runtime behavior lives in the r930_free_* +// wwfixtures per the retired carrier's header. Dropped machinery, +// not assertions: getpid()-keyed tmpdirs + rm/mkdir accounting +// (testenv.fresh/clean own scratch and assert cleanup). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("freenoop FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +// `drv noopfr.ww` with the asm on stdout — the carrier's exact shape +// (no -o), so the captured stdout IS the .s under comparison. +fn runstage(td: str, label: str, stage: str, drv: str, + out: *testenv.commandout) void = { + let av: []str = []; + append(av, drv); + append(av, "noopfr.ww"); + testenv.runcommand(td, td, stage, av, tmo(), out); + let ok: bool = out.termination == exec.termination.EXIT + && out.code == 0; + if (!ok) { fail(label, strings.concat(stage, " compile failed")); }; +}; + +fn nooprow(label: str, src: str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/noopfr.ww"), + strings.concat("package main;\n\n", src)); + let cs: testenv.commandout; + runstage(td, label, "cstage", testenv.driver("w6c"), &cs); + let ws: testenv.commandout; + runstage(td, label, "wwstage", testenv.driver("w6c_ww"), &ws); + if (!testenv.same(cs.stdout, ws.stdout)) { + fail(label, "cs != ww .s"); + }; + if (!testenv.same(cs.stderr, ws.stderr)) { + fail(label, "cs != ww compile stderr"); + }; + if (testenv.has(cs.stdout, "free") + || testenv.has(cs.stderr, "free")) { + fail(label, "'free' survives in the .s -- lowering is not a no-op"); + }; + testenv.clean(td); +}; + +// The five carrier rows: deref-after-free leak semantics, the N_DOT +// operand shape, operand side effects run per call, no stack damage +// across 1M no-op frees, and the verbatim Hare-port alloc/free +// round-trip (i64 sidesteps the filed narrow-pointee alloc(value) +// size divergence). +@test fn asmrows() void = { + nooprow("free_local_ptr_deref_after", strings.concat( + "export fn main() i32 = {\n", + " let x: i32 = 5;\n", + " let p: *i32 = &x;\n", + " free(p);\n", + " if (*p != 5) { return 1; };\n", + " return 0;\n", + "};\n")); + nooprow("free_struct_field", strings.concat( + "type holder = struct { p: *i32, n: i32 };\n", + "export fn main() i32 = {\n", + " let x: i32 = 3;\n", + " let h: holder = holder { p = &x, n = 4 };\n", + " free(h.p);\n", + " if (h.n != 4) { return 2; };\n", + " return 0;\n", + "};\n")); + nooprow("free_call_operand_effects", strings.concat( + "let g: i32 = 0;\n", + "fn bump(p: *i32) *i32 = {\n", + " g = g + 2;\n", + " return p;\n", + "};\n", + "export fn main() i32 = {\n", + " let x: i32 = 1;\n", + " free(bump(&x));\n", + " free(bump(&x));\n", + " if (g != 4) { return 3; };\n", + " return 0;\n", + "};\n")); + nooprow("free_loop_no_stack_damage", strings.concat( + "let g: i32 = 0;\n", + "fn bump(p: *i32) *i32 = {\n", + " g = g + 1;\n", + " return p;\n", + "};\n", + "export fn main() i32 = {\n", + " let x: i32 = 1;\n", + " let i: i32 = 0;\n", + " for (i < 1000000) {\n", + " free(bump(&x));\n", + " i += 1;\n", + " };\n", + " if (g != 1000000) { return 5; };\n", + " return 0;\n", + "};\n")); + nooprow("free_alloc_roundtrip", strings.concat( + "import rt;\n", + "export fn main() i32 = {\n", + " let p: *i64 = alloc(11i64)!;\n", + " free(p);\n", + " if (*p != 11i64) { return 4; };\n", + " return 0;\n", + "};\n")); +}; diff --git a/test/byteid/mangle_test.ww b/test/byteid/mangle_test.ww new file mode 100644 index 00000000..fcb103b0 --- /dev/null +++ b/test/byteid/mangle_test.ww @@ -0,0 +1,114 @@ +package mangle_test; + +// M1 (#22) symbol-name proof. Port of the retired native carrier +// test/wcc/989_m1mangle_sym.c; every assertion preserved. Builds a +// fixture importing the real nested DIRECTORY package encoding.utf8 +// to .s through BOTH driver stages and asserts the symbol table: +// +// needle | want | proves +// --------------------------+------+--------------------------------- +// "encoding.utf8.runesz" | yes | nested pkg path-mangles (the M1 +// | | DELTA: leaf utf8 -> path +// | | encoding.utf8) +// "TEXT main," | yes | root entry stays BARE (#32) +// "TEXT utf8.runesz," | no | pre-M1 leaf-only mangle is gone +// +// plus cstage.s == wwstage.s byte-for-byte (rule 10) over the +// re-baselined names. +// +// #93 sep layout: `build -S -o ` splits the asm across +// .sepwork/.s (root in __root.s, the import in +// encoding.utf8.s), so each stage's per-package .s are concatenated +// before the needle scans and the byte-id compare. Concat order is +// strengthened from the carrier's shell glob to explicit +// byte-lexicographic (the libbyteid_test move); an empty concat fails +// loudly exactly as the carrier's `cat .sepwork/*.s` did. +// +// Dropped C machinery, not assertions: the ww_ww-absent skip gate +// (the Make target declares both drivers), getpid /tmp keying + +// rm accounting (testenv.fresh/clean), and the shell `timeout 180` +// wrapper (runcommand's 180s deadline carries it). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("mangle FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +fn buildstage(td: str, stage: str, drv: str, stem: str) void = { + let av: []str = []; + append(av, drv); + append(av, "build"); + append(av, "-S"); + append(av, "-o"); + append(av, stem); + append(av, "m1sym.ww"); + let co: testenv.commandout; + testenv.runcommand(td, td, stage, av, tmo(), &co); + let ok: bool = co.termination == exec.termination.EXIT && co.code == 0; + if (!ok) { fail(stage, "build failed"); }; +}; + +// Every per-package .s under `sepdir`, concatenated in byte-sorted +// order; "" when the directory is missing or holds no .s. +fn catasm(sepdir: str) str = { + if (!testenv.isdir(sepdir)) { return ""; }; + let names: []str = testenv.listdir(sepdir); + let out: str = ""; + let i: i32 = 0; + for (i < names.len) { + if (strings.hassuffix(names[i], ".s")) { + let body: str = testenv.readfile( + strings.concat(sepdir, "/", names[i])); + out = strings.concat(out, body); + }; + i += 1; + }; + return out; +}; + +fn checksyms(stage: str, s: str) void = { + if (!testenv.has(s, "encoding.utf8.runesz")) { + fail(stage, + "'encoding.utf8.runesz' missing -- nested pkg path-mangle absent"); + }; + if (!testenv.has(s, "TEXT main,")) { + fail(stage, "'TEXT main,' missing -- root entry no longer bare"); + }; + if (testenv.has(s, "TEXT utf8.runesz,")) { + fail(stage, + "'TEXT utf8.runesz,' present -- pre-M1 leaf-only mangle returned"); + }; +}; + +@test fn m1mangle() void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/m1sym.ww"), strings.concat( + "package main;\n", + "import encoding.utf8;\n", + "export fn main() int = { return utf8.runesz('A'): int; };\n")); + let cstem: str = strings.concat(td, "/c_m1sym"); + let wstem: str = strings.concat(td, "/w_m1sym"); + buildstage(td, "cstage", testenv.driver("ww"), cstem); + buildstage(td, "wwstage", testenv.driver("ww_ww"), wstem); + let cs: str = catasm(strings.concat(cstem, ".sepwork")); + let ws: str = catasm(strings.concat(wstem, ".sepwork")); + if (cs.len == 0 || ws.len == 0) { fail("m1mangle", "empty .s concat"); }; + checksyms("cstage", cs); + checksyms("wwstage", ws); + if (!testenv.same(cs, ws)) { + fail("m1mangle", "cstage.s != wwstage.s (byte-id break)"); + }; + testenv.clean(td); +}; diff --git a/test/byteid/structabi_test.ww b/test/byteid/structabi_test.ww new file mode 100644 index 00000000..bd0d11c6 --- /dev/null +++ b/test/byteid/structabi_test.ww @@ -0,0 +1,240 @@ +package structabi_test; + +// Float-bearing struct-ABI byte-id + register-class gate. Port of the +// retired native carriers test/wcc/946_structparam_run.c (#165, the +// struct-PARAM SysV net) and test/wcc/946_structret_run.c (#171a, the +// struct-RETURN twin); every assertion preserved. +// +// For a pure internal ww call both ends ride the same (possibly +// wrong) registers, so runtime values AND cs==ww byte-id are +// necessary-not-sufficient: the genuine defect is SysV register-CLASS +// conformance, observable only in the emitted asm. Each row therefore +// compiles its inline source directly through w6c and w6c_ww +// (`-o out.s src.ww`), byte-ids the two .s (rule 10), and scans the +// cstage .s (byte-id makes the wwstage scan redundant, as in the C) +// for the SSE markers, PRESENT or ABSENT per row: +// +// param rows — callee receive marker "MOVSD\tX0, -" scoped to the +// TEXT main.use block. Scoping is load-bearing: main's own +// float-literal init also emits MOVSD X0, -off(BP), so a +// whole-file grep would not discriminate. +// ret rows — producer marker "(BP), X0" scoped to TEXT main.mk +// (its literal fills load via (SP), X0, never (BP)) plus consumer +// marker "MOVSD\tX0, -" scoped to TEXT main,; SEND and RECV +// presence must AGREE before comparing against the row polarity. +// +// The ABSENT rows prove the classification gate: pure-INT structs +// stay on the GP transport, and struct{f32,f32} (two f32 packing into +// ONE SSE eightbyte — deferred #165b/#171b) falls back to GP instead +// of wrongly riding two SSE regs. +// +// No runtime leg existed in the carriers (the sources embed value +// checks but were only ever compiled to .s; end-to-end values live in +// the r946_structparam_* / r946_structret_* fixtures) and none is +// invented here. Dropped machinery, not assertions: the hard +// w6c_ww-missing precondition (the Make target declares both tools; a +// missing driver still fails loudly in runcommand), getpid tmpdirs + +// rm/mkdir accounting (testenv.fresh/clean), and the 256KiB scan- +// buffer refusal (testenv.readfile is unbounded). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("structabi FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +fn emitstage(td: str, label: str, stage: str, drv: str, + outname: str) void = { + let av: []str = []; + append(av, drv); + append(av, "-o"); + append(av, outname); + append(av, "src.ww"); + let co: testenv.commandout; + testenv.runcommand(td, td, stage, av, tmo(), &co); + let ok: bool = co.termination == exec.termination.EXIT && co.code == 0; + if (!ok) { fail(label, strings.concat(stage, " compile failed")); }; +}; + +// One function's TEXT block: [first `head` .. next "\nTEXT " after +// it), to EOF when it is the last block. A missing head is a scan +// error, never a silent ABSENT verdict. +fn textblock(label: str, s: str, head: str) str = { + let hp: i32 = testenv.pos(s, head); + if (hp < 0) { + fail(label, strings.concat("cannot scan .s: no ", head)); + }; + let tail: str = strings.sub(s, hp, s.len); + let rest: str = strings.sub(tail, 1, tail.len); + let ep: i32 = testenv.pos(rest, "\nTEXT "); + if (ep < 0) { return tail; }; + return strings.sub(tail, 0, ep + 1); +}; + +// Compile both stages and gate rule-10 byte identity; returns the +// cstage .s body for the marker scan. +fn buildpair(label: str, src: str, td: str) str = { + testenv.writefile(strings.concat(td, "/src.ww"), src); + emitstage(td, label, "cstage", testenv.driver("w6c"), "cs.s"); + emitstage(td, label, "wwstage", testenv.driver("w6c_ww"), "ws.s"); + let cs: str = testenv.readfile(strings.concat(td, "/cs.s")); + let ws: str = testenv.readfile(strings.concat(td, "/ws.s")); + if (!testenv.same(cs, ws)) { + fail(label, "cstage/wwstage .s DIFFER (rule-10 byte-id violation)"); + }; + return cs; +}; + +fn paramrow(label: str, src: str, wantsse: i32) void = { + let td: str = testenv.fresh(); + let cs: str = buildpair(label, src, td); + let callee: str = textblock(label, cs, "TEXT main.use"); + let sse: i32 = 0; + if (testenv.has(callee, "MOVSD\tX0, -")) { sse = 1; }; + if (sse != wantsse) { + fail(label, + "callee SSE receive polarity wrong (register-class discriminator)"); + }; + testenv.clean(td); +}; + +fn retrow(label: str, src: str, wantsse: i32) void = { + let td: str = testenv.fresh(); + let cs: str = buildpair(label, src, td); + let mk: str = textblock(label, cs, "TEXT main.mk"); + let mn: str = textblock(label, cs, "TEXT main,"); + let send: i32 = 0; + if (testenv.has(mk, "(BP), X0")) { send = 1; }; + let recv: i32 = 0; + if (testenv.has(mn, "MOVSD\tX0, -")) { recv = 1; }; + if (send != recv) { + fail(label, "SEND and RECV SSE markers disagree"); + }; + if (send != wantsse) { + fail(label, + "struct return SSE polarity wrong (register-class discriminator)"); + }; + testenv.clean(td); +}; + +// #165 param rows: lone-f64 eightbytes ride X0/X1 on independent +// cursors; pure-INT and the f32f32 fallback stay GP-routed. +@test fn structparam() void = { + paramrow("f64f64_arg", strings.concat( + "package main;\n", + "type pff = struct { a: f64, b: f64 };\n", + "fn use(s: pff) f64 = { return s.a + s.b; };\n", + "export fn main() i32 = {\n", + "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n", + "\tif (use(s) != 8.0) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + paramrow("f64i64_arg", strings.concat( + "package main;\n", + "type pfi = struct { a: f64, b: i64 };\n", + "fn use(s: pfi) i64 = { return (s.a: i64) + s.b; };\n", + "export fn main() i32 = {\n", + "\tlet s: pfi = pfi { a = 3.0, b = 5 };\n", + "\tif (use(s) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + paramrow("i64f64_arg", strings.concat( + "package main;\n", + "type pif = struct { a: i64, b: f64 };\n", + "fn use(s: pif) i64 = { return s.a + (s.b: i64); };\n", + "export fn main() i32 = {\n", + "\tlet s: pif = pif { a = 3, b = 5.0 };\n", + "\tif (use(s) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + paramrow("i64i64_arg", strings.concat( + "package main;\n", + "type pii = struct { a: i64, b: i64 };\n", + "fn use(s: pii) i64 = { return s.a + s.b; };\n", + "export fn main() i32 = {\n", + "\tlet s: pii = pii { a = 3, b = 5 };\n", + "\tif (use(s) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 0); + paramrow("f32f32_arg_fallback", strings.concat( + "package main;\n", + "type pf32 = struct { a: f32, b: f32 };\n", + "fn use(s: pf32) i64 = { return (s.a: i64) + (s.b: i64); };\n", + "export fn main() i32 = {\n", + "\tlet s: pf32 = pf32 { a = 3.0f32, b = 5.0f32 };\n", + "\tif (use(s) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 0); + paramrow("scalar_plus_f64f64", strings.concat( + "package main;\n", + "type pff = struct { a: f64, b: f64 };\n", + "fn use(n: i64, s: pff) i64 = {\n", + "\treturn n + (s.a: i64) + (s.b: i64);\n", + "};\n", + "export fn main() i32 = {\n", + "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n", + "\tif (use(2, s) != 10) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); +}; + +// #171a return rows: the return twin — lone-f64 eightbytes ride the +// SSE return cursor, GP cursor independent (f64i32's i32 rides AX). +@test fn structret() void = { + retrow("f64f64_ret", strings.concat( + "package main;\n", + "type pff = struct { a: f64, b: f64 };\n", + "fn mk() pff = { return pff { a = 3.0, b = 5.0 }; };\n", + "export fn main() i32 = {\n", + "\tlet s: pff = mk();\n", + "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + retrow("f64i32_ret", strings.concat( + "package main;\n", + "type pfi = struct { a: f64, b: i32 };\n", + "fn mk() pfi = { return pfi { a = 3.0, b = 5 }; };\n", + "export fn main() i32 = {\n", + "\tlet s: pfi = mk();\n", + "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + retrow("i64f64_ret", strings.concat( + "package main;\n", + "type pif = struct { a: i64, b: f64 };\n", + "fn mk() pif = { return pif { a = 3, b = 5.0 }; };\n", + "export fn main() i32 = {\n", + "\tlet s: pif = mk();\n", + "\tif (s.a + (s.b: i64) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 1); + retrow("i64i64_ret", strings.concat( + "package main;\n", + "type pii = struct { a: i64, b: i64 };\n", + "fn mk() pii = { return pii { a = 3, b = 5 }; };\n", + "export fn main() i32 = {\n", + "\tlet s: pii = mk();\n", + "\tif (s.a + s.b != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 0); + retrow("f32f32_ret_fallback", strings.concat( + "package main;\n", + "type pf32 = struct { a: f32, b: f32 };\n", + "fn mk() pf32 = { return pf32 { a = 3.0f32, b = 5.0f32 }; };\n", + "export fn main() i32 = {\n", + "\tlet s: pf32 = mk();\n", + "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n", + "\treturn 0;\n", + "};\n"), 0); +}; diff --git a/test/wcc/753_convwrap_audit.c b/test/wcc/753_convwrap_audit.c deleted file mode 100644 index dc760e51..00000000 --- a/test/wcc/753_convwrap_audit.c +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 753_convwrap_audit — sentinel for task #17 (structural close of the - * #4-trio convenience-wrapper N_DOT-arm audit). wwstage cgen* helpers - * that take a *node callee and probe its return shape via bare-leaf - * fnretlookup stripped the N_DOT module hint, same wedge shape as #16 - * (callee_variadic_param, d9b0c90) through a different family of - * consumers. Eight latent sites caught in the sweep: - * - * cgenstmt.ww:170-189 cgreturn forwardtagged probe - * cgenstmt.ww:1077-1090 cgmlet tuple-return shape probe - * cgenexpr.ww:1729-1742 cgdot fn-rvalue probe (`mod.fn` LEAQ) - * cgenexpr.ww:155-176 cgtryprop succisstr probe - * cgenexpr.ww:200-220 cgtryunw succisstr probe - * cgenutil.ww:1528-1540 callsretsize (sret arg-prep) - * cgenutil.ww:1776-1794 inferletcalltype (`let x = f()?` tnode) - * cgenutil.ww:2894-2944 rhstaggedabicall N_CALL branch - * - * Each consumed callee.str (leaf) for both N_IDENT and N_DOT arms, - * then routed through bare fnretlookup. Post-#4e the bare walk is - * same-module-first then head-walk fallback — for a cross-module - * N_DOT call from a caller whose c.curmod doesn't match either side, - * the head walk returns whichever module's same-leaf fn sits at the - * head of c.fnrets. When that head-side fn has a divergent return - * shape (tagged-vs-scalar, str-vs-scalar pair, sret-vs-flat, etc.) - * the consumer fires the wrong dispatch arm. - * - * Cstage carries no sister bug: cmd/w6c/cgen.c reads every callee - * return shape from the typed `n->lhs->type` (TY_FN sig). Mirror of - * #4d / #28 / #31 / #34 / #16: cstage sidesteps every bare-leaf - * table. - * - * Pin: row 1 exercises cgmlet (cgenstmt.ww:1077-1090). alpha exports - * a same-leaf `foo` returning (i64, str), beta exports a same-leaf - * `foo` returning (i64, i64). Source order is beta first, main next, - * alpha LAST — so alpha.foo prepends to the head of c.fnrets. Pre- - * fix wwstage's cgmlet probe walks past main (no `foo`) into the - * head and grabs alpha.foo's (i64, str) tuple, makes s0_is_str=false - * but s1_is_str=true → fires the (scalar, str) emit branch (MOVQ DX - * → b.ptr + MOVQ CX → b.len) for a beta.foo return that's actually - * (AX=i64, DX=i64) — no CX defined on this ABI. Post-fix the probe - * routes through fnretlookupmod(c, "foo", "beta") and the (scalar, - * scalar) branch fires (MOVQ AX → a, MOVQ DX → b, no CX). - * - * Asserts: inside main.run TEXT sym before its first RET, no `MOVQ - * CX,` appears — pre-fix wwstage emits the str.len store that has - * no defined source on a (i64, i64) tuple return. cstage always - * picks beta.foo via typed AST, so the same pattern check holds - * on both stages. No byte-id check — `let a, b = call();` has a - * pre-existing cosmetic cs-vs-ws divergence (cstage emits a - * defensive PUSHQ/POPQ DX around the AX-store of `a` that wwstage - * skips). Out of #17 scope. - */ -#include -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - const char *textsym; /* TEXT sym containing the call */ - const char *want_imm; /* substring that MUST appear */ - const char *bad_imm; /* substring that MUST NOT appear */ -}; - -/* beta.foo first (registered first → tail of c.fnrets), main next, - * alpha LAST (registered last → head). Pre-fix bare fnretlookup in - * cgmlet falls through same-module to head walk → alpha.foo → its - * (i64, str) shape drives the (scalar, str) emit branch (MOVQ CX, - * — str.len store with no source defined by beta.foo's (i64, i64) - * tuple ABI). Post-fix the probe goes through fnretlookupmod with - * "beta" hint → beta.foo → scalar-pair branch (no CX). */ -static const struct row rows[] = { - { "cgmlet_tuple_modshadow", - "package beta;\n" - "export fn foo() (i64, i64) = { return (10i64, 20i64); };\n" - "package main;\n" - "import alpha;\n" - "import beta;\n" - "export fn run() i32 = {\n" - "\tlet a, b = beta.foo();\n" - "\tif (a != 10i64) { return 1; };\n" - "\tif (b != 20i64) { return 2; };\n" - "\treturn 0;\n" - "};\n" - "export fn main() i32 = { return run(); };\n" - "package alpha;\n" - "export fn foo() (i64, str) = { return (10i64, \"hi\"); };\n", - "TEXT main.run", "MOVQ\tAX,", "MOVQ\tCX," }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/cwa_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/cwa_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - int cleanfail = 0; - if (unlink(src) != 0 && errno != ENOENT) { - perror(src); - cleanfail = 1; - } - /* a failing compile can still leave a partial .s behind */ - if (rc != 0 && unlink(out_s) != 0 && errno != ENOENT) { - perror(out_s); - cleanfail = 1; - } - if (cleanfail && rc == 0) - rc = -1; - return rc; -} - -static int -check_imm(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, r->textsym); - if (!fn) { - fprintf(stderr, "row[%s][%s]: no %s in %s\n", - r->label, stage, r->textsym, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET"); - if (!ret) { - fprintf(stderr, "row[%s][%s]: no RET inside %s\n", - r->label, stage, r->textsym); - return -1; - } - const char *good = strstr(fn, r->want_imm); - if (!good || good >= ret) { - fprintf(stderr, - "row[%s][%s]: want_imm %s missing inside %s\n", - r->label, stage, r->want_imm, r->textsym); - return -1; - } - const char *bad = strstr(fn, r->bad_imm); - if (bad && bad < ret) { - fprintf(stderr, - "row[%s][%s]: bad_imm %s present inside %s — wrong-module shape probe fired\n", - r->label, stage, r->bad_imm, r->textsym); - return -1; - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - int cleanfail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "convwrap_audit[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_imm(cs_path, &rows[i], "cstage") != 0) fail++; - - if (!have_ww) { - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "convwrap_audit[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - total++; - if (check_imm(ws_path, &rows[i], "wwstage") != 0) fail++; - - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - if (unlink(ws_path) != 0 && errno != ENOENT) { - perror(ws_path); - cleanfail = 1; - } - } - - /* a cleanup failure must not pass silently, but must not mask a - * real assertion failure's own report either. */ - if (cleanfail && fail == 0) { - fprintf(stderr, "convwrap_audit: cleanup failed\n"); - return 1; - } - - if (fail) { - fprintf(stderr, - "convwrap_audit: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("convwrap_audit: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/754_slice_of_slice_index.c b/test/wcc/754_slice_of_slice_index.c deleted file mode 100644 index 49aae830..00000000 --- a/test/wcc/754_slice_of_slice_index.c +++ /dev/null @@ -1,273 +0,0 @@ -/* - * 754_slice_of_slice_index — sentinel for latent #20. Pre-fix, cstage's - * check.c typed `p[i]` for `p: *[]T` as `T` (element-of-element) via a - * bespoke TY_PTR-over-TY_SLICE clause that auto-decayed the pointer - * twice: first peeling the `*`, then peeling the `[]`. The Hare-faithful - * shape is `[]T` — a pointer-to-slice is a 1D array of slice headers, - * not a 2D array of T. The `*[N]T → T` auto-decay (TY_PTR over TY_ARRAY) - * IS the C-style array-to-element semantics callers expect; only the - * TY_SLICE arm of the same clause was wrong. - * - * Selfhost mirror lived in cgenutil.ww elemsizeof: for `*[]T`, the - * elem walk falls into `elem.kind == nkind.N_TSLICE` which had no - * recognition — fell through to the catch-all `return 8`. Post-fix - * the N_TSLICE arm returns 24 (slice header stride). Both stages now - * agree on stride; cstage already computed 24 via Type.size on the - * idx_eff(TY_PTR) sub. - * - * Surface impact (the wedge that flushed this latent): a `[][]u8` - * indexed through a `*[][]u8` (e.g. `tokens.ptr[0] = ...` for tokens: - * [][]u8) was rejected by cstage's `init u8 not assignable to declared - * []u8` and silently 8B-stored by wwstage. Symmetric fix: - * - cstage: drop the TY_SLICE branch in check.c N_INDEX so `*[]T[i]` - * falls through to the default `*U → U` path with U=[]T. - * - wwstage: extend cgenutil.ww elemsizeof to return 24 when the - * elem (post-N_TPTR peel) is N_TSLICE. - * - * Probes: - * row 0 — `let r: []u8 = p[0]` for `p: *[]u8`. Pre-fix cstage errors - * ("init u8 not assignable to declared []u8"); wwstage - * silently emits an 8B load. Post-fix both stages compile - * and emit the same MOVQ load with stride 24 (slice element - * span — read width is still 8B today; the full 24B slice - * element copy is a separately-tracked downstream gap noted - * at cmd/w6c/cgen.c:6518 ("Slice (24B) and struct/tuple/ - * tagged element arrays land in the same multi-word-store - * gap")). Asserts byte-identical asm between stages. - * - * row 1 — `let v: u8 = p[0]` for `p: *u8`. Default `*T → T` path: - * unchanged by the fix. Confirms shlex / getopt's - * `slice.ptr[i]` (where `slice.ptr` is `*T` for some - * non-slice T) still routes through the default and emits - * a narrow MOVZBQ. Byte-id between stages. - * - * No 24B-element-emit assertion: the 24B slice / 16B struct / etc. - * multi-word-element copy is a parallel codegen wedge already noted - * inline at cgen.c:6515-6523. Splitting keeps this commit bisect-clean - * (rule 11). - */ -#include -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - const char *stride_imm; /* expected `MOVQ\t$, CX` */ -}; - -static const struct row rows[] = { - /* `p: *[]u8` indexed: result type is `[]u8`, stride = 24 - * (slice header). Pre-fix cstage rejected the assignment; - * wwstage silently used stride 8. Post-fix both agree on 24. */ - { "ptr_to_slice", - "fn probe(p: *[]u8) i32 = {\n" - "\tlet r: []u8 = p[0];\n" - "\treturn r.len;\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVQ\t$24, CX" }, - /* `p: *u8` default path: stride = 1, no MOVQ-$-CX scaling at - * all. Confirms the TY_SLICE-clause removal doesn't re-route - * non-slice pointer indexing. */ - { "ptr_to_byte", - "fn probe(p: *u8) i32 = {\n" - "\tlet x: u8 = p[0];\n" - "\treturn x: i32;\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - NULL }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/sosi_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/sosi_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - int cleanfail = 0; - if (unlink(src) != 0 && errno != ENOENT) { - perror(src); - cleanfail = 1; - } - /* a failing compile can still leave a partial .s behind */ - if (rc != 0 && unlink(out_s) != 0 && errno != ENOENT) { - perror(out_s); - cleanfail = 1; - } - if (cleanfail && rc == 0) - rc = -1; - return rc; -} - -static int -check_stride(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) { - fprintf(stderr, - "row[%s][%s]: no TEXT probe in %s\n", - r->label, stage, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET\n"); - if (!ret) ret = fn + strlen(fn); - if (r->stride_imm) { - const char *m = strstr(fn, r->stride_imm); - if (!m || m >= ret) { - fprintf(stderr, - "row[%s][%s]: expected `%s` inside TEXT probe\n", - r->label, stage, r->stride_imm); - return -1; - } - } else { - const char *m = strstr(fn, "\tMOVQ\t$"); - while (m && m < ret) { - const char *eol = strchr(m, '\n'); - if (eol && eol < ret && - strstr(m, ", CX\n") && (strstr(m, ", CX\n") < eol)) { - fprintf(stderr, - "row[%s][%s]: unexpected MOVQ $imm, CX " - "(scale) inside TEXT probe — `*u8` path " - "should index with stride 1\n", - r->label, stage); - return -1; - } - m = eol ? strstr(eol, "\tMOVQ\t$") : NULL; - } - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - int cleanfail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "slice_of_slice_index[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_stride(cs_path, &rows[i], "cstage") != 0) fail++; - - if (!have_ww) { - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "slice_of_slice_index[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - total++; - if (check_stride(ws_path, &rows[i], "wwstage") != 0) fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "slice_of_slice_index[%s]: cstage vs wwstage asm " - "differs\n", - rows[i].label); - fail++; - } - - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - if (unlink(ws_path) != 0 && errno != ENOENT) { - perror(ws_path); - cleanfail = 1; - } - } - - /* a cleanup failure must not pass silently, but must not mask a - * real assertion failure's own report either. */ - if (cleanfail && fail == 0) { - fprintf(stderr, "slice_of_slice_index: cleanup failed\n"); - return 1; - } - - if (fail) { - fprintf(stderr, - "slice_of_slice_index: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("slice_of_slice_index: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/755_amp_dot_idx.c b/test/wcc/755_amp_dot_idx.c deleted file mode 100644 index c8811db2..00000000 --- a/test/wcc/755_amp_dot_idx.c +++ /dev/null @@ -1,304 +0,0 @@ -/* - * 755_amp_dot_idx — sentinel for latent #21 (wwstage vs cstage divergence - * in the `&N_DOT[N_INDEX]` cgen path). Two shapes ride the same wedge: - * - * Shape A — register polarity in the "complex base" arm of cgun's - * TK_AMP N_INDEX branch. cstage emits the lean - * PUSHQ AX ; cgexpr(base) ; POPQ BX ; ADDQ BX, AX - * using BX as the popped scratch. wwstage interposed an extra - * MOVQ AX, BX ; POPQ AX ; ADDQ BX, AX - * scratch shuffle with no semantic need (cstage's three-line shape - * also lands ptr+offset in AX). Polarity DOWN to cstage's leaner - * form (rule 10): no semantic asymmetry, just verbose-defensive - * redundancy on the wwstage side. - * - * Shape B — `indexbaseesz` over-applies the `.ptr` pseudo-field arm. - * cgenutil.ww:1148-1160 treated any field literally named "ptr" as - * a str/slice pseudo-field, falling to a hard-coded `return 8` when - * the base wasn't actually str/slice. Bites `&p.ptr[i]` (also the - * value-load path `p.ptr[i]`) for any `*struct{ ptr: *T, ... }` - * where T is a narrow primitive: stride scales to 8 instead of T's - * primsize, and for `*u8` callers the load width drops from MOVZBQ - * to MOVQ (silent miscompile, reading 8B at offset i*8 instead of - * 1B at offset i). cstage doesn't carry the sister bug: it reads - * `base->type->sub->size` directly off the typed AST. Fix: the - * `.ptr` arm now only matches when the inner is str or slice; a - * struct N_TNAME base falls through to the generic struct-field - * arm at 1162+, which already routes `*T` fields through - * `primsize` correctly. - * - * Both shapes share the `cgun` TK_AMP N_INDEX path. The polarity fix - * is the same edit for slice / struct / i64 / u8 element strides; the - * stride fix is the same edit for the u8 case at the `.ptr` arm; they - * compose at the cgun call site (esz from indexbaseesz, then either - * IMULQ-or-elide gate, then complex-base lean form). - * - * Rows 1-4 × (cstage, wwstage) — every row asserts: - * - the expected stride immediate appears (or no stride scale at - * all for esz=1), inside `TEXT probe`; - * - cstage and wwstage emit byte-identical asm. - * - * Pre-fix: all 4 rows fail the byte-id check (Shape A); row 3 also - * fails the stride check (Shape B: wwstage emits `MOVQ $8, CX` for - * a u8 element). Post-fix every row passes both. - * - * Probes mirror the .ai/probe_amp_*.ww shapes that surfaced the wedge: - * - row 0 / `slice_elem_24`: `&s.ptr[i]` where s: *[][]u8 — the - * `.ai/probe_amp_dot_idx.ww` shape 1:1. Stride 24 (slice header - * element span via indexbaseesz's `.ptr` arm with N_TSLICE inner). - * - row 1 / `struct_field_16`: `&p.items[i]` where items: *box16 - * (i64+i64). Stride 16 (struct slot size via structlookup + - * totsize in indexbaseesz's *S elem arm). - * - row 2 / `byte_field_1`: `&p.ptr[i]` where ptr: *u8. Pre-fix - * Shape B: stride 8 emitted erroneously. Post-fix stride is - * elided (esz=1, no IMULQ). - * - row 3 / `i64_field_8`: `&p.qs[i]` where qs: *i64. Stride 8 - * (already correct since the field name is "qs", not "ptr", - * so the bare path hits the generic struct-field arm). - */ -#include -#include -#include -#include -#include -#include -#include "wwtestpkg.h" - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - const char *stride_imm; /* expected `MOVQ\t$, CX` or NULL */ -}; - -static const struct row rows[] = { - /* slice_elem_24: `&s.ptr[i]` where s: *[][]u8. Stride 24 — the - * slice header element span via indexbaseesz's `.ptr` arm on - * an N_TSLICE inner (post-N_TPTR peel). Polarity-A only. - * Mirrors .ai/probe_amp_dot_idx.ww 1:1. */ - { "slice_elem_24", - "fn probe(s: *[][]u8, i: i32) *[]u8 = {\n" - "\treturn &s.ptr[i];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVQ\t$24, CX" }, - /* struct_field_16: `&p.items[i]` where items: *box16, box16 - * is two i64 fields → struct slot 16B. Stride 16 via - * indexbaseesz's `*S` arm (structlookup totsize). */ - { "struct_field_16", - "type box16 = struct { a: i64, b: i64 };\n" - "type holder = struct { items: *box16 };\n" - "fn probe(p: *holder, i: i32) *box16 = {\n" - "\treturn &p.items[i];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVQ\t$16, CX" }, - /* byte_field_1: `&p.ptr[i]` where ptr: *u8. Pre-fix wwstage - * emits stride 8 (`.ptr` arm overreach). Post-fix: no IMULQ - * because stride is 1 and the gate elides. */ - { "byte_field_1", - "type holder = struct { ptr: *u8 };\n" - "fn probe(p: *holder, i: i32) *u8 = {\n" - "\treturn &p.ptr[i];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - NULL }, - /* i64_field_8: `&p.qs[i]` where qs: *i64. Stride 8 via - * the generic struct-field arm (`primsize("i64")` = 8). - * Polarity-A only — the `.ptr` arm doesn't gate on "qs", - * so even pre-fix the stride is correct. */ - { "i64_field_8", - "type holder = struct { qs: *i64 };\n" - "fn probe(p: *holder, i: i32) *i64 = {\n" - "\treturn &p.qs[i];\n" - "};\n" - "export fn main() i32 = { return 0; };\n", - "MOVQ\t$8, CX" }, -}; - -static int -slurp(const char *path, char *buf, size_t cap) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, cap - 1, f); - fclose(f); - buf[n] = '\0'; - return (int)n; -} - -static int -emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) -{ - char src[96], cmd[1024]; - snprintf(src, sizeof src, "/tmp/adi_%d_%d.ww", getpid(), i); - snprintf(out_s, cap, "/tmp/adi_%d_%d_%s.s", - getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - wwtest_fputs(r->src, f); - fclose(f); - - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); - int rc = runwait(cmd); - int cleanfail = 0; - if (unlink(src) != 0 && errno != ENOENT) { - perror(src); - cleanfail = 1; - } - /* a failing compile can still leave a partial .s behind */ - if (rc != 0 && unlink(out_s) != 0 && errno != ENOENT) { - perror(out_s); - cleanfail = 1; - } - if (cleanfail && rc == 0) - rc = -1; - return rc; -} - -static int -check_stride(const char *spath, const struct row *r, const char *stage) -{ - char buf[1 << 14]; - if (slurp(spath, buf, sizeof buf) < 0) { - fprintf(stderr, "row[%s][%s]: cannot read %s\n", - r->label, stage, spath); - return -1; - } - const char *fn = strstr(buf, "TEXT main.probe"); - if (!fn) { - fprintf(stderr, - "row[%s][%s]: no TEXT probe in %s\n", - r->label, stage, spath); - return -1; - } - const char *ret = strstr(fn, "\tRET\n"); - if (!ret) ret = fn + strlen(fn); - if (r->stride_imm) { - const char *m = strstr(fn, r->stride_imm); - if (!m || m >= ret) { - fprintf(stderr, - "row[%s][%s]: expected `%s` inside TEXT probe\n", - r->label, stage, r->stride_imm); - return -1; - } - } else { - const char *m = strstr(fn, "\tMOVQ\t$"); - while (m && m < ret) { - const char *eol = strchr(m, '\n'); - const char *cxpos = strstr(m, ", CX\n"); - if (eol && cxpos && cxpos < eol) { - fprintf(stderr, - "row[%s][%s]: unexpected MOVQ $imm, CX " - "(scale) inside TEXT probe — stride-1 " - "path should elide the multiply\n", - r->label, stage); - return -1; - } - m = eol ? strstr(eol, "\tMOVQ\t$") : NULL; - } - } - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[640], w6c_ww[640]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - - int have_ww = (access(w6c_ww, X_OK) == 0); - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - int cleanfail = 0; - - for (int i = 0; i < n; i++) { - char cs_path[128], ws_path[128]; - - if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { - fprintf(stderr, - "amp_dot_idx[cstage][%s]: w6c failed\n", - rows[i].label); - fail++; total++; continue; - } - total++; - if (check_stride(cs_path, &rows[i], "cstage") != 0) fail++; - - if (!have_ww) { - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - - if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { - fprintf(stderr, - "amp_dot_idx[wwstage][%s]: w6c_ww failed\n", - rows[i].label); - fail++; total++; - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - continue; - } - total++; - if (check_stride(ws_path, &rows[i], "wwstage") != 0) fail++; - - total++; - char cmd[512]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); - if (runwait(cmd) != 0) { - fprintf(stderr, - "amp_dot_idx[%s]: cstage vs wwstage asm differs\n", - rows[i].label); - fail++; - } - - if (unlink(cs_path) != 0 && errno != ENOENT) { - perror(cs_path); - cleanfail = 1; - } - if (unlink(ws_path) != 0 && errno != ENOENT) { - perror(ws_path); - cleanfail = 1; - } - } - - /* a cleanup failure must not pass silently, but must not mask a - * real assertion failure's own report either. */ - if (cleanfail && fail == 0) { - fprintf(stderr, "amp_dot_idx: cleanup failed\n"); - return 1; - } - - if (fail) { - fprintf(stderr, - "amp_dot_idx: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("amp_dot_idx: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/758_cgalloc_str_field.c b/test/wcc/758_cgalloc_str_field.c deleted file mode 100644 index 28a232ed..00000000 --- a/test/wcc/758_cgalloc_str_field.c +++ /dev/null @@ -1,395 +0,0 @@ -/* - * 758_cgalloc_str_field — cgalloc N_STRUCTLIT str-field store (task #22) - * and the cgalloc CALL-site ffiresolve parity (task #25 / #24-part-A). - * - * Pre-fix (#22): wwstage cgalloc's per-field walk routed str-typed - * fields through the generic `MOVQ (SP), BX ; MOVQ AX, foff(BX)` path, - * which landed only AX (str.ptr) and clobbered BX (str.len) with the - * heap base. str.len silently stayed zero (rt_malloc is MAP_ANON-backed, - * so the slot was zero-init rather than garbage — but still wrong). - * 990 and 995 byte-identity didn't catch this because nothing in the - * bootstrapped selfhost source uses `alloc(T { strfield = "..." })!`. - * - * Fix (#22): a str-field-typed branch mirrors cmd/w6c/cgen.c:4184-4190 - * — route the heap base through CX so BX=len survives both stores - * (ptr at foff+0, len at foff+8). Task #23 (slice/tagged/fn-pair - * multi-word fields) is the broader follow-up; this row pins str. - * - * Pre-fix (#25): wwstage's cgalloc emitted a hardcoded `CALL - * rt_malloc(SB)` while cstage routed the same site through - * ffi_resolve("alloc"), so direct `w6c` vs `w6c_ww` on a fixture - * without the @symbol decl in scope diverged (cstage: `CALL - * alloc(SB)`; wwstage: `CALL rt_malloc(SB)`). The fix swaps both - * cgenexpr.ww and cgenstmt.ww cgalloc CALL sites to - * `ffiresolve(c, "malloc")`, aligning wwstage down to the leaner - * cstage shape (CLAUDE.md rule 10). The `asm_rows` table below pins - * the CALL line via single-file `w6c -o` / `w6c_ww -o`; `ww build` - * combines lib/rt/malloc.ww into the fixture and would always supply - * the @symbol decl, masking the regression. - * - * The runtime rows moved to the test/wcc/data/r75_alloc_{str_singleton, - * i32_then_str,str_then_i32,two_str,f64_str_i32,str_readback} fixtures; - * this carrier retains only the asm-line checks below. - * - * Full asm byte-identity is intentionally NOT checked here — the - * `(BX)` (cstage txt.c omits zero displacement) vs `0(BX)` (wwstage - * emitint(0) is unconditional) divergence still stands. The new str - * branch follows the existing float / int branch shape and inherits - * the same formatting; aligning all three with cstage is task #24 - * part B. - */ -#include -#include -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -/* asm_row — fixture for the ffiresolve CALL-line check. `want_sym` - * is the unqualified symbol expected inside `CALL\t(SB)` from - * both stages' .s output. Single-file `w6c` compile (no `ww build` - * combine), so the @symbol decl is only in scope when the fixture - * writes one — that's how `noscope_*` rows pin the pre-fix wwstage - * hardcoded-`rt_malloc` divergence and `withsym_*` rows pin that - * ffiresolve actually hits when the decl is present. */ -struct asm_row { const char *label; const char *src; const char *want_sym; }; - -static const struct asm_row asm_rows[] = { - /* Int-field, no @symbol in scope. ffiresolve("malloc") misses the - * ffi table and returns "malloc" unchanged on both stages. Pre-fix - * wwstage hardcoded `CALL rt_malloc(SB)` → diverged from cstage's - * ffi_resolve-mediated `CALL malloc(SB)`. */ - { "noscope_intfield", - "package main;\n" - "type holder = struct { n: i32 };\n" - "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", - "malloc" }, - /* Str-field, no @symbol in scope. Confirms the str-field branch - * (the #22 fix) still routes its CALL through ffiresolve, not a - * stray hardcoded literal copied alongside the #22 emit. */ - { "noscope_strfield", - "package main;\n" - "type holder = struct { s: str };\n" - "fn dummy() *holder = { return alloc(holder { s = \"hi\" })!; };\n", - "malloc" }, - /* Two-step let-then-assign. cglet's CALL site (cgenstmt.ww:647) - * is the second cgalloc emit point; this row covers it. */ - { "noscope_let_then_assign", - "package main;\n" - "type holder = struct { n: i32 };\n" - "fn dummy() *holder = {\n" - " let p: *holder = alloc(holder { n = 0 })!;\n" - " p.n = 7;\n" - " return p;\n" - "};\n", - "malloc" }, - /* Explicit @symbol decl in scope. ffiresolve("malloc") → "rt_malloc" - * so both stages emit `CALL rt_malloc(SB)` — positive confirmation - * that the ffi table lookup hits, complementing the noscope rows. */ - { "withsym_intfield", - "package main;\n" - "@symbol(\"rt_malloc\") export fn malloc(n: u64) *void;\n" - "type holder = struct { n: i32 };\n" - "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", - "rt_malloc" }, -}; - -/* asm_disp_row — pins the foff=0 displacement formatting (#24 part B). - * cstage's txt.c prints `(REG)` for zero displacement (cmd/w6c/txt.c - * prAdr D_INDIR); pre-fix wwstage emitted `0(REG)` via an unconditional - * `emitint(fi.foff)`. Each row supplies a `want_line` that MUST appear - * verbatim in BOTH stages' .s output. With the fix (4 emit sites routed - * through `emitdispreg` SSoT) wwstage matches cstage; without it the - * wwstage half fails because `0(REG)` is a different line. */ -struct asm_disp_row { const char *label; const char *src; const char *want_line; }; - -static const struct asm_disp_row asm_disp_rows[] = { - /* str at foff=0. Covers the str-field branch's first store - * (cgenexpr.ww cgalloc, MOVQ AX, (CX)). The companion +8 store - * always has a non-zero displacement so it's not the gate; the - * .ptr store is. */ - { "alloc_str_at_offset0", - "package main;\n" - "type holder = struct { s: str };\n" - "fn dummy() *holder = { return alloc(holder { s = \"x\" })!; };\n", - /* #1/Phase 3: str IS []u8 (24B), so the alloc-str-field store - * routes the heap base through DX (CX now holds the cap) and - * writes 3 words (ptr/len/cap). Was `(CX)` in the 16B world. */ - "\tMOVQ\tAX, (DX)\n" }, - /* str at non-zero foff. Pins that the displacement IS emitted - * (`8(CX)`) when foff != 0 — emitdispreg must not suppress - * non-zero offsets too. Pre-fix and post-fix both pass this; it - * guards against a future over-correction. */ - { "alloc_str_at_nonzero_offset", - "package main;\n" - "type holder = struct { pad: i64, s: str };\n" - "fn dummy() *holder = {\n" - " return alloc(holder { pad = 0, s = \"x\" })!;\n" - "};\n", - /* #1/Phase 3: DX base (str IS []u8, cap in CX); was `8(CX)`. */ - "\tMOVQ\tAX, 8(DX)\n" }, - /* Generic 8-byte field at foff=0 (non-str, non-float path). Covers - * the `else` branch's `MOVQ AX, (BX)` store via fieldstoreop. */ - { "alloc_int_at_offset0", - "package main;\n" - "type holder = struct { n: i64 };\n" - "fn dummy() *holder = { return alloc(holder { n = 42 })!; };\n", - "\tMOVQ\tAX, (BX)\n" }, - /* f64 at foff=0. Covers the float branch's `MOVSD X0, (BX)` - * store; mirrors the int row but routes through the MOVSD emit. */ - { "alloc_f64_at_offset0", - "package main;\n" - "type holder = struct { f: f64 };\n" - "fn dummy() *holder = { return alloc(holder { f = 1.0f64 })!; };\n", - "\tMOVSD\tX0, (BX)\n" }, -}; - -/* asm_call_check — compile via direct w6c / w6c_ww (no `ww build`), - * extract the `CALL\t(SB)` line referencing alloc/rt_malloc from - * each .s file, and verify (a) both stages emit the same line and - * (b) the symbol matches r->want_sym. Returns 0 on success. */ -static int -asm_call_check(const char *bin, const struct asm_row *r, int i) -{ - char src[64], cs[64], ws[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/wcas_asm_%d_%d.ww", getpid(), i); - snprintf(cs, sizeof cs, "/tmp/wcas_asm_%d_%d_c.s", getpid(), i); - snprintf(ws, sizeof ws, "/tmp/wcas_asm_%d_%d_w.s", getpid(), i); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - fputs(r->src, f); - fclose(f); - - int rc = 0; - snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "asm[%s]: w6c errored\n", r->label); - rc = -1; - goto cleanup; - } - snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", - bin, ws, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "asm[%s]: w6c_ww errored\n", r->label); - rc = -1; - goto cleanup; - } - - char want[64]; - snprintf(want, sizeof want, "\tCALL\t%s(SB)\n", r->want_sym); - - char cline[256] = {0}, wline[256] = {0}; - FILE *fc = fopen(cs, "rb"); - FILE *fw = fopen(ws, "rb"); - if (!fc || !fw) { - fprintf(stderr, "asm[%s]: open .s failed\n", r->label); - rc = -1; - } else { - char buf[256]; - while (fgets(buf, sizeof buf, fc)) { - if (strstr(buf, "\tCALL\t") - && (strstr(buf, "alloc(SB)") - || strstr(buf, "rt_malloc(SB)"))) { - strncpy(cline, buf, sizeof cline - 1); - break; - } - } - while (fgets(buf, sizeof buf, fw)) { - if (strstr(buf, "\tCALL\t") - && (strstr(buf, "alloc(SB)") - || strstr(buf, "rt_malloc(SB)"))) { - strncpy(wline, buf, sizeof wline - 1); - break; - } - } - if (cline[0] == '\0' || wline[0] == '\0') { - fprintf(stderr, - "asm[%s]: no CALL alloc line found (c=%d w=%d)\n", - r->label, cline[0] != '\0', wline[0] != '\0'); - rc = -1; - } else if (strcmp(cline, wline) != 0) { - /* strncpy may leave no '\n'; both lines came from - * fgets so they include it. Strip for cleaner err. */ - char *p; - if ((p = strchr(cline, '\n'))) *p = '\0'; - if ((p = strchr(wline, '\n'))) *p = '\0'; - fprintf(stderr, - "asm[%s]: cstage=<%s> wwstage=<%s>\n", - r->label, cline, wline); - rc = -1; - } else if (strcmp(cline, want) != 0) { - char *p; - if ((p = strchr(cline, '\n'))) *p = '\0'; - fprintf(stderr, - "asm[%s]: got=<%s> want=<\tCALL\t%s(SB)>\n", - r->label, cline, r->want_sym); - rc = -1; - } - } - if (fc) fclose(fc); - if (fw) fclose(fw); - -cleanup:; - /* a failing compile can still leave a partial .s — ENOENT is the - * only tolerable unlink error on the never-created legs. */ - int cleanfail = 0; - if (unlink(src) != 0 && errno != ENOENT) { - perror(src); - cleanfail = 1; - } - if (unlink(cs) != 0 && errno != ENOENT) { - perror(cs); - cleanfail = 1; - } - if (unlink(ws) != 0 && errno != ENOENT) { - perror(ws); - cleanfail = 1; - } - if (cleanfail && rc == 0) - rc = -1; - return rc; -} - -/* file_contains — true iff `path`'s contents contain `needle` as a - * substring. Tab/newline-bearing needles match the literal byte sequence - * the .s file holds, so `(CX)\n` does not collide with `0(CX)\n`. */ -static int -file_contains(const char *path, const char *needle) -{ - FILE *f = fopen(path, "rb"); - if (!f) return 0; - fseek(f, 0, SEEK_END); - long sz = ftell(f); - if (sz < 0 || sz > (1<<20)) { fclose(f); return 0; } - fseek(f, 0, SEEK_SET); - char *buf = (char*)malloc((size_t)sz + 1); - if (!buf) { fclose(f); return 0; } - size_t got = fread(buf, 1, (size_t)sz, f); - buf[got] = '\0'; - fclose(f); - int hit = strstr(buf, needle) != NULL; - free(buf); - return hit; -} - -/* asm_disp_check — compile via direct w6c / w6c_ww and verify that the - * literal `r->want_line` appears in BOTH stages' .s output. Pre-fix - * wwstage substituted `0(REG)` for `(REG)` so the foff=0 rows fail on - * the wwstage half; post-fix both halves carry the same text. */ -static int -asm_disp_check(const char *bin, const struct asm_disp_row *r, int i) -{ - char src[64], cs[64], ws[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/wcas_disp_%d_%d.ww", getpid(), i); - snprintf(cs, sizeof cs, "/tmp/wcas_disp_%d_%d_c.s", getpid(), i); - snprintf(ws, sizeof ws, "/tmp/wcas_disp_%d_%d_w.s", getpid(), i); - - FILE *f = fopen(src, "wb"); - if (!f) return -1; - fputs(r->src, f); - fclose(f); - - int rc = 0; - snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "disp[%s]: w6c errored\n", r->label); - rc = -1; - goto cleanup; - } - snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", - bin, ws, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "disp[%s]: w6c_ww errored\n", r->label); - rc = -1; - goto cleanup; - } - - int chit = file_contains(cs, r->want_line); - int whit = file_contains(ws, r->want_line); - if (!chit || !whit) { - fprintf(stderr, - "disp[%s]: want_line missing (cstage=%d wwstage=%d) " - "want=<%s>\n", - r->label, chit, whit, r->want_line); - rc = -1; - } - -cleanup:; - /* a failing compile can still leave a partial .s — ENOENT is the - * only tolerable unlink error on the never-created legs. */ - int cleanfail = 0; - if (unlink(src) != 0 && errno != ENOENT) { - perror(src); - cleanfail = 1; - } - if (unlink(cs) != 0 && errno != ENOENT) { - perror(cs); - cleanfail = 1; - } - if (unlink(ws) != 0 && errno != ENOENT) { - perror(ws); - cleanfail = 1; - } - if (cleanfail && rc == 0) - rc = -1; - return rc; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[1024]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - int total = 0, fail = 0; - - /* Asm CALL-line check via direct w6c / w6c_ww. Gated on w6c_ww - * existence — when wwstage isn't built yet the cstage half alone - * can't catch the divergence. */ - char w6c_ww[1024]; - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - if (access(w6c_ww, X_OK) == 0) { - int an = (int)(sizeof asm_rows / sizeof asm_rows[0]); - for (int i = 0; i < an; i++) { - total++; - if (asm_call_check(bin, &asm_rows[i], i) != 0) - fail++; - } - int dn = (int)(sizeof asm_disp_rows / sizeof asm_disp_rows[0]); - for (int i = 0; i < dn; i++) { - total++; - if (asm_disp_check(bin, &asm_disp_rows[i], i) != 0) - fail++; - } - } else { - fprintf(stderr, - "cgalloc_str_field: skip asm rows (no %s)\n", w6c_ww); - } - - if (fail) { - fprintf(stderr, - "cgalloc_str_field: %d/%d fixtures failed\n", - fail, total); - return 1; - } - printf("cgalloc_str_field: %d/%d ok\n", total, total); - return 0; -} diff --git a/test/wcc/930_free_noop_run.c b/test/wcc/930_free_noop_run.c deleted file mode 100644 index a4b16794..00000000 --- a/test/wcc/930_free_noop_run.c +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 930_free_noop_run — the free() builtin compiles to a documented - * NO-OP (#27). - * - * ww has no free by design (rt/alloc.s:30 — the bump allocator cannot - * reclaim a mid-chunk pointer; process exit does; drop-amalloc). The - * pre-#27 lowering emitted CALL ffi_resolve("free") — an undefined - * reference at w6l unless an @symbol decl happened to be in scope — - * and wwstage had no free arm at all (generic CALL free, same link - * failure). Post-#27 both stages evaluate the operand for side - * effects (Hare's free(expr) evaluates expr — regex fold-2b calls - * free() at 4+ sites; finish() ports verbatim) and emit nothing else. - * - * Rows pin: free of a plain local pointer with deref-after-free (the - * no-op's documented leak semantics — the pointee stays valid), free - * of a struct field, free of a CALL operand twice (the side effect - * must run per call — a global counter observes both evaluations), - * free in a 1M-iteration loop (the no-op must not accumulate stack - * damage — a leaked push per free would segfault), - * and the Hare-port shape alloc-then-free round-trip (import rt; - * *i64 — i64 sidesteps the pre-existing unrelated cs≠ww alloc(value) - * size divergence on narrow pointee types, filed separately). The - * 2-arg `os.free(p, n)` public API is NOT intercepted (the builtin - * gate requires exactly one arg) and keeps resolving via its - * @symbol("rt_free") decl — covered by the lisp example / stdlib - * suites, not re-pinned here. - * - * Runtime behavior is carried by the r930_free_* wwfixtures. This wrapper - * retains w6c vs w6c_ww byte identity plus the negative assembly assertion - * that no `free` symbol survives. - */ -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - int want; -}; - -static const struct row rows[] = { - /* free(ident): compiles, links, and the pointee stays valid — - * the no-op's documented semantics (a harmless leak, never a - * dangling pointer). */ - { "free_local_ptr_deref_after", - "export fn main() i32 = {\n" - " let x: i32 = 5;\n" - " let p: *i32 = &x;\n" - " free(p);\n" - " if (*p != 5) { return 1; };\n" - " return 0;\n" - "};\n", - 0 }, - /* free(field): the N_DOT operand shape (regex finish() frees - * re.insts / re.charsets through a pointer field). */ - { "free_struct_field", - "type holder = struct { p: *i32, n: i32 };\n" - "export fn main() i32 = {\n" - " let x: i32 = 3;\n" - " let h: holder = holder { p = &x, n = 4 };\n" - " free(h.p);\n" - " if (h.n != 4) { return 2; };\n" - " return 0;\n" - "};\n", - 0 }, - /* free(f(...)): Hare evaluates free's operand, so the call must - * run — twice freed, twice bumped. Pins the - * evaluate-for-side-effects half of the lowering (emitting - * nothing at all would leave g at 0). */ - { "free_call_operand_effects", - "let g: i32 = 0;\n" - "fn bump(p: *i32) *i32 = {\n" - " g = g + 2;\n" - " return p;\n" - "};\n" - "export fn main() i32 = {\n" - " let x: i32 = 1;\n" - " free(bump(&x));\n" - " free(bump(&x));\n" - " if (g != 4) { return 3; };\n" - " return 0;\n" - "};\n", - 0 }, - /* free() in a hot loop: the no-op must not accumulate stack - * damage — a leaked 8B push per free would blow the 8MiB stack - * long before 1M iterations (segfault, not a wrong exit code). */ - { "free_loop_no_stack_damage", - "let g: i32 = 0;\n" - "fn bump(p: *i32) *i32 = {\n" - " g = g + 1;\n" - " return p;\n" - "};\n" - "export fn main() i32 = {\n" - " let x: i32 = 1;\n" - " let i: i32 = 0;\n" - " for (i < 1000000) {\n" - " free(bump(&x));\n" - " i += 1;\n" - " };\n" - " if (g != 1000000) { return 5; };\n" - " return 0;\n" - "};\n", - 0 }, - /* The verbatim Hare-port shape: alloc then free, deref after. - * Pre-#27 this was THE w6l undefined-reference repro. */ - { "free_alloc_roundtrip", - "import rt;\n" - "export fn main() i32 = {\n" - " let p: *i64 = alloc(11i64)!;\n" - " free(p);\n" - " if (*p != 11i64) { return 4; };\n" - " return 0;\n" - "};\n", - 0 }, -}; - -static const char *g_bin; - -static int -compile_s(const char *tool, const char *src, const char *outpath) -{ - char cmd[1024]; - snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2>&1", - g_bin, tool, src, outpath); - return runwait(cmd); -} - -static int -file_eq(const char *a, const char *b) -{ - char cmd[1024]; - snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b); - return runwait(cmd) == 0; -} - -static int -has_free_sym(const char *s_path) -{ - char cmd[1024]; - snprintf(cmd, sizeof cmd, "grep -q 'free' %s", s_path); - return runwait(cmd) == 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - static char absbin[512]; - if (bin[0] != '/') { - char cwd[256]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - g_bin = bin; - - int n = (int)(sizeof rows / sizeof rows[0]); - int total = 0, fail = 0; - for (int i = 0; i < n; i++) { - const struct row *r = &rows[i]; - /* #8: source, .sepwork scratch, both driver binaries and the - * byte-id .s dumps all live under one tmpdir; rm -rf on every - * exit path. Symbol mangling is package/import-derived, not - * entry-path derived, so the in-tmpdir source keeps the cs==ww - * .s identical. Temp names must avoid the substring 'free': - * has_free_sym greps the whole .s, so an embedded path would - * false-fail every row. */ - char tmpdir[64], src[128], cs_s[128], ww_s[128], rmcmd[160]; - snprintf(tmpdir, sizeof tmpdir, "/tmp/noopfr_%d_d_%d", - getpid(), i); - /* an unowned path (stale dir, full /tmp) must not be compiled - * in — or rm -rf'd — below. */ - if (mkdir(tmpdir, 0755) != 0) { - perror(tmpdir); - return 1; - } - snprintf(src, sizeof src, "%s/noopfr_%d_%d.ww", - tmpdir, getpid(), i); - snprintf(cs_s, sizeof cs_s, "%s/cs.s", tmpdir); - snprintf(ww_s, sizeof ww_s, "%s/ww.s", tmpdir); - snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); - FILE *f = fopen(src, "wb"); - if (!f) { - perror(src); - if (runwait(rmcmd) != 0) - fprintf(stderr, "free_noop_run: cleanup " - "%s failed\n", tmpdir); - return 1; - } - fputs("package main;\n\n", f); - fputs(r->src, f); - fclose(f); - - total++; - int rowfail = 0; - int cs_rc = compile_s("w6c", src, cs_s); - int ww_rc = compile_s("w6c_ww", src, ww_s); - if (cs_rc != 0 || ww_rc != 0) { - fprintf(stderr, "FAIL row[%s]: compile rc cs=%d " - "ww=%d\n", r->label, cs_rc, ww_rc); - fail++; rowfail = 1; - } else { - if (!file_eq(cs_s, ww_s)) { - fprintf(stderr, "FAIL row[%s]: cs != ww .s\n", - r->label); - fail++; rowfail = 1; - } - if (has_free_sym(cs_s)) { - fprintf(stderr, "FAIL row[%s]: 'free' survives in " - "the .s — lowering is not a no-op\n", r->label); - fail++; rowfail = 1; - } - } - /* a silent cleanup failure must fail an otherwise-passing - * row without masking its own diagnostic. */ - if (runwait(rmcmd) != 0) { - fprintf(stderr, "FAIL row[%s]: cleanup %s failed\n", - r->label, tmpdir); - if (!rowfail) - fail++; - } - } - if (fail) { - fprintf(stderr, "free_noop_run: %d/%d rows failed\n", - fail, total); - return 1; - } - printf("free_noop_run: %d assembly rows ok\n", total); - return 0; -} diff --git a/test/wcc/946_structparam_run.c b/test/wcc/946_structparam_run.c deleted file mode 100644 index 3ae36f70..00000000 --- a/test/wcc/946_structparam_run.c +++ /dev/null @@ -1,300 +0,0 @@ -/* - * 946_structparam_run — byte-id + asm-pattern net for #165, the - * float-bearing struct-PARAM SysV ABI (the param twin of #171's struct - * RETURN, and the struct counterpart of #163's per-element tuple PARAM). - * - * THE BUG (#165, cs==ww but SysV-non-conformant on master): struct args - * ARE handled but GP-ONLY — the cgcall pop drained every struct eightbyte - * into the INTEGER arg regs (DI/SI/..) and the callee cgfnparams receive - * MOVQ'd them back from the same GP regs. So a `struct { a: f64, b: f64 }` - * arg landed in DI/SI instead of X0/X1. For a pure INTERNAL ww call (both - * ends compiled by the same stage) the f64 bits still round-trip through - * the GP regs intact, so the runtime VALUE was correct AND both stages - * were symmetric-GP — the cs==ww gate and a value check are therefore - * NECESSARY-NOT-SUFFICIENT here (cf. 907_f32arg_run, same situation). The - * genuine defect is SysV register-CLASS conformance, observable only in - * the emitted asm (and at a real ABI boundary). The discriminating - * dimension is therefore the asm pattern below. - * - * THE FIX: per-EIGHTBYTE SysV classification (structs classify per - * eightbyte, unlike #163's per-element tuples). Each 8-byte eightbyte that - * is a lone f64 rides the SSE arg cursor (X0..X7); a pure-INTEGER eightbyte - * rides the INTEGER cursor (DI/SI/..). SEND (cgcall pop) MOVSD's the float - * eightbyte off (SP) into the next XMM; RECV (cgfnparams) MOVSD's the XMM - * into the param slot. Symmetric across cstage (cmd/w6c/cgen.c - * struct_float_class + the cgcall pop arm + cgfnparams) and wwstage - * (cgenutil.ww structfloatclass + cgenexpr.ww cgcall pop + cgendecl.ww). - * - * THE FALLBACK GATE: only a struct whose every eightbyte is pure-INT or a - * lone f64 qualifies. An f32 field packs two f32 into ONE SSE eightbyte - * (needs packing — deferred #165b); a naive per-field reuse would WRONGLY - * route its two f32 to two SSE regs. struct{f32,f32} therefore STAYS on - * the GP transport (correct + byte-identical current behavior). The - * f32f32 row asserts BOTH the correct round-tripped value AND the ABSENCE - * of an SSE receive, proving the gate caught it. - * - * The end-to-end values now live in r946_structparam_* fixtures. This carrier - * retains the two observations unavailable to the fixture grammar: - * (a) w6c vs w6c_ww `.s` cmp — rule-10 byte-id (both stages identical). - * (b) asm-pattern on the CALLEE function's receive: a lone-f64 eightbyte - * is received `MOVSD Xn, -off(BP)` (XMM -> slot); a GP eightbyte is - * `MOVQ DI, ...`. The marker is scoped to the callee `main.use` text - * (main's float-literal init also emits `MOVSD X0, -off(BP)`, so a - * whole-file grep would not discriminate). PRESENT for the SSE-routed - * rows, ABSENT for the GP / fallback rows. A pre-fix (or GP-regressed) - * build receives every struct eightbyte via MOVQ -> marker absent, - * which is exactly what the pattern check catches. - */ -#include -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - int want_sse; /* 1 = callee must receive an f64 eightbyte via XMM */ -}; - -static const struct row rows[] = { - /* HEADLINE — struct{f64,f64}. Both eightbytes lone f64 -> X0,X1. The - * callee receives `MOVSD X0, slot` + `MOVSD X1, slot`. s.a+s.b = 8.0. */ - { "f64f64_arg", - "package main;\n" - "type pff = struct { a: f64, b: f64 };\n" - "fn use(s: pff) f64 = { return s.a + s.b; };\n" - "export fn main() i32 = {\n" - "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n" - "\tif (use(s) != 8.0) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* struct{f64,i64} — eb0 lone f64 (X0), eb1 pure-INT (DI). Class is - * independent of eightbyte position. (s.a:i64)+s.b = 3+5 = 8. */ - { "f64i64_arg", - "package main;\n" - "type pfi = struct { a: f64, b: i64 };\n" - "fn use(s: pfi) i64 = { return (s.a: i64) + s.b; };\n" - "export fn main() i32 = {\n" - "\tlet s: pfi = pfi { a = 3.0, b = 5 };\n" - "\tif (use(s) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* struct{i64,f64} — order-swap: eb0 pure-INT (DI), eb1 lone f64 (X0, - * the first float still gets X0). Confirms the float lands in the next - * XMM regardless of position. s.a + (s.b:i64) = 3+5 = 8. */ - { "i64f64_arg", - "package main;\n" - "type pif = struct { a: i64, b: f64 };\n" - "fn use(s: pif) i64 = { return s.a + (s.b: i64); };\n" - "export fn main() i32 = {\n" - "\tlet s: pif = pif { a = 3, b = 5.0 };\n" - "\tif (use(s) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* GP REGRESSION — struct{i64,i64}: every eightbyte pure-INT, no float - * to route, so it STAYS on the GP transport unchanged (the in-tree - * byte-id case). Callee receives via MOVQ DI/SI -> no SSE marker. */ - { "i64i64_arg", - "package main;\n" - "type pii = struct { a: i64, b: i64 };\n" - "fn use(s: pii) i64 = { return s.a + s.b; };\n" - "export fn main() i32 = {\n" - "\tlet s: pii = pii { a = 3, b = 5 };\n" - "\tif (use(s) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 0 }, - /* FALLBACK GATE — struct{f32,f32}: two f32 pack into ONE SSE eightbyte - * (8B struct). The gate rejects f32, so the struct stays GP-routed - * (received via MOVQ DI). The value still round-trips (the 8B MOVQ - * carries both f32) -> (s.a:i64)+(s.b:i64) = 3+5 = 8. Asserts BOTH the - * correct value AND no SSE receive, proving the gate caught it (NOT - * routed to 2 SSE regs). The 2-f32-per-eightbyte packing is #165b. */ - { "f32f32_arg_fallback", - "package main;\n" - "type pf32 = struct { a: f32, b: f32 };\n" - "fn use(s: pf32) i64 = { return (s.a: i64) + (s.b: i64); };\n" - "export fn main() i32 = {\n" - "\tlet s: pf32 = pf32 { a = 3.0f32, b = 5.0f32 };\n" - "\tif (use(s) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 0 }, - /* CURSOR INDEPENDENCE — i64 scalar + struct{f64,f64}. n->DI (INTEGER - * cursor), s.a->X0, s.b->X1 (SSE cursor), independent counters. - * n + s.a + s.b = 2+3+5 = 10. SSE receive present. */ - { "scalar_plus_f64f64", - "package main;\n" - "type pff = struct { a: f64, b: f64 };\n" - "fn use(n: i64, s: pff) i64 = {\n" - "\treturn n + (s.a: i64) + (s.b: i64);\n" - "};\n" - "export fn main() i32 = {\n" - "\tlet s: pff = pff { a = 3.0, b = 5.0 };\n" - "\tif (use(2, s) != 10) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - { NULL, NULL, 0 } -}; - -static int -slurp_eq(const char *a, const char *b) -{ - FILE *fa = fopen(a, "rb"); - FILE *fb = fopen(b, "rb"); - if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } - int rc = 0; - for (;;) { - int ca = fgetc(fa); - int cb = fgetc(fb); - if (ca != cb) { rc = -1; break; } - if (ca == EOF) break; - } - fclose(fa); fclose(fb); - return rc; -} - -/* Does the callee `main.use` receive an f64 eightbyte via an XMM reg? - * Isolate that function's text (its receive of an SSE eightbyte is the - * only `MOVSD Xn, -off(BP)` store-to-slot in it; the body of these probes - * stores no f64 local, and main's float-literal init — also a - * `MOVSD X0, -off(BP)` — lives in a different function), then look for an - * XMM-source store to a negative BP offset. Returns 1/0, or -1 on error. */ -static int -callee_receives_sse(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - static char buf[1 << 18]; - size_t n = fread(buf, 1, sizeof buf - 1, f); - /* a .s larger than the buffer would silently drop the region the - * marker scan needs — refuse instead of scanning a truncated tail. */ - if (n == sizeof buf - 1 && fgetc(f) != EOF) { - fclose(f); - return -1; - } - fclose(f); - buf[n] = '\0'; - - char *start = strstr(buf, "TEXT main.use"); - if (!start) return -1; - char *end = strstr(start + 1, "\nTEXT "); - if (end) *end = '\0'; - /* `MOVSD\tX0, -` is the receive of the first (or only) f64 eightbyte; - * a GP-routed struct never emits it (MOVQ DI instead). */ - return strstr(start, "MOVSD\tX0, -") != NULL ? 1 : 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[2200]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[2300], w6c_ww[2300]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - if (access(w6c_ww, X_OK) != 0) { - fprintf(stderr, "structparam: w6c_ww missing — cannot run the " - "cs==ww byte-id gate\n"); - return 1; - } - - int n = 0, fail = 0; - for (int i = 0; rows[i].src; i++, n++) { - /* The source and both .s files live under one tmpdir so - * the compiler's path-derived .sepwork scratch stays inside it; - * a single `rm -rf` at the end of all phases reclaims it. */ - char tmpdir[64], src[128]; - char cs_s[128], ws_s[128], rmcmd[160]; - snprintf(tmpdir, sizeof tmpdir, "/tmp/wwstp_%d_d_%d", - getpid(), i); - if (mkdir(tmpdir, 0755) != 0) { - /* not owned — must not rm a path we failed to create */ - perror(tmpdir); - fail++; - continue; - } - snprintf(src, sizeof src, "%s/wwstp_%d_%d.ww", - tmpdir, getpid(), i); - snprintf(cs_s, sizeof cs_s, "%s/wwstp_%d_%d_cs.s", - tmpdir, getpid(), i); - snprintf(ws_s, sizeof ws_s, "%s/wwstp_%d_%d_ww.s", - tmpdir, getpid(), i); - snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); - - FILE *f = fopen(src, "wb"); - if (f == NULL) { fail++; goto rowdone; } - fputs(rows[i].src, f); - fclose(f); - - char cmd[4096]; - - /* cs==ww byte-id gate: emit .s from both stages, cmp. */ - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", - w6c, cs_s, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); - fail++; goto rowdone; - } - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", - w6c_ww, ws_s, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c_ww failed\n", - rows[i].label); - fail++; goto rowdone; - } - if (slurp_eq(cs_s, ws_s) != 0) { - fprintf(stderr, - "row[%s]: cstage/wwstage .s DIFFER (rule-10 " - "byte-id violation)\n", rows[i].label); - fail++; - } - - /* byte-id proves ww_s mirrors cs_s, so checking cs_s suffices. */ - int sse = callee_receives_sse(cs_s); - if (sse < 0) { - fprintf(stderr, "row[%s]: cannot scan callee .s\n", - rows[i].label); - fail++; - } else if (sse != rows[i].want_sse) { - fprintf(stderr, - "row[%s]: callee SSE receive %s, want %s " - "(register-class discriminator)\n", - rows[i].label, sse ? "present" : "absent", - rows[i].want_sse ? "present" : "absent"); - fail++; - } -rowdone: - /* a failed rm must fail the carrier, not leak silently. */ - if (runwait(rmcmd) != 0) { - fprintf(stderr, "row[%s]: cleanup rm failed\n", - rows[i].label); - fail++; - } - } - - if (fail) { - fprintf(stderr, "%d/%d struct-param tests failed\n", fail, n); - return 1; - } - printf("structparam: %d/%d ok (cs==ww byte-id + " - "SSE-receive asm)\n", n, n); - return 0; -} diff --git a/test/wcc/946_structret_run.c b/test/wcc/946_structret_run.c deleted file mode 100644 index 81f50729..00000000 --- a/test/wcc/946_structret_run.c +++ /dev/null @@ -1,312 +0,0 @@ -/* - * 946_structret_run — byte-id + asm-pattern net for #171a, the - * float-bearing struct-RETURN SysV ABI (the return twin of #165's struct - * PARAM, and the struct counterpart of #164's per-element tuple RETURN). - * - * THE BUG (#171a, cs==ww but SysV-non-conformant on master): a <=16B - * struct returned by value materialised into a zero-padded 24B scratch and - * then loaded unconditionally into the INTEGER return regs (AX/DX/CX); the - * let-init receive MOVQ'd them back from the same GP regs. So a - * `struct { a: f64, b: f64 }` return rode AX/DX instead of X0/X1. For a - * pure INTERNAL ww call (both ends compiled by the same stage) the f64 - * bits still round-trip through the GP regs intact, so the runtime VALUE - * was correct AND both stages were symmetric-GP — the cs==ww gate and a - * value check are therefore NECESSARY-NOT-SUFFICIENT here (cf. - * 946_structparam_run, the param twin). The genuine defect is SysV - * register-CLASS conformance, observable only in the emitted asm (and at a - * real ABI boundary). The discriminating dimension is the asm pattern. - * - * THE FIX: per-EIGHTBYTE SysV classification (reusing struct_float_class / - * structfloatclass verbatim from #165). Each 8-byte eightbyte that is a - * lone f64 rides the SSE return cursor (X0,X1); a pure-INTEGER eightbyte - * rides the INTEGER return cursor (AX,DX) — on INDEPENDENT counters, so a - * float lands in the next XMM regardless of its positional eightbyte. SEND - * (cgreturn) loads the float eightbyte off the scratch into the next XMM; - * RECV (let-init) stores the XMM into the destination slot. Symmetric - * across cstage (cmd/w6c/cgen.c) and wwstage (selfhost/cmd/wcc/ - * cgenstmt.ww). - * - * THE FALLBACK GATE: only a struct whose every eightbyte is pure-INT or a - * lone f64 qualifies. An f32 field packs two f32 into ONE SSE eightbyte - * (needs packing — deferred #171b); struct{f32,f32} therefore STAYS on the - * GP transport (correct + byte-identical current behavior). The f32f32 row - * asserts BOTH the correct round-tripped value AND the ABSENCE of an SSE - * return/receive, proving the gate caught it. - * - * The end-to-end values now live in r946_structret_* fixtures. This carrier - * retains the two observations unavailable to the fixture grammar: - * (a) w6c vs w6c_ww `.s` cmp — rule-10 byte-id (both stages identical). - * (b) asm-pattern: the producer `main.mk` returns a lone-f64 eightbyte - * as `MOVSD (BP), Xn` (scratch -> XMM; a GP eightbyte is - * `MOVQ (BP), AX`), and the consumer `main` receives it as - * `MOVSD Xn, -off(BP)` (XMM -> slot). The producer marker is scoped - * to `TEXT main.mk` (its scratch fill loads f64 literals via - * `MOVSD (SP), X0`, never `(BP), X0`); the consumer marker to - * `TEXT main,` (a GP recv MOVQ's instead). PRESENT for the SSE-routed - * rows, ABSENT for the GP / fallback rows. A pre-fix (or GP-regressed) - * build routes every eightbyte via AX/DX/CX -> both markers absent, - * which is exactly what the pattern check catches. - */ -#include -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -struct row { - const char *label; - const char *src; - int want_sse; /* 1 = struct return rides an XMM eightbyte */ -}; - -static const struct row rows[] = { - /* HEADLINE — struct{f64,f64}. Both eightbytes lone f64 -> X0,X1. mk - * returns `MOVSD ..,X0` + `MOVSD ..,X1`; main receives `MOVSD X0,..` - * + `MOVSD X1,..`. s.a + s.b = 8.0. */ - { "f64f64_ret", - "package main;\n" - "type pff = struct { a: f64, b: f64 };\n" - "fn mk() pff = { return pff { a = 3.0, b = 5.0 }; };\n" - "export fn main() i32 = {\n" - "\tlet s: pff = mk();\n" - "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* CURSOR INDEPENDENCE — struct{f64,i32}: eb0 lone f64 (X0), eb1 - * pure-INT (the i32 rides AX, NOT DX — the GP cursor starts at 0 - * regardless of the float ahead of it). (s.a:i64)+(s.b:i64) = 8. */ - { "f64i32_ret", - "package main;\n" - "type pfi = struct { a: f64, b: i32 };\n" - "fn mk() pfi = { return pfi { a = 3.0, b = 5 }; };\n" - "export fn main() i32 = {\n" - "\tlet s: pfi = mk();\n" - "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* ORDER SWAP — struct{i64,f64}: eb0 pure-INT (AX), eb1 lone f64 (X0, - * the first float still gets X0). Confirms the float lands in the - * next XMM regardless of position. s.a + (s.b:i64) = 8. */ - { "i64f64_ret", - "package main;\n" - "type pif = struct { a: i64, b: f64 };\n" - "fn mk() pif = { return pif { a = 3, b = 5.0 }; };\n" - "export fn main() i32 = {\n" - "\tlet s: pif = mk();\n" - "\tif (s.a + (s.b: i64) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 1 }, - /* GP REGRESSION — struct{i64,i64}: every eightbyte pure-INT, no float - * to route, so it STAYS on the AX/DX/CX transport unchanged (the - * in-tree byte-id case). No SSE return/receive marker. */ - { "i64i64_ret", - "package main;\n" - "type pii = struct { a: i64, b: i64 };\n" - "fn mk() pii = { return pii { a = 3, b = 5 }; };\n" - "export fn main() i32 = {\n" - "\tlet s: pii = mk();\n" - "\tif (s.a + s.b != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 0 }, - /* FALLBACK GATE — struct{f32,f32}: two f32 pack into ONE SSE eightbyte - * (8B struct). The gate rejects f32, so the struct stays GP-routed - * (returned/received via AX). The value still round-trips (the 8B MOVQ - * carries both f32) -> (s.a:i64)+(s.b:i64) = 8. Asserts BOTH the - * correct value AND no SSE return/receive, proving the gate caught it - * (NOT routed to an SSE reg). The 2-f32-per-eightbyte packing is - * #171b. */ - { "f32f32_ret_fallback", - "package main;\n" - "type pf32 = struct { a: f32, b: f32 };\n" - "fn mk() pf32 = { return pf32 { a = 3.0f32, b = 5.0f32 }; };\n" - "export fn main() i32 = {\n" - "\tlet s: pf32 = mk();\n" - "\tif ((s.a: i64) + (s.b: i64) != 8) { return 1; };\n" - "\treturn 0;\n" - "};\n", 0 }, - { NULL, NULL, 0 } -}; - -static int -slurp_eq(const char *a, const char *b) -{ - FILE *fa = fopen(a, "rb"); - FILE *fb = fopen(b, "rb"); - if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } - int rc = 0; - for (;;) { - int ca = fgetc(fa); - int cb = fgetc(fb); - if (ca != cb) { rc = -1; break; } - if (ca == EOF) break; - } - fclose(fa); fclose(fb); - return rc; -} - -/* Isolate one function's TEXT block: from `head` to the next `\nTEXT `. - * Returns a malloc'd copy the caller frees, or NULL. */ -static char * -isolate(const char *buf, const char *head) -{ - const char *start = strstr(buf, head); - if (!start) return NULL; - const char *end = strstr(start + 1, "\nTEXT "); - size_t len = end ? (size_t)(end - start) : strlen(start); - char *out = malloc(len + 1); - if (!out) return NULL; - memcpy(out, start, len); - out[len] = '\0'; - return out; -} - -/* Does the struct return ride an XMM on BOTH ends? - * - producer `main.mk`: a lone-f64 eightbyte is loaded scratch -> XMM as - * `MOVSD\t(BP), X0` (the only `(BP), X0` in mk; its f64-literal - * fill loads via `MOVSD (SP), X0`). A GP-routed return is `MOVQ ..,AX`. - * - consumer `main`: the eightbyte is stored XMM -> slot as - * `MOVSD\tX0, -(BP)`. A GP recv is `MOVQ AX, ..`. - * Returns 1 iff both markers present, 0 iff both absent, -1 on a split or - * scan error (which would itself be a bug). */ -static int -ret_rides_sse(const char *path) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - static char buf[1 << 18]; - size_t n = fread(buf, 1, sizeof buf - 1, f); - /* a .s larger than the buffer would silently drop the region the - * marker scan needs — refuse instead of scanning a truncated tail. */ - if (n == sizeof buf - 1 && fgetc(f) != EOF) { - fclose(f); - return -1; - } - fclose(f); - buf[n] = '\0'; - - char *mk = isolate(buf, "TEXT main.mk"); - char *mn = isolate(buf, "TEXT main,"); - if (!mk || !mn) { free(mk); free(mn); return -1; } - - int send = strstr(mk, "(BP), X0") != NULL; - int recv = strstr(mn, "MOVSD\tX0, -") != NULL; - free(mk); free(mn); - if (send != recv) return -1; /* SEND and RECV must agree */ - return send; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[2200]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char w6c[2300], w6c_ww[2300]; - snprintf(w6c, sizeof w6c, "%s/w6c", bin); - snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - if (access(w6c_ww, X_OK) != 0) { - fprintf(stderr, "structret: w6c_ww missing — cannot run the " - "cs==ww byte-id gate\n"); - return 1; - } - - int n = 0, fail = 0; - for (int i = 0; rows[i].src; i++, n++) { - /* The source and both .s files live under one tmpdir so the - * compiler's .sepwork scratch (derived from the src path) lands - * inside it; a single rm -rf at the end clears every phase. */ - char tmpdir[64]; - snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsrt_%d_d_%d", - getpid(), i); - if (mkdir(tmpdir, 0755) != 0) { - /* not owned — must not rm a path we failed to create */ - perror(tmpdir); - fail++; - continue; - } - - char src[128], cs_s[128], ws_s[128], rmcmd[160]; - snprintf(src, sizeof src, "%s/wwsrt_%d_%d.ww", - tmpdir, getpid(), i); - snprintf(cs_s, sizeof cs_s, "%s/wwsrt_%d_%d_cs.s", - tmpdir, getpid(), i); - snprintf(ws_s, sizeof ws_s, "%s/wwsrt_%d_%d_ww.s", - tmpdir, getpid(), i); - snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); - - FILE *f = fopen(src, "wb"); - if (f == NULL) { fail++; goto rowdone; } - fputs(rows[i].src, f); - fclose(f); - - char cmd[4096]; - - /* cs==ww byte-id gate: emit .s from both stages, cmp. */ - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", - w6c, cs_s, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); - fail++; goto rowdone; - } - snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", - w6c_ww, ws_s, src); - if (runwait(cmd) != 0) { - fprintf(stderr, "row[%s]: w6c_ww failed\n", - rows[i].label); - fail++; goto rowdone; - } - if (slurp_eq(cs_s, ws_s) != 0) { - fprintf(stderr, - "row[%s]: cstage/wwstage .s DIFFER (rule-10 " - "byte-id violation)\n", rows[i].label); - fail++; - } - - /* byte-id proves ww_s mirrors cs_s, so checking cs_s suffices. */ - int sse = ret_rides_sse(cs_s); - if (sse < 0) { - fprintf(stderr, "row[%s]: cannot scan .s (or SEND/RECV " - "disagree)\n", rows[i].label); - fail++; - } else if (sse != rows[i].want_sse) { - fprintf(stderr, - "row[%s]: struct return SSE %s, want %s " - "(register-class discriminator)\n", - rows[i].label, sse ? "present" : "absent", - rows[i].want_sse ? "present" : "absent"); - fail++; - } -rowdone: - /* a failed rm must fail the carrier, not leak silently. */ - if (runwait(rmcmd) != 0) { - fprintf(stderr, "row[%s]: cleanup rm failed\n", - rows[i].label); - fail++; - } - } - - if (fail) { - fprintf(stderr, "%d/%d struct-return tests failed\n", fail, n); - return 1; - } - printf("structret: %d/%d ok (cs==ww byte-id + " - "SSE-return asm)\n", n, n); - return 0; -} diff --git a/test/wcc/989_m1mangle_sym.c b/test/wcc/989_m1mangle_sym.c deleted file mode 100644 index 74e45d01..00000000 --- a/test/wcc/989_m1mangle_sym.c +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 989_m1mangle_sym — M1 (#22) symbol-name proof. Builds a fixture that - * imports the real nested DIRECTORY package `encoding.utf8` to `.s` on - * BOTH stages and asserts the emitted symbol table: - * - * needle | want | proves - * --------------------------+------+----------------------------------- - * "encoding.utf8.runesz" | yes | nested pkg path-mangles (the M1 - * | | DELTA: leaf `utf8` → path - * | | `encoding.utf8`) - * "TEXT main," | yes | root entry stays BARE (#32) - * "TEXT utf8.runesz," | no | the pre-M1 leaf-only mangle is gone - * - * Plus cstage.s == wwstage.s byte-for-byte on the re-baselined names - * (rule-10 — the cs==ww gate over the new symbols). - */ -#include -#include -#include -#include -#include - -static int -runwait(const char *cmd) -{ - int rc = system(cmd); - if (rc == -1) return -1; - if (WIFEXITED(rc)) return WEXITSTATUS(rc); - return -1; -} - -static int -file_has(const char *path, const char *needle) -{ - FILE *f = fopen(path, "rb"); - if (!f) return -1; - char line[4096]; - int found = 0; - while (fgets(line, sizeof line, f)) { - if (strstr(line, needle)) { found = 1; break; } - } - fclose(f); - return found; -} - -struct check { const char *needle; int want; }; - -static const struct check checks[] = { - { "encoding.utf8.runesz", 1 }, - { "TEXT main,", 1 }, - { "TEXT utf8.runesz,", 0 }, -}; - -/* build_s — build `src` via `driver`, leaving the unit asm at `.s`; - * returns 0 on success. - * #93 sep layout: `-o ` splits the asm across - * .sepwork/.s (root in __root.s, the imported encoding.utf8 in - * encoding.utf8.s). The mangled `encoding.utf8.runesz` definition lives in - * the per-pkg .s and `TEXT main,` in __root.s, so we CONCAT all the - * per-unit .s (sorted glob order is deterministic) into a single .s - * for the needle scans + the rule-10 cmp downstream. */ -static int -build_s(const char *driver, const char *src, const char *stem) -{ - char cmd[1024]; - snprintf(cmd, sizeof cmd, - "timeout 180 %s build -S -o '%s' '%s' " - "2>/dev/null && cat '%s.sepwork/'*.s > '%s.s'", - driver, stem, src, stem, stem); - return runwait(cmd); -} - -static int -checkall(const char *name, const char *sp) -{ - int fail = 0; - for (int i = 0; i < (int)(sizeof checks / sizeof checks[0]); i++) { - int got = file_has(sp, checks[i].needle); - if (got != checks[i].want) { - fprintf(stderr, "m1mangle_sym[%s]: '%s' present=%d want=%d\n", - name, checks[i].needle, got, checks[i].want); - fail++; - } - } - return fail; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[1024]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - char cdrv[1024], wdrv[1024]; - snprintf(cdrv, sizeof cdrv, "%s/ww", bin); - snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); - - char src[64]; - snprintf(src, sizeof src, "/tmp/m1sym_%d.ww", getpid()); - FILE *f = fopen(src, "wb"); - if (!f) return 1; - fputs("package main;\n" - "import encoding.utf8;\n" - "export fn main() int = { return utf8.runesz('A'): int; };\n", f); - fclose(f); - - int fail = 0; - - char cstem[64], cs[80]; - snprintf(cstem, sizeof cstem, "/tmp/m1sym_c_%d", getpid()); - snprintf(cs, sizeof cs, "%s.s", cstem); - if (build_s(cdrv, src, cstem) != 0) { - fprintf(stderr, "m1mangle_sym: cstage build failed\n"); - fail++; - } else { - fail += checkall("cstage", cs); - } - - int have_ww = (access(wdrv, X_OK) == 0); - char wstem[64], ws[80]; - snprintf(wstem, sizeof wstem, "/tmp/m1sym_w_%d", getpid()); - snprintf(ws, sizeof ws, "%s.s", wstem); - if (have_ww) { - if (build_s(wdrv, src, wstem) != 0) { - fprintf(stderr, "m1mangle_sym: wwstage build failed\n"); - fail++; - } else { - fail += checkall("wwstage", ws); - /* rule-10: cs.s == ww.s on the new mangled names. */ - char cmp[256]; - snprintf(cmp, sizeof cmp, "cmp -s '%s' '%s'", cs, ws); - if (runwait(cmp) != 0) { - fprintf(stderr, "m1mangle_sym: cstage.s != " - "wwstage.s (byte-id break)\n"); - fail++; - } - } - } else { - fprintf(stderr, "m1mangle_sym: skip wwstage (no ww_ww)\n"); - } - - char cmd[512]; - /* #93: -rf also drops the per-stem .sepwork scratch. Exact owned - * paths only (`build -S` writes solely under .sepwork/, plus - * build_s's .s concat) — no prefix glob; a failed rm must fail - * the carrier, not leak silently. */ - snprintf(cmd, sizeof cmd, - "rm -rf '%s' '%s.s' '%s.sepwork' '%s.s' '%s.sepwork'", - src, cstem, cstem, wstem, wstem); - if (runwait(cmd) != 0) { - fprintf(stderr, "m1mangle_sym: cleanup rm failed\n"); - fail++; - } - - if (fail) { - fprintf(stderr, "m1mangle_sym: %d checks failed\n", fail); - return 1; - } - printf("m1mangle_sym: ok\n"); - return 0; -}