Files
ww/test/wcc/755_amp_dot_idx.c
Hojun-Cho d5e8d699d1 selfhost: graduate wwstage &N_DOT[N_INDEX] to cstage canonical lean form (#21)
Latent #21 has two surface shapes — register polarity in cgun
TK_AMP N_INDEX's complex-base arm, and indexbaseesz's
over-broad .ptr pseudo-field gate — that share a single semantic
path: &N_DOT[N_INDEX] where the inner N_DOT cannot be peeled
into a plain ident base. Polarity-A (cgenexpr.ww) lifted to
cstage's three-line shape; stride-B (cgenutil.ww) narrowed so
the .ptr arm only fires on actual str/slice inners and falls
through to the generic struct-field arm for struct N_TNAME
bases. The fixes compose at the same call site (esz from
indexbaseesz, then the IMULQ-or-elide gate, then complex-base
emit), so splitting them into two commits would leave a
half-fixed intermediate — neither half stands alone as a
bisect-clean closure. Sentinel 755_amp_dot_idx exercises both
shapes across 4 stride classes (slice-elem 24, struct-elem 16,
u8 stride-1 elide, i64 stride-8); pre-fix 5/12 fail, post-fix
12/12 ok. Latent silent miscompile in lib/memio + lib/bufio's
.ptr[i] shape also unmasked.
2026-05-19 13:43:45 +09:00

267 lines
8.6 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.
/*
* 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 <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 *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;
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;
}
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 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;
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) { unlink(cs_path); 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++;
unlink(cs_path); 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++;
}
unlink(cs_path); unlink(ws_path);
}
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;
}