w6c_ww/cgen: uniform tinfo esz for global str/slice index (fix #10)

Indexing a GLOBAL `str` or GLOBAL slice (`s[i]` / `g[i]` where s/g are
module-level lets) read a wide {ptr,len,cap} header with an 8-byte stride
and a full-word MOVQ load instead of the .ptr + element-width load. So
`s[1]` over a global str read 8 bytes at ptr+8 rather than the single byte
at ptr+1 (cstage emits MOVZBQ). LOCAL str/slice index was already clean.

Root: wwstage cgindex (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element
size + base-materialisation off the base tnode KIND, enumerating only
N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode
N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz
stayed at the default 8 and the base fell through to the wide-header
fallback. cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the
RESOLVED base type (`idx_eff(lhs->type)->sub->size`), uniform across
local/global/str/slice/ptr.

Fix aligns cgindex's global-resolution arm UP to cstage's uniform type-
driven dispatch — the same template the sister fn cgslice already uses:
resolve esz via elemsizeofc(c, tn) with no kind gate, then drive the base
load by tn.kind == N_TARRAY ? LEAQ : MOVQ name(SB). A global str/slice now
resolves esz=1 off the type table (elemsizeofc, just fixed in #8 to read
stamped tinfo) and routes through the EXISTING isglobalptr emission
(MOVQ name(SB),BX; ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage. The
element-kind flags (elemisstr/elemisslice) for a global `[]str`/`[][]u8`
element are still set by the downstream block, so those route to cgslicehdr
unchanged.

Close-by-construction: cgindex's one global-ident resolution arm is the
single site computing a global element base for the read-index path (the
&arr[i] address-of in cgun and the arr[i]=v store in cgassign are separate
node paths, out of scope). Any indexable global base now resolves esz off
the type table, exactly like cstage and like cgslice.

combined.ww embeds regenerate (w6c + wwdump). New 803_globalidx_run pins
runtime (cstage build+run) + cs==ww byte-id across global str index
(positions 0/1/2 + sum), global slice index (TEXT-only byte-id — a bare
`let g: []u8;` decl emits a divergent zero-header DATAW orthogonal to the
index read, the #7/#18 static-init family), and local str/slice/array
index regression pins. A stride-8 regression re-fails the 5 global rows.
This commit is contained in:
2026-06-03 15:24:56 +09:00
parent 5c1e9c0bfb
commit 23670d7c4e
5 changed files with 356 additions and 42 deletions

View File

@@ -372,6 +372,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_len_strglobal_run \
$(BIN)/test_litstr_pseudo_run \
$(BIN)/test_lenidx_run \
$(BIN)/test_globalidx_run \
$(BIN)/test_tuple_sret_callee \
$(BIN)/test_tuple_sret_receive_run \
$(BIN)/test_struct_tuple_field_slot \
@@ -902,6 +903,17 @@ $(BIN)/test_lenidx_run: test/wcc/802_lenidx_run.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# #10: indexing a GLOBAL str / GLOBAL slice (`s[i]` / `g[i]`) read a wide
# {ptr,len,cap} header with an 8-byte stride + full-word load instead of the
# element load — wwstage `cgindex` dispatched esz off the base tnode KIND
# (only N_TARRAY/N_TPTR), so a global str/slice matched neither and defaulted
# to esz=8. cstage is correct (type-driven dispatch). Runtime (cstage build+
# run) + cs==ww byte-id, both dimensions per row.
$(BIN)/test_globalidx_run: test/wcc/803_globalidx_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# #10 Fold A (wide tuple-return / sret, CALLEE side): an over-cap tuple
# return (> 4 GP or > 2 SSE eightbytes) now compiles via sret instead of
# loud-stopping at the SEND. Compile + cs==ww byte-id only — the receive

View File

@@ -20980,23 +20980,35 @@ fn cgindex(c: *cgen, n: *node) void = {
// A.2 cgdot widening site).
if (tn == nil) { tn = defvartnode(c, bn); };
if (tn != nil) {
// #10: dispatch esz + base-materialization off the
// global's RESOLVED type via elemsizeofc, NOT an
// N_TARRAY/N_TPTR kind whitelist. A global str (tnode
// N_TNAME "str") / slice (N_TSLICE) matched NEITHER old
// arm, so esz stayed at the default 8 and the base fell
// to the wide-header fallback below (8B stride + full-
// word MOVQ) instead of loading the .ptr + an element-
// width load. cstage dispatches uniformly off
// idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c
// N_INDEX); the sister fn cgslice (this file) already
// resolves esz via elemsizeofc and the base via
// N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align
// cgindex UP to that template: any indexable global
// resolves esz off the type table, N_TARRAY -> LEAQ (the
// symbol IS the storage), every other -> MOVQ name(SB)
// (the symbol's first word IS the .ptr). The existing
// isglobalptr emission (the loadopsz path below) then
// yields the cstage-identical MOVZBQ for a str byte
// (esz=1).
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
};
};

View File

@@ -1256,23 +1256,35 @@ fn cgindex(c: *cgen, n: *node) void = {
// A.2 cgdot widening site).
if (tn == nil) { tn = defvartnode(c, bn); };
if (tn != nil) {
// #10: dispatch esz + base-materialization off the
// global's RESOLVED type via elemsizeofc, NOT an
// N_TARRAY/N_TPTR kind whitelist. A global str (tnode
// N_TNAME "str") / slice (N_TSLICE) matched NEITHER old
// arm, so esz stayed at the default 8 and the base fell
// to the wide-header fallback below (8B stride + full-
// word MOVQ) instead of loading the .ptr + an element-
// width load. cstage dispatches uniformly off
// idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c
// N_INDEX); the sister fn cgslice (this file) already
// resolves esz via elemsizeofc and the base via
// N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align
// cgindex UP to that template: any indexable global
// resolves esz off the type table, N_TARRAY -> LEAQ (the
// symbol IS the storage), every other -> MOVQ name(SB)
// (the symbol's first word IS the .ptr). The existing
// isglobalptr emission (the loadopsz path below) then
// yields the cstage-identical MOVZBQ for a str byte
// (esz=1).
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
};
};

View File

@@ -20980,23 +20980,35 @@ fn cgindex(c: *cgen, n: *node) void = {
// A.2 cgdot widening site).
if (tn == nil) { tn = defvartnode(c, bn); };
if (tn != nil) {
// #10: dispatch esz + base-materialization off the
// global's RESOLVED type via elemsizeofc, NOT an
// N_TARRAY/N_TPTR kind whitelist. A global str (tnode
// N_TNAME "str") / slice (N_TSLICE) matched NEITHER old
// arm, so esz stayed at the default 8 and the base fell
// to the wide-header fallback below (8B stride + full-
// word MOVQ) instead of loading the .ptr + an element-
// width load. cstage dispatches uniformly off
// idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c
// N_INDEX); the sister fn cgslice (this file) already
// resolves esz via elemsizeofc and the base via
// N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align
// cgindex UP to that template: any indexable global
// resolves esz off the type table, N_TARRAY -> LEAQ (the
// symbol IS the storage), every other -> MOVQ name(SB)
// (the symbol's first word IS the .ptr). The existing
// isglobalptr emission (the loadopsz path below) then
// yields the cstage-identical MOVZBQ for a str byte
// (esz=1).
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
if (tn.kind == nkind.N_TPTR) {
} else {
isglobalptr = true;
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
};
};
};

View File

@@ -0,0 +1,266 @@
/*
* 803_globalidx_run — BUG #10. Runtime + cs==ww byte-id net for INDEXING
* a GLOBAL `str` / GLOBAL slice (`s[i]` / `g[i]` where s/g are module-level
* lets).
*
* THE BUG (wwstage WRONG, cstage correct — a rule-10 divergence):
* wwstage `cgindex` (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element
* size + base-materialisation off the base's tnode KIND, enumerating only
* N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode
* N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz
* stayed at the default 8 and the base fell to the wide-header fallback —
* it materialised the full {ptr,len,cap} header and indexed with an 8-byte
* stride + a full-word MOVQ load. So `s[1]` over a global str read 8 bytes
* at ptr+8 instead of the single byte at ptr+1 (cstage emits MOVZBQ).
* cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the RESOLVED
* base TYPE (`idx_eff(lhs->type)->sub->size`), uniform across local/global/
* str/slice/ptr — so it was already correct. LOCAL str/slice index was also
* already byte-id-clean (esz resolves off the local's tnode).
*
* THE FIX (#10): align cgindex's global-resolution arm UP to cstage's uniform
* type-driven dispatch — the same template the sister fn `cgslice` already
* uses (generic globaltn, esz = elemsizeofc(c, tn), base = N_TARRAY ? LEAQ :
* MOVQ name(SB)). A global str/slice now resolves esz=1 off the type table
* and routes through the existing isglobalptr emission (MOVQ name(SB),BX;
* ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage.
*
* EACH ROW CARRIES BOTH DIMENSIONS (802 model):
* (a) cstage `ww build` + run, asserting the exit — pins that the converged
* asm reads the correct element value, not a ptr word.
* (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10). A
* stride-8 regression of the fix re-diverges wwstage from cstage here.
*
* BYTE-ID SCOPE PER ROW:
* Most rows compare the FULL `.s`. The global-slice row compares the TEXT
* section only (lines before the first DATA/GLOBL directive): a bare
* module-level `let g: []u8;` decl emits a divergent zero-header DATAW in
* wwstage that cstage omits — a SEPARATE, pre-existing data-emission gap
* (module-level slice static-init, task #7/#18 family) orthogonal to the
* index read. The TEXT comparison still pins the index codegen exactly
* (where a stride-8 regression manifests: MOVZBQ vs IMULQ $8 + MOVQ), so
* the #10 fix is fully gated; only the unrelated DATA noise is excluded.
* (Same kind of orthogonal block 802 documents for its slice rows.)
*
* GATE POLARITY: must stay GREEN. A wrong exit means a global str/slice index
* regressed back to a ptr-word read; a byte-id FAIL means the stages diverged.
*/
#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_exit; int text_only; };
static const struct row rows[] = {
/* the exact #10 repro: a global str "ABC", s[1] == 'B' == 66.
* Pre-fix this read an 8-byte word at ptr+8 (wide-header stride). */
{ "gstr_mid",
"package main;\n"
"let s: str = \"ABC\";\n"
"export fn main() i32 = { return s[1]: i32; };\n", 66, 0 },
/* first element s[0] == 'A' == 65 — guards an off-by-one in the
* index scale (a wide stride would land elsewhere). */
{ "gstr_first",
"package main;\n"
"let s: str = \"ABC\";\n"
"export fn main() i32 = { return s[0]: i32; };\n", 65, 0 },
/* last element s[2] == 'C' == 67. */
{ "gstr_last",
"package main;\n"
"let s: str = \"ABC\";\n"
"export fn main() i32 = { return s[2]: i32; };\n", 67, 0 },
/* two global-str indices summed (65 + 66 == 131) — pins the byte
* load width: a full-word load would carry the high bytes. */
{ "gstr_sum",
"package main;\n"
"let s: str = \"ABC\";\n"
"export fn main() i32 = { return s[0]: i32 + s[1]: i32; };\n", 131, 0 },
/* global slice index: g[1] == 20. The bare `let g: []u8;` decl emits
* a divergent zero-header DATAW (orthogonal #7/#18 data gap), so this
* row is TEXT-only byte-id; cstage runtime pins the value, TEXT-id
* pins the index read. g is assigned at runtime to dodge the SEPARATE
* module-level slice-literal static-init gap (which fails to link in
* BOTH stages identically). */
{ "gslice_mid",
"package main;\n"
"let g: []u8;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" g = a;\n"
" return g[1]: i32;\n"
"};\n", 20, 1 },
/* global slice with a WIDTH>1, SIGNED element: g[1] - g[0] == 20 - (-5)
* == 25. Pins that the global path resolves esz=4 off the type table
* (not the default 8) AND sign-extends (MOVSXD) — a stride-8 regression
* re-diverges the TEXT (IMULQ $8 + MOVQ word vs IMULQ $4 + MOVSXD). Same
* bare-decl DATAW data-gap as gslice_mid, so TEXT-only. */
{ "gislice_mid",
"package main;\n"
"let g: []i32;\n"
"export fn main() i32 = {\n"
" let a: [3]i32 = [-5, 20, 30];\n"
" g = a;\n"
" return g[1] - g[0];\n"
"};\n", 25, 1 },
/* REGRESSION PIN: local str index (already byte-id-clean pre-fix) —
* the fix must not perturb the local path. s[1] == 'B' == 66. */
{ "lstr_mid",
"package main;\n"
"export fn main() i32 = { let s: str = \"ABC\"; return s[1]: i32; };\n",
66, 0 },
/* REGRESSION PIN: local slice index (already clean). g[2] == 30. */
{ "lslice_last",
"package main;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" let g: []u8 = a;\n"
" return g[2]: i32;\n"
"};\n", 30, 0 },
/* REGRESSION PIN: local array index (already clean). a[1] == 20. */
{ "larr_mid",
"package main;\n"
"export fn main() i32 = {\n"
" let a: [3]u8 = [10u8, 20u8, 30u8];\n"
" return a[1]: i32;\n"
"};\n", 20, 0 },
{ NULL, NULL, 0, 0 }
};
/* A directive line that begins the DATA/GLOBL section. The TEXT-only
* compare stops at the first such line (the index codegen lives entirely
* in the TEXT segment above it). */
static int
isdataline(const char *ln)
{
return strncmp(ln, "DATA", 4) == 0 || strncmp(ln, "GLOBL", 5) == 0;
}
/* Byte-compare two .s files. With text_only, both files are truncated at
* the first DATA/GLOBL line before comparison (orthogonal data-section
* divergence excluded; see the BYTE-ID SCOPE note above). */
static int
asm_eq(const char *a, const char *b, int text_only)
{
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;
char la[4096], lb[4096];
for (;;) {
char *ra = fgets(la, sizeof la, fa);
char *rb = fgets(lb, sizeof lb, fb);
if (text_only && ra && isdataline(la)) ra = NULL;
if (text_only && rb && isdataline(lb)) rb = NULL;
if (ra == NULL && rb == NULL) break;
if (ra == NULL || rb == NULL) { rc = -1; break; }
if (strcmp(la, lb) != 0) { rc = -1; break; }
}
fclose(fa); fclose(fb);
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;
}
char w6c[1100], w6c_ww[1100];
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, "globalidx: 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++) {
char src[64];
snprintf(src, sizeof src, "/tmp/wwgi_%d_%d.ww", getpid(), i);
FILE *f = fopen(src, "wb");
if (f == NULL) { fail++; continue; }
fputs(rows[i].src, f);
fclose(f);
/* (a) cstage build + run in a scratch dir. */
char tmpdir[64];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwgi_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
char cmd[2048];
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s",
tmpdir, bin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage build failed\n",
rows[i].label);
fail++;
unlink(src); rmdir(tmpdir);
continue;
}
char outbin[128];
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
if (got != rows[i].want_exit) {
fprintf(stderr, "row[%s]: cstage exit %d, want %d\n",
rows[i].label, got, rows[i].want_exit);
fail++;
}
unlink(outbin); rmdir(tmpdir);
/* (b) cs==ww byte-id gate. */
char cs_s[64], ws_s[64];
snprintf(cs_s, sizeof cs_s, "/tmp/wwgi_%d_%d_cs.s", getpid(), i);
snprintf(ws_s, sizeof ws_s, "/tmp/wwgi_%d_%d_ww.s", getpid(), i);
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++; unlink(src); continue;
}
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++; unlink(src); unlink(cs_s); continue;
}
if (asm_eq(cs_s, ws_s, rows[i].text_only) != 0) {
fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER%s "
"(rule-10 byte-id violation)\n", rows[i].label,
rows[i].text_only ? " (TEXT section)" : "");
fail++;
}
unlink(src); unlink(cs_s); unlink(ws_s);
}
if (fail) {
fprintf(stderr, "%d/%d globalidx tests failed\n", fail, n);
return 1;
}
printf("globalidx: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n);
return 0;
}