selfhost+lib+test: route cgcall + nodeis{slice,str} N_DOT through fnretlookupmod (#34)

Class A silent miscompile, surfaced by landing strings.slice in
Hare's natural delegation form `fromutf8_unsafe(utf8.slice(begin,
end))` (ref/hare/strings/iter.ha:75). strings.slice itself returns
str, so the inner utf8.slice (cross-module N_DOT) call's cgcall
return-ABI fixup hit post-#4e fnretlookup's same-module-first walk
and grabbed strings.slice's own str return — emitted a spurious
`MOVQ DX, BX` after the cross-module CALL even though utf8.slice
returns []u8 (selfhost/cmd/wcc/cgenexpr.ww cgcall return-ABI fixup,
line 3249-3261 pre-fix). Every other consumer of cgcall:3249's
str-shuffle decision sat on the same bare-leaf table and was
silently miscompiling on the same collision shape pre-#34.

Sibling: nodeisslice + nodeisstr N_CALL arms in
selfhost/cmd/wcc/cgenutil.ww were N_IDENT-only — for a cross-
module N_DOT call returning a slice or str, pushargsrev fell
through to the natural 1-word PUSHQ AX, dropping the `.len`
(and `.cap` for slices) of the return value when consumed as a
call arg. strings.slice's body passes utf8.slice's []u8 result
to fromutf8_unsafe; pre-fix wwstage pushed 1 word vs cstage's
3, breaking the receiver's slice-3-pop drain.

Cstage carries no sister bug: cmd/w6c/cgen.c reads return shape
from the typed `n->lhs->type` (TY_FN sig) for both str-shuffle
and slice-/str-arg push counts — module-aware via the typed AST,
sidestepping any bare-leaf table. Mirror of #4e's cstage-no-
sister-bug note.

Fix: route cgcall return-ABI fixup + nodeisslice/nodeisstr N_CALL
arms through fnretlookupmod with `callee.lhs.str` (N_DOT
qualifier) or `c.curmod` (N_IDENT). Mirror of #28
fnparamslookupmod / #31 fnretlookupmod N_DOT re-routing.
Remaining bare-leaf fnretlookup consumer sites (~8 sites across
cgenexpr/cgenutil/cgenstmt/cgendecl listed in task #34a) stay
on the graduated bare-leaf path — none of the present-corpus
N_DOT leaf collisions have return-shape divergence at those
sites. A future stdlib port introducing a return-shape-divergent
same-leaf N_DOT collision will need the *mod re-routing — filed
as #34a sibling-latents.

Bundled three concerns per rule 11: cgcall fix, nodeisslice/
nodeisstr fix, and strings.slice retire + sentinel. (a) alone
leaves strings.slice byte-id breaking on slice-arg push count.
(b) alone leaves a phantom MOVQ DX, BX on the inner cross-
module CALL. (c) alone fails 995_self_rebuild without (a)+(b).
The three cannot land separately bisect-cleanly; the 745
sentinel pins the primary repro (cgcall str-shuffle) which
sentinel-flips on a cgcall:3257 revert.

745_fnret34_modshadow pins the fix with 1 row: caller.slice
returns str (same leaf as the cross-module callee, divergent
return shape); caller.run calls myutf8.slice returning []u8.
Asserts CALL myutf8.slice present inside caller.run TEXT +
`MOVQ DX, BX` anti-check on each stage plus cs-vs-ws byte-id.

strings.slice retired in lib/strings/strings.ww: the deferral
block becomes the natural Hare delegation form with two local
utf8.decoder reconstructions for the iterator endpoints — ww
has no anonymous-embed (parallel to the existing `move` helper).
iter_slice_cases mirrors ref/hare/strings/iter.ha:110-127;
sidesteps the Hare `let t = s;` iterator-copy via fresh
strings.iter() to stay clear of #35's sibling latents.

119/119 ok. ww2 == ww3 == ww4 byte-id holds.
This commit is contained in:
2026-05-18 23:37:16 +09:00
parent d84704e389
commit 049ebc14a1
9 changed files with 495 additions and 56 deletions

View File

@@ -25,16 +25,13 @@
// - `iterator` is flattened (`offs`, `src`, `reverse` fields).
// Hare uses anonymous-embedded `utf8::decoder`
// (ref/hare/strings/iter.ha:6-9); ww has no anonymous-embed
// syntax, so `next`/`prev` copy `offs`/`src` into a local
// `utf8.decoder` for the call, then write `offs` back.
// syntax, so `next`/`prev`/`slice` copy `offs`/`src` into a
// local `utf8.decoder` for the call (and `next`/`prev` write
// `offs` back).
// - Hare's private `move()` helper dispatches on a `forward: bool`
// using a function-pointer `let fun = if (forward) &utf8::next
// else &utf8::prev`. ww has no fn-pointers in scope yet, so the
// dispatch is a branch on `forward` selecting the call site.
// - `slice` deferred — Hare's `fromutf8_unsafe(utf8::slice(begin,
// end))` (ref/hare/strings/iter.ha:76) hits wwstage fnretlookup
// same-name cross-module phantom return-ABI fixup (task #34).
// Lands once #34 clears.
package strings;
@@ -369,11 +366,20 @@ export fn iterstr(it: *iterator) str = {
return fromutf8_unsafe(r);
};
// strings.slice deferred — Hare's delegation form
// `fromutf8_unsafe(utf8::slice(begin, end))` (ref/hare/strings/iter.ha:76)
// triggers wwstage fnretlookup same-name cross-module phantom return-ABI
// fixup (task #34, Class A wwstage UNDER, sub-bug of #4e). Inlining the
// body would diverge from Hare at API level (rule 9). Land after #34.
// slice — borrowed substring between two iterator positions.
// ref/hare/strings/iter.ha:75. Hare passes `*iterator` directly where
// `*utf8::decoder` is expected via anonymous-embed coercion; ww has
// no anonymous embed, so we reconstruct a local utf8.decoder for each
// endpoint and forward — same pattern as `move` above.
export fn slice(begin: *iterator, end: *iterator) str = {
let b: utf8.decoder;
b.src = begin.src;
b.offs = begin.offs;
let e: utf8.decoder;
e.src = end.src;
e.offs = end.offs;
return fromutf8_unsafe(utf8.slice(&b, &e));
};
// position — byte-wise offset of the iterator in its source.
// ref/hare/strings/iter.ha:82.