cstage+selfhost+test: principled identity-cast skip (#33)

Generalizes b5632b1's single-site dst_is_enum gate. Skip the narrow-
clamp MOVL when src.width == dst.width && src.signed == dst.signed.
Closes #25's followup.

Both stages need symmetric source-type derivation for byte-id. cstage
deliberately throws away the checker's richer typed-AST and uses a
structural walker (castsrcprim) that mirrors wwstage's exprprimresolved
case-for-case. Otherwise cstage's `.len: i32` resolves to i32 (skip)
while wwstage's misses the pseudo-field (clamp) — bootstrap diverges.
Pseudo-fields, N_BIN, N_INDEX, N_CALL, match-bindings all yield sz=0
→ clamp emits defensively on both.

The N_TENUM walker now follows enum aliases in wwstage's
typenodeprimresolved (was the original lacuna behind #25), and bool
is excluded early in the same helper (mirrors cstage's
type_isint(TY_BOOL)=false). bool→bool keeps its dedicated is_bool
ANDQ $255 emit; bool→i8 / bool→u8 etc. fall through to the clamp on
both stages.

Walker shape (cstage castsrcprim / wwstage exprprimresolved):
  N_INTLIT     → tsuffix gated, untyped excluded
  N_IDENT      → trust local's resolved tnode
  N_CAST       → recurse on declared dst
  N_UN         → recurse on operand
  N_DOT        → real-struct only (TY_STRUCT or TY_PTR→TY_STRUCT)
  others       → sz=0 → identity false → clamp emits

Test 710 grew from 5 → 16 rows: 6 identity-width pins (u32/i32/u8/
i8/u16/i16 self), 1 sign-change pin (u32→i32 clamp MUST fire), 2
silent-miscompile exit-validating rows (truncate via divide), 1
pseudo-field defensive pin (`s.len: i32`), 1 bool-source pin
(`b: i8`). Asm byte-id asserted on every row.

Out of scope: redundant clamps remain for patterns wwstage can't
structurally derive (N_BIN, N_CALL, N_INDEX, pseudo-fields). A
sibling task extending wwstage's type inference closes those.
This commit is contained in:
2026-05-16 12:20:56 +09:00
parent 993da52333
commit 2fb594748c
6 changed files with 860 additions and 217 deletions

View File

@@ -168,6 +168,84 @@ fldstoreop(Type *t, int sz)
return A_MOVQ;
}
/* castsrcprim — structural (size, unsigned) of an N_CAST's source
* expression, mirroring wwstage's exprprimresolved in
* selfhost/cmd/wcc/cgenutil.ww. The cgen-stage match has to be
* structural, not "use n->type": cstage's checker decorates every
* node with a precise Type, but wwstage has no checker and must
* derive the source type from the AST shape. To keep cstage and
* wwstage emitting byte-identical asm under the #33 identity-width
* identity-sign clamp-skip, both must agree on what a "knowable
* source type" is. The shape menu:
* N_INTLIT — typed literal (`7u32`) via tsuffix.
* N_IDENT, N_CAST — type set by checker; trust it. Wwstage
* reaches the same answer via localfindnode +
* typenodeprimresolved (alias / enum walk)
* and via the cast's rhs type-node.
* N_UN — recurse on operand.
* N_DOT real field — base resolves to TY_STRUCT (or ptr-to);
* use the field's checker-set type. Pseudo-
* fields .len/.cap/.ptr are excluded — they
* are i32 / *T but wwstage's exprprimresolved
* doesn't recognise them, and asymmetry there
* breaks 995_self_rebuild. Tuple positional
* access likewise excluded.
* default — sz=0, identity check fails, clamp emits.
* Matches wwstage's conservative fallback. */
static void
castsrcprim(Node *n, int *sz, int *unsignd)
{
*sz = 0;
*unsignd = 0;
if (n == NULL) return;
Type *t = NULL;
switch (n->kind) {
case N_INTLIT:
/* tsuffix-typed literal: checker resolved n->type via
* lookup_builtin. Untyped int leaves n->type at
* TY_UNTYPED_INT — we conservatively skip those (wwstage
* matches: no tsuffix → sz=0). */
if (n->tsuffix && n->type) {
Type *u = (n->type->kind == TY_NAMED)
? n->type->under : n->type;
if (u && u->kind != TY_UNTYPED_INT
&& u->kind != TY_UNTYPED_RUNE
&& type_isint(u)) {
t = u;
}
}
break;
case N_IDENT:
case N_CAST:
t = n->type;
break;
case N_UN:
castsrcprim(n->lhs, sz, unsignd);
return;
case N_DOT: {
/* Real struct field only. .len / .cap / .ptr on str /
* slice / array are pseudo-fields wwstage doesn't see. */
Type *bt = n->lhs ? n->lhs->type : NULL;
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (bu && bu->kind == TY_PTR) {
Type *st = bu->sub;
bu = (st && st->kind == TY_NAMED) ? st->under : st;
}
if (bu && bu->kind == TY_STRUCT) {
t = n->type;
}
break;
}
default:
break;
}
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
if (u && type_isint(u)) {
*sz = (int)u->size;
*unsignd = type_isunsigned(u);
}
}
/* localloadop — read instruction for a scalar local/let load. Same
* dispatch as fldloadop, but keyed on the value's own type. Lets the
* caller emit MOVSXD / MOVSWQ / MOVSBQ on a signed-narrow slot instead
@@ -4793,20 +4871,40 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (!from_f && !to_f && n->type) {
Type *tt = n->type;
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
/* Wwstage's cgcast walker only steps through N_TBANG /
* N_TNAME alias links; an enum's `aliaslookup` returns
* the N_TENUM body, which breaks the loop and skips the
* clamp. Mirror that here so cstage agrees byte-for-byte
* on a u32→enum-u32 cast (task #25). Predicate recursion
* through TY_ENUM in `type_isint`/`type_isunsigned`
* stays — other call sites rely on it; only this site
* gates on TY_ENUM explicitly. Skip is by dst kind only:
* `enum-u32 → u32` still emits the clamp, matching
* wwstage's asymmetry. (See followup: identity-width
* identity-sign hygiene across both stages.) */
int dst_is_enum = tu && tu->kind == TY_ENUM;
/* Identity-width identity-sign cast is a no-op at the
* machine-int level: src and dst share both width and
* signedness, so the natural slot/load already carries
* the right canonical 64-bit shape and the narrow-clamp
* is dead. Replaces b5632b1's single-site `!dst_is_enum`
* gate (task #25) which mirrored wwstage's N_TENUM
* lacuna; the lacuna is fixed there too, so this gate
* stays symmetric across both stages (#33). Source side
* uses `castsrcprim` (a structural walk matching
* wwstage's exprprimresolved exactly), NOT n->lhs->type
* — cstage's checker has richer type info than wwstage
* can derive without a checker, and the asymmetric
* coverage broke 995_self_rebuild's byte-id. The cost
* is that some casts (`.len: i32`, N_BIN result, call
* return, match-bound payload) still emit a redundant
* clamp on both stages; closing those gaps is a
* sibling task that extends wwstage's type inference.
* Incidentally fixes a silent miscompile #25's
* dst-kind-only skip left in place: u32→enum-u8 (and
* similar narrow-to-enum casts) was suppressing the
* clamp, so the upper bits of the source value leaked
* through register-chained downstream uses. Caveat:
* removing the defensive MOVL exposes any upstream
* cgen path that leaves garbage in upper RAX when
* producing a sub-word value — the contract is
* producers leave the value in canonical width-
* extended form. */
int src_w = 0, src_unsignd = 0;
castsrcprim(n->lhs, &src_w, &src_unsignd);
int dst_w = (tu && type_isint(tu)) ? (int)tu->size : 0;
int identity = dst_w > 0 && src_w == dst_w
&& src_unsignd == type_isunsigned(tu);
if (tu && type_isint(tu) && tu->size > 0
&& tu->size < 8 && !dst_is_enum) {
&& tu->size < 8 && !identity) {
if (type_isunsigned(tu)) {
if (tu->size == 4) {
ins2(c, A_MOVL,