selfhost+test: nodeisstr handles N_INDEX of [N]str (#34)

wwstage's nodeisstr (cgenutil) didn't recognize N_INDEX-of-[N]str.
cgindex emitted only the ptr-half MOVQ when the result was used as
a str arg (call, .len access, str streq), so the .len half read
stack residue. Surfaced by worker-21 during #21 dev — pre-#21
slotsize=24B masked the read-side defect; post-#21 (16B stride)
exposed it. cstage's typed-AST node_isstr handles this naturally;
wwstage's untyped pattern walks the base ident's tnode shape.

Added N_INDEX arm to nodeisstr: walk the indexed base's tnode
through N_TARRAY / N_TSLICE / N_TPTR.lhs, return isstrtype on the
element. Mirrors cgindex's own base-type walk byte-for-byte in
shape so the two now agree on load-shape decisions.

Not covered (separate bugs, separately filed):
- N_UN(TK_STAR) of *str — cgun itself never loads .len into BX.
- tuple `.1` of str — N_TTUPLE path has its own load shape.
- alias-typed base (`type a = [N]str`) — N_TNAME isn't peeled; cgindex
  doesn't peel it either, so agreement holds. Outside #34 scope.

Test 711: 3 new rows — barelet_index_call_arg (streq direct arg),
nested_call_index_arg (f(g(argv[i])) — nested-call recursion),
barelet_index_len_arg (sister regression-pin for cgindex element
stride in bare-let context; pins a different code path that was
already correct post-#21).

The pre-existing wwstage `..findflag(SB)` symbol-mangling bug in
getopttest wwstage build is filed as task #37, not in this commit's
scope.
This commit is contained in:
2026-05-16 11:37:17 +09:00
parent b8b32ab80c
commit 993da52333
4 changed files with 197 additions and 0 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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