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

@@ -1294,6 +1294,102 @@ fn primsize(name: str) i32 = {
return 0;
};
// typenodeprimresolved — walk N_TBANG / N_TENUM / N_TNAME alias
// chains to the underlying primitive, returning its byte size and
// signedness. Sets *sz_out = 0 when the type doesn't reduce to a
// width-known primitive (composite, unresolved name, default-storage
// enum, etc.). Mirrors cstage's `type_isint(t) ? t->size : 0` /
// `type_isunsigned` recursion through TY_NAMED and TY_ENUM. Used by
// cgcast's identity-width identity-sign clamp-skip predicate (#33).
export fn typenodeprimresolved(c: *cgen, t: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
let cur: *node = t;
for (cur != nil) {
let k: nkind = cur.kind;
if (k == nkind.N_TBANG) { cur = cur.lhs; }
else { if (k == nkind.N_TENUM) { cur = cur.lhs; }
else { if (k == nkind.N_TNAME) {
let nm: str = cur.str;
// bool is excluded from the int-prim contract: cstage's
// `type_isint(TY_BOOL)` is false, so its identity check
// leaves src_w=0 on a bool source. Match that here so a
// `let y: i8 = b: i8;` (bool b) doesn't fire identity in
// wwstage and skip the MOVSBQ that cstage emits. Other
// call sites (slot sizing, etc.) still want
// primsize("bool")=1, so the exclusion stays local. The
// dedicated `is_bool` path in cgcast owns bool→bool's
// ANDQ $255 on both stages.
if (streq(nm, "bool")) { return; };
let ps: i32 = primsize(nm);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typenameisunsigned(nm);
return;
};
let al: *node = aliaslookup(c, nm);
if (al == nil) { return; };
cur = al;
}
else { return; }; }; };
};
};
// exprprimresolved — best-effort static (primsize, signedness) for an
// expression. Used by cgcast (#33) to derive the source-side primitive
// width and signedness so the identity-width identity-sign clamp-skip
// predicate fires. Sets *sz_out = 0 when the type can't be derived
// (untyped literal, call result with no return-type lookup, etc.);
// caller treats sz=0 as "not identity", which conservatively keeps
// the clamp. Mirror of cstage's `n->lhs->type` lookup with the same
// TY_NAMED / TY_ENUM recursion through type_isint / type_isunsigned.
export fn exprprimresolved(c: *cgen, n: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == nkind.N_INTLIT) {
// Typed-int literal: `7u32` has tsuffix = "u32". Mirrors
// cstage's `cexpr` which assigns `lookup_builtin(tsuffix)`
// as the node's type — without this, wwstage misses the
// suffix and emits a defensive clamp where cstage skips,
// breaking byte-id on rows like `let y: mymode = 7u32:
// mymode;` (mymode = enum u32).
let s: str = n.tsuffix;
if (s.len > 0) {
let ps: i32 = primsize(s);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typenameisunsigned(s);
};
};
return;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
typenodeprimresolved(c, lc.tnode,
sz_out, unsigned_out);
};
return;
};
if (k == nkind.N_CAST) {
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
return;
};
if (k == nkind.N_UN) {
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
return;
};
if (k == nkind.N_DOT) {
typenodeprimresolved(c, dotfieldtnode(c, n),
sz_out, unsigned_out);
return;
};
};
// variantnamematch — tagged-union variant names are compared as if
// they'd been alias-resolved. Pattern names can be module-qualified
// (`strconv.invalid` from a `case let e: strconv.invalid =>`),