From 049ebc14a135358a8e7c490746318f2f3efb99bd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 23:37:16 +0900 Subject: [PATCH] selfhost+lib+test: route cgcall + nodeis{slice,str} N_DOT through fnretlookupmod (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 5 + lib/strings/strings.ww | 28 ++-- lib/strings/stringstest.ww | 45 ++++++ selfhost/cmd/w6c/main.combined.ww | 82 ++++++++-- selfhost/cmd/wcc/cgenexpr.ww | 19 ++- selfhost/cmd/wcc/cgenutil.ww | 35 ++++- selfhost/cmd/wwdump/main.combined.ww | 82 ++++++++-- selfhost/test/smoke.combined.ww | 28 ++-- test/wcc/745_fnret34_modshadow.c | 227 +++++++++++++++++++++++++++ 9 files changed, 495 insertions(+), 56 deletions(-) create mode 100644 test/wcc/745_fnret34_modshadow.c diff --git a/Makefile b/Makefile index ec4f8073..7e8fb66f 100644 --- a/Makefile +++ b/Makefile @@ -279,6 +279,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_letcopy_struct \ $(BIN)/test_fnparams_bare_leaf_shadow \ $(BIN)/test_fnret_bare_leaf_shadow \ + $(BIN)/test_fnret34_modshadow \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -684,6 +685,10 @@ $(BIN)/test_fnret_bare_leaf_shadow: test/wcc/731_fnret_bare_leaf_shadow.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fnret34_modshadow: test/wcc/745_fnret34_modshadow.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww index 8bb66b83..3aaeed6e 100644 --- a/lib/strings/strings.ww +++ b/lib/strings/strings.ww @@ -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. diff --git a/lib/strings/stringstest.ww b/lib/strings/stringstest.ww index 80d27840..7a8a9fa0 100644 --- a/lib/strings/stringstest.ww +++ b/lib/strings/stringstest.ww @@ -513,6 +513,50 @@ fn streq(a: str, b: str) bool = { if (strings.position(&it) != 5) { fail(); }; }; +// ref/hare/strings/iter.ha:110 @test fn slice. Hare uses `let t = s;` +// to copy the iterator; ww re-initialises t from the same source to +// stay in scope of #32 (local struct ident rhs already fixed) without +// reaching for #35's sibling latents. +@test fn iter_slice_cases() void = { + let s: strings.iterator = strings.iter("こんにちは"); + let t: strings.iterator = strings.iter("こんにちは"); + if (strings.slice(&s, &t).len != 0) { fail(); }; + if (strings.slice(&t, &s).len != 0) { fail(); }; + let i: i32 = 0; + for (i < 2) { + match (strings.next(&s)) { + case let r: rune => void; + case utf8.done => { fail(); }; + }; + match (strings.next(&t)) { + case let r: rune => void; + case utf8.done => { fail(); }; + }; + i += 1; + }; + if (strings.slice(&s, &t).len != 0) { fail(); }; + if (strings.slice(&t, &s).len != 0) { fail(); }; + i = 0; + for (i < 3) { + match (strings.next(&t)) { + case let r: rune => void; + case utf8.done => { fail(); }; + }; + i += 1; + }; + if (!streq(strings.slice(&s, &t), "にちは")) { fail(); }; + i = 0; + for (i < 3) { + match (strings.next(&s)) { + case let r: rune => void; + case utf8.done => { fail(); }; + }; + i += 1; + }; + if (strings.slice(&s, &t).len != 0) { fail(); }; + if (strings.slice(&t, &s).len != 0) { fail(); }; +}; + @test fn iter_iterstr_reverse_cases() void = { // Reverse iter: iterstr is `src[0:offs]` — bytes BEFORE the cursor // (the still-to-be-walked region in reverse direction). @@ -552,5 +596,6 @@ export fn main() i32 = { signalled = 24; iter_full_cases(); signalled = 25; iter_position_cases(); signalled = 26; iter_iterstr_reverse_cases(); + signalled = 27; iter_slice_cases(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index bddce3ae..114654ed 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1467,16 +1467,13 @@ export fn position(d: *decoder) i32 = { // - `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; @@ -1811,11 +1808,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. @@ -7602,11 +7608,27 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop // side under-drains by 2 words, leaving R8/R9 unset for the // receiver. Mirrors nodeisstr's N_CALL arm just below. + // N_DOT (cross-module callee, #34): route through fnretlookupmod + // so a same-leaf caller-module fn with diverging return shape + // doesn't shadow the explicit `mod.f()` qualifier — surfaced by + // strings.slice returning `fromutf8_unsafe(utf8.slice(...))` + // where strings.slice itself returns str. if (k == nkind.N_CALL) { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let rt: *node = fnretlookup(c, callee.str); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isslicetype(c, rt); + }; + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isslicetype(c, rt); }; }; @@ -7722,8 +7744,21 @@ fn nodeisstr(c: *cgen, n: *node) bool = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isstrtype(c, rt); + }; + // #34: cross-module N_DOT — route through fnretlookupmod + // so a same-leaf caller-module fn (different return shape) + // doesn't shadow the explicit qualifier. + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isstrtype(c, rt); }; }; @@ -14133,8 +14168,25 @@ fn cgcall(c: *cgen, n: *node) void = { }; // SysV returns 16-byte aggregates in (AX, DX). Our str // convention is (AX, BX), so shuffle for str-returning calls. + // Route through fnretlookupmod: for N_DOT cross-module callees, + // the bare-leaf fnretlookup's same-module-first walk (#4e) would + // pick the caller-module's same-leaf fn — a str-returning + // caller-side `slice` over a []u8-returning `mod.slice` then + // emits a phantom MOVQ DX, BX after the cross-module CALL (#34). if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cmod = c.curmod; }; + if (callee.kind == nkind.N_DOT) { + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; + }; + let rt: *node = fnretlookupmod(c, calleename, cmod); if (isstrtype(c, rt)) { emitline("\tMOVQ\tDX, BX\n"); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e164f781..7b66fea6 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3253,8 +3253,25 @@ fn cgcall(c: *cgen, n: *node) void = { }; // SysV returns 16-byte aggregates in (AX, DX). Our str // convention is (AX, BX), so shuffle for str-returning calls. + // Route through fnretlookupmod: for N_DOT cross-module callees, + // the bare-leaf fnretlookup's same-module-first walk (#4e) would + // pick the caller-module's same-leaf fn — a str-returning + // caller-side `slice` over a []u8-returning `mod.slice` then + // emits a phantom MOVQ DX, BX after the cross-module CALL (#34). if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cmod = c.curmod; }; + if (callee.kind == nkind.N_DOT) { + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; + }; + let rt: *node = fnretlookupmod(c, calleename, cmod); if (isstrtype(c, rt)) { emitline("\tMOVQ\tDX, BX\n"); }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 930ae210..c42a5398 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -489,11 +489,27 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop // side under-drains by 2 words, leaving R8/R9 unset for the // receiver. Mirrors nodeisstr's N_CALL arm just below. + // N_DOT (cross-module callee, #34): route through fnretlookupmod + // so a same-leaf caller-module fn with diverging return shape + // doesn't shadow the explicit `mod.f()` qualifier — surfaced by + // strings.slice returning `fromutf8_unsafe(utf8.slice(...))` + // where strings.slice itself returns str. if (k == nkind.N_CALL) { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let rt: *node = fnretlookup(c, callee.str); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isslicetype(c, rt); + }; + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isslicetype(c, rt); }; }; @@ -609,8 +625,21 @@ fn nodeisstr(c: *cgen, n: *node) bool = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isstrtype(c, rt); + }; + // #34: cross-module N_DOT — route through fnretlookupmod + // so a same-leaf caller-module fn (different return shape) + // doesn't shadow the explicit qualifier. + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isstrtype(c, rt); }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index affc3f99..fd06c60a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1467,16 +1467,13 @@ export fn position(d: *decoder) i32 = { // - `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; @@ -1811,11 +1808,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. @@ -7602,11 +7608,27 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop // side under-drains by 2 words, leaving R8/R9 unset for the // receiver. Mirrors nodeisstr's N_CALL arm just below. + // N_DOT (cross-module callee, #34): route through fnretlookupmod + // so a same-leaf caller-module fn with diverging return shape + // doesn't shadow the explicit `mod.f()` qualifier — surfaced by + // strings.slice returning `fromutf8_unsafe(utf8.slice(...))` + // where strings.slice itself returns str. if (k == nkind.N_CALL) { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let rt: *node = fnretlookup(c, callee.str); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isslicetype(c, rt); + }; + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isslicetype(c, rt); }; }; @@ -7722,8 +7744,21 @@ fn nodeisstr(c: *cgen, n: *node) bool = { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, callee.str, c.curmod); + return isstrtype(c, rt); + }; + // #34: cross-module N_DOT — route through fnretlookupmod + // so a same-leaf caller-module fn (different return shape) + // doesn't shadow the explicit qualifier. + if (callee.kind == nkind.N_DOT) { + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + let rt: *node = fnretlookupmod(c, callee.str, cmod); return isstrtype(c, rt); }; }; @@ -14133,8 +14168,25 @@ fn cgcall(c: *cgen, n: *node) void = { }; // SysV returns 16-byte aggregates in (AX, DX). Our str // convention is (AX, BX), so shuffle for str-returning calls. + // Route through fnretlookupmod: for N_DOT cross-module callees, + // the bare-leaf fnretlookup's same-module-first walk (#4e) would + // pick the caller-module's same-leaf fn — a str-returning + // caller-side `slice` over a []u8-returning `mod.slice` then + // emits a phantom MOVQ DX, BX after the cross-module CALL (#34). if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee != nil) { + if (callee.kind == nkind.N_IDENT) { cmod = c.curmod; }; + if (callee.kind == nkind.N_DOT) { + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; + }; + let rt: *node = fnretlookupmod(c, calleename, cmod); if (isstrtype(c, rt)) { emitline("\tMOVQ\tDX, BX\n"); }; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 75e435b2..6717cdc7 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -1358,16 +1358,13 @@ export fn position(d: *decoder) i32 = { // - `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; @@ -1702,11 +1699,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. diff --git a/test/wcc/745_fnret34_modshadow.c b/test/wcc/745_fnret34_modshadow.c new file mode 100644 index 00000000..72b59136 --- /dev/null +++ b/test/wcc/745_fnret34_modshadow.c @@ -0,0 +1,227 @@ +/* + * 745_fnret34_modshadow — sentinel for task #34 (sub-bug of #4e): + * wwstage's cgcall return-ABI fixup keyed bare-leaf fnretlookup, not + * fnretlookupmod. For N_DOT cross-module callees the post-#4e same- + * module-first walk grabbed the caller-module's same-leaf fn's + * return type — when that caller-side fn returned `str` and the + * cross-module callee returned a non-str ([]u8, struct, scalar) the + * `isstrtype(c, rt)` guard fired against the wrong type and emitted + * a spurious `MOVQ DX, BX` shuffle after the cross-module CALL + * (selfhost/cmd/wcc/cgenexpr.ww cgcall return-ABI fixup). + * + * Sibling: nodeisslice / nodeisstr N_CALL arms were N_IDENT-only + * (selfhost/cmd/wcc/cgenutil.ww), so pushargsrev's slice/str word- + * count fell through to the 1-word natural-push for any cross- + * module N_DOT call result — dropping `.len` (and `.cap` for + * slices). Hare's `strings.slice` body + * (`fromutf8_unsafe(utf8.slice(begin, end))`, ref/hare/strings/ + * iter.ha:75-77) hits both bugs at once because strings.slice + * itself returns str (str-shuffle phantom on the inner utf8.slice + * call) AND passes the inner []u8 to a same-module fn (.len/.cap + * dropped on the push). + * + * Cstage carries no sister bug: cmd/w6c/cgen.c reads the return + * shape from the typed `n->lhs->type` (TY_FN sig), module-aware via + * the typed AST. Both cgcall's str-shuffle and the slice/str-arg + * push count come off n->type directly. Mirror of #4e: cstage + * sidesteps every fnretlookup-keyed bare-leaf table. + * + * Pin: row 1 — caller-module exports `fn slice(s: str) str` (same + * leaf as the cross-module callee, return shape diverges); calls + * `myutf8.slice(&d)` returning []u8. Pre-fix wwstage emits a phantom + * `MOVQ DX, BX` after the CALL (and drops the slice 3-word push if + * the result is consumed as a slice arg). Post-fix the TEXT sym + * for caller.run carries no `MOVQ DX, BX` between the CALL and the + * RET. Asserts CALL myutf8.slice present + bad_imm anti-check on + * each stage plus cs-vs-ws byte-id. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { + const char *label; + const char *src; + const char *textsym; /* TEXT sym containing the call */ + const char *want_imm; /* substring that MUST appear */ + const char *bad_imm; /* substring that MUST NOT appear */ +}; + +/* caller.slice (same leaf as myutf8.slice, returns str) lives in the + * same source so caller's fnret table holds a slice-named fn returning + * str. caller.run calls the cross-module myutf8.slice (returning []u8). + * Pre-fix wwstage's cgcall fnretlookup picks caller.slice (same-module- + * first) and emits a phantom MOVQ DX, BX after the cross-module CALL. */ +static const struct row rows[] = { + { "cross_module_same_leaf_str_shuffle", + "package caller;\n" + "import myutf8;\n" + "export fn main() i32 = { return 0; };\n" + "export fn slice(s: str) str = { return s; };\n" + "export fn run() i32 = {\n" + "\tlet d: myutf8.decoder;\n" + "\td.x = 0;\n" + "\tlet s: []u8 = myutf8.slice(&d);\n" + "\treturn s.len;\n" + "};\n" + "package myutf8;\n" + "export type decoder = struct { x: i32 };\n" + "export fn slice(d: *myutf8.decoder) []u8 = {\n" + "\tlet r: []u8;\n" + "\tr.ptr = nil: *u8;\n" + "\tr.len = d.x;\n" + "\tr.cap = d.x;\n" + "\treturn r;\n" + "};\n", + "TEXT caller.run", "CALL\tmyutf8.slice", "MOVQ\tDX, BX" }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/fr34_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/fr34_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +/* Inside the named TEXT sym, before its first RET, want_imm MUST + * appear and bad_imm MUST NOT. bad_imm flags pre-fix wwstage str- + * shuffle firing on a non-str cross-module callee. */ +static int +check_imm(const char *spath, const struct row *r, const char *stage) +{ + char buf[1 << 14]; + if (slurp(spath, buf, sizeof buf) < 0) { + fprintf(stderr, "row[%s][%s]: cannot read %s\n", + r->label, stage, spath); + return -1; + } + const char *fn = strstr(buf, r->textsym); + if (!fn) { + fprintf(stderr, "row[%s][%s]: no %s in %s\n", + r->label, stage, r->textsym, spath); + return -1; + } + const char *ret = strstr(fn, "\tRET"); + if (!ret) { + fprintf(stderr, "row[%s][%s]: no RET inside %s\n", + r->label, stage, r->textsym); + return -1; + } + const char *good = strstr(fn, r->want_imm); + if (!good || good >= ret) { + fprintf(stderr, + "row[%s][%s]: want_imm %s missing inside %s\n", + r->label, stage, r->want_imm, r->textsym); + return -1; + } + const char *bad = strstr(fn, r->bad_imm); + if (bad && bad < ret) { + fprintf(stderr, + "row[%s][%s]: bad_imm %s present inside %s — wrong-module str-shuffle phantom fired\n", + r->label, stage, r->bad_imm, r->textsym); + return -1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "fnret34_modshadow[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_imm(cs_path, &rows[i], "cstage") != 0) fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "fnret34_modshadow[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); continue; + } + total++; + if (check_imm(ws_path, &rows[i], "wwstage") != 0) fail++; + + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "fnret34_modshadow[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "fnret34_modshadow: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("fnret34_modshadow: %d/%d ok\n", total, total); + return 0; +}