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:
@@ -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 =>`),
|
||||
|
||||
Reference in New Issue
Block a user