cgen: str N_DOT field read -> 3-word {ptr,len,cap} -- Phase 2 C4.6 (both stages)

Reading a str-typed struct field loaded only 2 words (ptr,len), dropping the cap word. Fold the str-field read onto the adjacent proven slice-field arm by widening its kind-gate to include str (type_isstr/isstrtype, never size==24). Sites: S1 direct struct field (local BP + global CX base) and S2 field through a *struct local (pst.f). cstage==wwstage byte-identical; the slice-field arms stay unchanged for slices.

C4.6 bundles the S1 local-field fold with a FORCED global-field lift -- the rule-11 reason they cannot split: cstage reads a field with ONE unified base_reg arm, so folding str covers local AND global together. For byte-id, ww's global field path must then lift in the SAME commit -- but ww splits local/global and its global arm has no slice sibling, so it is authored as ww's own local slice-field arm retargeted to the CX base (cap->CX last, base survives). The underlying cstage-unifies / ww-splits field-arm divergence is a separate filed structural follow-up, not resolved here.

test/wcc/933: table-driven runtime .cap-survives over local/global/*struct field reads, both drivers; verified fail-before/pass-after. The local-field row interposes a CX-clobbering call so a 2-word read cannot coincidentally pass on stale CX (the field store otherwise leaves the cap word lingering in CX).

main.combined.ww regenerated via the canonical make path (md5-stable), per the 1140a59 precedent.
This commit is contained in:
2026-05-24 13:31:58 +09:00
parent 97707155ae
commit 90deeb3821
6 changed files with 301 additions and 131 deletions

View File

@@ -5891,24 +5891,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
(void)is_global;
break;
}
/* str field: load (ptr, len) into (AX, BX) so the
* value flows through the str-rhs convention. */
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (str_fu && str_fu->kind == TY_STR) {
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 8),
areg(D_BX));
break;
}
/* slice field: load (ptr, len, cap) into (AX, BX, CX)
* so the value flows through the slice-rhs convention.
/* str IS []u8 — same 3-word {ptr,len,cap} as a slice
* field: load (ptr, len, cap) into (AX, BX, CX) so the
* value flows through the slice-rhs convention. str
* folds onto the slice arm (#1/Phase 3 collapse).
* base_reg may be CX for globals; load .cap LAST so
* the base survives the earlier reads. */
if (str_fu && str_fu->kind == TY_SLICE) {
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if ((str_fu && str_fu->kind == TY_SLICE) ||
type_isstr(f->type)) {
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 0),
areg(D_AX));
@@ -6008,27 +6000,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_R8));
break;
}
/* str field through *struct: read len into a
* scratch first (it's at +8) so loading ptr
* into AX last leaves (AX=ptr, BX=len). We
* use CX as the scratch, then move CX→BX. */
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (str_fu && str_fu->kind == TY_STR) {
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 8),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 0),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
break;
}
/* slice field through *struct: load (ptr, len,
/* str IS []u8 — same 3-word {ptr,len,cap} as a
* slice field through *struct: load (ptr, len,
* cap) into (AX, BX, CX). BX holds the *struct
* pointer, so load .len LAST — the earlier loads
* still index off the original base. */
if (str_fu && str_fu->kind == TY_SLICE) {
* still index off the original base. str folds
* onto the slice arm (#1/Phase 3 collapse). */
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if ((str_fu && str_fu->kind == TY_SLICE) ||
type_isstr(f->type)) {
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 0),
areg(D_AX));