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.
This commit is contained in:
2026-05-25 11:16:08 +09:00
parent b7e1ad1a4b
commit 0d1ae17dd0
6 changed files with 1173 additions and 74 deletions

View File

@@ -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) {