check: widen const def-ref to declared int type in def init (#113)
A def initializer whose rhs references another def -- `def INT_MIN: int = I32_MIN;`, `def SIZE_MAX: size = U64_MAX;` -- failed to compile: an N_IDENT->SK_DEF types as the referent's DECLARED type (i32, u64), so the def-init assignability check (type_assignable) rejected i32 -> int / u64 -> size, even though the value is a compile-time constant that fits. This blocked faithful types/types::c limit defs (no cast in the Hare source). In a def initializer the rhs is a flexible constant. When it folds to a compile-time integer (the #88 eval_def_const path: sibling/imported def refs, casts, arithmetic) and the value fits the declared integer target, re-flexibilize it to UNTYPED_INT so the existing untyped-int->typed assignability path accepts it. This emulates Hare's flexible-constant promotion (ICONST -> promote_flexible/lower_flexible, ref/harec/src/types.c:860); def_cast_fits is the range check that keeps a genuine out-of-range narrowing a loud "not assignable" error, never a silent truncation (rule 7). It is strictly the const subset: the general CONCRETE (non-const) integer widening Hare does at types.c:1021-1037 is intentionally stricter in ww -- #115. cstage-only: the wwstage checker (selfhost/cmd/wcc/check.ww, "let init / return assignability") intentionally never checks def-init assignability (it stays quiet, leaving full inference to the C side), so it never rejected the widening -- the #88 stamp already laid the correct DATA row. Relaxing the cstage aligns the richer side DOWN to the leaner side (rule 10); both stages stamp the identical folded value, so emitted asm is byte-identical. The bootstrap corpus has zero cross-prim-width def-ref defs, so the new path is dead there and 990-997 are unperturbed. Coverage: test/wcc/760_def_widen_const (i32->int neg, u64->size, byte-id on each, cstage-only out-of-range narrowing fail-loud).
This commit is contained in:
310
test/wcc/760_def_widen_const.c
Normal file
310
test/wcc/760_def_widen_const.c
Normal file
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 760_def_widen_const — const-scoped cross-prim-width integer widening
|
||||
* in a `def` initializer (PROJECT #113).
|
||||
*
|
||||
* The gap: a def-reference types as the referent's DECLARED type, so
|
||||
* `def INT_MIN: int = I32_MIN;` saw the rhs as i32 and the def-init
|
||||
* assignability check (cmd/wcc/check.c, type_assignable) rejected i32 →
|
||||
* int. The fix: in a def initializer the rhs is a flexible constant, so
|
||||
* a const integer that folds to a concrete primitive width widens to
|
||||
* the declared integer type exactly as an UNTYPED_INT literal would,
|
||||
* when the value fits the target (def_cast_fits). This is the
|
||||
* const-scoped subset of Hare's general integer-widening; non-const
|
||||
* widening stays stricter in ww (PROJECT #115). It rides the #88
|
||||
* eval_def_const fold + stamp, so cgen's literal-only emit lays the
|
||||
* 8-byte DATA row with no codegen change.
|
||||
*
|
||||
* The fix is cstage-only: the wwstage checker (selfhost/cmd/wcc/check.ww,
|
||||
* "let init / return assignability") intentionally never checks def-init
|
||||
* assignability (it stays quiet, leaving full inference to the C side),
|
||||
* so it never rejected the widening — the #88 stamp already laid the
|
||||
* correct row. Relaxing the cstage aligns the richer side DOWN to the
|
||||
* leaner side (CLAUDE.md rule 10), and both stages stamp the identical
|
||||
* folded value, so the emitted asm is byte-identical.
|
||||
*
|
||||
* Coverage:
|
||||
* 1. EXEC (cstage `ww`, + wwstage `ww_ww` if built): i32 → int (a
|
||||
* negative value, to pin sign-extension across the widen) and
|
||||
* u64 → size (a value above the u32 range, to pin the full 8-byte
|
||||
* slot) — each read back at a use site to prove the DATA row links
|
||||
* and carries the right value.
|
||||
* 2. BYTE-ID: cstage w6c vs wwstage w6c_ww `.s` for every exec row
|
||||
* (proves the const-fold stamp is bit-identical across stages).
|
||||
* 3. FAIL-LOUD (rule 7), cstage only: an out-of-range narrowing
|
||||
* def-ref (`def S: i16 = BIG;` where BIG overflows i16) must still
|
||||
* fail the cstage build — def_cast_fits keeps the widening from
|
||||
* becoming a silent truncation. Asserted on the cstage alone
|
||||
* because the wwstage has no def-init assignability check at all
|
||||
* (pre-existing leanness, unchanged by #113).
|
||||
*/
|
||||
#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;
|
||||
}
|
||||
|
||||
/* ---- 1. EXEC rows (compile+link+run) -------------------------------- */
|
||||
|
||||
struct row { const char *label; const char *src; int want; };
|
||||
|
||||
static const struct row exec_rows[] = {
|
||||
/* i32 → int widening of a negative value: `def INT_MIN`-shaped.
|
||||
* I32_MIN = -2147483648; read back as int it must sign-extend to
|
||||
* the full 8-byte slot. main returns 42 iff the value round-trips. */
|
||||
{ "i32-to-int-neg",
|
||||
"def A_I32: i32 = -2147483648;\n"
|
||||
"def A_INT: int = A_I32;\n"
|
||||
"fn main() i32 = {\n"
|
||||
"\tlet x: int = A_INT;\n"
|
||||
"\tif (x == -2147483648) { return 42; };\n"
|
||||
"\treturn 1;\n"
|
||||
"};\n",
|
||||
42 },
|
||||
|
||||
/* small negative i32 → int, to pin -7 specifically (the task's
|
||||
* worked example). */
|
||||
{ "i32-to-int-small",
|
||||
"def A_I32: i32 = -7;\n"
|
||||
"def A_INT: int = A_I32;\n"
|
||||
"fn main() i32 = {\n"
|
||||
"\tlet x: int = A_INT;\n"
|
||||
"\tif (x == -7) { return 42; };\n"
|
||||
"\treturn 1;\n"
|
||||
"};\n",
|
||||
42 },
|
||||
|
||||
/* u64 → size widening of a value above the u32 range: pins the
|
||||
* full 8-byte slot survives the widen (0x1_0000_0003 = 4294967299). */
|
||||
{ "u64-to-size",
|
||||
"def BIG_U64: u64 = 4294967299;\n"
|
||||
"def S_SIZE: size = BIG_U64;\n"
|
||||
"fn main() i32 = {\n"
|
||||
"\tlet x: size = S_SIZE;\n"
|
||||
"\tif (x == 4294967299) { 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/dwc_%d_%d.ww", getpid(), i);
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/dwc_%d_d_%d", getpid(), i);
|
||||
|
||||
FILE *f = fopen(src, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(r->src, f);
|
||||
fclose(f);
|
||||
|
||||
mkdir(tmpdir, 0755);
|
||||
/* timeout 180 per repo convention; test/run does not bound
|
||||
* individual binaries, so an unguarded hang would stall make test. */
|
||||
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %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;
|
||||
}
|
||||
|
||||
/* ---- shared .s emit -------------------------------------------------- */
|
||||
|
||||
static int
|
||||
emit_s(const char *w6c, const char *src, char *out_s, size_t cap)
|
||||
{
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof cmd, "timeout 180 %s -o %s %s 2>/dev/null",
|
||||
w6c, out_s, src);
|
||||
(void)cap;
|
||||
return runwait(cmd);
|
||||
}
|
||||
|
||||
/* ---- 2. byte-identity of cstage vs wwstage .s ----------------------- */
|
||||
|
||||
static int
|
||||
asm_byte_identical(const char *bin, const char *src, const char *label, int i)
|
||||
{
|
||||
char wwsrc[64], cs[64], ws[64];
|
||||
snprintf(wwsrc, sizeof wwsrc, "/tmp/dwc_bi_%d_%d.ww", getpid(), i);
|
||||
snprintf(cs, sizeof cs, "/tmp/dwc_bi_%d_%d_c.s", getpid(), i);
|
||||
snprintf(ws, sizeof ws, "/tmp/dwc_bi_%d_%d_w.s", getpid(), i);
|
||||
|
||||
FILE *f = fopen(wwsrc, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(src, f);
|
||||
fclose(f);
|
||||
|
||||
char w6c[640], w6c_ww[640];
|
||||
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
|
||||
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
|
||||
|
||||
if (emit_s(w6c, wwsrc, cs, sizeof cs) != 0) {
|
||||
fprintf(stderr, "row[%s]: w6c errored\n", label);
|
||||
unlink(wwsrc);
|
||||
return -1;
|
||||
}
|
||||
if (emit_s(w6c_ww, wwsrc, ws, sizeof ws) != 0) {
|
||||
fprintf(stderr, "row[%s]: w6c_ww errored\n", label);
|
||||
unlink(wwsrc); 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",
|
||||
label);
|
||||
unlink(wwsrc); unlink(cs); unlink(ws);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---- 3. fail-loud (cstage only): out-of-range narrowing def-ref ----- */
|
||||
|
||||
/* `def BIG: i32 = 70000; def S: i16 = BIG;` — 70000 overflows i16, so
|
||||
* def_cast_fits says the value does not fit and the const-scoped widen
|
||||
* does NOT fire; the def-init assignability check then rejects i32 →
|
||||
* i16. Compile via w6c only; success means the build FAILED as required
|
||||
* (nonzero w6c exit), never a silent truncation. */
|
||||
static const char narrow_src[] =
|
||||
"def BIG_I32: i32 = 70000;\n"
|
||||
"def S_I16: i16 = BIG_I32;\n"
|
||||
"export fn main() i32 = { let s: i16 = S_I16; return s: i32; };\n";
|
||||
|
||||
static int
|
||||
cstage_narrow_fails(const char *w6c)
|
||||
{
|
||||
char src[64], s[64], cmd[1024];
|
||||
snprintf(src, sizeof src, "/tmp/dwc_nl_%d.ww", getpid());
|
||||
snprintf(s, sizeof s, "/tmp/dwc_nl_%d.s", getpid());
|
||||
|
||||
FILE *f = fopen(src, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(narrow_src, f);
|
||||
fclose(f);
|
||||
|
||||
snprintf(cmd, sizeof cmd, "timeout 180 %s -o %s %s 2>/dev/null",
|
||||
w6c, s, src);
|
||||
int rc = runwait(cmd);
|
||||
unlink(src); unlink(s);
|
||||
if (rc == 0) {
|
||||
fprintf(stderr,
|
||||
"narrow[cstage]: w6c exited 0 (expected loud failure, "
|
||||
"no silent truncation)\n");
|
||||
return -1;
|
||||
}
|
||||
if (rc == 124) {
|
||||
fprintf(stderr, "narrow[cstage]: w6c timed out\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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], w6c[1024], w6c_ww[1024];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
|
||||
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
|
||||
|
||||
int have_ww = (access(w6c_ww, X_OK) == 0);
|
||||
int total = 0, fail = 0;
|
||||
|
||||
/* 1. exec via cstage `ww` (+ wwstage `ww_ww` if built) */
|
||||
struct { const char *name; const char *path; int gated; }
|
||||
drivers[] = {
|
||||
{ "cstage", cdrv, 0 },
|
||||
{ "wwstage", wdrv, 1 },
|
||||
{ NULL, NULL, 0 },
|
||||
};
|
||||
int nexec = (int)(sizeof exec_rows / sizeof exec_rows[0]);
|
||||
for (int d = 0; drivers[d].name; d++) {
|
||||
if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) {
|
||||
fprintf(stderr, "def_widen_const: skip %s (no %s)\n",
|
||||
drivers[d].name, drivers[d].path);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < nexec; i++) {
|
||||
int got = run_driver(drivers[d].path, &exec_rows[i], i);
|
||||
total++;
|
||||
if (got != exec_rows[i].want) {
|
||||
fprintf(stderr,
|
||||
"def_widen_const[%s][%s]: exit=%d want=%d\n",
|
||||
drivers[d].name, exec_rows[i].label,
|
||||
got, exec_rows[i].want);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. byte-id on every exec row (only when wwstage is built) */
|
||||
if (have_ww) {
|
||||
for (int i = 0; i < nexec; i++) {
|
||||
total++;
|
||||
if (asm_byte_identical(bin, exec_rows[i].src,
|
||||
exec_rows[i].label, i) != 0)
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. fail-loud (cstage only) */
|
||||
total++;
|
||||
if (cstage_narrow_fails(w6c) != 0) fail++;
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr,
|
||||
"def_widen_const: %d/%d fixtures failed\n", fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("def_widen_const: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user