From 6402d8deb7c02f0958da40df1b35fe130e629dac Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 14 May 2026 23:29:35 +0900 Subject: [PATCH] w6c+selfhost: cgen N_DOT slice-field through *T root in call args (closes #29) --- Makefile | 7 + cmd/w6c/cgen.c | 69 ++++++ selfhost/cmd/w6c/main.combined.ww | 166 ++++++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 87 +++++++- selfhost/cmd/wcc/cgenutil.ww | 79 ++++++- selfhost/cmd/wwdump/main.combined.ww | 166 ++++++++++++++- test/wcc/691_dot_slice_arg.c | 307 +++++++++++++++++++++++++++ 7 files changed, 857 insertions(+), 24 deletions(-) create mode 100644 test/wcc/691_dot_slice_arg.c diff --git a/Makefile b/Makefile index 15fed889..b26cffb8 100644 --- a/Makefile +++ b/Makefile @@ -220,6 +220,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \ $(BIN)/test_arr_elem_field_write \ $(BIN)/test_dot_str_chained_arg \ + $(BIN)/test_dot_slice_arg \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ @@ -318,6 +319,12 @@ $(BIN)/test_dot_str_chained_arg: test/wcc/692_dot_str_chained_arg.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dot_slice_arg: test/wcc/691_dot_slice_arg.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index c5d2b9d9..69f76a81 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4487,6 +4487,28 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_BX)); goto dot_done; } + if (fu && fu->kind == TY_SLICE) { + /* Slice leaf: load all three header + * words into (AX=ptr, BX=len, CX=cap) + * so the value follows the canonical + * slice-rhs convention. base_reg may + * be CX (global / `*T` root); load + * .cap LAST so the base survives the + * earlier reads. */ + ins2(c, A_MOVQ, + amem(base_reg, + base_disp + total_off + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(base_reg, + base_disp + total_off + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(base_reg, + base_disp + total_off + 16), + areg(D_CX)); + goto dot_done; + } int g_isf32 = 0; if (fld_isfloat(leaf_type, &g_isf32)) { int mov = g_isf32 ? A_MOVSS : A_MOVSD; @@ -4658,6 +4680,22 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_BX)); break; } + /* slice field: load (ptr, len, cap) into (AX, BX, CX) + * so the value flows through the slice-rhs convention. + * base_reg may be CX for globals; load .cap LAST so + * the base survives the earlier reads. */ + if (str_fu && str_fu->kind == TY_SLICE) { + ins2(c, A_MOVQ, + amem(base_reg, base_disp + (int)f->offset + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(base_reg, base_disp + (int)f->offset + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(base_reg, base_disp + (int)f->offset + 16), + areg(D_CX)); + break; + } /* f64/f32 field: route through X0 (MOVSD/MOVSS). * Loading via MOVQ AX would put the bits in the * integer reg, and any downstream consumer that @@ -4722,6 +4760,22 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); break; } + /* slice field through *struct: load (ptr, len, + * cap) into (AX, BX, CX). BX holds the *struct + * pointer, so load .len LAST — the earlier loads + * still index off the original base. */ + if (str_fu && str_fu->kind == TY_SLICE) { + ins2(c, A_MOVQ, + amem(D_BX, (int)f->offset + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(D_BX, (int)f->offset + 16), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_BX, (int)f->offset + 8), + areg(D_BX)); + break; + } /* f64/f32 field via *struct: load into X0. * BX already holds the struct pointer from * the MOVQ amem(D_BP,off) above. */ @@ -4772,6 +4826,21 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_AX)); goto dot_done; } + /* slice field: load (ptr, len, cap) into + * (AX, BX, CX). AX is the *struct base, so + * load .ptr (which targets AX) LAST. */ + if (fu && fu->kind == TY_SLICE) { + ins2(c, A_MOVQ, + amem(D_AX, (int)f->offset + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(D_AX, (int)f->offset + 16), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_AX, (int)f->offset + 0), + areg(D_AX)); + goto dot_done; + } /* f64/f32 chained field: read into X0. */ int g_isf32 = 0; if (fld_isfloat(ft, &g_isf32)) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 2d177d77..78a4b1f1 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -5983,6 +5983,75 @@ fn nodeisslice(c: *cgen, n: *node) bool = { }; if (k == nkind.N_SLICE) { return true; }; if (k == nkind.N_CAST) { return isslicetype(c, n.rhs); }; + // N_DOT to a slice field: resolve the field through the struct + // (or *struct) the base ident / inner chain lands on, then check + // the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg + // push/pop counts 3 words for `p.sl` and `p.inner.sl` shapes. + // `.ptr` / `.len` / `.cap` are pseudo-fields — they yield ptr + // (*u8) and i32, not a slice — so we exclude them up front. + if (k == nkind.N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = nkind.N_NONE; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == nkind.N_TNAME) { sname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + if (base.kind == nkind.N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == nkind.N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + return isslicetype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + // Chained dot through value-struct hops (`o.inner.sl`, + // `p.inner.sl`): dotinnerstructptr above only walks + // *struct fields, so a value-struct chain falls through. + // dotchainresolve handles arbitrary depth through value + // struct AND `*T` root, returning the leaf fieldinfo. + let rootnm: str = ""; + let rootoff: i32 = 0; + let totaloff: i32 = 0; + let lfi: *fieldinfo = nil; + let sdelta: i32 = -1; + let isglobal: bool = false; + let ptrroot: bool = false; + let ok: bool = dotchainresolve(c, n, + &rootnm, &rootoff, &totaloff, + &lfi, &sdelta, &isglobal, &ptrroot); + if (ok && sdelta < 0 && lfi != nil) { + return isslicetype(c, lfi.tnode); + }; + }; + return false; + }; return false; }; @@ -6064,12 +6133,10 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; // Chained dot through value-struct hops (`p.inner.s`): - // dotinnerstructptr above only walks *struct fields, so - // a value-struct chain falls through and the call-arg - // path then pushes only 1 word for the str instead of - // 2 (ptr+len), silently dropping the len half. - // dotchainresolve handles arbitrary depth through value - // struct AND `*T` root, returning the leaf fieldinfo. + // dotinnerstructptr above only walks *struct fields; + // dotchainresolve handles arbitrary depth through + // value struct AND `*T` root. Mirror of the nodeisslice + // fallback so chained str-field args also push 2 words. let rootnm: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; @@ -9335,6 +9402,21 @@ fn cgdot(c: *cgen, n: *node) void = { emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tCX, BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field via *struct: load + // (ptr, len, cap) into (AX, BX, CX). + // BX holds the *struct pointer, so + // load .len LAST so the earlier + // reads still index off the base. + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "BX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "BX"); + emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 via *struct: route through X0. // MOVQ into AX leaves the SSE reg stale @@ -9355,7 +9437,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -9381,6 +9463,19 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). Base is BP so + // no aliasing — order doesn't matter. + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 16): i64); + emitline("(BP), CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; @@ -9397,7 +9492,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -9806,6 +9901,45 @@ fn cgdot(c: *cgen, n: *node) void = { }; return; }; + if (isslicetype(c, leaffi.tnode)) { + // Slice leaf: load all three header words into + // (AX=ptr, BX=len, CX=cap). For the viacx path + // (global or `*T` root) CX is the base; load + // .cap LAST so the base survives the earlier + // reads. For BP-rooted locals the registers + // don't alias so order is free. + if (viacx) { + if (ptrroot) { + emitline("\tMOVQ\t"); + emitoff(rootoff: i64); + emitline("(BP), CX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, rootname); + emitline("(SB), CX\n"); + }; + emitline("\tMOVQ\t"); + emitdispreg(totaloff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 16): i64, "CX"); + emitline(", CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 16): i64); + emitline("(BP), CX\n"); + }; + return; + }; if (isfloattype(c, leaffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, leaffi.tnode)) { mov = "MOVSS"; }; @@ -9900,6 +10034,22 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", AX\n"); return; }; + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). AX is the *struct + // base, so load .ptr (which targets + // AX) LAST. + if (isslicetype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; // f64/f32 chained field: route through X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 43000aad..0f7c6b0c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1202,6 +1202,21 @@ fn cgdot(c: *cgen, n: *node) void = { emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tCX, BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field via *struct: load + // (ptr, len, cap) into (AX, BX, CX). + // BX holds the *struct pointer, so + // load .len LAST so the earlier + // reads still index off the base. + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "BX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "BX"); + emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 via *struct: route through X0. // MOVQ into AX leaves the SSE reg stale @@ -1222,7 +1237,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -1248,6 +1263,19 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). Base is BP so + // no aliasing — order doesn't matter. + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 16): i64); + emitline("(BP), CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; @@ -1264,7 +1292,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -1673,6 +1701,45 @@ fn cgdot(c: *cgen, n: *node) void = { }; return; }; + if (isslicetype(c, leaffi.tnode)) { + // Slice leaf: load all three header words into + // (AX=ptr, BX=len, CX=cap). For the viacx path + // (global or `*T` root) CX is the base; load + // .cap LAST so the base survives the earlier + // reads. For BP-rooted locals the registers + // don't alias so order is free. + if (viacx) { + if (ptrroot) { + emitline("\tMOVQ\t"); + emitoff(rootoff: i64); + emitline("(BP), CX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, rootname); + emitline("(SB), CX\n"); + }; + emitline("\tMOVQ\t"); + emitdispreg(totaloff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 16): i64, "CX"); + emitline(", CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 16): i64); + emitline("(BP), CX\n"); + }; + return; + }; if (isfloattype(c, leaffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, leaffi.tnode)) { mov = "MOVSS"; }; @@ -1767,6 +1834,22 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", AX\n"); return; }; + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). AX is the *struct + // base, so load .ptr (which targets + // AX) LAST. + if (isslicetype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; // f64/f32 chained field: route through X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index e485be7d..3c30f5ac 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -413,6 +413,75 @@ fn nodeisslice(c: *cgen, n: *node) bool = { }; if (k == nkind.N_SLICE) { return true; }; if (k == nkind.N_CAST) { return isslicetype(c, n.rhs); }; + // N_DOT to a slice field: resolve the field through the struct + // (or *struct) the base ident / inner chain lands on, then check + // the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg + // push/pop counts 3 words for `p.sl` and `p.inner.sl` shapes. + // `.ptr` / `.len` / `.cap` are pseudo-fields — they yield ptr + // (*u8) and i32, not a slice — so we exclude them up front. + if (k == nkind.N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = nkind.N_NONE; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == nkind.N_TNAME) { sname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + if (base.kind == nkind.N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == nkind.N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + return isslicetype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + // Chained dot through value-struct hops (`o.inner.sl`, + // `p.inner.sl`): dotinnerstructptr above only walks + // *struct fields, so a value-struct chain falls through. + // dotchainresolve handles arbitrary depth through value + // struct AND `*T` root, returning the leaf fieldinfo. + let rootnm: str = ""; + let rootoff: i32 = 0; + let totaloff: i32 = 0; + let lfi: *fieldinfo = nil; + let sdelta: i32 = -1; + let isglobal: bool = false; + let ptrroot: bool = false; + let ok: bool = dotchainresolve(c, n, + &rootnm, &rootoff, &totaloff, + &lfi, &sdelta, &isglobal, &ptrroot); + if (ok && sdelta < 0 && lfi != nil) { + return isslicetype(c, lfi.tnode); + }; + }; + return false; + }; return false; }; @@ -494,12 +563,10 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; // Chained dot through value-struct hops (`p.inner.s`): - // dotinnerstructptr above only walks *struct fields, so - // a value-struct chain falls through and the call-arg - // path then pushes only 1 word for the str instead of - // 2 (ptr+len), silently dropping the len half. - // dotchainresolve handles arbitrary depth through value - // struct AND `*T` root, returning the leaf fieldinfo. + // dotinnerstructptr above only walks *struct fields; + // dotchainresolve handles arbitrary depth through + // value struct AND `*T` root. Mirror of the nodeisslice + // fallback so chained str-field args also push 2 words. let rootnm: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6fa76813..900edd32 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -5983,6 +5983,75 @@ fn nodeisslice(c: *cgen, n: *node) bool = { }; if (k == nkind.N_SLICE) { return true; }; if (k == nkind.N_CAST) { return isslicetype(c, n.rhs); }; + // N_DOT to a slice field: resolve the field through the struct + // (or *struct) the base ident / inner chain lands on, then check + // the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg + // push/pop counts 3 words for `p.sl` and `p.inner.sl` shapes. + // `.ptr` / `.len` / `.cap` are pseudo-fields — they yield ptr + // (*u8) and i32, not a slice — so we exclude them up front. + if (k == nkind.N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = nkind.N_NONE; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == nkind.N_TNAME) { sname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + if (base.kind == nkind.N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == nkind.N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + return isslicetype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + // Chained dot through value-struct hops (`o.inner.sl`, + // `p.inner.sl`): dotinnerstructptr above only walks + // *struct fields, so a value-struct chain falls through. + // dotchainresolve handles arbitrary depth through value + // struct AND `*T` root, returning the leaf fieldinfo. + let rootnm: str = ""; + let rootoff: i32 = 0; + let totaloff: i32 = 0; + let lfi: *fieldinfo = nil; + let sdelta: i32 = -1; + let isglobal: bool = false; + let ptrroot: bool = false; + let ok: bool = dotchainresolve(c, n, + &rootnm, &rootoff, &totaloff, + &lfi, &sdelta, &isglobal, &ptrroot); + if (ok && sdelta < 0 && lfi != nil) { + return isslicetype(c, lfi.tnode); + }; + }; + return false; + }; return false; }; @@ -6064,12 +6133,10 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; // Chained dot through value-struct hops (`p.inner.s`): - // dotinnerstructptr above only walks *struct fields, so - // a value-struct chain falls through and the call-arg - // path then pushes only 1 word for the str instead of - // 2 (ptr+len), silently dropping the len half. - // dotchainresolve handles arbitrary depth through value - // struct AND `*T` root, returning the leaf fieldinfo. + // dotinnerstructptr above only walks *struct fields; + // dotchainresolve handles arbitrary depth through + // value struct AND `*T` root. Mirror of the nodeisslice + // fallback so chained str-field args also push 2 words. let rootnm: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; @@ -9335,6 +9402,21 @@ fn cgdot(c: *cgen, n: *node) void = { emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tCX, BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field via *struct: load + // (ptr, len, cap) into (AX, BX, CX). + // BX holds the *struct pointer, so + // load .len LAST so the earlier + // reads still index off the base. + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "BX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "BX"); + emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 via *struct: route through X0. // MOVQ into AX leaves the SSE reg stale @@ -9355,7 +9437,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -9381,6 +9463,19 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\n"); + } else { if (isslicetype(c, fi.tnode)) { + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). Base is BP so + // no aliasing — order doesn't matter. + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + fi.foff + 16): i64); + emitline("(BP), CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; @@ -9397,7 +9492,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); - }; }; + }; }; }; return; }; fi = fi.finext; @@ -9806,6 +9901,45 @@ fn cgdot(c: *cgen, n: *node) void = { }; return; }; + if (isslicetype(c, leaffi.tnode)) { + // Slice leaf: load all three header words into + // (AX=ptr, BX=len, CX=cap). For the viacx path + // (global or `*T` root) CX is the base; load + // .cap LAST so the base survives the earlier + // reads. For BP-rooted locals the registers + // don't alias so order is free. + if (viacx) { + if (ptrroot) { + emitline("\tMOVQ\t"); + emitoff(rootoff: i64); + emitline("(BP), CX\n"); + } else { + emitline("\tLEAQ\t"); + emitsymname(c, rootname); + emitline("(SB), CX\n"); + }; + emitline("\tMOVQ\t"); + emitdispreg(totaloff: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totaloff + 16): i64, "CX"); + emitline(", CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 8): i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((rootoff + totaloff + 16): i64); + emitline("(BP), CX\n"); + }; + return; + }; if (isfloattype(c, leaffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, leaffi.tnode)) { mov = "MOVSS"; }; @@ -9900,6 +10034,22 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", AX\n"); return; }; + // slice field: load (ptr, len, cap) + // into (AX, BX, CX). AX is the *struct + // base, so load .ptr (which targets + // AX) LAST. + if (isslicetype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; // f64/f32 chained field: route through X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; diff --git a/test/wcc/691_dot_slice_arg.c b/test/wcc/691_dot_slice_arg.c new file mode 100644 index 00000000..f5ebb65a --- /dev/null +++ b/test/wcc/691_dot_slice_arg.c @@ -0,0 +1,307 @@ +/* + * 691_dot_slice_arg — pass `.slicefield` as a call argument + * (and return one too). + * + * Task #29: the cgen N_DOT-slice-field paths previously loaded only + * .ptr into AX, leaving BX/CX stale. The slice-arg push at the call + * site then pushed three words from AX/BX/CX, so .len/.cap silently + * came from whatever the prior expression left in those registers. + * Surfaced as `998_bufio_run`'s bstreamunread mid-read len mismatch. + * + * Each fixture passes (or returns) a slice header through a call and + * reads .len / .cap / .ptr on the callee side to prove all three + * words round-trip. Rows cover: + * - single dot through `*T` root (`p.sl`) + * - chained through `*T` root (`p.inner.sl`) — pins + * task #22's spine walker still works with the slice-leaf fix + * in place + * - local value-struct root, single dot (`o.sl`) + * - local value-struct root, chained (`o.inner.sl`) + * - direct slice arg (no dot) — baseline / negative + * control: confirms the existing fast-path still works + * - slice + int neighbor arg — pins call-arg eval + * order doesn't drop the integer slot beside the slice + * - slice mutation through `.ptr` — pins `.ptr` half + * still arrives correctly post-fix + * - N_RETURN of slice N_DOT (probe 3) — exercises the same + * cgen N_DOT slice-load path on the return-stmt site, not + * just the call-arg site + * + * Exercises both stages via `ww` (cstage) and `ww_ww` (wwstage). + */ +#include +#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; int want; }; + +static const struct row rows[] = { + /* Single dot through *T root. `slen(p.sl)` should see len=5, + * cap=8, ptr[0]=7 (we set arr[0]=7 in main). */ + { "ptr_root_slice_arg", + "type outer = struct { sl: []u8, x: i32 };\n" + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 7u8;\n" + " let o: outer;\n" + " o.sl.ptr = &arr[0]; o.sl.len = 5; o.sl.cap = 8;\n" + " let p: *outer = &o;\n" + " if (slen(p.sl) != 5) { return 1; };\n" + " if (scap(p.sl) != 8) { return 2; };\n" + " if (sfirst(p.sl) != 7) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Chained through *T root with a value-struct hop — the #22 + * spine walker shape, leaf is a slice. */ + { "ptr_root_chained_slice_arg", + "type inner = struct { sl: []u8, pad: i32 };\n" + "type outer = struct { i: inner, tag: i32 };\n" + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n" + "fn main() i32 = {\n" + " let arr: [16]u8; arr[0] = 11u8;\n" + " let o: outer;\n" + " o.i.sl.ptr = &arr[0]; o.i.sl.len = 9; o.i.sl.cap = 16;\n" + " let p: *outer = &o;\n" + " if (slen(p.i.sl) != 9) { return 1; };\n" + " if (scap(p.i.sl) != 16) { return 2; };\n" + " if (sfirst(p.i.sl) != 11) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Local value-struct rooted, single dot. The previous bug was + * present here too — the "real struct field" cgen branch loaded + * only AX. */ + { "local_root_slice_arg", + "type outer = struct { sl: []u8, x: i32 };\n" + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 3u8;\n" + " let o: outer;\n" + " o.sl.ptr = &arr[0]; o.sl.len = 4; o.sl.cap = 8;\n" + " if (slen(o.sl) != 4) { return 1; };\n" + " if (scap(o.sl) != 8) { return 2; };\n" + " if (sfirst(o.sl) != 3) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Local value-struct rooted, chained value-struct hop. Spine + * walker without ptr_root. */ + { "local_root_chained_slice_arg", + "type inner = struct { sl: []u8, pad: i32 };\n" + "type outer = struct { i: inner, tag: i32 };\n" + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn main() i32 = {\n" + " let arr: [16]u8;\n" + " let o: outer;\n" + " o.i.sl.ptr = &arr[0]; o.i.sl.len = 6; o.i.sl.cap = 16;\n" + " if (slen(o.i.sl) != 6) { return 1; };\n" + " if (scap(o.i.sl) != 16) { return 2; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Chained through a `*struct` mid-hop (`o.i.sl` where `i: *inner`). + * Distinct cgen site from rows 2/4: the spine walker (dotchain- + * resolve) only walks value-struct intermediate hops, so a *struct + * mid-hop falls through to the "chained N_DOT through *struct + * field" branch (cgen.c ~4832, cgenexpr.ww ~1834) — the fourth + * TY_SLICE site touched by this commit. AX is the *struct base + * after `cgexpr(o.i)`, so the load order must end with .ptr → AX. */ + { "ptr_field_chained_slice_arg", + "type inner = struct { sl: []u8, pad: i32 };\n" + "type outer = struct { i: *inner, tag: i32 };\n" + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 17u8;\n" + " let inn: inner;\n" + " inn.sl.ptr = &arr[0]; inn.sl.len = 3; inn.sl.cap = 8;\n" + " let o: outer; o.i = &inn; o.tag = 0;\n" + " if (slen(o.i.sl) != 3) { return 1; };\n" + " if (scap(o.i.sl) != 8) { return 2; };\n" + " if (sfirst(o.i.sl) != 17) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Negative control: direct slice arg, no dot. Pre-existing fast + * path; confirms the fix didn't disturb it. */ + { "direct_slice_arg_baseline", + "fn slen(s: []u8) i32 = { return s.len: i32; };\n" + "fn scap(s: []u8) i32 = { return s.cap: i32; };\n" + "fn sfirst(s: []u8) i32 = { return s[0]: i32; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 9u8;\n" + " let s: []u8;\n" + " s.ptr = &arr[0]; s.len = 5; s.cap = 8;\n" + " if (slen(s) != 5) { return 1; };\n" + " if (scap(s) != 8) { return 2; };\n" + " if (sfirst(s) != 9) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Mixed args: pass a slice-field next to another arg, so the + * fix has to leave neighbouring arg-slot registers undisturbed. + * AX/BX/CX are clobbered while building the slice; the integer + * `tag` arg goes in DI on SysV, so this pins the call-arg eval + * order doesn't drop the tag. */ + { "ptr_root_slice_with_neighbor", + "type outer = struct { sl: []u8, x: i32 };\n" + "fn slen_tag(s: []u8, t: i32) i32 = { return s.len: i32 + t; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8;\n" + " let o: outer;\n" + " o.sl.ptr = &arr[0]; o.sl.len = 5; o.sl.cap = 8;\n" + " let p: *outer = &o;\n" + " let v: i32 = slen_tag(p.sl, 37);\n" + " if (v != 42) { return 1; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Mutation through the passed slice: callee writes via s[0], + * caller reads arr[0] after. Proves the .ptr half made it + * across — even before the fix this *would have* worked (only + * .len/.cap dropped), but co-testing it guards against a future + * regression that swaps ptr for len. */ + { "ptr_root_slice_mutate_ptr", + "type outer = struct { sl: []u8, x: i32 };\n" + "fn poke(s: []u8) void = { s[0] = 0x42u8; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 0u8;\n" + " let o: outer;\n" + " o.sl.ptr = &arr[0]; o.sl.len = 1; o.sl.cap = 8;\n" + " let p: *outer = &o;\n" + " poke(p.sl);\n" + " if (arr[0]: i32 != 0x42) { return 1; };\n" + " return 42;\n" + "};\n", + 42 }, + /* Probe 3: N_RETURN of a slice N_DOT — a function returns + * `p.sl` directly. cgen's return-stmt site also has to leave + * (AX=ptr, BX=len, CX=cap), or the caller's `let s: []u8 = + * fetch(p)` will lose .len/.cap. Distinct site from the call- + * arg push but exercises the same cgen N_DOT slice-load path. */ + { "ptr_root_slice_return", + "type outer = struct { sl: []u8, x: i32 };\n" + "fn fetch(p: *outer) []u8 = { return p.sl; };\n" + "fn main() i32 = {\n" + " let arr: [8]u8; arr[0] = 19u8;\n" + " let o: outer;\n" + " o.sl.ptr = &arr[0]; o.sl.len = 6; o.sl.cap = 8;\n" + " let p: *outer = &o;\n" + " let s: []u8 = fetch(p);\n" + " if (s.len: i32 != 6) { return 1; };\n" + " if (s.cap: i32 != 8) { return 2; };\n" + " if (s[0]: i32 != 19) { return 3; };\n" + " return 42;\n" + "};\n", + 42 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wdsa_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wdsa_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "dot_slice_arg: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "dot_slice_arg[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "dot_slice_arg: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("dot_slice_arg: %d/%d ok\n", total, total); + return 0; +}