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:
202
cmd/wcc/check.c
202
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user