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:
124
cmd/w6c/cgen.c
124
cmd/w6c/cgen.c
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user