From 0d1ae17dd059d73dab6b9d1a5df10265a897952c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 25 May 2026 11:16:08 +0900 Subject: [PATCH] check: def rhs const-fold resolves sibling/imported defs + casts (#88) ww top-level def rhs const-fold was literal-only (fold_int_literal at the codegen emit-defs step), so a def referencing another def, an imported def, or a cast was inexpressible -- blocking faithful types/types::c/math/strconv ports whose defs cross-reference. Fold at CHECK time: a recursive eval_def_const (pass-2 N_DEF arm, both stages) resolves N_IDENT/N_DOT via the checker's existing scope lookup to the target def's rhs, evaluates N_BIN through a shared fold_binop core (factored out of eval_enum_value so both compile-time-int-eval paths share one wrap/shift/divide table), strips identity/widening casts, and stamps rhs -> N_INTLIT. cgen is UNTOUCHED -- its existing literal-emit lays the DATA row. Gated to fire only when the plain literal fold fails, so existing defs keep their node and emitted asm is byte-identical (990-997 unperturbed by construction). Guards (rule 7): recursion depth cap fails loud on a def cycle (same/cross-module); a narrowing cast (rhs outside target range) fails loud rather than silently truncating. Both stages' eval_def_const stamp identically (shared fold_binop semantics) so the substituted literal -- and byte-id -- holds across stages (rule 10, at the check pass). a1 (same-module) + a2 (cross-module imported def) land together: the driver concatenates imports into one flat scope. Coverage: test/wcc/732_def_const_fold. --- Makefile | 7 + cmd/wcc/check.c | 202 ++++++++++-- selfhost/cmd/w6c/main.combined.ww | 200 ++++++++++-- selfhost/cmd/wcc/check.ww | 200 ++++++++++-- selfhost/cmd/wwdump/main.combined.ww | 200 ++++++++++-- test/wcc/732_def_const_fold.c | 438 +++++++++++++++++++++++++++ 6 files changed, 1173 insertions(+), 74 deletions(-) create mode 100644 test/wcc/732_def_const_fold.c diff --git a/Makefile b/Makefile index 040f2c96..849e28d1 100644 --- a/Makefile +++ b/Makefile @@ -217,6 +217,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(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_def_neg_global \ + $(BIN)/test_def_const_fold \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ $(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \ $(BIN)/test_arr_elem_field_write \ @@ -390,6 +391,12 @@ $(BIN)/test_def_neg_global: test/wcc/631_def_neg_global.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_const_fold: test/wcc/732_def_const_fold.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/wcc/check.c b/cmd/wcc/check.c index 3c4fb06a..19c4d34e 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -207,6 +207,32 @@ fold_int_literal(Node *n, u64 *out) } } +/* fold_binop — apply one constant binary op. The shared arithmetic + * core of the two compile-time-int-eval paths: eval_enum_value (enum + * member exprs) and eval_def_const (top-level def rhs, #88). Both + * route here so wrap/shift/divide semantics are defined ONCE — rule + * 10 demands the cstage and wwstage stamp the bit-identical literal, + * and a single op table is the only way to keep them from drifting. + * Returns 0 on division by zero or an op outside the constant subset; + * the caller maps that to its own diagnostic. */ +static int +fold_binop(Tkind op, u64 a, u64 b, u64 *out) +{ + switch (op) { + case TK_PLUS: *out = a + b; return 1; + case TK_MINUS: *out = a - b; return 1; + case TK_STAR: *out = a * b; return 1; + case TK_SLASH: if (b == 0) return 0; *out = a / b; return 1; + case TK_PERCENT: if (b == 0) return 0; *out = a % b; return 1; + case TK_AMP: *out = a & b; return 1; + case TK_PIPE: *out = a | b; return 1; + case TK_CARET: *out = a ^ b; return 1; + case TK_LSHIFT: *out = a << b; return 1; + case TK_RSHIFT: *out = a >> b; return 1; + 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 @@ -238,28 +264,13 @@ eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out) if (!eval_enum_value(c, n->lhs, prev, &a) || !eval_enum_value(c, n->rhs, prev, &b)) return 0; - switch (n->op) { - case TK_PLUS: *out = a + b; return 1; - case TK_MINUS: *out = a - b; return 1; - case TK_STAR: *out = a * b; return 1; - case TK_SLASH: - if (b == 0) goto divzero; - *out = a / b; return 1; - case TK_PERCENT: - if (b == 0) goto divzero; - *out = a % b; return 1; - case TK_AMP: *out = a & b; return 1; - case TK_PIPE: *out = a | b; return 1; - case TK_CARET: *out = a ^ b; return 1; - case TK_LSHIFT: *out = a << b; return 1; - case TK_RSHIFT: *out = a >> b; return 1; - default: + if (fold_binop(n->op, a, b, out)) + return 1; + if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0) + err(c, n->pos, "enum value: division by zero"); + else err(c, n->pos, "enum value: unsupported binary op %s", tokname(n->op)); - return 0; - } - divzero: - err(c, n->pos, "enum value: division by zero"); return 0; } case N_UN: { @@ -283,6 +294,146 @@ eval_enum_value(Checker *c, Node *n, Tfield *prev, u64 *out) } } +/* def_cast_fits — for a def-rhs `value: T` cast strip (#88), does the + * already-folded u64 `v` survive narrowing to integer target `t`? + * Identity / widening / same-width casts always fit. A genuine + * narrowing cast whose value falls outside the target's range must + * NOT be silently truncated (rule 7 / drew): the caller turns a + * miss into a loud error. Width comes from the type table (t->size, + * rule 13) — never a hardcoded layout literal. Pure-u64 arithmetic + * so the cstage and wwstage range check stay bit-identical (rule 10). + * The `8`s here are CHAR_BIT and the u64 byte-width, not type-layout + * sizes, so they are outside rule 13's scope. */ +static int +def_cast_fits(Type *t, u64 v) +{ + if (!type_isint(t)) return 1; /* non-int target: keep value as-is */ + u64 w = t->size; + if (w >= 8) return 1; /* 64-bit target: no narrowing */ + u64 bits = w * 8; + if (type_isunsigned(t)) + return (v >> bits) == 0; + /* signed: truncate to `bits` then sign-extend; fits iff unchanged */ + u64 mask = ((u64)1 << bits) - 1; + u64 sign = (u64)1 << (bits - 1); + u64 ext = ((v & mask) ^ sign) - sign; + return ext == v; +} + +/* eval_def_const — fold a top-level def's rhs to a u64 constant, + * resolving sibling and imported def references, casts, and + * arithmetic (#88). Reuses the shared fold_int_literal leaf/unary + * fold and the fold_binop arith core; the ONLY thing it does that + * eval_enum_value doesn't is resolve an identifier through the + * checker's flat scope (scope_lookup_prefer for a bare sibling ref, + * scope_lookup_in_module for a `mod.NAME` qualified ref) to the + * referent def's own rhs, then recurse. + * + * Why this stays a distinct evaluator from eval_enum_value rather + * than a full merge (rule 8 WHY): enum-member eval carries implicit + * prev+1 auto-increment and forward-only sibling lookup over a Tfield + * chain; def eval has neither — it resolves through the scope/decl + * graph, which can reference forward and across modules. The two + * lookup models don't reconcile cleanly, so they share the arith + * core (fold_binop) + leaf fold (fold_int_literal) and keep separate + * top-level shapes. + * + * `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A, + * incl. cross-module) hits the cap and fails loud rather than hanging + * (rule 7), mirroring the cgen.c nsteps>=16 abort precedent. */ +static int +eval_def_const(Checker *c, Node *n, u64 *out, int depth) +{ + if (n == NULL) return 0; + if (depth >= 16) { + err(c, n->pos, + "def value: reference chain too deep (cycle?)"); + return 0; + } + if (fold_int_literal(n, out)) return 1; + switch (n->kind) { + case N_BIN: { + u64 a, b; + if (!eval_def_const(c, n->lhs, &a, depth + 1) || + !eval_def_const(c, n->rhs, &b, depth + 1)) + return 0; + if (fold_binop(n->op, a, b, out)) + return 1; + if ((n->op == TK_SLASH || n->op == TK_PERCENT) && b == 0) + err(c, n->pos, "def value: division by zero"); + else + err(c, n->pos, "def value: unsupported binary op %s", + tokname(n->op)); + return 0; + } + case N_UN: { + /* fold_int_literal already covers unary-over-leaf; this + * arm catches unary over a resolved ref, e.g. `-A`. */ + u64 v; + if (!eval_def_const(c, n->lhs, &v, depth + 1)) 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: + err(c, n->pos, + "def value: unsupported unary op %s", + tokname(n->op)); + return 0; + } + } + case N_CAST: { + /* lhs = value, n->type = resolved target (set by cexpr's + * N_CAST arm in pass 2). Strip the cast, keeping the value; + * a narrowing cast that loses the value fails loud. */ + u64 v; + if (!eval_def_const(c, n->lhs, &v, depth + 1)) return 0; + if (!def_cast_fits(n->type, v)) { + err(c, n->pos, + "def value: narrowing cast loses value"); + return 0; + } + *out = v; + return 1; + } + case N_IDENT: { + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str); + if (s == NULL || s->kind != SK_DEF || + s->decl == NULL || s->decl->rhs == NULL) + return 0; + return eval_def_const(c, s->decl->rhs, out, depth + 1); + } + case N_DOT: { + if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0; + Sym *s = scope_lookup_in_module(c->cur, n->lhs->str, n->str); + if (s == NULL || s->kind != SK_DEF || + s->decl == NULL || s->decl->rhs == NULL) + return 0; + return eval_def_const(c, s->decl->rhs, out, depth + 1); + } + default: + return 0; + } +} + +/* stamp_intlit — rewrite a const-folded def rhs in place to the + * literal it evaluates to, preserving the node's cexpr-resolved type + * so the downstream DATA-row emit width and the invariant checks see + * a properly-typed literal leaf. Lets cgen's existing literal-only + * fold lay down the row with no codegen change (#88). */ +static void +stamp_intlit(Checker *c, Node *n, u64 v) +{ + n->kind = N_INTLIT; + n->uval = v; + n->op = 0; + n->str = aprintf(c->a, "%llu", (unsigned long long)v); + n->strlen = strlen(n->str); + n->lhs = n->rhs = n->cond = n->body = n->els = n->list = NULL; + n->tsuffix = NULL; + /* n->type left intact (the type cexpr inferred for the rhs). */ +} + static Type * resolve_type(Checker *c, Node *n) { @@ -2013,6 +2164,17 @@ check_file(Checker *c, Node *file) err(c, d->pos, "def %s init %s not assignable to %s", d->str, type_name(c->a, rt), type_name(c->a, d->type)); + /* #88: const-fold sibling/imported def refs, + * casts, and arithmetic so cgen's literal-only + * emit can lay down the DATA row. GATED on the + * plain literal fold missing first, so existing + * literal/unary defs keep their rhs node and the + * emitted bytes stay byte-identical. */ + u64 dv; + if (rt != ty_err + && !fold_int_literal(d->rhs, &dv) + && eval_def_const(c, d->rhs, &dv, 0)) + stamp_intlit(c, d->rhs, dv); } break; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1e736f7b..d3c7bc10 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8060,6 +8060,174 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; +// foldbinop — shared constant binary-op core for the two compile-time +// integer evaluators in this file: enumvalfold (enum member exprs) and +// evaldefconst (top-level def rhs, #88). One op table so the cstage +// (cmd/wcc/check.c fold_binop) and wwstage stamp the bit-identical +// literal — rule 10 lives at the check pass for #88. Returns false on +// division by zero or an op outside the constant subset; the caller +// maps that to its own diagnostic. +fn foldbinop(op: tkind, a: u64, b: u64, out: *u64) bool = { + if (op == tkind.TK_PLUS) { *out = a + b; return true; }; + if (op == tkind.TK_MINUS) { *out = a - b; return true; }; + if (op == tkind.TK_STAR) { *out = a * b; return true; }; + if (op == tkind.TK_SLASH) { + if (b == 0u64) { return false; }; + *out = a / b; return true; + }; + if (op == tkind.TK_PERCENT) { + if (b == 0u64) { return false; }; + *out = a % b; return true; + }; + if (op == tkind.TK_AMP) { *out = a & b; return true; }; + if (op == tkind.TK_PIPE) { *out = a | b; return true; }; + if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; + if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; + if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; + return false; +}; + +// deffolderr — loud diagnostic + checker error count bump for an +// unfoldable def rhs (cycle / narrowing-cast / bad op). c.errs > 0 +// gates cgen off in main.ww:165, so this fails the build rather than +// emitting a missing DATA row silently (rule 7). cstage twin: err() +// in cmd/wcc/check.c. +fn deffolderr(c: *checker, n: *node, msg: str) void = { + os.write(2, n.file.ptr, n.file.len: u64); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +// defcastfits — wwstage twin of cstage def_cast_fits (cmd/wcc/check.c): +// does the folded u64 `v` survive narrowing to integer target `t`? +// Identity / widening / same-width casts always fit; a genuine +// narrowing cast whose value falls outside the target range must NOT +// be silently truncated (rule 7 / drew). Width via the type table +// (t.size, rule 13); the 8s are CHAR_BIT and the u64 byte-width, not +// type-layout sizes, so they sit outside rule 13's scope. Pure-u64 so +// the range check is bit-identical to cstage (rule 10). +fn defcastfits(t: *tinfo, v: u64) bool = { + if (!typeisint(t)) { return true; }; // non-int target: keep value + let w: u64 = t.size; + if (w >= 8u64) { return true; }; // 64-bit target: no narrowing + let bits: u64 = w * 8u64; + if (typeisunsigned(t)) { return (v >> bits) == 0u64; }; + // signed: truncate to `bits` then sign-extend; fits iff unchanged + let mask: u64 = (1u64 << bits) - 1u64; + let sign: u64 = 1u64 << (bits - 1u64); + let ext: u64 = ((v & mask) ^ sign) - sign; + return ext == v; +}; + +// evaldefconst — fold a top-level def's rhs to a u64 constant, +// resolving sibling and imported def references, casts, and +// arithmetic (#88). Reuses foldintliteral (leaf/unary) + foldbinop +// (arith); the ONLY thing it does that enumvalfold doesn't is resolve +// an identifier through the checker's flat scope (scopelookupprefer +// for a bare sibling ref, scopelookupinmodule for `mod.NAME`) to the +// referent def's own rhs, then recurse. +// +// Why this stays distinct from enumvalfold rather than a full merge +// (rule 8 WHY): enum-member eval carries implicit prev+1 auto-increment +// and forward-only sibling lookup over the member chain; def eval has +// neither — it resolves through the scope/decl graph, which references +// forward and across modules. The two lookup models don't reconcile +// cleanly, so they share the arith core (foldbinop) + leaf fold +// (foldintliteral) and keep separate top-level shapes. +// +// `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A, +// incl. cross-module) hits the cap and fails loud rather than hanging +// (rule 7), mirroring the cgen nsteps>=16 abort precedent. +fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { + if (n == nil) { return false; }; + if (depth >= 16) { + deffolderr(c, n, "def value: reference chain too deep (cycle?)"); + return false; + }; + if (foldintliteral(n, out)) { return true; }; + let k: nkind = n.kind; + if (k == nkind.N_BIN) { + let a: u64 = 0u64; + let b: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; }; + if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; }; + if (foldbinop(n.op, a, b, out)) { return true; }; + if ((n.op == tkind.TK_SLASH || n.op == tkind.TK_PERCENT) && b == 0u64) { + deffolderr(c, n, "def value: division by zero"); + } else { + deffolderr(c, n, "def value: unsupported binary op"); + }; + return false; + }; + if (k == nkind.N_UN) { + // foldintliteral already covers unary-over-leaf; this arm + // catches unary over a resolved ref, e.g. `-A`. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + if (n.op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (n.op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (n.op == tkind.TK_PLUS) { *out = v; return true; }; + deffolderr(c, n, "def value: unsupported unary op"); + return false; + }; + if (k == nkind.N_CAST) { + // n.lhs = value; n.type_ = resolved target (stamped by + // exprtype's N_CAST arm during resolvewalk). Strip the cast + // keeping the value; a narrowing cast that loses it fails loud. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + let t: *tinfo = (n.type_): *tinfo; + if (!defcastfits(t, v)) { + deffolderr(c, n, "def value: narrowing cast loses value"); + return false; + }; + *out = v; + return true; + }; + if (k == nkind.N_IDENT) { + let s: *sym = scopelookupprefer(c.cur, c.curmod, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + if (k == nkind.N_DOT) { + if (n.lhs == nil) { return false; }; + if (n.lhs.kind != nkind.N_IDENT) { return false; }; + let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + return false; +}; + +// stampintlit — rewrite a const-folded def rhs in place to its literal +// value, preserving the node's resolved type_ so the DATA-row emit +// width and pass-3 asserttyped see a properly-typed literal leaf. Lets +// cgen's existing emitdefconstants lay down the row with no codegen +// change (#88). Shape mirrors foldtointlit (the #42 stamp). +fn stampintlit(n: *node, v: u64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v; + n.op = tkind.TK_NONE; + n.str = arenau64tos(v); + n.lhs = nil; + n.rhs = nil; + n.cond = nil; + n.body = nil; + n.els = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; + // n.type_ left intact (the type exprtype inferred for the rhs). +}; + // enumvalfold — fold an enum member's value expression to a u64 // constant. The Hare-fidelity set: literal leaves, unary +/-/~, // binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>), @@ -8105,24 +8273,7 @@ fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = { let b: u64 = 0u64; if (!enumvalfold(body, until, e.lhs, &a)) { return false; }; if (!enumvalfold(body, until, e.rhs, &b)) { return false; }; - let op: tkind = e.op; - if (op == tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == tkind.TK_STAR) { *out = a * b; return true; }; - if (op == tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == tkind.TK_AMP) { *out = a & b; return true; }; - if (op == tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; - return false; + return foldbinop(e.op, a, b, out); }; if (k == nkind.N_IDENT) { let prev: u64 = (-1i64): u64; @@ -10273,6 +10424,19 @@ export fn checkfile(c: *checker, file: *node) void = { } else { if (k == nkind.N_DEF) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; + // #88: const-fold sibling/imported def refs, casts, and + // arithmetic so cgen's literal-only emitdefconstants can + // lay down the DATA row. GATED on the plain literal fold + // missing first, so existing literal/unary defs keep + // their rhs node and the emitted bytes stay byte-identical. + if (d.rhs != nil) { + let dv: u64 = 0u64; + if (!foldintliteral(d.rhs, &dv)) { + if (evaldefconst(c, d.rhs, &dv, 0)) { + stampintlit(d.rhs, dv); + }; + }; + }; } else { if (k == nkind.N_TYPEDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; } else { if (k == nkind.N_LET) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index fd3ae73c..f1edd42c 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -949,6 +949,174 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; +// foldbinop — shared constant binary-op core for the two compile-time +// integer evaluators in this file: enumvalfold (enum member exprs) and +// evaldefconst (top-level def rhs, #88). One op table so the cstage +// (cmd/wcc/check.c fold_binop) and wwstage stamp the bit-identical +// literal — rule 10 lives at the check pass for #88. Returns false on +// division by zero or an op outside the constant subset; the caller +// maps that to its own diagnostic. +fn foldbinop(op: tkind, a: u64, b: u64, out: *u64) bool = { + if (op == tkind.TK_PLUS) { *out = a + b; return true; }; + if (op == tkind.TK_MINUS) { *out = a - b; return true; }; + if (op == tkind.TK_STAR) { *out = a * b; return true; }; + if (op == tkind.TK_SLASH) { + if (b == 0u64) { return false; }; + *out = a / b; return true; + }; + if (op == tkind.TK_PERCENT) { + if (b == 0u64) { return false; }; + *out = a % b; return true; + }; + if (op == tkind.TK_AMP) { *out = a & b; return true; }; + if (op == tkind.TK_PIPE) { *out = a | b; return true; }; + if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; + if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; + if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; + return false; +}; + +// deffolderr — loud diagnostic + checker error count bump for an +// unfoldable def rhs (cycle / narrowing-cast / bad op). c.errs > 0 +// gates cgen off in main.ww:165, so this fails the build rather than +// emitting a missing DATA row silently (rule 7). cstage twin: err() +// in cmd/wcc/check.c. +fn deffolderr(c: *checker, n: *node, msg: str) void = { + os.write(2, n.file.ptr, n.file.len: u64); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +// defcastfits — wwstage twin of cstage def_cast_fits (cmd/wcc/check.c): +// does the folded u64 `v` survive narrowing to integer target `t`? +// Identity / widening / same-width casts always fit; a genuine +// narrowing cast whose value falls outside the target range must NOT +// be silently truncated (rule 7 / drew). Width via the type table +// (t.size, rule 13); the 8s are CHAR_BIT and the u64 byte-width, not +// type-layout sizes, so they sit outside rule 13's scope. Pure-u64 so +// the range check is bit-identical to cstage (rule 10). +fn defcastfits(t: *tinfo, v: u64) bool = { + if (!typeisint(t)) { return true; }; // non-int target: keep value + let w: u64 = t.size; + if (w >= 8u64) { return true; }; // 64-bit target: no narrowing + let bits: u64 = w * 8u64; + if (typeisunsigned(t)) { return (v >> bits) == 0u64; }; + // signed: truncate to `bits` then sign-extend; fits iff unchanged + let mask: u64 = (1u64 << bits) - 1u64; + let sign: u64 = 1u64 << (bits - 1u64); + let ext: u64 = ((v & mask) ^ sign) - sign; + return ext == v; +}; + +// evaldefconst — fold a top-level def's rhs to a u64 constant, +// resolving sibling and imported def references, casts, and +// arithmetic (#88). Reuses foldintliteral (leaf/unary) + foldbinop +// (arith); the ONLY thing it does that enumvalfold doesn't is resolve +// an identifier through the checker's flat scope (scopelookupprefer +// for a bare sibling ref, scopelookupinmodule for `mod.NAME`) to the +// referent def's own rhs, then recurse. +// +// Why this stays distinct from enumvalfold rather than a full merge +// (rule 8 WHY): enum-member eval carries implicit prev+1 auto-increment +// and forward-only sibling lookup over the member chain; def eval has +// neither — it resolves through the scope/decl graph, which references +// forward and across modules. The two lookup models don't reconcile +// cleanly, so they share the arith core (foldbinop) + leaf fold +// (foldintliteral) and keep separate top-level shapes. +// +// `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A, +// incl. cross-module) hits the cap and fails loud rather than hanging +// (rule 7), mirroring the cgen nsteps>=16 abort precedent. +fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { + if (n == nil) { return false; }; + if (depth >= 16) { + deffolderr(c, n, "def value: reference chain too deep (cycle?)"); + return false; + }; + if (foldintliteral(n, out)) { return true; }; + let k: nkind = n.kind; + if (k == nkind.N_BIN) { + let a: u64 = 0u64; + let b: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; }; + if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; }; + if (foldbinop(n.op, a, b, out)) { return true; }; + if ((n.op == tkind.TK_SLASH || n.op == tkind.TK_PERCENT) && b == 0u64) { + deffolderr(c, n, "def value: division by zero"); + } else { + deffolderr(c, n, "def value: unsupported binary op"); + }; + return false; + }; + if (k == nkind.N_UN) { + // foldintliteral already covers unary-over-leaf; this arm + // catches unary over a resolved ref, e.g. `-A`. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + if (n.op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (n.op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (n.op == tkind.TK_PLUS) { *out = v; return true; }; + deffolderr(c, n, "def value: unsupported unary op"); + return false; + }; + if (k == nkind.N_CAST) { + // n.lhs = value; n.type_ = resolved target (stamped by + // exprtype's N_CAST arm during resolvewalk). Strip the cast + // keeping the value; a narrowing cast that loses it fails loud. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + let t: *tinfo = (n.type_): *tinfo; + if (!defcastfits(t, v)) { + deffolderr(c, n, "def value: narrowing cast loses value"); + return false; + }; + *out = v; + return true; + }; + if (k == nkind.N_IDENT) { + let s: *sym = scopelookupprefer(c.cur, c.curmod, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + if (k == nkind.N_DOT) { + if (n.lhs == nil) { return false; }; + if (n.lhs.kind != nkind.N_IDENT) { return false; }; + let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + return false; +}; + +// stampintlit — rewrite a const-folded def rhs in place to its literal +// value, preserving the node's resolved type_ so the DATA-row emit +// width and pass-3 asserttyped see a properly-typed literal leaf. Lets +// cgen's existing emitdefconstants lay down the row with no codegen +// change (#88). Shape mirrors foldtointlit (the #42 stamp). +fn stampintlit(n: *node, v: u64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v; + n.op = tkind.TK_NONE; + n.str = arenau64tos(v); + n.lhs = nil; + n.rhs = nil; + n.cond = nil; + n.body = nil; + n.els = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; + // n.type_ left intact (the type exprtype inferred for the rhs). +}; + // enumvalfold — fold an enum member's value expression to a u64 // constant. The Hare-fidelity set: literal leaves, unary +/-/~, // binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>), @@ -994,24 +1162,7 @@ fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = { let b: u64 = 0u64; if (!enumvalfold(body, until, e.lhs, &a)) { return false; }; if (!enumvalfold(body, until, e.rhs, &b)) { return false; }; - let op: tkind = e.op; - if (op == tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == tkind.TK_STAR) { *out = a * b; return true; }; - if (op == tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == tkind.TK_AMP) { *out = a & b; return true; }; - if (op == tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; - return false; + return foldbinop(e.op, a, b, out); }; if (k == nkind.N_IDENT) { let prev: u64 = (-1i64): u64; @@ -3162,6 +3313,19 @@ export fn checkfile(c: *checker, file: *node) void = { } else { if (k == nkind.N_DEF) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; + // #88: const-fold sibling/imported def refs, casts, and + // arithmetic so cgen's literal-only emitdefconstants can + // lay down the DATA row. GATED on the plain literal fold + // missing first, so existing literal/unary defs keep + // their rhs node and the emitted bytes stay byte-identical. + if (d.rhs != nil) { + let dv: u64 = 0u64; + if (!foldintliteral(d.rhs, &dv)) { + if (evaldefconst(c, d.rhs, &dv, 0)) { + stampintlit(d.rhs, dv); + }; + }; + }; } else { if (k == nkind.N_TYPEDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; } else { if (k == nkind.N_LET) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6306327c..5bc152ab 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8060,6 +8060,174 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; +// foldbinop — shared constant binary-op core for the two compile-time +// integer evaluators in this file: enumvalfold (enum member exprs) and +// evaldefconst (top-level def rhs, #88). One op table so the cstage +// (cmd/wcc/check.c fold_binop) and wwstage stamp the bit-identical +// literal — rule 10 lives at the check pass for #88. Returns false on +// division by zero or an op outside the constant subset; the caller +// maps that to its own diagnostic. +fn foldbinop(op: tkind, a: u64, b: u64, out: *u64) bool = { + if (op == tkind.TK_PLUS) { *out = a + b; return true; }; + if (op == tkind.TK_MINUS) { *out = a - b; return true; }; + if (op == tkind.TK_STAR) { *out = a * b; return true; }; + if (op == tkind.TK_SLASH) { + if (b == 0u64) { return false; }; + *out = a / b; return true; + }; + if (op == tkind.TK_PERCENT) { + if (b == 0u64) { return false; }; + *out = a % b; return true; + }; + if (op == tkind.TK_AMP) { *out = a & b; return true; }; + if (op == tkind.TK_PIPE) { *out = a | b; return true; }; + if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; + if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; + if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; + return false; +}; + +// deffolderr — loud diagnostic + checker error count bump for an +// unfoldable def rhs (cycle / narrowing-cast / bad op). c.errs > 0 +// gates cgen off in main.ww:165, so this fails the build rather than +// emitting a missing DATA row silently (rule 7). cstage twin: err() +// in cmd/wcc/check.c. +fn deffolderr(c: *checker, n: *node, msg: str) void = { + os.write(2, n.file.ptr, n.file.len: u64); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + c.errs += 1; +}; + +// defcastfits — wwstage twin of cstage def_cast_fits (cmd/wcc/check.c): +// does the folded u64 `v` survive narrowing to integer target `t`? +// Identity / widening / same-width casts always fit; a genuine +// narrowing cast whose value falls outside the target range must NOT +// be silently truncated (rule 7 / drew). Width via the type table +// (t.size, rule 13); the 8s are CHAR_BIT and the u64 byte-width, not +// type-layout sizes, so they sit outside rule 13's scope. Pure-u64 so +// the range check is bit-identical to cstage (rule 10). +fn defcastfits(t: *tinfo, v: u64) bool = { + if (!typeisint(t)) { return true; }; // non-int target: keep value + let w: u64 = t.size; + if (w >= 8u64) { return true; }; // 64-bit target: no narrowing + let bits: u64 = w * 8u64; + if (typeisunsigned(t)) { return (v >> bits) == 0u64; }; + // signed: truncate to `bits` then sign-extend; fits iff unchanged + let mask: u64 = (1u64 << bits) - 1u64; + let sign: u64 = 1u64 << (bits - 1u64); + let ext: u64 = ((v & mask) ^ sign) - sign; + return ext == v; +}; + +// evaldefconst — fold a top-level def's rhs to a u64 constant, +// resolving sibling and imported def references, casts, and +// arithmetic (#88). Reuses foldintliteral (leaf/unary) + foldbinop +// (arith); the ONLY thing it does that enumvalfold doesn't is resolve +// an identifier through the checker's flat scope (scopelookupprefer +// for a bare sibling ref, scopelookupinmodule for `mod.NAME`) to the +// referent def's own rhs, then recurse. +// +// Why this stays distinct from enumvalfold rather than a full merge +// (rule 8 WHY): enum-member eval carries implicit prev+1 auto-increment +// and forward-only sibling lookup over the member chain; def eval has +// neither — it resolves through the scope/decl graph, which references +// forward and across modules. The two lookup models don't reconcile +// cleanly, so they share the arith core (foldbinop) + leaf fold +// (foldintliteral) and keep separate top-level shapes. +// +// `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A, +// incl. cross-module) hits the cap and fails loud rather than hanging +// (rule 7), mirroring the cgen nsteps>=16 abort precedent. +fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { + if (n == nil) { return false; }; + if (depth >= 16) { + deffolderr(c, n, "def value: reference chain too deep (cycle?)"); + return false; + }; + if (foldintliteral(n, out)) { return true; }; + let k: nkind = n.kind; + if (k == nkind.N_BIN) { + let a: u64 = 0u64; + let b: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; }; + if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; }; + if (foldbinop(n.op, a, b, out)) { return true; }; + if ((n.op == tkind.TK_SLASH || n.op == tkind.TK_PERCENT) && b == 0u64) { + deffolderr(c, n, "def value: division by zero"); + } else { + deffolderr(c, n, "def value: unsupported binary op"); + }; + return false; + }; + if (k == nkind.N_UN) { + // foldintliteral already covers unary-over-leaf; this arm + // catches unary over a resolved ref, e.g. `-A`. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + if (n.op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; + if (n.op == tkind.TK_TILDE) { *out = ~v; return true; }; + if (n.op == tkind.TK_PLUS) { *out = v; return true; }; + deffolderr(c, n, "def value: unsupported unary op"); + return false; + }; + if (k == nkind.N_CAST) { + // n.lhs = value; n.type_ = resolved target (stamped by + // exprtype's N_CAST arm during resolvewalk). Strip the cast + // keeping the value; a narrowing cast that loses it fails loud. + let v: u64 = 0u64; + if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; + let t: *tinfo = (n.type_): *tinfo; + if (!defcastfits(t, v)) { + deffolderr(c, n, "def value: narrowing cast loses value"); + return false; + }; + *out = v; + return true; + }; + if (k == nkind.N_IDENT) { + let s: *sym = scopelookupprefer(c.cur, c.curmod, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + if (k == nkind.N_DOT) { + if (n.lhs == nil) { return false; }; + if (n.lhs.kind != nkind.N_IDENT) { return false; }; + let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); + if (s == nil) { return false; }; + if (s.skind != skind.SK_DEF) { return false; }; + if (s.decl == nil) { return false; }; + if (s.decl.rhs == nil) { return false; }; + return evaldefconst(c, s.decl.rhs, out, depth + 1); + }; + return false; +}; + +// stampintlit — rewrite a const-folded def rhs in place to its literal +// value, preserving the node's resolved type_ so the DATA-row emit +// width and pass-3 asserttyped see a properly-typed literal leaf. Lets +// cgen's existing emitdefconstants lay down the row with no codegen +// change (#88). Shape mirrors foldtointlit (the #42 stamp). +fn stampintlit(n: *node, v: u64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v; + n.op = tkind.TK_NONE; + n.str = arenau64tos(v); + n.lhs = nil; + n.rhs = nil; + n.cond = nil; + n.body = nil; + n.els = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; + // n.type_ left intact (the type exprtype inferred for the rhs). +}; + // enumvalfold — fold an enum member's value expression to a u64 // constant. The Hare-fidelity set: literal leaves, unary +/-/~, // binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>), @@ -8105,24 +8273,7 @@ fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = { let b: u64 = 0u64; if (!enumvalfold(body, until, e.lhs, &a)) { return false; }; if (!enumvalfold(body, until, e.rhs, &b)) { return false; }; - let op: tkind = e.op; - if (op == tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == tkind.TK_STAR) { *out = a * b; return true; }; - if (op == tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == tkind.TK_AMP) { *out = a & b; return true; }; - if (op == tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; - return false; + return foldbinop(e.op, a, b, out); }; if (k == nkind.N_IDENT) { let prev: u64 = (-1i64): u64; @@ -10273,6 +10424,19 @@ export fn checkfile(c: *checker, file: *node) void = { } else { if (k == nkind.N_DEF) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; + // #88: const-fold sibling/imported def refs, casts, and + // arithmetic so cgen's literal-only emitdefconstants can + // lay down the DATA row. GATED on the plain literal fold + // missing first, so existing literal/unary defs keep + // their rhs node and the emitted bytes stay byte-identical. + if (d.rhs != nil) { + let dv: u64 = 0u64; + if (!foldintliteral(d.rhs, &dv)) { + if (evaldefconst(c, d.rhs, &dv, 0)) { + stampintlit(d.rhs, dv); + }; + }; + }; } else { if (k == nkind.N_TYPEDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; } else { if (k == nkind.N_LET) { diff --git a/test/wcc/732_def_const_fold.c b/test/wcc/732_def_const_fold.c new file mode 100644 index 00000000..6aefad10 --- /dev/null +++ b/test/wcc/732_def_const_fold.c @@ -0,0 +1,438 @@ +/* + * 732_def_const_fold — top-level `def` rhs const-fold extended to + * sibling/imported def references, casts, and arithmetic (PROJECT #88). + * + * Predecessor 631_def_neg_global lifted the LITERAL fold (INTLIT/RUNELIT/ + * TRUE/FALSE/NIL + unary +/-/~) into the shared fold_int_literal / + * foldintliteral helper; it explicitly left N_CAST, sibling-ident, and + * N_BIN OUT OF SCOPE (see 631's header, "negative-u32-cast"). #88 brings + * them in: a Hare-faithful `def SCHAR_MAX = types::I8_MAX;` or + * `def MASK: i32 = (1 << 7) - 1;` was previously inexpressible — the rhs + * folded to nothing, no DATA row was emitted, and any reference failed + * to link. + * + * The fix folds at CHECK time (both stages' pass-2 N_DEF arm) via a new + * eval_def_const / evaldefconst that reuses the shared fold_binop arith + * core (factored out of eval_enum_value / enumvalfold) and the checker's + * EXISTING scope lookup (scope_lookup_prefer for a sibling ref, + * scope_lookup_in_module for a `mod.NAME` qualified ref). On success the + * rhs is stamped to an N_INTLIT in place, so cgen's literal-only + * emit_defs / emitdefconstants lays down the DATA row with no codegen + * change. The fold is GATED on the plain literal fold missing first, so + * every pre-#88 def keeps its node and the emitted bytes stay + * byte-identical (the bootstrap has zero def-ref-def, so 990-997 never + * touch this latent path — hence this targeted fixture). + * + * Coverage: + * 1. EXEC (same-module, via the `ww` driver, + `ww_ww` if built): + * sibling-ref, sibling+arith, def->def->def chain, shift+arith, + * and a widening cast — 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. + * 3. CROSS-MODULE (#88 a2): a combined multi-`package` source (the + * driver's internal concat form, fed straight to w6c like + * 728_match_4arm_cross_module) where `def K = a.J + 1;` resolves + * the imported `a.J`. Assert the folded DATA row value AND byte-id. + * 4. FAIL-LOUD (rule 7): a same-module cycle (def A=B; def B=A), a + * cross-module cycle, and a narrowing cast whose value overflows + * the target must each fail the build on BOTH stages (never a + * silent missing row / silent truncation). + */ +#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; +} + +/* ---- 1. same-module EXEC rows (compile+link+run) -------------------- */ + +struct row { const char *label; const char *src; int want; }; + +static const struct row exec_rows[] = { + /* Bare sibling reference: `def B = A;`. Pre-#88 the rhs (N_IDENT) + * folded to nothing. */ + { "sibling-ref", + "def A: i32 = 7;\n" + "def B: i32 = A;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = B;\n" + "\tif (x == 7) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Sibling + arithmetic: the headline `def B = A + 1;`. */ + { "sibling-add", + "def A: i32 = 5;\n" + "def B: i32 = A + 1;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = B;\n" + "\tif (x == 6) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* def -> def -> def chain: C resolves through B through A. Pins + * the recursive resolve (and that the depth guard doesn't trip on + * a legitimate short chain). */ + { "def-chain", + "def A: i32 = 5;\n" + "def B: i32 = A + 1;\n" + "def C: i32 = B * A;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = C;\n" + "\tif (x == 30) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Pure arithmetic with a shift: `(1 << 7) - 1` == 127. Exercises + * the fold_binop shift + subtract path off any sibling ref. */ + { "shift-arith", + "def MASK: i32 = (1 << 7) - 1;\n" + "fn main() i32 = {\n" + "\tlet x: i32 = MASK;\n" + "\tif (x == 127) { return 42; };\n" + "\treturn 1;\n" + "};\n", + 42 }, + + /* Widening cast over a sibling ref: `A: i64` where A: i32 = 5. + * The N_CAST is stripped (i32->i64 widens, value fits), so W folds + * to 5 in an 8-byte slot. */ + { "widening-cast", + "def A: i32 = 5;\n" + "def W: i64 = A: i64;\n" + "fn main() i32 = {\n" + "\tlet x: i64 = W;\n" + "\tif (x == 5i64) { 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/dcf_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dcf_%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 (990-997); 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 + slurp ----------------------------------------- */ + +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); + return runwait(cmd); +} + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +/* ---- 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/dcf_bi_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/dcf_bi_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/dcf_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. cross-module fold: a combined multi-package source ---------- + * Fed straight to w6c (the driver's internal concat form — `ww build` + * can't expand an already-concatenated source). `def K = a.J + 1;` + * resolves the imported `a.J` (41) through scope_lookup_in_module and + * folds to 42, whose DATA row first byte is '*' (0x2a). */ +static const char xmod_src[] = + "package a;\n" + "export def J: i32 = 41;\n" + "package b;\n" + "import a;\n" + "def K: i32 = a.J + 1;\n" + "export fn main() i32 = {\n" + "\tlet k: i32 = K;\n" + "\tif (k == 42) { return 42; };\n" + "\treturn 1;\n" + "};\n"; + +/* Assert the folded DATA row for symbol `b.K` is present with value 42 + * (first byte '*'). Returns 0 on success. */ +static int +xmod_fold_present(const char *w6c, const char *stage) +{ + char src[64], s[64], buf[1 << 15]; + snprintf(src, sizeof src, "/tmp/dcf_xm_%d_%s.ww", getpid(), stage); + snprintf(s, sizeof s, "/tmp/dcf_xm_%d_%s.s", getpid(), stage); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(xmod_src, f); + fclose(f); + + if (emit_s(w6c, src, s, sizeof s) != 0) { + fprintf(stderr, "xmod[%s]: w6c emit failed\n", stage); + unlink(src); + return -1; + } + int rc = 0; + if (slurp(s, buf, sizeof buf) < 0) { + rc = -1; + } else if (strstr(buf, "b.K(SB),\"*") == NULL) { + fprintf(stderr, + "xmod[%s]: no folded `DATA b.K(SB),\"*` (value 42) row\n", + stage); + rc = -1; + } + unlink(src); unlink(s); + return rc; +} + +/* ---- 4. fail-loud rows: must fail the build on BOTH stages ---------- */ + +struct failrow { const char *label; const char *src; }; + +static const struct failrow fail_rows[] = { + /* Same-module cycle: the depth guard must trip and fail loud, not + * hang and not silently drop the row. */ + { "same-module-cycle", + "def A: i32 = B;\n" + "def B: i32 = A;\n" + "export fn main() i32 = { return A; };\n" }, + + /* Cross-module cycle (combined source): a.J -> b.K -> a.J. */ + { "cross-module-cycle", + "package a;\n" + "import b;\n" + "export def J: i32 = b.K;\n" + "package b;\n" + "import a;\n" + "export def K: i32 = a.J;\n" + "export fn main() i32 = { return 0; };\n" }, + + /* Narrowing cast that loses the value: 0x1FF (511) does not fit a + * u8, so the cast must fail loud rather than silently truncate. */ + { "narrowing-cast", + "def Z: u8 = 0x1FF: u8;\n" + "export fn main() i32 = { let z: u8 = Z; return z: i32; };\n" }, +}; + +/* Compile via w6c only (no link/run); success means the build FAILED as + * required (nonzero w6c exit). */ +static int +compile_fails(const char *w6c, const struct failrow *r, const char *stage, int i) +{ + char src[64], s[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dcf_fl_%d_%d.ww", getpid(), i); + snprintf(s, sizeof s, "/tmp/dcf_fl_%d_%d.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + /* A regressed depth guard would HANG the cycle rows rather than fail + * loud (rule 7). timeout's 124 is distinct from a clean guard failure + * -- fold it back to a TEST failure so a hang can't pass as fail-loud. */ + 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, + "failrow[%s][%s]: w6c exited 0 (expected loud failure)\n", + r->label, stage); + return -1; + } + if (rc == 124) { + fprintf(stderr, + "failrow[%s][%s]: w6c timed out (depth guard hung?)\n", + r->label, stage); + 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. same-module 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_const_fold: 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_const_fold[%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. cross-module fold: DATA row value on cstage, + byte-id */ + total++; + if (xmod_fold_present(w6c, "cstage") != 0) fail++; + if (have_ww) { + total++; + if (xmod_fold_present(w6c_ww, "wwstage") != 0) fail++; + total++; + if (asm_byte_identical(bin, xmod_src, "xmod-fold", 900) != 0) + fail++; + } + + /* 4. fail-loud on BOTH stages */ + int nfail = (int)(sizeof fail_rows / sizeof fail_rows[0]); + for (int i = 0; i < nfail; i++) { + total++; + if (compile_fails(w6c, &fail_rows[i], "cstage", i) != 0) fail++; + if (have_ww) { + total++; + if (compile_fails(w6c_ww, &fail_rows[i], "wwstage", + 1000 + i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "def_const_fold: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("def_const_fold: %d/%d ok\n", total, total); + return 0; +}