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:
2026-08-08 04:41:42 +09:00
parent fa4b9a134b
commit 81e7f95548
14 changed files with 890 additions and 2297 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}