/* * 660_field_signed — sub-word signed struct/tuple/index field LOAD * must sign-extend (MOVSBQ / MOVSWQ / MOVSXD), not zero-extend. * Companion to 640 (N_CAST narrow) and 650 (chained N_DOT spine); * pins task #10's field-load fix and task #5's TY_ENUM recursion * through the predicate. Each fixture round-trips a high-bit-set * value through a field, then widens back to i64 and compares. * * Exercises both stages via `ww` (cstage) and `ww_ww` (wwstage) * when present, so a regression on either side is caught here. */ #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[] = { /* i8 field LOAD: write -1 (0xFF), widen back; must read -1, not 255. */ { "i8_field_load", "type box = struct { v: i8, pad: i8 };\n" "fn main() i32 = {\n" " let b: box; b.v = -1i64: i8; b.pad = 0i64: i8;\n" " let z: i64 = b.v: i64;\n" " if (z == -1i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* i16 field LOAD: write -32768 (0x8000), widen back; must read -32768. */ { "i16_field_load", "type box = struct { v: i16, pad: i16 };\n" "fn main() i32 = {\n" " let b: box; b.v = -32768i64: i16; b.pad = 0i64: i16;\n" " let z: i64 = b.v: i64;\n" " if (z == -32768i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* i32 field LOAD: pins MOVSXD on the field-read path. */ { "i32_field_load", "type box = struct { v: i32, pad: i32 };\n" "fn main() i32 = {\n" " let b: box; b.v = -2147483648i64: i32; b.pad = 0i32;\n" " let z: i64 = b.v: i64;\n" " if (z == -2147483648i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* u8 field LOAD with high bit: must zero-extend (regression check * — the symmetric helper must NOT sign-extend u8). */ { "u8_field_load", "type box = struct { v: u8, pad: u8 };\n" "fn main() i32 = {\n" " let b: box; b.v = 0xFFu64: u8; b.pad = 0u8;\n" " let z: u64 = b.v: u64;\n" " if (z == 0xFFu64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* u16 / u32 field LOAD: zero-extension on the field-read path. */ { "u16_field_load", "type box = struct { v: u16, pad: u16 };\n" "fn main() i32 = {\n" " let b: box; b.v = 0xFFFFu64: u16; b.pad = 0u16;\n" " let z: u64 = b.v: u64;\n" " if (z == 0xFFFFu64) { return 42; };\n" " return 0;\n" "};\n", 42 }, { "u32_field_load", "type box = struct { v: u32, pad: u32 };\n" "fn main() i32 = {\n" " let b: box; b.v = 0xFFFFFFFFu64: u32; b.pad = 0u32;\n" " let z: u64 = b.v: u64;\n" " if (z == 0xFFFFFFFFu64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* bool field LOAD: zero-extends (0 or 1). The new helper must not * sign-extend bool just because it's not in typenameisunsigned. */ { "bool_field_load", "type box = struct { v: bool, pad: bool };\n" "fn main() i32 = {\n" " let b: box; b.v = true; b.pad = false;\n" " if (b.v) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Chained dotted load over an i8 leaf: spine walker must pick * MOVSBQ at the terminal. Complements 650's i32/u32 leaf rows. */ { "chained_i8_leaf_load", "type inner = struct { v: i8, pad: i8 };\n" "type outer = struct { i: inner, tag: i32 };\n" "fn main() i32 = {\n" " let o: outer; o.i.v = -1i64: i8; o.i.pad = 0i8; o.tag = 0;\n" " let z: i64 = o.i.v: i64;\n" " if (z == -1i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Chained i16 leaf — pins MOVSWQ through the spine walker. */ { "chained_i16_leaf_load", "type inner = struct { v: i16, pad: i16 };\n" "type outer = struct { i: inner, tag: i32 };\n" "fn main() i32 = {\n" " let o: outer; o.i.v = -32768i64: i16; o.i.pad = 0i16; o.tag = 0;\n" " let z: i64 = o.i.v: i64;\n" " if (z == -32768i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Enum-aliased signed sub-word: `type myflag = i8` field. The * principled predicate (task #5: TY_ENUM recursion in * type_isunsigned, alias recursion in fieldissignedc) is what * makes this fire MOVSBQ instead of MOVZBQ. */ { "enum_signed_i8_alias_field_load", "type myflag = i8;\n" "type box = struct { v: myflag, pad: i8 };\n" "fn main() i32 = {\n" " let b: box; b.v = -1i64: myflag; b.pad = 0i8;\n" " let z: i64 = b.v: i64;\n" " if (z == -1i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* N_CAST narrow on enum-aliased source. Without TY_ENUM recursion * in type_isunsigned, the symmetric narrow gate misses this case * and the cast is a silent no-op. */ { "enum_alias_cast_narrow", "type myflag = i8;\n" "fn main() i32 = {\n" " let x: i64 = 0xFF80i64;\n" " let y: myflag = x: myflag;\n" " let z: i64 = y: i64;\n" " if (z == -128i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Enum-aliased unsigned narrow: the symmetric helper must * zero-extend an enum aliased to u8. */ { "enum_unsigned_u8_alias_field_load", "type byteflag = u8;\n" "type box = struct { v: byteflag, pad: u8 };\n" "fn main() i32 = {\n" " let b: box; b.v = 0xFFu64: byteflag; b.pad = 0u8;\n" " let z: u64 = b.v: u64;\n" " if (z == 0xFFu64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* `*p OP= v` with *p:u32 and high bit set — pins the hidden * bug at cgen.c N_ASSIGN deref-compound (was hardcoded MOVSXD * for sz=4, sign-extending u32). Old path: load 0x80000001 → * MOVSXD → 0xFFFFFFFF80000001, SHRQ 1 → 0x7FFFFFFFC0000000, * MOVL store → 0xC0000000. New path: MOVL → 0x80000001, SHRQ 1 * → 0x40000000 (the correct u32 logical right-shift result). */ { "deref_compound_u32_high_bit", "fn main() i32 = {\n" " let x: u32 = 0x80000001u32;\n" " let p: *u32 = &x;\n" " *p >>= 1u32;\n" " if (x == 0x40000000u32) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 pin (widen-deref). Natural shape `fn f(p: *i32)` * with `*p = -16i32` lowers to MOVL (4B store) into the * caller's 8B slot. The caller's slot was zero-init via MOVQ, * so the upper 4B stay zero; a later i64-widening read emits * MOVQ (8B raw) and returns 0x00000000_FFFFFFF0 = 4294967280 * instead of -16. Surfaced inside dotchainresolve when * worker-retire-dotchain tried i32 out-params; the helper now * widens all numeric out-params to *i64 as a workaround until * #19 lands. This row pins the WORKAROUND: out-param typed * *i64, caller reads i64 → MOVQ store/load pair agree, -16 * round-trips intact. */ { "i64_out_param_neg_widen_deref_workaround", "fn setneg(p: *i64) void = { *p = -16i64; };\n" "fn main() i32 = {\n" " let x: i64 = 0i64;\n" " setneg(&x);\n" " if (x == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix. Natural `*p: *i32` shape — `*p = -16i32` lowers * to MOVL (correct 4B store to a 4B pointee), and the caller's * later i64-widening read must MOVSXD the slot so the sign bit * propagates. Without the fix, the MOVQ read of the slot returns * 0x00000000_FFFFFFF0 (4294967280) and the equality fails. */ { "i32_out_param_neg_widen_deref", "fn setneg(p: *i32) void = { *p = -16i32; };\n" "fn main() i32 = {\n" " let x: i32 = 0i32;\n" " setneg(&x);\n" " let z: i64 = x: i64;\n" " if (z == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix, i16 sibling — `*p: *i16 = -16i16` writes 2B, * caller widens to i64. Without MOVSWQ the read sees 0xFFF0 * (65520). */ { "i16_out_param_neg_widen_deref", "fn setneg(p: *i16) void = { *p = -16i64: i16; };\n" "fn main() i32 = {\n" " let x: i16 = 0i16;\n" " setneg(&x);\n" " let z: i64 = x: i64;\n" " if (z == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix, i8 sibling — `*p: *i8 = -16i8` writes 1B, caller * widens to i64. Without MOVSBQ the read sees 0xF0 (240). */ { "i8_out_param_neg_widen_deref", "fn setneg(p: *i8) void = { *p = -16i64: i8; };\n" "fn main() i32 = {\n" " let x: i8 = 0i8;\n" " setneg(&x);\n" " let z: i64 = x: i64;\n" " if (z == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix, top-level let target. Same shape but the local * `x` is replaced by a top-level let so the read goes through * the RIP-relative path. Without the fix the read is MOVQ from * &x(SB) and sees zero-extended garbage. */ { "i32_global_neg_widen_deref", "let g: i32 = 0i32;\n" "fn setneg(p: *i32) void = { *p = -16i32; };\n" "fn main() i32 = {\n" " setneg(&g);\n" " let z: i64 = g: i64;\n" " if (z == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix, compound RMW on a narrow local that was last * written via a deref-store. Pins the cgassign restructure that * routes the load half of `x OP= v` through localloadop (so the * upper bytes feeding the combine come from a sign-extending * load, not raw 8B off the slot) and gates the direct-mem * ADDQ/SUBQ shortcut on `load_op == MOVQ`. Reads x as i32 to * exercise the in-band path without an explicit i64 widen. */ { "i32_compound_rmw_after_deref_store", "fn setv(p: *i32, v: i32) void = { *p = v; };\n" "fn main() i32 = {\n" " let x: i32 = 0i32;\n" " setv(&x, 0x7FFFFFFEi32);\n" " x += 1i32;\n" " if (x == 0x7FFFFFFFi32) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Task #19 fix, aliased narrow read. `type myinv = !i32` wraps * i32 with the err flag — its tnode is N_TBANG(N_TNAME("i32")). * Pins the TBANG/TENUM/TNAME → aliaslookup loop inside the * wwstage `localloadop`: without it `fieldsize` falls back to 8 * on the bare alias name and the read picks MOVQ, so the deref- * stored -16 round-trips as 4294967280 instead of -16. cstage * resolves the alias through Type.size and type_isunsigned. */ { "alias_bang_i32_widen_deref", "type myinv = !i32;\n" "fn setneg(p: *myinv) void = { *p = -16i64: myinv; };\n" "fn main() i32 = {\n" " let x: myinv = 0i64: myinv;\n" " setneg(&x);\n" " let z: i64 = x: i64;\n" " if (z == -16i64) { return 42; };\n" " return 0;\n" "};\n", 42 }, }; static int run_driver(const char *driver, const struct row *r, int i) { char src[128], tmpdir[64], outbin[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwfs_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/wwfs_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/wwfs_%d_%d", tmpdir, getpid(), i); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(src, "wb"); if (!f) { runwait(rmcmd); return -1; } fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s", driver, outbin, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); runwait(rmcmd); return -1; } int got = runwait(outbin); runwait(rmcmd); return got; } 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, "field_signed: 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, "field_signed[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (fail) { fprintf(stderr, "field_signed: %d/%d fixtures failed\n", fail, total); return 1; } printf("field_signed: %d/%d ok\n", total, total); return 0; }