From b9692b14f171f4496c7bdcd3c12c5acf0a24af8f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 20 Jun 2026 20:12:11 +0900 Subject: [PATCH] check: reject non-integer index operand in wwstage (catB-17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors cstage check.c:1491-1495 (type_isint via the syntax.typeisint tinfo chaser, which chases TY_NAMED.under/TY_ENUM.sub — not the AST-keyed isinttypeast that would falsely reject an alias-int index). Record-and-continue, before the base-bail. Reject path emits no asm so cstage==wwstage byte-id holds (453 green). Pre-existing index double-emit deferred (#6). --- Makefile | 7 + selfhost/cmd/wcc/check.ww | 17 ++- test/wcc/851_index_int_reject.c | 242 ++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 test/wcc/851_index_int_reject.c diff --git a/Makefile b/Makefile index bba66f4f..f2648723 100644 --- a/Makefile +++ b/Makefile @@ -249,6 +249,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_const_reassign_reject \ $(BIN)/test_dupfield_reject \ $(BIN)/test_enum_reject \ + $(BIN)/test_index_int_reject \ $(BIN)/test_size_untyped_int \ $(BIN)/test_tagged_staticinit \ $(BIN)/test_arr_infer_len \ @@ -1556,6 +1557,12 @@ $(BIN)/test_enum_reject: test/wcc/850_enum_reject.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_index_int_reject: test/wcc/851_index_int_reject.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_tagged_staticinit: test/wcc/843_tagged_staticinit.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/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 6f0547d3..f4816aed 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2988,14 +2988,27 @@ fn unoptype(c: *checker, e: *syntax.node) *syntax.node = { }; // indexresult — derive the result tnode of an N_INDEX expression. -// Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c +// Mirrors cstage cmd/wcc/check.c:1491-1535 (N_INDEX arm) and harec ref/harec/src/types.c // type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays // to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the // generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D // array of slices, not of T); generic *T → T. fn indexresult(c: *checker, e: *syntax.node) *syntax.node = { let basetn: *syntax.node = exprtype(c, e.lhs, nil); - let _idx: *syntax.node = exprtype(c, e.rhs, nil); + let idxtn: *syntax.node = exprtype(c, e.rhs, nil); + // cstage cmd/wcc/check.c:1491-1495: index operand must be integer. + // typeisint is the tinfo chaser (follows TY_ENUM.sub / TY_NAMED.under), + // so an alias-int or named-enum index passes; the AST-keyed isinttypeast + // would falsely reject `type ix = i32`. Both nil-guards mirror cstage's + // `idx != ty_err` cascade-suppression so a broken subexpr emits one error. + if (idxtn != nil) { + let it: *syntax.tinfo = tinfofornode(c, idxtn); + if (it != nil && !syntax.typeisint(it)) { + cerr(e.file); + cerr(": error: index must be integer\n"); + c.errs += 1; + }; + }; let u: *syntax.node = resolvealias(c, unwrapbang(basetn)); // basetn nil → propagation from inherent-IDENT bail at exprtype // N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24). diff --git a/test/wcc/851_index_int_reject.c b/test/wcc/851_index_int_reject.c new file mode 100644 index 00000000..2506332d --- /dev/null +++ b/test/wcc/851_index_int_reject.c @@ -0,0 +1,242 @@ +/* + * 851_index_int_reject (catB-17) — cstage and wwstage LOUD-REJECT an + * N_INDEX whose index operand is not an integer (float, str, pointer, + * bool). cstage already rejected at cmd/wcc/check.c:1491-1495 (the + * N_INDEX arm: `if (idx != ty_err && !type_isint(idx)) err(...)`). + * wwstage silently ACCEPTED — indexresult() computed the index type and + * dropped it (`let _idx = exprtype(...)`), so `xs[f64]` / `xs[str]` / + * `xs[*T]` / `xs[bool]` compiled. The fix derives the index tinfo and + * gates syntax.typeisint (selfhost/cmd/wcc/check.ww indexresult), the + * rule-10 twin of cstage's once-per-N_INDEX site, sibling to + * validateenummembers (catB-2) / validatestructfields (catB-7/14). + * + * The ALIAS-INT accept row is load-bearing: typeisint is the tinfo + * chaser (follows TY_NAMED.under / TY_ENUM.sub), so `type ix = i32` + * indexes clean. The AST-keyed isinttypeast would falsely reject it — + * a vacuous plain-int-only test would not catch that. + * + * Byte-id: pure diagnostic, no n.type_ / checker-state mutation beyond + * c.errs; the valid corpus indexes only by integer types, so codegen of + * every valid program is unchanged → cstage == wwstage byte-id holds. + * + * reject row | index operand type | expect + * -------------+----------------------+-------- + * by_float | f64 | REJECT + * by_str | str | REJECT + * by_ptr | *i32 | REJECT + * by_bool | bool | REJECT + * + * accept row | index operand type | expect + * -------------+----------------------+-------- + * by_int | int | BUILD, run 20 + * by_uint | uint | BUILD, run 30 + * by_enum | named enum value | BUILD, run 20 + * by_alias_int | type ix = i32 | BUILD, run 30 (load-bearing) + */ +#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; +} + +static void +outbin(char *dst, size_t n, const char *tmpdir, const char *src) +{ + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(dst, n, "%s/%s", tmpdir, base); + char *dot = strrchr(dst, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; +} + +static int +build_should_fail(const char *driver, const char *label, const char *src, + int i) +{ + char s[64], tmpdir[64], cmd[1024], bin[128]; + snprintf(s, sizeof s, "/tmp/idxr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/idxr_%d_d_%d", getpid(), i); + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, s); + int rc = runwait(cmd); + outbin(bin, sizeof bin, tmpdir, s); + unlink(s); + unlink(bin); + rmdir(tmpdir); + if (rc == 0) + fprintf(stderr, "idxr[%s][%s]: built ok, expected a reject\n", + driver, label); + return rc == 0 ? -1 : 0; +} + +static int +run_build(const char *driver, const char *label, const char *src, int i) +{ + char s[64], tmpdir[64], cmd[1024], bin[128]; + snprintf(s, sizeof s, "/tmp/idxr_ok_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/idxr_ok_%d_d_%d", getpid(), i); + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, s); + if (runwait(cmd) != 0) { + fprintf(stderr, "idxr[%s][%s]: build failed (integer index " + "must compile)\n", driver, label); + unlink(s); rmdir(tmpdir); + return -1; + } + outbin(bin, sizeof bin, tmpdir, s); + int got = runwait(bin); + unlink(s); unlink(bin); rmdir(tmpdir); + return got; +} + +struct rejrow { const char *label; const char *src; }; + +static const struct rejrow reject_rows[] = { + { "by_float", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let f: f64 = 1.0;\n" + " return a[f];\n" + "};\n" }, + { "by_str", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let s: str = \"x\";\n" + " return a[s];\n" + "};\n" }, + { "by_ptr", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let p: *i32 = nil;\n" + " return a[p];\n" + "};\n" }, + { "by_bool", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let b: bool = true;\n" + " return a[b];\n" + "};\n" }, +}; + +struct okrow { const char *label; const char *src; int want; }; + +static const struct okrow ok_rows[] = { + { "by_int", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let i: int = 1;\n" + " return a[i];\n" + "};\n", + 20 }, + { "by_uint", + "package main;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let i: uint = 2;\n" + " return a[i];\n" + "};\n", + 30 }, + { "by_enum", + "package main;\n" + "type col = enum { A, B, C };\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " return a[col.B];\n" + "};\n", + 20 }, + { "by_alias_int", + "package main;\n" + "type ix = i32;\n" + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " let i: ix = 2;\n" + " return a[i];\n" + "};\n", + 30 }, +}; + +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], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated; } drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int nrej = (int)(sizeof reject_rows / sizeof reject_rows[0]); + int nok = (int)(sizeof ok_rows / sizeof ok_rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "idxr: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < nrej; i++) { + total++; + if (build_should_fail(drivers[d].path, + reject_rows[i].label, reject_rows[i].src, + d * 100 + i) != 0) + fail++; + } + for (int i = 0; i < nok; i++) { + total++; + int got = run_build(drivers[d].path, ok_rows[i].label, + ok_rows[i].src, d * 100 + i); + if (got != ok_rows[i].want) { + fprintf(stderr, "idxr[%s][%s]: exit=%d want=%d\n", + drivers[d].name, ok_rows[i].label, + got, ok_rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "idxr: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("idxr: %d/%d ok\n", total, total); + return 0; +}