From 7b0e09e0654614b8179913eb8c9552331aeb6470 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 8 Jun 2026 16:19:29 +0900 Subject: [PATCH] =?UTF-8?q?wcc/cgen:=20GAP-A.len=20def-global=20array=20.l?= =?UTF-8?q?en=20=E2=80=94=20def-twin=20cgdot=20arm=20(#7=20lineage,=20wwst?= =?UTF-8?q?age=20align-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage .len on a def-global array fell to the cgdot SB-fallback (w6l: undefined reference to 'len') — the #7 let-array arm gates on letvartnode (c.lets only), so def-globals (c.defs) missed it. Add a def .len-only arm in cgdot using the existing defvartnode (the def-side mirror of letvartnode), emitting the length immediate from the #11-stamped N_TARRAY length child. cstage cgen.c was already correct, so this is a wwstage-only source change: w6c unchanged, w6c_ww + wwdump regen'd (they embed the wcc cgen). .ptr (cstage itself buggy — emits LEAQ (BP), filed GAP-A.ptr) and .cap (wwstage silent garbage; arrays have no cap, filed GAP-A.cap) are NOT folded (rule-11, separate concerns). Pin: table-driven test/wcc/816_def_arr_len (def [3] + [_] inferred + 1-elem + u8 stride .len, both stages + byte-id), teeth-proven. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 31 ++++ selfhost/cmd/wcc/cgenexpr.ww | 31 ++++ selfhost/cmd/wwdump/main.combined.ww | 31 ++++ test/wcc/816_def_arr_len.c | 242 +++++++++++++++++++++++++++ 5 files changed, 342 insertions(+) create mode 100644 test/wcc/816_def_arr_len.c diff --git a/Makefile b/Makefile index 0299bf9e..598688c4 100644 --- a/Makefile +++ b/Makefile @@ -244,6 +244,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_tagged_elem \ $(BIN)/test_arr_infer_len \ $(BIN)/test_def_arr_infer_len \ + $(BIN)/test_def_arr_len \ $(BIN)/test_slice_str_global_zero \ $(BIN)/test_slice_literal_global \ $(BIN)/test_global_arr_elem_field \ @@ -640,6 +641,12 @@ $(BIN)/test_def_arr_infer_len: test/wcc/814_def_arr_infer_len.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_arr_len: test/wcc/816_def_arr_len.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arr_tagged_elem: test/wcc/685_arr_tagged_elem.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 370afe53..e8630f3f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -24962,6 +24962,37 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // GAP-A (#7 def-twin): `def NAME: [N]T = arrlit;` `.len` = static + // elem count. The let-global arm above resolves via letvartnode + // (c.lets only); a def lives in c.defs, misses it, and falls to the + // SB fallback → MOVQ len(SB) (w6l: undefined 'len'). cstage cgen.c + // emits MOVQ $alen here. defvartnode is the def-side mirror of + // letvartnode (returns dtnode = the N_TARRAY whose .rhs length child + // is #11-stamped). `.ptr`/`.cap` are NOT mirrored: cstage's def .ptr + // is itself buggy (LEAQ (BP), task GAP-A.ptr) and arrays have no + // .cap (cstage rejects, task GAP-A.cap). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (streq(fld, "len")) { + let dtn: *node = defvartnode(c, lhs.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { + let lenn: *node = dtn.rhs; + let alen: i64 = 0i64; + if (lenn != nil) { + if (lenn.kind == nkind.N_INTLIT) { + alen = lenn.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Top-level TUPLE global positional read (C-t3, #48): `g.N` — // LEAQ name(SB) into CX, then load at the element's SLOT offset // (C-t0 layout), the element's natural width. Mirrors the local diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 7a9377dc..8aef37ac 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3455,6 +3455,37 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // GAP-A (#7 def-twin): `def NAME: [N]T = arrlit;` `.len` = static + // elem count. The let-global arm above resolves via letvartnode + // (c.lets only); a def lives in c.defs, misses it, and falls to the + // SB fallback → MOVQ len(SB) (w6l: undefined 'len'). cstage cgen.c + // emits MOVQ $alen here. defvartnode is the def-side mirror of + // letvartnode (returns dtnode = the N_TARRAY whose .rhs length child + // is #11-stamped). `.ptr`/`.cap` are NOT mirrored: cstage's def .ptr + // is itself buggy (LEAQ (BP), task GAP-A.ptr) and arrays have no + // .cap (cstage rejects, task GAP-A.cap). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (streq(fld, "len")) { + let dtn: *node = defvartnode(c, lhs.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { + let lenn: *node = dtn.rhs; + let alen: i64 = 0i64; + if (lenn != nil) { + if (lenn.kind == nkind.N_INTLIT) { + alen = lenn.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Top-level TUPLE global positional read (C-t3, #48): `g.N` — // LEAQ name(SB) into CX, then load at the element's SLOT offset // (C-t0 layout), the element's natural width. Mirrors the local diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 99ddbf40..72ae63dd 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -24962,6 +24962,37 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // GAP-A (#7 def-twin): `def NAME: [N]T = arrlit;` `.len` = static + // elem count. The let-global arm above resolves via letvartnode + // (c.lets only); a def lives in c.defs, misses it, and falls to the + // SB fallback → MOVQ len(SB) (w6l: undefined 'len'). cstage cgen.c + // emits MOVQ $alen here. defvartnode is the def-side mirror of + // letvartnode (returns dtnode = the N_TARRAY whose .rhs length child + // is #11-stamped). `.ptr`/`.cap` are NOT mirrored: cstage's def .ptr + // is itself buggy (LEAQ (BP), task GAP-A.ptr) and arrays have no + // .cap (cstage rejects, task GAP-A.cap). + if (lhs != nil) { + if (lhs.kind == nkind.N_IDENT) { + if (streq(fld, "len")) { + let dtn: *node = defvartnode(c, lhs.str); + if (dtn != nil) { + if (dtn.kind == nkind.N_TARRAY) { + let lenn: *node = dtn.rhs; + let alen: i64 = 0i64; + if (lenn != nil) { + if (lenn.kind == nkind.N_INTLIT) { + alen = lenn.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; // Top-level TUPLE global positional read (C-t3, #48): `g.N` — // LEAQ name(SB) into CX, then load at the element's SLOT offset // (C-t0 layout), the element's natural width. Mirrors the local diff --git a/test/wcc/816_def_arr_len.c b/test/wcc/816_def_arr_len.c new file mode 100644 index 00000000..6b6d97e9 --- /dev/null +++ b/test/wcc/816_def_arr_len.c @@ -0,0 +1,242 @@ +/* + * 816_def_arr_len — cstage and wwstage agree, byte-for-byte and at runtime, + * that `.len` on a module-level `def NAME: [N]T = [...]` array is the static + * element count (an immediate `MOVQ $alen, AX`). + * + * The bug (GAP-A.len, WWSTAGE-ONLY): the #7 let-global `.len` cgdot arm in + * selfhost/cmd/wcc/cgenexpr.ww gated on letvartnode (c.lets only). A `def` + * lives in c.defs, missed that arm, and fell through to the module-qualified + * SB fallback that mis-emitted `MOVQ len(SB)` → w6l: undefined reference to + * 'len' (a build/link failure, not silent). cstage cgen.c already emitted the + * immediate, so the two stages also DIVERGED. The fix adds a def-twin `.len` + * arm using defvartnode (the def-side mirror of letvartnode) — `.len` ONLY. + * + * SCOPE: `.len` only. `.ptr` (cstage itself emits LEAQ (BP), task GAP-A.ptr) + * and `.cap` (arrays have no cap; cstage rejects, wwstage silently emits + * garbage, task GAP-A.cap) are SEPARATE bugs, not mirrored here. + * + * Covers BOTH the explicit-length `def [3]int` and the inferred `def [_]int` + * (#11-stamped) so the deftnode/.rhs immediate path is exercised for the + * inference case too. + * + * row | shape | want + * ---------------+---------------------------------------------+----- + * explicit_len | def A: [3]int=[10,20,30], A.len | 3 + * infer_len | def B: [_]int=[1,2,3,4], B.len | 4 + * sum | A.len + B.len (ken oracle) | 7 + * one_elem | def C: [_]int=[9], C.len | 1 + * u8_len | def D: [_]u8=[1u8..5u8], D.len | 5 + */ +#include +#include +#include +#include +#include +#include + +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[] = { + { "explicit_len", + "package main;\n" + "def A: [3]int = [10, 20, 30];\n" + "export fn main() i32 = {\n" + "\treturn A.len: i32;\n" + "};\n", + 3 }, + + { "infer_len", + "package main;\n" + "def B: [_]int = [1, 2, 3, 4];\n" + "export fn main() i32 = {\n" + "\treturn B.len: i32;\n" + "};\n", + 4 }, + + /* ken oracle: explicit + inferred together = 7. */ + { "sum", + "package main;\n" + "def A: [3]int = [10, 20, 30];\n" + "def B: [_]int = [1, 2, 3, 4];\n" + "export fn main() i32 = {\n" + "\treturn (A.len: i32) + (B.len: i32);\n" + "};\n", + 7 }, + + { "one_elem", + "package main;\n" + "def C: [_]int = [9];\n" + "export fn main() i32 = {\n" + "\treturn C.len: i32;\n" + "};\n", + 1 }, + + /* narrow stride — `.len` is a count, independent of element width. */ + { "u8_len", + "package main;\n" + "def D: [_]u8 = [1u8, 2u8, 3u8, 4u8, 5u8];\n" + "export fn main() i32 = {\n" + "\treturn D.len: i32;\n" + "};\n", + 5 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dal_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dal_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. + * Post-fix both stages emit `MOVQ $alen, AX`; the rule-10 convergence + * invariant. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dal_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/dal_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/dal_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + 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 cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "def_arr_len: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "def_arr_len[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "def_arr_len: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("def_arr_len: %d/%d ok\n", total, total); + return 0; +}