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:
@@ -7537,6 +7537,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 =>`),
|
||||
@@ -9824,55 +9920,79 @@ fn cgcast(c: *cgen, n: *node) void = {
|
||||
// Mirrors cmd/w6c/cgen.c's N_CAST clamp. Unsigned narrow clears
|
||||
// the upper bits via MOVL/ANDQ; signed narrow sign-extends via
|
||||
// MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates.
|
||||
//
|
||||
// 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. Skip the clamp in that case. Symmetric with cstage's
|
||||
// principled gate (#33). Replaces the previous N_TENUM lacuna in
|
||||
// this walker (the alias-step missed `N_TENUM`, so any cast to
|
||||
// an enum dst landed on tn==nil and skipped the clamp by
|
||||
// accident — task #25 mirrored that into cstage as a single-site
|
||||
// gate, and #33 retires both). The walker now follows N_TENUM
|
||||
// too so a narrow-to-enum cast (u32→enum-u8, i64→enum-i32)
|
||||
// resolves to the underlying primitive and the clamp fires —
|
||||
// fixing a silent miscompile in the process.
|
||||
if (srcfk == 0 && dstfk == 0) {
|
||||
let tn: *node = n.rhs;
|
||||
// Walk through alias chains (`type random = u64`) and the
|
||||
// `!T` error-flag wrapper (`type invalid = !i32`) — the
|
||||
// bang is a tagged-union marker, not a representational
|
||||
// change, so it must not block the narrow-cast clamp.
|
||||
for (tn != nil) {
|
||||
if (tn.kind == nkind.N_TBANG) { tn = tn.lhs; }
|
||||
else { if (tn.kind != nkind.N_TNAME) { tn = nil; }
|
||||
else {
|
||||
let nm: str = tn.str;
|
||||
if (primsize(nm) > 0) { break; };
|
||||
let alias: *node = aliaslookup(c, nm);
|
||||
if (alias == nil) { tn = nil; }
|
||||
else { tn = alias; };
|
||||
}; };
|
||||
let sz: i32 = 0;
|
||||
let is_unsigned: bool = false;
|
||||
typenodeprimresolved(c, n.rhs, &sz, &is_unsigned);
|
||||
let src_sz: i32 = 0;
|
||||
let src_unsigned: bool = false;
|
||||
exprprimresolved(c, n.lhs, &src_sz, &src_unsigned);
|
||||
let identity: bool = false;
|
||||
if (sz > 0) { if (src_sz == sz) {
|
||||
if (src_unsigned == is_unsigned) { identity = true; };
|
||||
}; };
|
||||
// Detect bool dst by walking n.rhs to the leaf TNAME. bool
|
||||
// keeps its dedicated ANDQ $255 contract regardless of
|
||||
// upstream shape; it stays off the identity path.
|
||||
let leaf_tn: *node = n.rhs;
|
||||
for (leaf_tn != nil) {
|
||||
let lk: nkind = leaf_tn.kind;
|
||||
if (lk == nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; }
|
||||
else { if (lk == nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; }
|
||||
else { if (lk == nkind.N_TNAME) {
|
||||
let lnm: str = leaf_tn.str;
|
||||
if (primsize(lnm) > 0) { break; };
|
||||
let lal: *node = aliaslookup(c, lnm);
|
||||
if (lal == nil) { leaf_tn = nil; }
|
||||
else { leaf_tn = lal; };
|
||||
}
|
||||
else { leaf_tn = nil; }; }; };
|
||||
};
|
||||
if (tn != nil) {
|
||||
let nm: str = tn.str;
|
||||
let sz: i32 = primsize(nm);
|
||||
let is_unsigned: bool = typenameisunsigned(nm);
|
||||
let is_bool: bool = streq(nm, "bool");
|
||||
// Symmetric narrow on signed vs unsigned (task #5):
|
||||
// unsigned (incl. rune) clears upper bits; signed
|
||||
// sign-extends. bool is size 1 but neither — falls
|
||||
// through to its dedicated ANDQ $255 below.
|
||||
if (sz > 0) { if (sz < 8) { if (!is_bool) {
|
||||
if (is_unsigned) {
|
||||
if (sz == 4) {
|
||||
emitline("\tMOVL\tAX, AX\n");
|
||||
} else {
|
||||
let mask: i64 = 0xFFi64;
|
||||
if (sz == 2) { mask = 0xFFFFi64; };
|
||||
emitline("\tANDQ\t$");
|
||||
emitint(mask);
|
||||
emitline(", AX\n");
|
||||
};
|
||||
let is_bool: bool = false;
|
||||
if (leaf_tn != nil) {
|
||||
if (leaf_tn.kind == nkind.N_TNAME) {
|
||||
is_bool = streq(leaf_tn.str, "bool");
|
||||
};
|
||||
};
|
||||
// Symmetric narrow on signed vs unsigned (task #5):
|
||||
// unsigned (incl. rune) clears upper bits; signed
|
||||
// sign-extends. bool is size 1 but neither — falls
|
||||
// through to its dedicated ANDQ $255 below.
|
||||
if (sz > 0) { if (sz < 8) { if (!is_bool) { if (!identity) {
|
||||
if (is_unsigned) {
|
||||
if (sz == 4) {
|
||||
emitline("\tMOVL\tAX, AX\n");
|
||||
} else {
|
||||
if (sz == 1) {
|
||||
emitline("\tMOVSBQ\tAX, AX\n");
|
||||
} else { if (sz == 2) {
|
||||
emitline("\tMOVSWQ\tAX, AX\n");
|
||||
} else { if (sz == 4) {
|
||||
emitline("\tMOVSXD\tAX, AX\n");
|
||||
}; }; };
|
||||
let mask: i64 = 0xFFi64;
|
||||
if (sz == 2) { mask = 0xFFFFi64; };
|
||||
emitline("\tANDQ\t$");
|
||||
emitint(mask);
|
||||
emitline(", AX\n");
|
||||
};
|
||||
}; }; };
|
||||
if (is_bool) { emitline("\tANDQ\t$255, AX\n"); };
|
||||
};
|
||||
} else {
|
||||
if (sz == 1) {
|
||||
emitline("\tMOVSBQ\tAX, AX\n");
|
||||
} else { if (sz == 2) {
|
||||
emitline("\tMOVSWQ\tAX, AX\n");
|
||||
} else { if (sz == 4) {
|
||||
emitline("\tMOVSXD\tAX, AX\n");
|
||||
}; }; };
|
||||
};
|
||||
}; }; }; };
|
||||
if (is_bool) { emitline("\tANDQ\t$255, AX\n"); };
|
||||
return;
|
||||
};
|
||||
if (srcfk == 0 && dstfk == 2) {
|
||||
|
||||
Reference in New Issue
Block a user