Flip the soft-default to a hard "missing package clause" error symmetrically in both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a primary section with empty pathmod/resetmod and no seen clause is now rejected. Closes the documented soft-default divergence (the 63-wrapper carve-out). The gate flip can't be split from the migration it breaks, so this is one atomic commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf> root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the bare rt_ensure/rt_malloc linker names. Root mangling narrows: the executable entry `main` stays bare (existing carve-out), but root helper symbols become main.X. The #84 cluster is rewritten to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery are retained — still load-bearing for package-less module-reset deps. New table-driven test 782_strict_package.c (6 rows, both stages). Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the .wwi pathmod so the body's package clause asserts), not a vestige; fusing its removal here would be a silent mismatch. All byte-id gates green; full make test reports "all 335 tests passed".
278 lines
8.2 KiB
C
278 lines
8.2 KiB
C
/*
|
||
* 730_sret_narrow_field — Class A asm-presence + byte-id for the
|
||
* sret callee's N_IDENT word-copy tail when the returned struct
|
||
* ends in a narrow primitive (bool / u8 / i8 / i16 / i32).
|
||
*
|
||
* Task #33. Cstage pre-fix used the slot-padded `rt->size` to drive
|
||
* the field-copy loop's MOVQ/MOVL/MOVB tail; for a struct whose
|
||
* natural size is unaligned (e.g. `{ i32, []u8, bool }` natural=33,
|
||
* padded=40), the trailing MOVQ at offset 32 widened a 1-byte bool
|
||
* into an 8-byte load+store — diverged from wwstage's correct MOVB
|
||
* (which used natural size via `sretretsize` / `structnaturalsize`).
|
||
* Corpus-coverage-blind: no in-tree stdlib struct had a bool field
|
||
* at any offset until lib/strings.iterator landed (Hare's
|
||
* `iterator { dec, reverse }` shape).
|
||
*
|
||
* Sentinel per row: in the `mk` body, between the final pre-RET
|
||
* "RAX = @sretarg load" pair (the SysV return-the-pointer
|
||
* discipline) and the first MOVQ word-store into (BX), assert
|
||
* the last copy instruction targets the narrow MOV width derived
|
||
* from the final field's type — NOT MOVQ. The trailing-field offset
|
||
* is hard-coded per row (structurally fixed) and we pin both the
|
||
* read-from-(BP) and store-to-(BX) sides.
|
||
*
|
||
* Plus byte-id between stages per row.
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.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;
|
||
}
|
||
|
||
/* `off` = byte offset of the trailing narrow field inside the
|
||
* struct (= natural offset of that field). `mov` = expected mnemonic
|
||
* for the trailing field-copy at that offset (MOVB / MOVW / MOVL).
|
||
* Rows all share the shape `{ i32 a, []u8 s, NARROW r }` so a sits
|
||
* at 0..4, s at 8..32, r at 32..32+sz(r). */
|
||
struct row { const char *label; const char *src; int off; const char *mov; };
|
||
|
||
static const struct row rows[] = {
|
||
{ "trailing_bool",
|
||
"type t = struct { a: i32, s: []u8, r: bool };\n"
|
||
"export fn mk() t = {\n"
|
||
" let v: t; let z: []u8;\n"
|
||
" v.s = z; v.a = 0; v.r = false;\n"
|
||
" return v;\n"
|
||
"};\n"
|
||
"export fn main() i32 = {\n"
|
||
" let x: t = mk();\n"
|
||
" if (x.r) { return 1; };\n"
|
||
" return 0;\n"
|
||
"};\n", 32, "MOVB" },
|
||
{ "trailing_u8",
|
||
"type t = struct { a: i32, s: []u8, r: u8 };\n"
|
||
"export fn mk() t = {\n"
|
||
" let v: t; let z: []u8;\n"
|
||
" v.s = z; v.a = 0; v.r = 0u8;\n"
|
||
" return v;\n"
|
||
"};\n"
|
||
"export fn main() i32 = {\n"
|
||
" let x: t = mk();\n"
|
||
" if (x.r != 0u8) { return 1; };\n"
|
||
" return 0;\n"
|
||
"};\n", 32, "MOVB" },
|
||
/* Trailing i16: natural size 34. The chained loop has only
|
||
* MOVQ/MOVL/MOVB arms (matching wwstage's identical chain),
|
||
* so a 2-byte tail falls through to 2× MOVB at offsets 32, 33.
|
||
* That's still narrow + byte-id; the bug-check is the negative
|
||
* "no MOVQ at offset 32". Promoting both stages to a MOVW arm
|
||
* is a separate refactor (preserves byte-id but out of #33). */
|
||
{ "trailing_i16",
|
||
"type t = struct { a: i32, s: []u8, r: i16 };\n"
|
||
"export fn mk() t = {\n"
|
||
" let v: t; let z: []u8;\n"
|
||
" v.s = z; v.a = 0; v.r = 0i16;\n"
|
||
" return v;\n"
|
||
"};\n"
|
||
"export fn main() i32 = {\n"
|
||
" let x: t = mk();\n"
|
||
" if (x.r != 0i16) { return 1; };\n"
|
||
" return 0;\n"
|
||
"};\n", 32, "MOVB" },
|
||
{ "trailing_i32",
|
||
"type t = struct { a: i32, s: []u8, r: i32 };\n"
|
||
"export fn mk() t = {\n"
|
||
" let v: t; let z: []u8;\n"
|
||
" v.s = z; v.a = 0; v.r = 0;\n"
|
||
" return v;\n"
|
||
"};\n"
|
||
"export fn main() i32 = {\n"
|
||
" let x: t = mk();\n"
|
||
" if (x.r != 0) { return 1; };\n"
|
||
" return 0;\n"
|
||
"};\n", 32, "MOVL" },
|
||
};
|
||
|
||
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/sret_narrow_%d_%d.ww", getpid(), i);
|
||
snprintf(out_s, cap, "/tmp/sret_narrow_%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);
|
||
unlink(src);
|
||
return rc;
|
||
}
|
||
|
||
/* Asm-presence sentinel: inside mk's body (between `TEXT mk,` and
|
||
* the first `RET`), the trailing field-copy at offset `r->off` must
|
||
* use the narrow MOV mnemonic — both the read-from-(BP) and the
|
||
* store-to-(BX) lines. Pre-fix the cstage emitted MOVQ at that
|
||
* offset (8-byte over-wide) because the loop drove off slot-padded
|
||
* size; wwstage emitted the narrow MOV via natural size. */
|
||
static int
|
||
check_narrow_mov(const char *path, const struct row *r)
|
||
{
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return -1;
|
||
char line[1024];
|
||
int in_mk = 0;
|
||
int saw_store = 0;
|
||
int saw_load = 0;
|
||
char store_needle[64], load_needle[64];
|
||
snprintf(store_needle, sizeof store_needle, "%s\tAX, %d(BX)\n",
|
||
r->mov, r->off);
|
||
snprintf(load_needle, sizeof load_needle, "%d(BP), AX",
|
||
-16); /* not used as exact-match — see contains below */
|
||
(void)load_needle;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (!in_mk) {
|
||
if (strstr(line, "TEXT main.mk,")
|
||
|| strstr(line, "TEXT\tmain.mk,"))
|
||
in_mk = 1;
|
||
continue;
|
||
}
|
||
if (strstr(line, "\tRET\n")) break;
|
||
if (strstr(line, store_needle)) saw_store = 1;
|
||
/* load: `\tMOV?\t-K(BP), AX\n` with the matching width. */
|
||
char loadkey[16];
|
||
snprintf(loadkey, sizeof loadkey, "%s\t", r->mov);
|
||
if (strstr(line, loadkey) && strstr(line, "(BP), AX"))
|
||
saw_load = 1;
|
||
}
|
||
fclose(f);
|
||
if (!saw_store) {
|
||
fprintf(stderr,
|
||
"row[%s]: expected `%s AX, %d(BX)` in mk body\n",
|
||
r->label, r->mov, r->off);
|
||
return -1;
|
||
}
|
||
if (!saw_load) {
|
||
fprintf(stderr,
|
||
"row[%s]: expected narrow `%s -K(BP), AX` load in mk\n",
|
||
r->label, r->mov);
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* Negative sentinel: pre-fix bug pattern. cstage emitted
|
||
* `MOVQ AX, <r->off>(BX)` for the narrow trailing field — assert
|
||
* absence in mk body. */
|
||
static int
|
||
check_no_movq_at_off(const char *path, const struct row *r)
|
||
{
|
||
FILE *f = fopen(path, "rb");
|
||
if (!f) return -1;
|
||
char line[1024];
|
||
int in_mk = 0;
|
||
char needle[64];
|
||
snprintf(needle, sizeof needle, "MOVQ\tAX, %d(BX)\n", r->off);
|
||
int bad = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (!in_mk) {
|
||
if (strstr(line, "TEXT main.mk,")
|
||
|| strstr(line, "TEXT\tmain.mk,"))
|
||
in_mk = 1;
|
||
continue;
|
||
}
|
||
if (strstr(line, "\tRET\n")) break;
|
||
if (strstr(line, needle)) { bad = 1; break; }
|
||
}
|
||
fclose(f);
|
||
if (bad) {
|
||
fprintf(stderr,
|
||
"row[%s]: unexpected `MOVQ AX, %d(BX)` (pre-#33"
|
||
" over-wide pattern) in mk\n", r->label, r->off);
|
||
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;
|
||
|
||
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, "row[%s]: w6c failed\n", rows[i].label);
|
||
fail++; total++; continue;
|
||
}
|
||
total += 2;
|
||
if (check_narrow_mov(cs_path, &rows[i]) != 0) fail++;
|
||
if (check_no_movq_at_off(cs_path, &rows[i]) != 0) fail++;
|
||
|
||
if (!have_ww) { unlink(cs_path); continue; }
|
||
|
||
if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) {
|
||
fprintf(stderr,
|
||
"row[%s]: w6c_ww failed\n", rows[i].label);
|
||
fail++; total++;
|
||
unlink(cs_path);
|
||
continue;
|
||
}
|
||
total += 2;
|
||
if (check_narrow_mov(ws_path, &rows[i]) != 0) fail++;
|
||
if (check_no_movq_at_off(ws_path, &rows[i]) != 0) fail++;
|
||
|
||
total++;
|
||
char cmd[512];
|
||
snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path);
|
||
if (runwait(cmd) != 0) {
|
||
fprintf(stderr,
|
||
"row[%s]: cstage vs wwstage asm differs\n",
|
||
rows[i].label);
|
||
fail++;
|
||
}
|
||
|
||
unlink(cs_path); unlink(ws_path);
|
||
}
|
||
|
||
if (fail) {
|
||
fprintf(stderr,
|
||
"sret_narrow_field: %d/%d fixtures failed\n",
|
||
fail, total);
|
||
return 1;
|
||
}
|
||
printf("sret_narrow_field: %d/%d ok\n", total, total);
|
||
return 0;
|
||
}
|