wcc/ww: registerstruct field offset is the checker's natural tfield.offset

wwstage carried TWO struct-layout sources. registerstruct (cgenutil.ww)
recomputed each field's `fi.foff` via fieldsize — slot-padded, round-8 —
for the WRITE (construction / field store) path, while the READ path
(cgplaceaddr / dotbaseaddr) used the checker's natural `tfield.offset`.
They diverged iff a struct had a nested sub-8 composite field
(slotsize != size) plus a successor: ww wrote the successor at the
slot-padded offset and read it at the natural offset, mis-addressing its
own field. cstage has no structinfo and reads tfield directly, self-
consistently natural (cmd/w6c/cgen.c).

Fix: make `fi.foff` a VIEW of the checker's already-built natural layout.
Lock-step walk tstruct.list (AST N_TFIELD) and ti.fields (tfield) — both
head-first declared order, both skip non-TFIELD identically — and copy
foff = tf.offset, fsz = tf.type_.size. si.totsize keeps the slot-padded
stack-slot number (ti.slotsize, already 8-rounded at check.ww:2259).
fieldsize is no longer called here (its `*p OP=` scalar-width caller is
untouched). LOUD nil-guards on tstruct.type_ / tichase / a tfield walk
desync — all unreachable post-check, never silent. fi.tnode stays the
AST node (its node-keyed readers need it); repointing the ~60 fi.foff
readers to tfield is the out-of-scope (ii-b) follow-up.

This unifies ww's second source onto the value cstage already emits, so
cs==ww is preserved, not newly created (wwstage-cgen only; no cstage
edit). The shape is corpus-absent — ww uses both sources on its own
structs, so a divergent struct would have broken the bootstrap — hence
gate-blind; 989_nestfield_run is the proof (nested inner{x:u8,y:u8} in
outer{a:u8,p:inner[,z:i64]}, every field read back == written, dual-stage
cs==ww). It also makes 681 ragged_tail_12B genuinely correct: the
predecessor #71 already shrank the whole-struct copy to the source's
natural length, so packing mark at natural offset 12 no longer clobbers.
This commit is contained in:
2026-06-13 13:18:21 +09:00
parent a00d052833
commit 037d59cf4e
5 changed files with 322 additions and 51 deletions

View File

@@ -256,6 +256,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_chainidx_run \
$(BIN)/test_tupfieldsize_run \
$(BIN)/test_tagtupfieldsize_run \
$(BIN)/test_nestfield_run \
$(BIN)/test_arrlit_tail_zero_run \
$(BIN)/test_defdim_slice_run \
$(BIN)/test_trystr_run \
@@ -761,6 +762,20 @@ $(BIN)/test_tagtupfieldsize_run: test/wcc/989_tagtupfieldsize_run.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# 989_nestfield_run (#44/#55): a struct with a nested sub-8 composite field
# (inner{x:u8,y:u8}, slotsize 8 != size 2) plus a successor must address
# every field at the checker's NATURAL offset on BOTH the write
# (construction) and read (field-access) paths. wwstage's registerstruct
# rebuilt fi.foff slot-padded for the write while the read used tfield
# natural — gate-blind (the shape is corpus-absent). Builds+runs on BOTH
# driver twins (rule-10), pinning the absolute 0 (all fields read back).
$(BIN)/test_nestfield_run: test/wcc/989_nestfield_run.c \
$(BIN)/ww $(BIN)/ww_ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# 989_arrlit_tail_zero_run (#13): an under-length array literal zero-fills the
# unspecified tail, not the last value. Builds+runs on BOTH driver twins
# (rule-10), pinning the absolute value (pre-fix ww tail = last value).