diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9d03a411..a7e34c62 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6731,6 +6731,21 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // nodeisstr — best-effort surface check: does this expression // evaluate to a str value? Used to drive the call-arg push convention // (str args take two slots: ptr + len). +// +// TODO(#11): every consumer of "is-str" here reconstructs the answer +// from raw N_kind because wwstage has no typed AST. Each new expression +// shape needs an explicit arm or it silently falls through to false, +// which downstream drops the second slot (BX/len) at the call site. +// A typed AST check (cstage reads n->type) would replace this whole +// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed), +// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base), +// N_DOT (struct field / chained / pseudo-fields excluded), N_CAST. +// Not covered (separate bugs / out of scope): +// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX` +// and never loads .len into BX; fixing the recognizer alone won't +// help. Tracked alongside the broader cgun-load-shape gap. +// - N_DOT to a tuple positional `t.1` of a str element — wwstage's +// cgdot loads (AX, BX) but tuple-as-arg has independent issues. fn nodeisstr(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; @@ -6758,6 +6773,34 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; return false; }; + // N_INDEX: `arr[i]` whose base is an indexable type carrying a + // str element. cgindex correctly loads (AX=ptr, BX=len) for a + // 16B element; without this arm pushargsrev only pushes AX and + // the call-arg pop reads .len from stack residue. Mirror of + // cstage's node_isstr → type_isstr(n->type), where n->type is + // the resolved element type after check. + if (k == nkind.N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bt: *node = nil; + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { bt = lc.tnode; } + else { bt = letvartnode(c, base.str); }; + if (bt != nil) { + let elem: *node = nil; + let bk: nkind = bt.kind; + if (bk == nkind.N_TARRAY) { elem = bt.lhs; }; + if (bk == nkind.N_TSLICE) { elem = bt.lhs; }; + if (bk == nkind.N_TPTR) { elem = bt.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + }; + return false; + }; if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.str; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 37c67564..17f3fa97 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -488,6 +488,21 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // nodeisstr — best-effort surface check: does this expression // evaluate to a str value? Used to drive the call-arg push convention // (str args take two slots: ptr + len). +// +// TODO(#11): every consumer of "is-str" here reconstructs the answer +// from raw N_kind because wwstage has no typed AST. Each new expression +// shape needs an explicit arm or it silently falls through to false, +// which downstream drops the second slot (BX/len) at the call site. +// A typed AST check (cstage reads n->type) would replace this whole +// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed), +// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base), +// N_DOT (struct field / chained / pseudo-fields excluded), N_CAST. +// Not covered (separate bugs / out of scope): +// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX` +// and never loads .len into BX; fixing the recognizer alone won't +// help. Tracked alongside the broader cgun-load-shape gap. +// - N_DOT to a tuple positional `t.1` of a str element — wwstage's +// cgdot loads (AX, BX) but tuple-as-arg has independent issues. fn nodeisstr(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; @@ -515,6 +530,34 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; return false; }; + // N_INDEX: `arr[i]` whose base is an indexable type carrying a + // str element. cgindex correctly loads (AX=ptr, BX=len) for a + // 16B element; without this arm pushargsrev only pushes AX and + // the call-arg pop reads .len from stack residue. Mirror of + // cstage's node_isstr → type_isstr(n->type), where n->type is + // the resolved element type after check. + if (k == nkind.N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bt: *node = nil; + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { bt = lc.tnode; } + else { bt = letvartnode(c, base.str); }; + if (bt != nil) { + let elem: *node = nil; + let bk: nkind = bt.kind; + if (bk == nkind.N_TARRAY) { elem = bt.lhs; }; + if (bk == nkind.N_TSLICE) { elem = bt.lhs; }; + if (bk == nkind.N_TPTR) { elem = bt.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + }; + return false; + }; if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.str; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a850686b..a5d49984 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6731,6 +6731,21 @@ fn nodeisslice(c: *cgen, n: *node) bool = { // nodeisstr — best-effort surface check: does this expression // evaluate to a str value? Used to drive the call-arg push convention // (str args take two slots: ptr + len). +// +// TODO(#11): every consumer of "is-str" here reconstructs the answer +// from raw N_kind because wwstage has no typed AST. Each new expression +// shape needs an explicit arm or it silently falls through to false, +// which downstream drops the second slot (BX/len) at the call site. +// A typed AST check (cstage reads n->type) would replace this whole +// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed), +// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base), +// N_DOT (struct field / chained / pseudo-fields excluded), N_CAST. +// Not covered (separate bugs / out of scope): +// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX` +// and never loads .len into BX; fixing the recognizer alone won't +// help. Tracked alongside the broader cgun-load-shape gap. +// - N_DOT to a tuple positional `t.1` of a str element — wwstage's +// cgdot loads (AX, BX) but tuple-as-arg has independent issues. fn nodeisstr(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; @@ -6758,6 +6773,34 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; return false; }; + // N_INDEX: `arr[i]` whose base is an indexable type carrying a + // str element. cgindex correctly loads (AX=ptr, BX=len) for a + // 16B element; without this arm pushargsrev only pushes AX and + // the call-arg pop reads .len from stack residue. Mirror of + // cstage's node_isstr → type_isstr(n->type), where n->type is + // the resolved element type after check. + if (k == nkind.N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bt: *node = nil; + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { bt = lc.tnode; } + else { bt = letvartnode(c, base.str); }; + if (bt != nil) { + let elem: *node = nil; + let bk: nkind = bt.kind; + if (bk == nkind.N_TARRAY) { elem = bt.lhs; }; + if (bk == nkind.N_TSLICE) { elem = bt.lhs; }; + if (bk == nkind.N_TPTR) { elem = bt.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + }; + return false; + }; if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.str; diff --git a/test/wcc/711_arrlit_str_full.c b/test/wcc/711_arrlit_str_full.c index a90b63a2..3a629c51 100644 --- a/test/wcc/711_arrlit_str_full.c +++ b/test/wcc/711_arrlit_str_full.c @@ -135,6 +135,74 @@ static const struct row rows[] = { " return (a[0] + a[1] + a[2]): i32;\n" "};\n", 6 }, + /* 8. Bare-let [N]str index as a call arg (task #34). Pre-#34 + * wwstage emitted PUSHQ AX only for argv[i] — the str value's + * BX=len half was dropped, and streq's `a.len` parameter read + * stack residue. Symptom under getopttest: rc=11 pre-#21, then + * 139 once #21 doubled the slot stride. Now byte-identical to + * cstage at the call site. streq("files.txt",argv[2]) → 0 (eq) + * → return 0. Pre-fix wwstage returned 11. */ + { "barelet_index_call_arg", + "fn streq(a: str, b: str) bool = {\n" + " if (a.len != b.len) { return false; };\n" + " let i: i32 = 0;\n" + " for (i < a.len) {\n" + " if (a[i] != b[i]) { return false; };\n" + " i += 1;\n" + " };\n" + " return true;\n" + "};\n" + "fn main() i32 = {\n" + " let argv: [3]str;\n" + " argv[0] = \"ls\";\n" + " argv[1] = \"-Fahs\";\n" + " argv[2] = \"files.txt\";\n" + " if (!streq(argv[2], \"files.txt\")) { return 11; };\n" + " if (!streq(argv[1], \"-Fahs\")) { return 12; };\n" + " if (!streq(argv[0], \"ls\")) { return 13; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 9. Nested call: `f(g(argv[i]))` exercises the full call-arg + * packer with an N_INDEX-of-str inside an N_CALL inside another + * N_CALL. Pins that the inner N_INDEX recognition rides through + * the same pushargsrev path that the simple row uses. dup1 here + * is identity-on-str so the outer streq receives g(argv[2]), + * which must be 9 bytes ("files.txt"). */ + { "nested_call_index_arg", + "fn dup1(s: str) str = { return s; };\n" + "fn streq(a: str, b: str) bool = {\n" + " if (a.len != b.len) { return false; };\n" + " let i: i32 = 0;\n" + " for (i < a.len) {\n" + " if (a[i] != b[i]) { return false; };\n" + " i += 1;\n" + " };\n" + " return true;\n" + "};\n" + "fn main() i32 = {\n" + " let argv: [3]str;\n" + " argv[0] = \"a\";\n" + " argv[1] = \"bb\";\n" + " argv[2] = \"files.txt\";\n" + " if (!streq(dup1(argv[2]), \"files.txt\")) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 10. Index .len as a call arg via a one-arg consumer. Confirms + * cgindex's (AX, BX) load still drives a single-i32 push when + * the field selector picks the .len half off the str element. + * Different code path from rows 8-9 (no str-arg push) but + * uses the same N_INDEX base. 5 chars in "hello". */ + { "barelet_index_len_arg", + "fn ident(n: i32) i32 = { return n; };\n" + "fn main() i32 = {\n" + " let xs: [2]str;\n" + " xs[0] = \"hi\";\n" + " xs[1] = \"hello\";\n" + " return ident(xs[1].len: i32);\n" + "};\n", + 5 }, }; static int