From 796d41bb9fb0e44581849e3cd81d57ec20b5fff4 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 13:22:41 +0900 Subject: [PATCH] wcc+w6c_ww: len() over place-resolved operands (F2/FA2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C5 (tasks #10 + #41): the len() builtin's operand handling was an arm enumeration that leaked FOUR siblings over time (#235 tuple-elem → #19 indexed-elem → F2 len(xs[i].field) → FA2/FB1 len(*p)) — every unhandled slice/str operand shape fell to a bare cgexpr fallback that returned the slice DATA POINTER as the length. Silent ptr-garbage, byte-id both stages, gate-blind. Probing at d642017 surfaced the full family: len(*p) (param 48 / local 64), len(xs[i].field) (147), len((*p)[i].field) (10), len(s.field) (75), len(p.field) (87) — plus the same garbage for non-place operands len("abc") (40), len(xs[1:3]) (48), len(mk()) (0). Review widened it twice more: len(**pp) (chained deref, garbage 208 at e481cb8) and the EMPTY-slice deref (len 0 reported as .ptr — masked by exit-code truncation, hence the branchy test row). The enumeration is closed by construction (ken's verdict): enumerated fast-paths keep their pre-fix asm byte-identically (ident local/global, #235 tuple element — not resolver-addressable, cgplaceaddr has no TY_TUPLE hop — #19 indexed element, TY_ARRAY const fold), then ONE uniform header-place route via cgplaceaddr resolves every other slice/str place and reads the .len word at place+8 (the same offset math as the ident arm). Non-place operands (string literal, slicing expr, call result) die LOUD per rule 7 — previously the same silent ptr-garbage; Hare instead const-folds len of literals, that parity is filed as #46. The ident arm's off==0 non-let residue (MOVQ 8(BP) garbage) now also routes resolver-or-loud. cstage's dispatch peel is aligned to wwstage's existing TY_NAMED loop-chase (single-peel + loud tail would have surfaced as cs-rejects/ww-accepts on 2-level aliases). Asm-neutrality: all five embedded main.combined.ww corpora compile byte-identically under pristine-parent w6c vs fixed w6c; per-shape pins (ident local/global, tuple, index, array) NEUTRAL + cs==ww. 802_lenidx_run grows 14 rows: the nine garbage shapes (incl. computed index through a deref spine, param-vs-local *p, chained **pp, empty slice), two neutrality controls (global and tuple fast-paths have dedicated runs: 797, 903), three reject rows pinning the exact rule-7 text in BOTH stages; all fix rows verified FAILING against a pristine build of the parent e481cb8 (12/19 fail there, 19/19 green here). Consumers unblocked: regex fold-2b tranche C ha:795/798 len(threads[i].captures); lib/regex add_thread's (*threads).len dodge (regex.ww:238, WHY comment cites #41) reverts to len(*threads) with the tranche-C port, not here. --- cmd/w6c/cgen.c | 75 +++++++--- selfhost/cmd/w6c/main.combined.ww | 215 +++++++++++++++------------ selfhost/cmd/wcc/cgenexpr.ww | 215 +++++++++++++++------------ selfhost/cmd/wwdump/main.combined.ww | 215 +++++++++++++++------------ test/wcc/802_lenidx_run.c | 207 +++++++++++++++++++++++++- 5 files changed, 615 insertions(+), 312 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 47977fb7..ab274b68 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6599,10 +6599,17 @@ cgexpr(Cg *c, Node *n, Local *locals) if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "len") == 0 && n->list) { Node *a = n->list; - Type *at = a->type; - Type *u = (at && at->kind == TY_NAMED) ? at->under : at; - if (u && (u->kind == TY_SLICE || u->kind == TY_STR) - && a->kind == N_IDENT) { + /* loop-peel: the wwstage mirror already chases + * multi-level TY_NAMED; cstage single-peeled, so a + * 2-level alias fell to the old silent fallback. With + * the loud tail below that asymmetry would surface as + * cs-rejects / ww-accepts — same predicate both + * stages. */ + Type *u = type_chase_named(a->type); + int hdrish = u && (u->kind == TY_SLICE + || u->kind == TY_STR); + int lendone = 0; + if (hdrish && a->kind == N_IDENT) { int off = localfind(locals, a->str); if (off == 0 && let_islet(a->str)) { /* #231: str/slice GLOBAL — the .len word @@ -6617,21 +6624,26 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX)); - } else { + lendone = 1; + } else if (off != 0) { ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + lendone = 1; } - } else if (u && (u->kind == TY_SLICE || u->kind == TY_STR) + /* off==0 non-let ident (e.g. a DATA-backed + * def): the old arm emitted MOVQ 8(BP) — + * garbage. Falls to the resolver route. */ + } else if (hdrish && a->kind == N_DOT && a->lhs && a->lhs->kind == N_IDENT && a->str) { /* #235: len() of a tuple-element slice/str - * (`len(t.N)`). The tuple-element read leaves only - * AX=.ptr — it has no slice-header sibling (that - * gap is #238) — so the bare cgexpr fallback below - * returned .ptr AS the length. Load the element's - * .len word directly at BP + element_off + 8, - * mirroring the N_IDENT slice arm above and the - * tuple-field-offset walk (cgen.c N_DOT TY_TUPLE). */ + * (`len(t.N)`). Kept as an enumerated arm: tuples + * are not resolver-addressable (cgplaceaddr has no + * TY_TUPLE hop — that gap is #238). Load the + * element's .len word directly at + * BP + element_off + 8, mirroring the N_IDENT + * slice arm above and the tuple-field-offset walk + * (cgen.c N_DOT TY_TUPLE). */ Type *bt = a->lhs->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; @@ -6651,27 +6663,46 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_AX)); - } else { - cgexpr(c, a, locals); + lendone = 1; } - } else if (u && (u->kind == TY_SLICE || u->kind == TY_STR) - && a->kind == N_INDEX) { + /* struct-field N_DOT (`len(s.field)`): the old + * inner fallback returned .ptr as the length. + * Falls to the resolver route. */ + } else if (hdrish && a->kind == N_INDEX) { /* #19: len() of an INDEXED str/slice element * (`len(xs[i])`). The N_INDEX str/slice load leaves * AX=.ptr, BX=.len, CX=.cap (cgslicehdr) — the bare - * cgexpr fallback below then returned AX (the ptr) AS - * the length. Shuffle BX (the len word) into AX, the + * cgexpr fallback returned AX (the ptr) AS the + * length. Shuffle BX (the len word) into AX, the * same MOVQ BX,AX shape as the #14 .len pseudo-field * fix. Same family as #18 (shared cstage==wwstage gap, * not a rule-10 divergence). */ cgexpr(c, a, locals); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + lendone = 1; } else if (u && u->kind == TY_ARRAY) { ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX)); - } else { - /* fall back: load via .len pseudo-field */ - cgexpr(c, a, locals); + lendone = 1; } + /* #10 (F2) + #41 (FA2/FB1): ONE uniform header-place + * route for every other slice/str place — the arm + * enumeration above leaked four siblings + * (#235 → #19 → F2 → FA2/FB1) because each new operand + * shape fell to a bare cgexpr fallback that returned + * the slice DATA POINTER as the length. Resolve the + * operand's header address (cgplaceaddr — deref / + * index / dot spines) and read the .len word at +8; + * non-place operands (call result, slicing expr, + * string literal — all previously the same silent + * ptr-garbage) die LOUD per rule 7. */ + if (!lendone && hdrish + && cgplaceaddr(c, a, D_BX, locals)) { + ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX)); + lendone = 1; + } + if (!lendone) + fatal("#10/#41: len() operand shape not " + "place-resolvable (rule-7)"); break; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d30252e2..0301d773 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -25221,18 +25221,24 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297. - // Required for byte-id when compiler-imported lib code uses - // len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)` - // over the [800]u8 field). Without this intercept wwstage falls - // through to a regular CALL len(SB) while cstage folds to - // `MOVQ $alen, AX` — rule-10 byte-id break (#131). + // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c N_CALL "len" + // arm. Required for byte-id when compiler-imported lib code + // uses len(fixedarray) (e.g. lib/strconv/decimal.ha's + // `len(d.digits)` over the [800]u8 field). Without this + // intercept wwstage falls through to a regular CALL len(SB) + // while cstage folds to `MOVQ $alen, AX` — rule-10 byte-id + // break (#131). // - // Argument-type-driven branches: - // TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8. - // TY_ARRAY → fold `MOVQ $alen, AX`. - // else → evaluate operand (cstage's pseudo-.len fallback — - // unlikely to fire on Hare-shaped sources). + // Dispatch (#10/#41): enumerated fast-paths keep their + // pre-fix asm (ident local/global, #235 tuple element, #19 + // indexed element, TY_ARRAY const fold), then ONE uniform + // header-place route via cgplaceaddr (.len at place+8) for + // every other slice/str place — the arm enumeration leaked + // four siblings (#235 → #19 → F2 → FA2/FB1), each new operand + // shape falling to a cgexpr fallback that returned the slice + // DATA POINTER as the length. Non-place operands (call + // result, slicing expr, string literal — previously the same + // silent ptr-garbage) die LOUD per rule 7. if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; @@ -25241,94 +25247,102 @@ fn cgcall(c: *cgen, n: *node) void = { for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let hdrish: bool = false; if (u != nil) { - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.str); - if (lc != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + 8): i64); - emitline("(BP), AX\n"); - return; - }; - // #231: str/slice GLOBAL — the - // local-only path above lacked it, - // so cgexpr fell through and left - // AX=.ptr (not .len). The .len word - // lives at the global's address+8; - // route the LEAQ through the post-#1 - // value mangle (c.curmod) so a - // private same-leaf global isn't - // mis-resolved. - if (isletvar(c, a.str)) { - emitline("\tLEAQ\t"); - emitsymnamehint(c, a.str, c.curmod); - emitline("(SB), CX\n"); - emitline("\tMOVQ\t8(CX), AX\n"); - return; - }; + if (u.kind == tykind.TY_SLICE + || u.kind == tykind.TY_STR) { + hdrish = true; }; - // #235: len() of a tuple-element slice/str - // (`len(t.N)`). The tuple-element read leaves - // only AX=.ptr — no slice-header sibling (that - // gap is #238) — so the cgexpr fallback below - // returned .ptr AS the length. Load the element's - // .len word directly at BP + element_off + 8, - // mirroring the N_IDENT slice arm above and the - // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_DOT - && a.lhs != nil - && a.lhs.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.lhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - for (tn != nil && tn.kind == nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(a.str); - if (idx >= 0) { - let tp: *node = tn.list; - let foff: i32 = 0; - let i: i32 = 0; - for (i < idx) { - if (tp == nil) { i = idx; } - else { - foff += slotsize(c, tp.lhs); - tp = tp.next; - i += 1; - }; - }; - if (tp != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + foff + 8): i64); - emitline("(BP), AX\n"); - return; + }; + if (hdrish && a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + 8): i64); + emitline("(BP), AX\n"); + return; + }; + // #231: str/slice GLOBAL — the + // local-only path above lacked it, + // so cgexpr fell through and left + // AX=.ptr (not .len). The .len word + // lives at the global's address+8; + // route the LEAQ through the post-#1 + // value mangle (c.curmod) so a + // private same-leaf global isn't + // mis-resolved. + if (isletvar(c, a.str)) { + emitline("\tLEAQ\t"); + emitsymnamehint(c, a.str, c.curmod); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + return; + }; + // non-local non-let ident (DATA-backed + // def): the old arm fell to the silent + // cgexpr fallback. Falls to the + // resolver route below. + }; + // #235: len() of a tuple-element slice/str + // (`len(t.N)`). Kept as an enumerated arm: + // tuples are not resolver-addressable + // (cgplaceaddr has no TY_TUPLE hop — that gap + // is #238). Load the element's .len word + // directly at BP + element_off + 8, mirroring + // the N_IDENT slice arm above and the + // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). + if (hdrish && a.kind == nkind.N_DOT + && a.lhs != nil + && a.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.lhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + for (tn != nil && tn.kind == nkind.N_TNAME) { + tn = aliaslookup(c, tn.str); + }; + if (tn != nil) { + if (tn.kind == nkind.N_TTUPLE) { + let idx: i32 = fldnumidx(a.str); + if (idx >= 0) { + let tp: *node = tn.list; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += slotsize(c, tp.lhs); + tp = tp.next; + i += 1; }; }; + if (tp != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 8): i64); + emitline("(BP), AX\n"); + return; + }; }; }; }; }; - // #19: len() of an INDEXED str/slice element - // (`len(xs[i])`). The N_INDEX str/slice load - // leaves AX=.ptr, BX=.len, CX=.cap — the bare - // cgexpr fallback below returned AX (the ptr) - // AS the length. Shuffle BX (the len word) - // into AX, the same MOVQ BX,AX shape as the - // #14 .len pseudo-field fix. Same family as - // #18 (shared cstage==wwstage gap, not rule-10). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_INDEX) { - cgexpr(c, a); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; + // struct-field N_DOT (`len(s.field)`): the + // old fallback returned .ptr as the length. + // Falls to the resolver route below. + }; + // #19: len() of an INDEXED str/slice element + // (`len(xs[i])`). The N_INDEX str/slice load + // leaves AX=.ptr, BX=.len, CX=.cap — the bare + // cgexpr fallback returned AX (the ptr) AS the + // length. Shuffle BX (the len word) into AX, + // the same MOVQ BX,AX shape as the #14 .len + // pseudo-field fix. Same family as #18 (shared + // cstage==wwstage gap, not rule-10). + if (hdrish && a.kind == nkind.N_INDEX) { + cgexpr(c, a); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (u != nil) { if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); @@ -25336,11 +25350,20 @@ fn cgcall(c: *cgen, n: *node) void = { return; }; }; - // Fallback: evaluate the argument and let AX carry - // whatever the value-load shape yields. Mirrors - // cstage's `cgexpr(c, a, locals)` fallthrough. - cgexpr(c, a); - return; + // #10 (F2) + #41 (FA2/FB1): ONE uniform + // header-place route for every other slice/str + // place — resolve the operand's header address + // (cgplaceaddr: deref / index / dot spines) and + // read the .len word at +8. + if (hdrish) { + if (cgplaceaddr(c, a, "BX")) { + emitline("\tMOVQ\t8(BX), AX\n"); + return; + }; + }; + let mlen: str = "#10/#41: len() operand shape not place-resolvable (rule-7)\n"; + os.write(2, mlen.ptr, mlen.len: u64); + os.exit(1); }; }; // free(x) — documented NO-OP, mirror of cstage's cgexpr diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index d98e177d..ae9719e7 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5101,18 +5101,24 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297. - // Required for byte-id when compiler-imported lib code uses - // len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)` - // over the [800]u8 field). Without this intercept wwstage falls - // through to a regular CALL len(SB) while cstage folds to - // `MOVQ $alen, AX` — rule-10 byte-id break (#131). + // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c N_CALL "len" + // arm. Required for byte-id when compiler-imported lib code + // uses len(fixedarray) (e.g. lib/strconv/decimal.ha's + // `len(d.digits)` over the [800]u8 field). Without this + // intercept wwstage falls through to a regular CALL len(SB) + // while cstage folds to `MOVQ $alen, AX` — rule-10 byte-id + // break (#131). // - // Argument-type-driven branches: - // TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8. - // TY_ARRAY → fold `MOVQ $alen, AX`. - // else → evaluate operand (cstage's pseudo-.len fallback — - // unlikely to fire on Hare-shaped sources). + // Dispatch (#10/#41): enumerated fast-paths keep their + // pre-fix asm (ident local/global, #235 tuple element, #19 + // indexed element, TY_ARRAY const fold), then ONE uniform + // header-place route via cgplaceaddr (.len at place+8) for + // every other slice/str place — the arm enumeration leaked + // four siblings (#235 → #19 → F2 → FA2/FB1), each new operand + // shape falling to a cgexpr fallback that returned the slice + // DATA POINTER as the length. Non-place operands (call + // result, slicing expr, string literal — previously the same + // silent ptr-garbage) die LOUD per rule 7. if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; @@ -5121,94 +5127,102 @@ fn cgcall(c: *cgen, n: *node) void = { for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let hdrish: bool = false; if (u != nil) { - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.str); - if (lc != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + 8): i64); - emitline("(BP), AX\n"); - return; - }; - // #231: str/slice GLOBAL — the - // local-only path above lacked it, - // so cgexpr fell through and left - // AX=.ptr (not .len). The .len word - // lives at the global's address+8; - // route the LEAQ through the post-#1 - // value mangle (c.curmod) so a - // private same-leaf global isn't - // mis-resolved. - if (isletvar(c, a.str)) { - emitline("\tLEAQ\t"); - emitsymnamehint(c, a.str, c.curmod); - emitline("(SB), CX\n"); - emitline("\tMOVQ\t8(CX), AX\n"); - return; - }; + if (u.kind == tykind.TY_SLICE + || u.kind == tykind.TY_STR) { + hdrish = true; }; - // #235: len() of a tuple-element slice/str - // (`len(t.N)`). The tuple-element read leaves - // only AX=.ptr — no slice-header sibling (that - // gap is #238) — so the cgexpr fallback below - // returned .ptr AS the length. Load the element's - // .len word directly at BP + element_off + 8, - // mirroring the N_IDENT slice arm above and the - // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_DOT - && a.lhs != nil - && a.lhs.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.lhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - for (tn != nil && tn.kind == nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(a.str); - if (idx >= 0) { - let tp: *node = tn.list; - let foff: i32 = 0; - let i: i32 = 0; - for (i < idx) { - if (tp == nil) { i = idx; } - else { - foff += slotsize(c, tp.lhs); - tp = tp.next; - i += 1; - }; - }; - if (tp != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + foff + 8): i64); - emitline("(BP), AX\n"); - return; + }; + if (hdrish && a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + 8): i64); + emitline("(BP), AX\n"); + return; + }; + // #231: str/slice GLOBAL — the + // local-only path above lacked it, + // so cgexpr fell through and left + // AX=.ptr (not .len). The .len word + // lives at the global's address+8; + // route the LEAQ through the post-#1 + // value mangle (c.curmod) so a + // private same-leaf global isn't + // mis-resolved. + if (isletvar(c, a.str)) { + emitline("\tLEAQ\t"); + emitsymnamehint(c, a.str, c.curmod); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + return; + }; + // non-local non-let ident (DATA-backed + // def): the old arm fell to the silent + // cgexpr fallback. Falls to the + // resolver route below. + }; + // #235: len() of a tuple-element slice/str + // (`len(t.N)`). Kept as an enumerated arm: + // tuples are not resolver-addressable + // (cgplaceaddr has no TY_TUPLE hop — that gap + // is #238). Load the element's .len word + // directly at BP + element_off + 8, mirroring + // the N_IDENT slice arm above and the + // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). + if (hdrish && a.kind == nkind.N_DOT + && a.lhs != nil + && a.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.lhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + for (tn != nil && tn.kind == nkind.N_TNAME) { + tn = aliaslookup(c, tn.str); + }; + if (tn != nil) { + if (tn.kind == nkind.N_TTUPLE) { + let idx: i32 = fldnumidx(a.str); + if (idx >= 0) { + let tp: *node = tn.list; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += slotsize(c, tp.lhs); + tp = tp.next; + i += 1; }; }; + if (tp != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 8): i64); + emitline("(BP), AX\n"); + return; + }; }; }; }; }; - // #19: len() of an INDEXED str/slice element - // (`len(xs[i])`). The N_INDEX str/slice load - // leaves AX=.ptr, BX=.len, CX=.cap — the bare - // cgexpr fallback below returned AX (the ptr) - // AS the length. Shuffle BX (the len word) - // into AX, the same MOVQ BX,AX shape as the - // #14 .len pseudo-field fix. Same family as - // #18 (shared cstage==wwstage gap, not rule-10). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_INDEX) { - cgexpr(c, a); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; + // struct-field N_DOT (`len(s.field)`): the + // old fallback returned .ptr as the length. + // Falls to the resolver route below. + }; + // #19: len() of an INDEXED str/slice element + // (`len(xs[i])`). The N_INDEX str/slice load + // leaves AX=.ptr, BX=.len, CX=.cap — the bare + // cgexpr fallback returned AX (the ptr) AS the + // length. Shuffle BX (the len word) into AX, + // the same MOVQ BX,AX shape as the #14 .len + // pseudo-field fix. Same family as #18 (shared + // cstage==wwstage gap, not rule-10). + if (hdrish && a.kind == nkind.N_INDEX) { + cgexpr(c, a); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (u != nil) { if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); @@ -5216,11 +5230,20 @@ fn cgcall(c: *cgen, n: *node) void = { return; }; }; - // Fallback: evaluate the argument and let AX carry - // whatever the value-load shape yields. Mirrors - // cstage's `cgexpr(c, a, locals)` fallthrough. - cgexpr(c, a); - return; + // #10 (F2) + #41 (FA2/FB1): ONE uniform + // header-place route for every other slice/str + // place — resolve the operand's header address + // (cgplaceaddr: deref / index / dot spines) and + // read the .len word at +8. + if (hdrish) { + if (cgplaceaddr(c, a, "BX")) { + emitline("\tMOVQ\t8(BX), AX\n"); + return; + }; + }; + let mlen: str = "#10/#41: len() operand shape not place-resolvable (rule-7)\n"; + os.write(2, mlen.ptr, mlen.len: u64); + os.exit(1); }; }; // free(x) — documented NO-OP, mirror of cstage's cgexpr diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index cf7cbd8d..3efbe775 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -25221,18 +25221,24 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297. - // Required for byte-id when compiler-imported lib code uses - // len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)` - // over the [800]u8 field). Without this intercept wwstage falls - // through to a regular CALL len(SB) while cstage folds to - // `MOVQ $alen, AX` — rule-10 byte-id break (#131). + // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c N_CALL "len" + // arm. Required for byte-id when compiler-imported lib code + // uses len(fixedarray) (e.g. lib/strconv/decimal.ha's + // `len(d.digits)` over the [800]u8 field). Without this + // intercept wwstage falls through to a regular CALL len(SB) + // while cstage folds to `MOVQ $alen, AX` — rule-10 byte-id + // break (#131). // - // Argument-type-driven branches: - // TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8. - // TY_ARRAY → fold `MOVQ $alen, AX`. - // else → evaluate operand (cstage's pseudo-.len fallback — - // unlikely to fire on Hare-shaped sources). + // Dispatch (#10/#41): enumerated fast-paths keep their + // pre-fix asm (ident local/global, #235 tuple element, #19 + // indexed element, TY_ARRAY const fold), then ONE uniform + // header-place route via cgplaceaddr (.len at place+8) for + // every other slice/str place — the arm enumeration leaked + // four siblings (#235 → #19 → F2 → FA2/FB1), each new operand + // shape falling to a cgexpr fallback that returned the slice + // DATA POINTER as the length. Non-place operands (call + // result, slicing expr, string literal — previously the same + // silent ptr-garbage) die LOUD per rule 7. if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; @@ -25241,94 +25247,102 @@ fn cgcall(c: *cgen, n: *node) void = { for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; + let hdrish: bool = false; if (u != nil) { - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.str); - if (lc != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + 8): i64); - emitline("(BP), AX\n"); - return; - }; - // #231: str/slice GLOBAL — the - // local-only path above lacked it, - // so cgexpr fell through and left - // AX=.ptr (not .len). The .len word - // lives at the global's address+8; - // route the LEAQ through the post-#1 - // value mangle (c.curmod) so a - // private same-leaf global isn't - // mis-resolved. - if (isletvar(c, a.str)) { - emitline("\tLEAQ\t"); - emitsymnamehint(c, a.str, c.curmod); - emitline("(SB), CX\n"); - emitline("\tMOVQ\t8(CX), AX\n"); - return; - }; + if (u.kind == tykind.TY_SLICE + || u.kind == tykind.TY_STR) { + hdrish = true; }; - // #235: len() of a tuple-element slice/str - // (`len(t.N)`). The tuple-element read leaves - // only AX=.ptr — no slice-header sibling (that - // gap is #238) — so the cgexpr fallback below - // returned .ptr AS the length. Load the element's - // .len word directly at BP + element_off + 8, - // mirroring the N_IDENT slice arm above and the - // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_DOT - && a.lhs != nil - && a.lhs.kind == nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.lhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - for (tn != nil && tn.kind == nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(a.str); - if (idx >= 0) { - let tp: *node = tn.list; - let foff: i32 = 0; - let i: i32 = 0; - for (i < idx) { - if (tp == nil) { i = idx; } - else { - foff += slotsize(c, tp.lhs); - tp = tp.next; - i += 1; - }; - }; - if (tp != nil) { - emitline("\tMOVQ\t"); - emitoff((lc.off + foff + 8): i64); - emitline("(BP), AX\n"); - return; + }; + if (hdrish && a.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.str); + if (lc != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + 8): i64); + emitline("(BP), AX\n"); + return; + }; + // #231: str/slice GLOBAL — the + // local-only path above lacked it, + // so cgexpr fell through and left + // AX=.ptr (not .len). The .len word + // lives at the global's address+8; + // route the LEAQ through the post-#1 + // value mangle (c.curmod) so a + // private same-leaf global isn't + // mis-resolved. + if (isletvar(c, a.str)) { + emitline("\tLEAQ\t"); + emitsymnamehint(c, a.str, c.curmod); + emitline("(SB), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + return; + }; + // non-local non-let ident (DATA-backed + // def): the old arm fell to the silent + // cgexpr fallback. Falls to the + // resolver route below. + }; + // #235: len() of a tuple-element slice/str + // (`len(t.N)`). Kept as an enumerated arm: + // tuples are not resolver-addressable + // (cgplaceaddr has no TY_TUPLE hop — that gap + // is #238). Load the element's .len word + // directly at BP + element_off + 8, mirroring + // the N_IDENT slice arm above and the + // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). + if (hdrish && a.kind == nkind.N_DOT + && a.lhs != nil + && a.lhs.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, a.lhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + for (tn != nil && tn.kind == nkind.N_TNAME) { + tn = aliaslookup(c, tn.str); + }; + if (tn != nil) { + if (tn.kind == nkind.N_TTUPLE) { + let idx: i32 = fldnumidx(a.str); + if (idx >= 0) { + let tp: *node = tn.list; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += slotsize(c, tp.lhs); + tp = tp.next; + i += 1; }; }; + if (tp != nil) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 8): i64); + emitline("(BP), AX\n"); + return; + }; }; }; }; }; - // #19: len() of an INDEXED str/slice element - // (`len(xs[i])`). The N_INDEX str/slice load - // leaves AX=.ptr, BX=.len, CX=.cap — the bare - // cgexpr fallback below returned AX (the ptr) - // AS the length. Shuffle BX (the len word) - // into AX, the same MOVQ BX,AX shape as the - // #14 .len pseudo-field fix. Same family as - // #18 (shared cstage==wwstage gap, not rule-10). - if ((u.kind == tykind.TY_SLICE - || u.kind == tykind.TY_STR) - && a.kind == nkind.N_INDEX) { - cgexpr(c, a); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; + // struct-field N_DOT (`len(s.field)`): the + // old fallback returned .ptr as the length. + // Falls to the resolver route below. + }; + // #19: len() of an INDEXED str/slice element + // (`len(xs[i])`). The N_INDEX str/slice load + // leaves AX=.ptr, BX=.len, CX=.cap — the bare + // cgexpr fallback returned AX (the ptr) AS the + // length. Shuffle BX (the len word) into AX, + // the same MOVQ BX,AX shape as the #14 .len + // pseudo-field fix. Same family as #18 (shared + // cstage==wwstage gap, not rule-10). + if (hdrish && a.kind == nkind.N_INDEX) { + cgexpr(c, a); + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + if (u != nil) { if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); @@ -25336,11 +25350,20 @@ fn cgcall(c: *cgen, n: *node) void = { return; }; }; - // Fallback: evaluate the argument and let AX carry - // whatever the value-load shape yields. Mirrors - // cstage's `cgexpr(c, a, locals)` fallthrough. - cgexpr(c, a); - return; + // #10 (F2) + #41 (FA2/FB1): ONE uniform + // header-place route for every other slice/str + // place — resolve the operand's header address + // (cgplaceaddr: deref / index / dot spines) and + // read the .len word at +8. + if (hdrish) { + if (cgplaceaddr(c, a, "BX")) { + emitline("\tMOVQ\t8(BX), AX\n"); + return; + }; + }; + let mlen: str = "#10/#41: len() operand shape not place-resolvable (rule-7)\n"; + os.write(2, mlen.ptr, mlen.len: u64); + os.exit(1); }; }; // free(x) — documented NO-OP, mirror of cstage's cgexpr diff --git a/test/wcc/802_lenidx_run.c b/test/wcc/802_lenidx_run.c index b17be96a..39fe6c3c 100644 --- a/test/wcc/802_lenidx_run.c +++ b/test/wcc/802_lenidx_run.c @@ -38,6 +38,26 @@ * divergent frame offset (cstage -24(BP) vs wwstage -40(BP)). That gap is * unrelated to len() (it reproduces with the `.len` pseudo-field too) and * is filed separately; these rows stay on str elements to avoid it. + * + * C5 SWEEP (#10 F2 + #41 FA2/FB1): the arm enumeration leaked FOUR + * siblings over time (#235 → #19 → F2 → FA2/FB1) — every unhandled + * operand shape fell to a bare cgexpr fallback returning the slice DATA + * POINTER as the length, silent and byte-id-blind. The fix replaces the + * fallback with ONE uniform header-place route (cgplaceaddr → .len at + * place+8) in BOTH stages; non-place operands (string literal, slicing + * expr, call result — all previously the same silent ptr-garbage) now + * die LOUD (rule 7). Rows below pin: len(*p) param + local (FB1), + * len(*p) on an EMPTY slice (a bare exit-code row can't pin this — the + * garbage ptr's low byte masks to 0 — hence the branchy discriminator), + * len(**pp) (chained deref, same resolver spine), len(xs[i].field) + * (F2), len((*p)[i].field) incl. computed index, len(s.field) + + * len(p.field) (struct-field cousins), the [N]T-const and ident-str + * neutrality controls (global and tuple fast-paths have dedicated runs: + * 797, 903), and reject rows with the exact rule-7 text. All + * garbage/reject rows verified FAILING at the parent e481cb8 (the + * per-row garbage exits below cite the d642017 probes; garbage is + * frame-layout-dependent and drifts between commits — the FAILING + * status is the pin, not the value). */ #include #include @@ -55,7 +75,10 @@ runwait(const char *cmd) return -1; } -struct row { const char *label; const char *src; int want_exit; }; +/* reject != NULL marks a build-reject row: both stages must refuse the + * source LOUD and the diagnostic must contain that exact text. */ +struct row { const char *label; const char *src; int want_exit; + const char *reject; }; static const struct row rows[] = { /* the exact #19 repro: a [3]str table, len(t[1]) == 3. Pre-fix this @@ -99,9 +122,152 @@ static const struct row rows[] = { " if (*p != 'b') { return 92; };\n" " return len(t[1]): i32;\n" "};\n", 3 }, - { NULL, NULL, 0 } + + /* ---- C5 sweep rows (#10 F2 + #41 FA2/FB1) ---- */ + + /* FB1 exact repro: len(*p) through a *[]T PARAM loaded the slice + * header's word 0 (the data pointer) as the length — silent + * ptr-garbage (exit 48 at d642017), byte-id both stages. */ + { "deref_param", + "package main;\n" + "fn n(p: *[]i64) i32 = { return len(*p): i32; };\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [10, 20, 30];\n" + " return n(&xs);\n" + "};\n", 3, NULL }, + /* FB1, local-ptr variant (exit 64 at d642017). */ + { "deref_local", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [10, 20, 30];\n" + " let p: *[]i64 = &xs;\n" + " return len(*p): i32;\n" + "};\n", 3, NULL }, + /* EMPTY-slice deref: the off-by-header bug returns .ptr (nonzero), + * len() must say 0. A bare exit row can't pin it — the garbage + * ptr's low byte happens to mask to 0 (verified at e481cb8) — so + * branch on len()!=0 and return distinct codes. */ + { "deref_empty", + "package main;\n" + "fn n(p: *[]i64) i32 = {\n" + " if (len(*p) != 0) { return 9; };\n" + " return 42;\n" + "};\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [1];\n" + " let ys: []i64 = xs[0:0];\n" + " return n(&ys);\n" + "};\n", 42, NULL }, + /* chained deref len(**pp) — the resolver spine must recurse through + * BOTH hops (exit 208 garbage at e481cb8). */ + { "deref_chain", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [10, 20, 30];\n" + " let p: *[]i64 = &xs;\n" + " let pp: **[]i64 = &p;\n" + " return len(**pp): i32;\n" + "};\n", 3, NULL }, + /* F2 exact repro: len(xs[i].field) — N_DOT over an N_INDEX base + * matched no arm (the #235 arm requires an IDENT base) and fell to + * the ptr-garbage fallback (exit 147 at d642017). */ + { "idx_field", + "package main;\n" + "type S = struct { pad: i64, name: str };\n" + "export fn main() i32 = {\n" + " let xs: [2]S = [S{ pad = 1, name = \"a\" }, S{ pad = 2, name = \"bcde\" }];\n" + " return len(xs[1].name): i32;\n" + "};\n", 4, NULL }, + /* full deref spine: len((*p)[i].field) (exit 10 at d642017). */ + { "deref_idx_field", + "package main;\n" + "type S = struct { pad: i64, name: str };\n" + "fn n(p: *[]S, i: i64) i32 = { return len((*p)[i].name): i32; };\n" + "export fn main() i32 = {\n" + " let arr: [2]S = [S{ pad = 1, name = \"a\" }, S{ pad = 2, name = \"bcde\" }];\n" + " let xs: []S = arr;\n" + " return n(&xs, 1);\n" + "};\n", 4, NULL }, + /* computed index through the spine — cgplaceaddr cgexpr's the index + * operand, so i+1 must resolve identically to a constant. */ + { "deref_idx_field_computed", + "package main;\n" + "type S = struct { pad: i64, name: str };\n" + "fn n(p: *[]S, i: i64) i32 = { return len((*p)[i + 1].name): i32; };\n" + "export fn main() i32 = {\n" + " let arr: [2]S = [S{ pad = 1, name = \"a\" }, S{ pad = 2, name = \"bcde\" }];\n" + " let xs: []S = arr;\n" + " return n(&xs, 0);\n" + "};\n", 4, NULL }, + /* struct-field cousins: len(s.field) (exit 75 at d642017) and + * len(p.field) through a *struct (exit 87 at d642017) — the #235 + * arm's inner non-tuple fallback was the same ptr-garbage. */ + { "dot_field", + "package main;\n" + "type S = struct { pad: i64, name: str };\n" + "export fn main() i32 = {\n" + " let s: S = S{ pad = 7, name = \"abc\" };\n" + " return len(s.name): i32;\n" + "};\n", 3, NULL }, + { "ptr_field", + "package main;\n" + "type S = struct { pad: i64, name: str };\n" + "export fn main() i32 = {\n" + " let s: S = S{ pad = 7, name = \"abc\" };\n" + " let p: *S = &s;\n" + " return len(p.name): i32;\n" + "};\n", 3, NULL }, + /* neutrality controls — already correct pre-sweep, pin that the + * enumerated fast-paths ([N]T const fold, ident str) still hold. */ + { "neutral_array_const", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [5]u8 = [1, 2, 3, 4, 5];\n" + " return len(a): i32;\n" + "};\n", 5, NULL }, + { "neutral_ident_str", + "package main;\n" + "export fn main() i32 = {\n" + " let s: str = \"abcd\";\n" + " return len(s): i32;\n" + "};\n", 4, NULL }, + /* non-place operands: previously the same SILENT ptr-garbage + * (strlit exit 40, slice-expr exit 48, call-result exit 0 at + * master); now LOUD per rule 7 in both stages. */ + { "reject_strlit", + "package main;\n" + "export fn main() i32 = { return len(\"abc\"): i32; };\n", + 0, "#10/#41: len() operand shape not place-resolvable (rule-7)" }, + { "reject_sliceexpr", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [10, 20, 30, 40];\n" + " return len(xs[1:3]): i32;\n" + "};\n", + 0, "#10/#41: len() operand shape not place-resolvable (rule-7)" }, + { "reject_callres", + "package main;\n" + "fn mk() []i64 = {\n" + " let xs: []i64 = [10, 20, 30];\n" + " return xs;\n" + "};\n" + "export fn main() i32 = { return len(mk()): i32; };\n", + 0, "#10/#41: len() operand shape not place-resolvable (rule-7)" }, + { NULL, NULL, 0, NULL } }; +static int +file_contains(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + buf[got] = '\0'; + fclose(f); + return strstr(buf, needle) != NULL; +} + static int slurp_eq(const char *a, const char *b) { @@ -150,6 +316,43 @@ main(void) fputs(rows[i].src, f); fclose(f); + /* reject rows: BOTH stages must refuse with the exact rule-7 + * text — a silent accept means the ptr-garbage fallback is + * back. */ + if (rows[i].reject) { + char errf[64], cmd2[2048]; + int bad = 0; + snprintf(errf, sizeof errf, "/tmp/wwli_%d_%d.err", + getpid(), i); + snprintf(cmd2, sizeof cmd2, "%s -o /dev/null %s 2>%s", + w6c, src, errf); + if (runwait(cmd2) == 0) { + fprintf(stderr, "row[%s]: w6c ACCEPTED a " + "reject row\n", rows[i].label); + bad = 1; + } else if (!file_contains(errf, rows[i].reject)) { + fprintf(stderr, "row[%s]: w6c rejected but " + "without the rule-7 text\n", + rows[i].label); + bad = 1; + } + snprintf(cmd2, sizeof cmd2, "%s -o /dev/null %s 2>%s", + w6c_ww, src, errf); + if (runwait(cmd2) == 0) { + fprintf(stderr, "row[%s]: w6c_ww ACCEPTED a " + "reject row\n", rows[i].label); + bad = 1; + } else if (!file_contains(errf, rows[i].reject)) { + fprintf(stderr, "row[%s]: w6c_ww rejected but " + "without the rule-7 text\n", + rows[i].label); + bad = 1; + } + if (bad) fail++; + unlink(errf); unlink(src); + continue; + } + /* (a) cstage build + run in a scratch dir. */ char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwli_%d_d_%d", getpid(), i);