From cf24af8b26b7f60fc36b48c7111e5afb46b4d385 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 16 May 2026 03:00:34 +0900 Subject: [PATCH] cstage+selfhost+test: fold unary-over-literal in def DATA emit (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level `def NEG: i32 = -100;` skipped DATA emission in both stages — cstage's emit_defs and wwstage's emitdefconstants each carried a literal-leaf whitelist that excluded N_UN nodes. Same gap in check.c's eval_enum_value cstage-side. Surfaced during #10 (lib/os forced an `at` enum for AT_FDCWD=-100 etc. as workaround). Factor a single fold_int_literal helper (cstage check.c; wwstage cgen.ww). Handles N_INTLIT / N_RUNELIT / N_TRUE / N_FALSE / N_NIL plus N_UN with TK_MINUS / TK_TILDE / TK_PLUS recursively. Consume from eval_enum_value, emit_defs, emitdefconstants, enumevalmember — single source of truth for "is this a literal-leaf foldable". Side effect: cstage's def-emit set widens from {INTLIT, RUNELIT, TRUE} to match wwstage's pre-existing 5-shape set plus the new unary peel. Bootstrap byte-id holds (995_self_rebuild green). Test 631 (def_neg_global): 6 rows × cstage/wwstage run + asm byte-identity diff. Covers all three unary arms (-, ~, +), positive regression-pin, i32 + i64 + u32 slots. Follows up #26: revert lib/os.ww `at` enum to three top-level defs. --- Makefile | 8 +- cmd/w6c/cgen.c | 25 +-- cmd/wcc/check.c | 54 +++-- cmd/wcc/ww.h | 7 + selfhost/cmd/w6c/main.combined.ww | 42 +++- selfhost/cmd/wcc/cgen.ww | 42 +++- selfhost/cmd/wwdump/main.combined.ww | 42 +++- test/wcc/631_def_neg_global.c | 285 +++++++++++++++++++++++++++ 8 files changed, 457 insertions(+), 48 deletions(-) create mode 100644 test/wcc/631_def_neg_global.c diff --git a/Makefile b/Makefile index f7cb6894..17e93010 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6l $(BIN)/test_data_link \ $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ - $(BIN)/test_at_test $(BIN)/test_let_global \ + $(BIN)/test_at_test $(BIN)/test_let_global $(BIN)/test_def_neg_global \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ $(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \ $(BIN)/test_arr_elem_field_write \ @@ -305,6 +305,12 @@ $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_neg_global: test/wcc/631_def_neg_global.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_int_cast_signed: test/wcc/640_int_cast_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 31a5bee9..e4cfc326 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6919,23 +6919,24 @@ emit_lets(Cg *c, FILE *out, Node *file) } } -/* Emit DATA directives for top-level `def` constants whose value is - * an integer/rune literal. The w6a side stores the bytes inside .text - * and accesses are RIP-relative. - */ +/* Emit DATA directives for top-level `def` constants whose value + * folds to an integer literal. The w6a side stores the bytes inside + * .text and accesses are RIP-relative. + * + * fold_int_literal (cmd/wcc/check.c) gates: int/rune literal, + * true/false/nil, and a unary +/-/~ over the same. `def NEG: i32 = + * -100;` arrives as N_UN(TK_MINUS, N_INTLIT) — the unary peel is + * exactly what the gate is for. Anything richer (sibling refs, + * arithmetic) falls through; emit_defs has no scope to resolve + * names. */ static void emit_defs(Cg *c, FILE *out, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; - u64 v = 0; - if (d->rhs->kind == N_INTLIT || d->rhs->kind == N_RUNELIT) { - v = d->rhs->uval; - } else if (d->rhs->kind == N_TRUE) { - v = 1; - } else { - continue; /* skip non-integer-literal defs */ - } + u64 v; + if (!fold_int_literal(d->rhs, &v)) + continue; /* not a fold-able literal constant */ fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str)); for (int i = 0; i < 8; i++) { unsigned b = (unsigned)((v >> (i * 8)) & 0xff); diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 75cddec0..131a7fc2 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -169,23 +169,55 @@ tagged_success_type(Type *u) return u->params ? u->params->type : NULL; } -/* eval_enum_value — fold an enum member-value expression to a u64 - * constant. Sees prior siblings via the `prev` Tfield list (each - * carries the member's name and resolved value in .offset). Returns - * 1 on success; on failure emits the error and returns 0. The op set - * is the constant subset typical of Hare-style flag enums: - * literal, sibling-ident, + - * / % & | ^ << >>, unary - and ~. */ -static int -eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out) +/* fold_int_literal — fold the literal subset usable for top-level + * constant slots: int/rune literal, true/false/nil, and a unary + * +/-/~ over the same. No diagnostics; the caller decides what a + * miss means. Shared between eval_enum_value (literal leaves) and + * emit_defs (top-level def rhs). + * + * Whitelist kept tight on purpose: no N_IDENT (no sibling lookup, + * no symbol resolution), no N_BIN. Anything richer belongs in + * eval_enum_value, which calls this for its literal leaves and + * handles sibling/op recursion itself. */ +int +fold_int_literal(Node *n, u64 *out) { if (n == NULL) return 0; switch (n->kind) { case N_INTLIT: case N_RUNELIT: - *out = n->uval; - return 1; + *out = n->uval; return 1; case N_TRUE: *out = 1; return 1; - case N_FALSE: *out = 0; return 1; + case N_FALSE: + case N_NIL: *out = 0; return 1; + case N_UN: { + u64 v; + if (!fold_int_literal(n->lhs, &v)) return 0; + switch (n->op) { + case TK_MINUS: *out = (u64)(-(i64)v); return 1; + case TK_TILDE: *out = ~v; return 1; + case TK_PLUS: *out = v; return 1; + default: return 0; + } + } + default: return 0; + } +} + +/* eval_enum_value — fold an enum member-value expression to a u64 + * constant. Sees prior siblings via the `prev` Tfield list (each + * carries the member's name and resolved value in .offset). Returns + * 1 on success; on failure emits the error and returns 0. The op set + * is the constant subset typical of Hare-style flag enums: + * literal, sibling-ident, + - * / % & | ^ << >>, unary - and ~. + * Literal leaves and unary-over-literal are delegated to + * fold_int_literal so the fold logic lives in one place. */ +static int +eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out) +{ + if (n == NULL) return 0; + if (fold_int_literal(n, out)) return 1; + switch (n->kind) { case N_IDENT: { for (Tfield *f = prev; f; f = f->next) { if (f->name && n->str && diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 2f7250cd..a8dd9b45 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -531,4 +531,11 @@ struct Checker { void check_init(Checker*, Arena*); void check_file(Checker*, Node *file); +/* fold_int_literal — fold an integer-literal-leaf expression to its + * u64 value. Accepts int/rune literal, true/false/nil, and a unary + * +/-/~ over the same (any depth). No sibling-ident, no binary op. + * Returns 1 on success; the call site decides what a miss means + * (eval_enum_value's leaf delegation, emit_defs's DATA-row gate). */ +int fold_int_literal(Node*, u64*); + #endif /* WW_H */ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0b2da955..d5028b1e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -16933,13 +16933,39 @@ fn aliaslookup(c: *cgen, name: str) *node = { // member's u64 value (supporting auto-increment and sibling refs), // and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX. -fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { +// foldintliteral — fold the literal subset usable for top-level +// constant slots: int/rune literal, true/false/nil, and a unary +// +/-/~ over the same (any depth). No sibling-ident, no binary op. +// Shared between enumevalmember (literal leaves) and +// emitdefconstants (top-level def rhs). +// +// Whitelist kept tight on purpose: anything richer (sibling refs, +// arithmetic) belongs in enumevalmember, which calls this for its +// literal leaves and handles the rest itself. +fn foldintliteral(e: *node, out: *u64) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == nkind.N_TRUE) { *out = 1u64; return true; }; if (k == nkind.N_FALSE) { *out = 0u64; return true; }; + if (k == nkind.N_NIL) { *out = 0u64; return true; }; + if (k == nkind.N_UN) { + let v: u64; + if (!foldintliteral(e.lhs, &v)) { return false; }; + let op: tkind = e.op; + if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (op == tkind.TK_PLUS) { *out = v; return true; }; + return false; + }; + return false; +}; + +fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { + if (e == nil) { return false; }; + if (foldintliteral(e, out)) { return true; }; + let k: nkind = e.kind; if (k == nkind.N_IDENT) { let m: *enummember = prev; for (m != nil) { @@ -18065,8 +18091,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; -// emitdefconstants — DATA directive per top-level int-literal `def`. -// 8 bytes little-endian to match what the C cgen emits. +// emitdefconstants — DATA directive per top-level fold-to-literal +// `def`. 8 bytes little-endian to match what the C cgen emits. +// foldintliteral gates: int/rune literal, true/false/nil, and a +// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as +// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the +// gate is for. fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -18075,11 +18105,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { let v: u64 = 0u64; let ok: bool = false; if (r != nil) { - if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; - if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; - if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + ok = foldintliteral(r, &v); }; if (ok) { emitline("DATA "); diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 9275992f..d027f2eb 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -117,13 +117,39 @@ fn aliaslookup(c: *cgen, name: str) *node = { // member's u64 value (supporting auto-increment and sibling refs), // and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX. -fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { +// foldintliteral — fold the literal subset usable for top-level +// constant slots: int/rune literal, true/false/nil, and a unary +// +/-/~ over the same (any depth). No sibling-ident, no binary op. +// Shared between enumevalmember (literal leaves) and +// emitdefconstants (top-level def rhs). +// +// Whitelist kept tight on purpose: anything richer (sibling refs, +// arithmetic) belongs in enumevalmember, which calls this for its +// literal leaves and handles the rest itself. +fn foldintliteral(e: *node, out: *u64) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == nkind.N_TRUE) { *out = 1u64; return true; }; if (k == nkind.N_FALSE) { *out = 0u64; return true; }; + if (k == nkind.N_NIL) { *out = 0u64; return true; }; + if (k == nkind.N_UN) { + let v: u64; + if (!foldintliteral(e.lhs, &v)) { return false; }; + let op: tkind = e.op; + if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (op == tkind.TK_PLUS) { *out = v; return true; }; + return false; + }; + return false; +}; + +fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { + if (e == nil) { return false; }; + if (foldintliteral(e, out)) { return true; }; + let k: nkind = e.kind; if (k == nkind.N_IDENT) { let m: *enummember = prev; for (m != nil) { @@ -1249,8 +1275,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; -// emitdefconstants — DATA directive per top-level int-literal `def`. -// 8 bytes little-endian to match what the C cgen emits. +// emitdefconstants — DATA directive per top-level fold-to-literal +// `def`. 8 bytes little-endian to match what the C cgen emits. +// foldintliteral gates: int/rune literal, true/false/nil, and a +// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as +// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the +// gate is for. fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -1259,11 +1289,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { let v: u64 = 0u64; let ok: bool = false; if (r != nil) { - if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; - if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; - if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + ok = foldintliteral(r, &v); }; if (ok) { emitline("DATA "); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4b4c704b..60fb7012 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -16933,13 +16933,39 @@ fn aliaslookup(c: *cgen, name: str) *node = { // member's u64 value (supporting auto-increment and sibling refs), // and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX. -fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { +// foldintliteral — fold the literal subset usable for top-level +// constant slots: int/rune literal, true/false/nil, and a unary +// +/-/~ over the same (any depth). No sibling-ident, no binary op. +// Shared between enumevalmember (literal leaves) and +// emitdefconstants (top-level def rhs). +// +// Whitelist kept tight on purpose: anything richer (sibling refs, +// arithmetic) belongs in enumevalmember, which calls this for its +// literal leaves and handles the rest itself. +fn foldintliteral(e: *node, out: *u64) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == nkind.N_TRUE) { *out = 1u64; return true; }; if (k == nkind.N_FALSE) { *out = 0u64; return true; }; + if (k == nkind.N_NIL) { *out = 0u64; return true; }; + if (k == nkind.N_UN) { + let v: u64; + if (!foldintliteral(e.lhs, &v)) { return false; }; + let op: tkind = e.op; + if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (op == tkind.TK_PLUS) { *out = v; return true; }; + return false; + }; + return false; +}; + +fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { + if (e == nil) { return false; }; + if (foldintliteral(e, out)) { return true; }; + let k: nkind = e.kind; if (k == nkind.N_IDENT) { let m: *enummember = prev; for (m != nil) { @@ -18065,8 +18091,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; -// emitdefconstants — DATA directive per top-level int-literal `def`. -// 8 bytes little-endian to match what the C cgen emits. +// emitdefconstants — DATA directive per top-level fold-to-literal +// `def`. 8 bytes little-endian to match what the C cgen emits. +// foldintliteral gates: int/rune literal, true/false/nil, and a +// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as +// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the +// gate is for. fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -18075,11 +18105,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { let v: u64 = 0u64; let ok: bool = false; if (r != nil) { - if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; - if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; - if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; - if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; + ok = foldintliteral(r, &v); }; if (ok) { emitline("DATA "); diff --git a/test/wcc/631_def_neg_global.c b/test/wcc/631_def_neg_global.c new file mode 100644 index 00000000..e741043c --- /dev/null +++ b/test/wcc/631_def_neg_global.c @@ -0,0 +1,285 @@ +/* + * 631_def_neg_global — top-level `def X: T = N;` for non-trivially- + * literal N. The pre-fix bug (#24): `def NEG: i32 = -100;` parsed + * the rhs as N_UN(TK_MINUS, N_INTLIT) and both stages' emit_defs / + * emitdefconstants skipped any non-leaf-literal shape — no DATA row + * was emitted, and any reference to NEG failed to link with + * "undefined reference". Workaround in tree had been bundling such + * flags into an enum (see the `at` enum at lib/os/os.ww:393). + * + * The fix lifts the literal-fold core into a shared helper + * (fold_int_literal / foldintliteral) that handles + * INTLIT/RUNELIT/TRUE/FALSE/NIL plus a unary +/-/~ wrapper over the + * same. emit_defs (cstage cmd/w6c/cgen.c) and emitdefconstants + * (wwstage selfhost/cmd/wcc/cgen.ww) both gate on it. The shared + * helper is reused by eval_enum_value / enumevalmember so the fold + * logic lives in one place per stage. + * + * Rows pin: + * - negative-i32: the headline bug. exit=42. + * - positive-i32: regression check; the same emit path must still + * produce a DATA row for an unwrapped literal. + * - tilde-i32: unary ~ over N_INTLIT (the other op in the unary + * whitelist beyond TK_MINUS / TK_PLUS). + * - negative-i64: 8-byte slot via the same path; verifies sign + * extension through the i64 load. + * - negative-u32-cast: `def X: u32 = (-1): u32;` exercises an + * N_CAST wrapping the N_UN. The fold gate doesn't peel N_CAST + * — but cgen sees the cast and consumes it on the read side — + * so this is OUT OF SCOPE for the gate; we use u32 differently. + * Instead the u32 row uses a tilde to get the all-ones pattern: + * `def X: u32 = ~0u32;` truncates cleanly into a u32 slot and + * reads back as 0xFFFFFFFF. + * - unary-plus-i32: `def X: i32 = +5;` — TK_PLUS noop, completes + * the unary whitelist coverage. + * + * Per-fixture: compile+link+run via the `ww` driver (cstage) and + * via `ww_ww` (wwstage) if it exists. Asm byte-identity diff between + * cstage's w6c and wwstage's w6c_ww closes the symmetry corner. + */ +#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[] = { + /* The headline #24 case: N_UN(TK_MINUS, N_INTLIT) rhs. + * Pre-fix this failed at link time with + * "undefined reference to '.NEG'". */ + { "negative-i32", + "def NEG: i32 = -100;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = NEG;\n" + "\tif (x == -100) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Regression check: an unwrapped N_INTLIT rhs must still + * emit a DATA row. If the fold-gate rewrite accidentally + * narrows the whitelist, this row catches it. */ + { "positive-i32", + "def POS: i32 = 100;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = POS;\n" + "\tif (x == 100) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Unary tilde over N_INTLIT — the other arithmetic op in + * the unary whitelist. ~0 is -1 in i32 two's complement; + * reading and comparing as i32 pins the slot's full + * sign-extended form. */ + { "tilde-i32", + "def NTIL: i32 = ~0;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = NTIL;\n" + "\tif (x == -1) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* 8-byte slot via the same fold path. Catches any width- + * specific bug in the DATA-row emit (DATA always writes + * 8 bytes; the i64 typed def is the natural cardinality + * match for that slot). */ + { "negative-i64", + "def NEG: i64 = -1234567890i64;\n" + "fn main() i32 = {\n" + "\tlet x: i64 = NEG;\n" + "\tif (x == -1234567890i64) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Unsigned (u32) slot. `~0u32` is all-ones; reading it + * back through a u32 local and comparing against the + * literal pins both the fold (TK_TILDE) and the + * sign-vs-zero-extend on load. */ + { "tilde-u32", + "def UMAX: u32 = ~0u32;\n" + "fn main() i32 = {\n" + "\tlet x: u32 = UMAX;\n" + "\tif (x == 4294967295u32) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Unary plus — TK_PLUS noop in the fold. Completes the + * unary whitelist coverage. */ + { "unary-plus-i32", + "def P: i32 = +5;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = P;\n" + "\tif (x == 5) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, +}; + +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/dng_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dng_%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 — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff. Pins the symmetric-emit contract: if either + * stage's fold helper drifts (e.g. one accepts N_NIL the other + * doesn't), the DATA row differs and ww2!=ww3 byte-id breaks. */ +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/dng_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/dng_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/dng_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_neg_global: 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_neg_global[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + /* Asm byte-identity diff, only when wwstage is built. */ + 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_neg_global: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("def_neg_global: %d/%d ok\n", total, total); + return 0; +}