wcc: whole-struct field-copy completes the ragged tail greedily, both stages

A `x.f = o` copy of a whole struct field emits a MOVQ run for the
8-byte chunks plus a tail. Both stages inlined a tail that handled only
{4,1}: a 4-byte remainder went MOVL, a 1-byte MOVB, but {2,3,5,6,7} fell
through to an 8-byte MOVQ that OVER-READS the source and OVER-WRITES the
field's natural-offset successor. With #44 packing a successor at its
natural offset, that is a live clobber: outer2{i:inner2{u8,u8}, mark:i32}
copies i with `MOVQ -8(BP),AX; MOVQ AX,-16(BP)` and wipes mark@-12; the
correct move is a single MOVW. Same defect in cstage (cgen.c) and the
four wwstage field-copy sites (cgenexpr.ww: via-ptr, direct-BP-local,
global, and the multi-hop dot-chain CX variant).

Fix: replace each inline {4,1} tail with the descending greedy 4/2/1
(MOVL/MOVW/MOVB) the canonical aggregate-copy emitters already use, so
the tail is complete on every natural size. This is path (alpha) of the
#73 brief — a corpus-neutral, no-workaround completion of the inline
tail. Routing field copies through the shared aggcopy/cg_aggcopy choke-
point (beta) is the balloon: those emitters hardcode (SI)->(BX) at offset
k with zero base displacement, but the four field-copy dsts are
heterogeneous (foff(BX), boff+foff(BP) with no base reg, totaloff(CX)),
so routing forces per-site-per-stage LEAQ src->SI + LEAQ dst->BX rewrites
with no mechanical cross-stage mirror at the CX site = a gate-blind
cs!=ww risk. The emitter extraction is filed as a later addressing-
unification arc (#12). The ragged tail is corpus-absent (every corpus
field copy is tail in {0,4}, where greedy 4/2/1 emits exactly what the
old {4,1} tail did), so this is CLASS-N: zero corpus move on both stages,
byte-id holds by construction.

The cstage <=24 N_CALL receive site (cgen.c:5234) is a different copy
family (sret result read from AX/DX/CX, not a mem-to-mem field copy) and
already handles 4/2/1; left untouched. The str/slice/tagged/tuple 4/1
sites (#76) are likewise a separate family, filed not folded.

989_structcopytail_run pins it on both driver twins: tail2 (MOVW), tail6
(MOVL+MOVW), tail7 (the full MOVL+MOVW+MOVB ladder, the MOVB-path row),
plus an 8-aligned ctl8 (tail-0 control). Pre-fix cstage clobbers mark and
exits non-zero -> cs!=ww; post-fix 4/4 ok cs==ww.
This commit is contained in:
2026-06-13 15:03:07 +09:00
parent 074e0e585e
commit dd24de1134
6 changed files with 576 additions and 148 deletions

View File

@@ -258,6 +258,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_tagtupfieldsize_run \
$(BIN)/test_nestfield_run \
$(BIN)/test_structlocal_frame \
$(BIN)/test_structcopytail_run \
$(BIN)/test_arrlit_tail_zero_run \
$(BIN)/test_defdim_slice_run \
$(BIN)/test_trystr_run \
@@ -789,6 +790,22 @@ $(BIN)/test_structlocal_frame: test/wcc/989_structlocal_frame.c \
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# 989_structcopytail_run (#73): a whole-struct field-copy `x.i = o` whose
# source natural size %8 lands in {2,3,5,6,7} must move exactly that many
# bytes via a descending greedy 8->4->2->1 tail (MOVL/MOVW/MOVB), not round
# the ragged remainder up to an 8-byte MOVQ that over-writes the field's
# natural-offset successor. BOTH stages inlined a tail handling only {4,1};
# tail-2 (inner2{u8,u8} in outer2{i,mark:i32}) and tail-6 (inner6{u16,u16,
# u16} in outer6{i,mark:u8}) over-wrote MARK with an 8B MOVQ. Builds+runs on
# BOTH driver twins (rule-10), pinning the absolute 0; ctl8 (8-aligned, zero
# tail) pins the unchanged path.
$(BIN)/test_structcopytail_run: test/wcc/989_structcopytail_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).