test: port the asm-pattern observers to ww; byteid carrier partition empty
Four ww tests carry the last eight native byteid carriers' assertions: asmwindow (753 convwrap beta/main/alpha order + window polarity, 754 slice stride + negative scale scan, 755 amp-dot-idx four rows, 758 first-CALL-line extraction + tab-framed disp literals, direct w6c/w6c_ww never ww build), freenoop (930 byte-id strengthened to per-stream compare + negative 'free' grep), structabi (946 param/ret MOVSD windows with polarity tables and SEND/RECV agreement), mangle (989_m1mangle needles + concat byte-id, glob order strengthened to byte-lexicographic). Dead want/stage_mask row fields documented, not invented into runtime legs; the w6c_ww-absent skip gates drop because the Make target declares the tools. BYTEID_WRAPPER_SOURCES, its bins, and test-native-byteid are deleted — the native byte/artifact partition is EMPTY (11 -> 0 this session); docs counts move to 123 carriers.
This commit is contained in:
364
test/byteid/asmwindow_test.ww
Normal file
364
test/byteid/asmwindow_test.ww
Normal file
@@ -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 <sym>" 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");
|
||||
};
|
||||
144
test/byteid/freenoop_test.ww
Normal file
144
test/byteid/freenoop_test.ww
Normal file
@@ -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"));
|
||||
};
|
||||
114
test/byteid/mangle_test.ww
Normal file
114
test/byteid/mangle_test.ww
Normal file
@@ -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 <stem>` splits the asm across
|
||||
// <stem>.sepwork/<pkg>.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 <stem>.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);
|
||||
};
|
||||
240
test/byteid/structabi_test.ww
Normal file
240
test/byteid/structabi_test.ww
Normal file
@@ -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);
|
||||
};
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.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 *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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#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$<stride>, 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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#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$<stride>, 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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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<sym>(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<sym>(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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.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;
|
||||
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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.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;
|
||||
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;
|
||||
}
|
||||
@@ -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 <off>(BP), Xn` (scratch -> XMM; a GP eightbyte is
|
||||
* `MOVQ <off>(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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.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;
|
||||
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<off>(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, -<off>(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;
|
||||
}
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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 `<stem>.s`;
|
||||
* returns 0 on success.
|
||||
* #93 sep layout: `-o <stem>` splits the asm across
|
||||
* <stem>.sepwork/<pkg>.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 <stem>.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 <stem>.sepwork/, plus
|
||||
* build_s's <stem>.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;
|
||||
}
|
||||
Reference in New Issue
Block a user