Files
ww/test/wcc/744_letcopy_struct.c
Hojun-Cho 7b9488706b parse: enforce strict-package — reject package-less files (#24a)
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".
2026-06-29 03:55:26 +09:00

377 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 744_letcopy_struct — sentinel for STATUS-6 #32. Class B-symmetric
* cgen miscompile: `let p2: T = p1;` where T is a struct >8B and rhs
* is a local ident silently zero-inits p2 (cstage emitted nothing for
* the copy; wwstage emitted a single MOVQ AX + stale BX from the
* sz==16 str-init tail). Reads after the let saw whatever the stack
* held — silent partial-copy / silent zero on a fresh frame.
*
* Both stages now byte-copy the source slot to the dest slot
* per-qword with a sized tail (MOVL/MOVB) for natural sizes not
* 8-aligned. Mirrors cg_widen_tagged_store's struct-ident payload
* copy. See cmd/w6c/cgen.c N_LET and selfhost/cmd/wcc/cgenstmt.ww
* cglet.
*
* Rows exercise the 4 struct shapes the task brief called out:
* (a) 3-field i32 (sz=12, MOVQ + MOVL tail).
* (b) i32 + str field (sz=24, 3 × MOVQ).
* (c) i32 + []u8 slice field (sz=32, 4 × MOVQ).
* (d) i32 + tagged (i32|str) field (sz=32, 4 × MOVQ).
*
* Each row pins:
* - asm-presence in both stages: produce body emits `min_loads`
* `MOVQ -K(BP), AX` source-slot loads (the bug pin — pre-fix
* cstage emitted 0; wwstage emitted 1 with garbage BX tail).
* - asm byte-id between stages (cmp -s on the produce-bearing .s).
* - runtime: produce() returns 0 (every field round-tripped).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.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;
}
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
count_substr(const char *start, const char *end, const char *needle)
{
int n = 0;
size_t nlen = strlen(needle);
const char *p = start;
while (p + nlen <= end) {
if (memcmp(p, needle, nlen) == 0) { n++; p += nlen; }
else { p++; }
}
return n;
}
struct row {
const char *label;
/* Direct-w6c source: bare fn produce, no package/imports. */
const char *asm_src;
/* Driver source: `package main; import os; ...` runtime wrapper. */
const char *run_src;
/* Source-slot loads expected in produce: at least N for the copy.
* Pre-fix cstage emitted 0; pre-fix wwstage emitted 1 with stale BX.
* `MOVQ -K(BP), AX` is also emitted by p2.a / p2.s.len reads in the
* checks, so set min_loads to a value the copy alone surpasses. */
int min_loads;
};
static const struct row rows[] = {
/* (a) 3-field i32 struct, sz=12. Copy = 1 × MOVQ + 1 × MOVL.
* p1 inits via structlit to sidestep the pre-existing no-rhs
* zero-init divergence for non-8-multiple struct sizes
* (cstage uses natural sz=12 → MOVQ+MOVL, wwstage uses slot
* sz=16 → MOVQ+MOVQ). Tracked as the #15/#26c rule-10
* size-strategy convergence follow-up. */
{ "tri_i32",
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"export fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
1 },
/* (b) i32 + str field, sz=24. Copy = 3 × MOVQ. */
{ "field_str",
"type ws = struct { a: i32, s: str };\n"
"export fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type ws = struct { a: i32, s: str };\n"
"fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
3 },
/* (c) i32 + []u8 slice field, sz=32. Copy = 4 × MOVQ. */
{ "field_slice",
"type wsl = struct { a: i32, b: []u8 };\n"
"export fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type wsl = struct { a: i32, b: []u8 };\n"
"fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
/* (d) i32 + tagged (i32|str) field, sz=32. Copy = 4 × MOVQ.
* Runtime checks only p2.a; the asm load-count (>=4) and the
* byte-id gate together prove the full 32B slot copied — match
* on p2.t avoided because wwstage's match scrutinee spill grows
* the frame and reorders ABI loads vs cstage (pre-existing
* #15/#26c rule-10 size-strategy / match-spill divergence). */
{ "field_tagged",
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"export fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" match (p2.t) {\n"
" case let x: i32 => if (x != 42) { return 12; };\n"
" case let s: str => return 13;\n"
" };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
};
/* Locate `TEXT produce` body in the asm file and count source-slot
* loads (`MOVQ\t-K(BP), AX`) inside it. Pre-fix cstage produce had 0;
* pre-fix wwstage produce had 1 (no tail). */
static int
check_copy_loads(const char *spath, const struct row *r)
{
static char buf[1 << 16];
if (slurp(spath, buf, sizeof buf) < 0) return -1;
const char *body = strstr(buf, "TEXT main.produce");
if (!body) {
fprintf(stderr, "row[%s]: no TEXT produce label in %s\n",
r->label, spath);
return -1;
}
/* End at the next TEXT (or EOF). */
const char *end = strstr(body + 1, "\nTEXT ");
if (!end) end = buf + strlen(buf);
int loads = count_substr(body, end, "MOVQ\t-");
if (loads < r->min_loads) {
fprintf(stderr,
"row[%s]: only %d `MOVQ -K(BP), AX` source loads in "
"produce; expected >= %d (copy missing)\n",
r->label, loads, r->min_loads);
return -1;
}
return 0;
}
static int
emit_s(const char *w6c, const struct row *r, int i, int stage,
char *out_s, size_t cap)
{
char src[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/lcs_asm_%d_%d_%d.ww",
getpid(), i, stage);
snprintf(out_s, cap, "/tmp/lcs_asm_%d_%d_%d.s",
getpid(), i, stage);
FILE *f = fopen(src, "wb");
if (!f) return -1;
wwtest_fputs(r->asm_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;
}
static int
run_driver(const char *driver, const struct row *r, int i, int stage)
{
char tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/lcs_run_%d_d_%d_%d",
getpid(), i, stage);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/lcs_run_%d_%d_%d.ww",
tmpdir, getpid(), i, stage);
snprintf(outbin, sizeof outbin, "%s/lcs_run_%d_%d_%d",
tmpdir, getpid(), i, stage);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
wwtest_fputs(r->run_src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null",
driver, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s] build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
return got;
}
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], cdrv[640], wdrv[640];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int have_ww = (access(w6c_ww, X_OK) == 0);
int have_wdrv = (access(wdrv, X_OK) == 0);
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];
char cs_path[160], ws_path[160];
/* cstage asm-presence. */
if (emit_s(w6c, r, i, 0, cs_path, sizeof cs_path) != 0) {
fprintf(stderr,
"letcopy_struct[cstage][%s]: w6c failed\n",
r->label);
fail++; total++; continue;
}
total++;
if (check_copy_loads(cs_path, r) != 0) fail++;
/* wwstage asm-presence + byte-id. */
if (have_ww) {
if (emit_s(w6c_ww, r, i, 1, ws_path, sizeof ws_path)
!= 0) {
fprintf(stderr,
"letcopy_struct[wwstage][%s]: "
"w6c_ww failed\n", r->label);
fail++; total++;
unlink(cs_path);
continue;
}
total++;
if (check_copy_loads(ws_path, r) != 0) fail++;
total++;
char cmd[512];
snprintf(cmd, sizeof cmd, "cmp -s %s %s",
cs_path, ws_path);
if (runwait(cmd) != 0) {
fprintf(stderr,
"letcopy_struct[%s]: cstage vs wwstage "
"asm differs\n", r->label);
fail++;
}
unlink(ws_path);
}
unlink(cs_path);
/* Runtime correctness via cstage driver. */
total++;
int got = run_driver(cdrv, r, i, 2);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
/* Runtime correctness via wwstage driver. */
if (have_wdrv) {
total++;
got = run_driver(wdrv, r, i, 3);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww_ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"letcopy_struct: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("letcopy_struct: %d/%d ok\n", total, total);
return 0;
}