cstage+selfhost+test: fix (*p).f silent drop in N_DOT lhs (read+write)

Pre-existing landmine surfaced by #5 (whole-STRUCT N_ASSIGN). Both
stages' N_DOT dispatch gated on `lhs->lhs->kind == N_IDENT`; the
parser produces N_UN(STAR, IDENT(p)) for `(*p).f`, so both sides fell
off:
  - Write side (cgassign N_DOT base): emitted nothing, store dropped.
  - Read side (case N_DOT pointer-auto-deref): cgexpr derefed the
    pointer as a scalar, AX = first qword of struct, field offset
    dropped.

Fix: retarget base / dot_lhs to the inner IDENT when shape is
N_UN(STAR, IDENT). The existing via_ptr branch fires identically to
`p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` (non-IDENT
pointer expression) is tracked separately as task #19.

702 covers 7 rows: write_i64/i32/str, read_i64/i32/str_len, roundtrip
This commit is contained in:
2026-05-15 18:13:02 +09:00
parent f24fc113a7
commit c4347499ee
6 changed files with 456 additions and 21 deletions

View File

@@ -1834,10 +1834,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
/* p.x = v or p.x += v where p.x is a struct field
* (direct or via *struct). For compound ops we read-modify-
* write the field; for plain `=` we just write. */
* write the field; for plain `=` we just write. The base
* accepts two parser shapes: a bare IDENT (auto-deref when
* the IDENT's type is *T, value-struct otherwise) and the
* explicit-deref form `(*p).f = ...` where the parser emits
* N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the
* inner IDENT so the via_ptr branch fires identically to
* `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f
* (non-IDENT inner) falls through to the existing drop
* behaviour pending follow-up task. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->lhs->kind == N_IDENT) {
(n->lhs->lhs->kind == N_IDENT ||
(n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR
&& n->lhs->lhs->lhs
&& n->lhs->lhs->lhs->kind == N_IDENT))) {
Node *base = n->lhs->lhs;
if (base->kind == N_UN) base = base->lhs;
Type *bt = base->type;
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
int via_ptr = 0;
@@ -3480,6 +3492,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
}
/* Tagged-union field: delegate to the
* shared widening writer. Handles
* str, scalar, struct literal/ident
* payload, and tagged-subset
* forwarding (with tag remap). */
Type *fu = (ft
&& ft->kind == TY_NAMED)
? ft->under : ft;
@@ -4808,7 +4825,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16).
* Arrays don't carry a header; .len uses the static size and
* .ptr is the address of the first element. */
Type *bt = n->lhs ? n->lhs->type : NULL;
/* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR,
* IDENT(p)) with type T (post-deref struct). Pull the inner
* IDENT in as dot_lhs so bt resolves to *T and the pointer-
* auto-deref branch below fires (mirror of the N_ASSIGN
* N_DOT lhs retarget). v1 scope: N_IDENT inner only;
* (*expr).f follow-up task pending. Branches that gate on
* `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw
* n->lhs since (*p) isn't either of those shapes. */
Node *dot_lhs = n->lhs;
if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR
&& dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT)
dot_lhs = dot_lhs->lhs;
Type *bt = dot_lhs ? dot_lhs->type : NULL;
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
/* Module-qualified value reference: `mod.name`. The checker
* leaves SK_USE idents untyped (NULL/ty_err); detect that and
@@ -5224,14 +5253,17 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
/* pointer-to-slice/str field: deref and read pseudo-field.
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). */
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs
* gates the N_IDENT check so `(*p).len` (parser N_UN(STAR,
* IDENT)) emits the same load as `p.len` after the case-top
* retarget. */
if (u && u->kind == TY_PTR && u->sub) {
Type *inner = u->sub;
if (inner->kind == TY_NAMED) inner = inner->under;
if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR)
&& (lenfld || capfld || ptrfld)
&& n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
&& dot_lhs && dot_lhs->kind == N_IDENT) {
int off = localfind(locals, dot_lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX));
@@ -5239,13 +5271,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
/* pointer-to-struct field: deref and load. Common pattern:
* fn move(p: *point) ... { p.x += dx; ... } */
* fn move(p: *point) ... { p.x += dx; ... }
* dot_lhs gates this branch so both `p.f` (n->lhs is IDENT)
* and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to
* inner IDENT at case-top) emit the same load sequence. */
if (u && u->kind == TY_PTR && u->sub) {
Type *inner = u->sub;
if (inner->kind == TY_NAMED) inner = inner->under;
if (inner && inner->kind == TY_STRUCT
&& n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
&& dot_lhs && dot_lhs->kind == N_IDENT) {
int off = localfind(locals, dot_lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
for (Tfield *f = inner->fields; f; f = f->next) {
if (strcmp(f->name, n->str) != 0) continue;