From 37febab9d5ab5716499ca1bbd4c43367413997dd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 06:41:12 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c+w6c=5Fww:=20delete()=20builtin=20?= =?UTF-8?q?=E2=80=94=20single-element=20slice=20removal=20(part=20of=20#35?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hare's delete(xs[i]) (ref/harec/src/check.c:1981-2027): checker accepts an N_INDEX over a slice-typed base, stamps void; loud-rejects the range form delete(xs[i..j]) (stays filed on #35 — regex fold-2b's consumers are all single-element), non-index operands, array bases, wrong arity. Lowering (both stages, converged byte-identical by construction): ascending word-copy loop shifts [i+1..len) down one esz stride, then hdr.len -= 1; cap unchanged. The move is a same-type whole-stride byte copy — src and dst are elements of the SAME slice, so no boxing exists for any element kind; one loop serves scalar/narrow/str/struct/tagged. esz off the STAMPED base type (#34/#48 discipline). Base shapes: local slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ — the fold-2b delete_thread shape); others rule-7 loud-stop. test/804: 38 fixtures — first/middle/last/to-empty, esz 1/4/8/24/56 (MOVB/MOVL tails + 7-qword tagged), cap-unchanged, (*threads)[i], 4 checker reject rows; every accept row cs==ww asm byte-id. --- Makefile | 11 + cmd/w6c/cgen.c | 113 +++++++ cmd/wcc/check.c | 31 ++ selfhost/cmd/w6c/main.combined.ww | 183 +++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 139 +++++++++ selfhost/cmd/wcc/check.ww | 44 ++- selfhost/cmd/wwdump/main.combined.ww | 183 +++++++++++- test/wcc/804_delete_elem.c | 422 +++++++++++++++++++++++++++ 8 files changed, 1120 insertions(+), 6 deletions(-) create mode 100644 test/wcc/804_delete_elem.c diff --git a/Makefile b/Makefile index 9152aef5..1c99f516 100644 --- a/Makefile +++ b/Makefile @@ -388,6 +388,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_sret_callee \ $(BIN)/test_tuple_sret_receive_run \ $(BIN)/test_append_wide_elem \ + $(BIN)/test_delete_elem \ $(BIN)/test_struct_tuple_field_slot \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ @@ -1000,6 +1001,16 @@ $(BIN)/test_append_wide_elem: test/wcc/800_append_wide_elem.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #35 (delete-half): delete(xs[i]) single-element slice removal — runtime +# rows across every element kind (scalar/narrow/str/struct/56B tagged) + +# the deref-of-local regex fold-2b shape + checker reject rows + cs==ww +# asm byte-id (the converged-by-construction shift loop). +$(BIN)/test_delete_elem: test/wcc/804_delete_elem.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #237: a tuple-typed struct field must contribute its real slot width to # the enclosing struct's slotsize — pure cs==ww .s byte-id (frame size). $(BIN)/test_struct_tuple_field_slot: test/wcc/930_struct_tuple_field_slot.c \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index eaff2a6a..490f52d5 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6252,6 +6252,119 @@ cgexpr(Cg *c, Node *n, Local *locals) cgexpr(c, n->list, locals); break; } + if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && + strcmp(n->lhs->str, "delete") == 0 && n->list && + n->list->next == NULL) { + /* delete(xs[i]) — single-element slice removal, the + * delete-half of #35. Shift [i+1..len) down one + * stride, len -= 1, cap unchanged. The move is a + * same-type whole-stride byte copy: src and dst are + * elements of the SAME slice, so no boxing/coercion + * exists for any element kind (str/slice 24B header, + * tagged box, struct body) — one word-copy loop + * serves all kinds, unlike append's value-store + * dispatch (#34) which boxes from a foreign source. + * Ascending j keeps src (j+1) ahead of dst (j), the + * safe memmove-down direction. Bounds are implicit + * (no index check, matching the rest of cgen). + * + * ; AX = i (cgexpr) + * ; PUSHQ AX ; 8(SP) = j counter + * ; LEAQ/MOVQ off(BP), AX ; &hdr (ident / *p base) + * ; PUSHQ AX ; (SP) = &hdr + * ; del_l: + * ; MOVQ (SP), DX ; MOVQ 8(SP), CX + * ; MOVQ 8(DX), BX ; SUBQ $1, BX + * ; CMPQ BX, CX ; JGE del_e ; j >= len-1 + * ; [IMULQ esz, CX] + * ; MOVQ (DX), BX ; ADDQ CX, BX ; BX = dst + * ; word-copy esz bytes esz(BX) -> (BX) + * ; ADDQ $1, 8(SP) ; JMP del_l + * ; del_e: + * ; MOVQ (SP), DX ; SUBQ $1, 8(DX) + * ; ADDQ $16, SP */ + Node *d = n->list; /* N_INDEX, checker-validated */ + Node *base = d->lhs; + Type *su = type_chase_named(base->type); + Type *esub = (su && su->sub) + ? type_chase_named(su->sub) : NULL; + int esz = esub ? (int)esub->size : 0; + if (esz <= 0) + fatal("#35: delete() element size unresolved " + "(rule-7)"); + int hdr_lea = 0; + int hdr_off = 0; + int hdr_ok = 0; + if (base->kind == N_IDENT && + localfind(locals, base->str) != 0) { + hdr_lea = 1; + hdr_off = localfind(locals, base->str); + hdr_ok = 1; + } + /* (*p)[i]: the header lives behind a local + * ptr-to-slice — the regex fold-2b delete_thread + * shape (threads: *[]thread). */ + if (!hdr_ok && base->kind == N_UN && + base->op == TK_STAR && base->lhs && + base->lhs->kind == N_IDENT && + localfind(locals, base->lhs->str) != 0) { + hdr_off = localfind(locals, base->lhs->str); + hdr_ok = 1; + } + if (!hdr_ok) + fatal("#35: delete() base shape unsupported " + "(rule-7: local slice ident or " + "deref-of-local only)"); + cgexpr(c, d->rhs, locals); /* AX = i */ + ins1(c, A_PUSHQ, areg(D_AX)); + if (hdr_lea) + ins2(c, A_LEAQ, amem(D_BP, hdr_off), areg(D_AX)); + else + ins2(c, A_MOVQ, amem(D_BP, hdr_off), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + char *dll = mklabel(c, "del_l"); + char *dle = mklabel(c, "del_e"); + label(c, dll); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX)); + ins2(c, A_SUBQ, aimm(1), areg(D_BX)); + ins2(c, A_CMPQ, areg(D_BX), areg(D_CX)); + ins1(c, A_JGE, abranch(dle)); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_AX)); + ins2(c, A_IMULQ, areg(D_AX), areg(D_CX)); + } + ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX)); + ins2(c, A_ADDQ, areg(D_CX), areg(D_BX)); + int dk = 0; + for (; dk + 8 <= esz; dk += 8) { + ins2(c, A_MOVQ, amem(D_BX, esz + dk), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, dk)); + } + if (dk + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BX, esz + dk), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BX, dk)); + dk += 4; + } + if (dk + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BX, esz + dk), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BX, dk)); + dk += 2; + } + if (dk + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BX, esz + dk), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BX, dk)); + dk += 1; + } + ins2(c, A_ADDQ, aimm(1), amem(D_SP, 8)); + ins1(c, A_JMP, abranch(dll)); + label(c, dle); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX)); + ins2(c, A_SUBQ, aimm(1), amem(D_DX, 8)); + ins2(c, A_ADDQ, aimm(16), areg(D_SP)); + break; + } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->type == ty_err && n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 && diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 1afd6b73..0804e5ab 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1435,6 +1435,37 @@ cexpr(Checker *c, Node *n) n->lhs->type = ty_err; return n->type; } + /* delete(xs[i]) — single-element slice removal, the + * delete-half of #35 (insert() stays deferred). harec + * ref/harec/src/check.c:1981-2027 also accepts the range + * form delete(xs[i..j]) (EXPR_SLICE) — out of scope here + * (regex fold-2b's consumers are all single-element); + * loud-rejected below, the range form stays filed on #35. */ + if (n->lhs && n->lhs->kind == N_IDENT && + n->lhs->str && strcmp(n->lhs->str, "delete") == 0) { + Node *d = n->list; + if (d == NULL || d->next != NULL) + err(c, n->pos, "delete: takes exactly one argument"); + if (d != NULL) { + (void)cexpr(c, d); + if (d->kind == N_SLICE) + err(c, n->pos, "delete: range form delete(xs[i..j]) unimplemented (task #35)"); + else if (d->kind != N_INDEX) + err(c, n->pos, "delete: operand must be an indexing expression xs[i]"); + else { + Type *bt = d->lhs ? d->lhs->type : NULL; + while (bt && bt->kind == TY_NAMED) + bt = bt->under; + /* harec check.c:2024 wording; a + * fixed-size [N]T base lands here. */ + if (bt == NULL || bt->kind != TY_SLICE) + err(c, n->pos, "delete must operate on a slice"); + } + } + n->type = ty_void; + n->lhs->type = ty_err; + return n->type; + } /* assert(cond[, msg]) / abort([msg]) — runtime checks that * call into rt_abort. msg must be a str when present. * Only treated as builtins when no user symbol shadows the diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a45de1f6..e2026f80 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10424,12 +10424,14 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "nomem", skind.SK_TYPE, nil, nomemdecl); // `nil`, `true`, `false` are keywords — handled at the lex/parser // level, no symbol needed. - // `len`, `alloc`, `free`, `append` are pseudo-builtins; scopedefine - // them so their use sites resolve. The actual semantics live in cgen. + // `len`, `alloc`, `free`, `append`, `delete` are pseudo-builtins; + // scopedefine them so their use sites resolve. The actual semantics + // live in cgen. scopedefine(c.top, "len", skind.SK_FN, nil, nil); scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + scopedefine(c.top, "delete", skind.SK_FN, nil, nil); // #42: typed builtins folded to integer literals at check time — // `size(T)` / `align(T)` (arg is a type-expression planted by the // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is @@ -12889,6 +12891,44 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // delete(xs[i]) — single-element slice removal, the + // delete-half of #35 (insert() deferred). Mirrors + // cstage cmd/wcc/check.c's delete arm. harec + // ref/harec/src/check.c:1981-2027 also accepts the + // range form delete(xs[i..j]) (EXPR_SLICE) — + // loud-rejected here, filed on #35 (regex fold-2b's + // consumers are all single-element). + if (streq(callee.str, "delete")) { + let d: *node = e.list; + if (d == nil || d.next != nil) { + cerr("delete: takes exactly one argument\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("delete: range form delete(xs[i..j]) unimplemented (task #35)\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("delete: operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + let basetn: *node = exprtype(c, d.lhs, nil); + let u: *node = resolvealias(c, unwrapbang(basetn)); + // harec check.c:2024 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; }; let nm: str; nm.ptr = nil; nm.len = 0; @@ -24478,6 +24518,135 @@ fn cgappend(c: *cgen, n: *node) void = { return; }; +// cgdelete — Hare `delete(xs[i])`: single-element slice removal, the +// delete-half of #35. Shift [i+1..len) down one stride, len -= 1, cap +// unchanged. The move is a same-type whole-stride byte copy: src and +// dst are elements of the SAME slice, so no boxing/coercion exists for +// any element kind (str/slice 24B header, tagged box, struct body) — +// one word-copy loop serves all kinds, unlike append's value-store +// dispatch (#34) which boxes from a foreign source. Ascending j keeps +// src (j+1) ahead of dst (j), the safe memmove-down direction. Mirrors +// cmd/w6c/cgen.c's N_CALL delete arm instruction-for-instruction +// (rule-10 byte-id). +fn cgdelete(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + // esz off the STAMPED base tinfo (#34/#48 discipline — never the + // value node). Peel TY_NAMED on indexable AND element, mirroring + // cstage's type_chase_named on both (#8 family). + let sti: *tinfo = base.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + let esub: *tinfo = nil; + if (sti != nil) { esub = sti.sub; }; + for (esub != nil && esub.kind == tykind.TY_NAMED) { esub = esub.under; }; + let esz: i32 = 0; + if (esub != nil) { esz = esub.size: i32; }; + if (esz <= 0) { + let m35a: str = "#35: delete() element size unresolved (rule-7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; + let hdr_lea: bool = false; + let hdr_off: i32 = 0; + let hdr_ok: bool = false; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + hdr_lea = true; + hdr_off = lc.off; + hdr_ok = true; + }; + }; + // (*p)[i]: the header lives behind a local ptr-to-slice — the + // regex fold-2b delete_thread shape (threads: *[]thread). + if (!hdr_ok && base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR && base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let pc: *local = localfindnode(c, base.lhs.str); + if (pc != nil) { + hdr_off = pc.off; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + cgexpr(c, d.rhs); // AX = i + emitline("\tPUSHQ\tAX\n"); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + let dll: str = mklabel(c, "del_l"); + let dle: str = mklabel(c, "del_e"); + emitlabel(dll); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\t$1, BX\n"); + emitline("\tCMPQ\tBX, CX\n"); + emitline("\tJGE\t"); emitline(dle); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + let dk: i32 = 0; + for (dk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 8; + }; + if (dk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 4; + }; + if (dk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 2; + }; + if (dk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(dll); emitline("\n"); + emitlabel(dle); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tSUBQ\t$1, 8(DX)\n"); + emitline("\tADDQ\t$16, SP\n"); +}; + fn cgcall(c: *cgen, n: *node) void = { // Hare-style `append(s, v)` / `append(s, items...)` builtin — // special-cased before pushargsrev so the spread variant can run @@ -24650,6 +24819,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // delete(xs[i]) — #35 delete-half; mirror of cstage + // cgen.c's N_CALL delete arm. + if (streq(callee.str, "delete")) { + if (n.list != nil) { + if (n.list.next == nil) { + cgdelete(c, n); + return; + }; + }; + }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a729f9c2..1d18ff6c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -4608,6 +4608,135 @@ fn cgappend(c: *cgen, n: *node) void = { return; }; +// cgdelete — Hare `delete(xs[i])`: single-element slice removal, the +// delete-half of #35. Shift [i+1..len) down one stride, len -= 1, cap +// unchanged. The move is a same-type whole-stride byte copy: src and +// dst are elements of the SAME slice, so no boxing/coercion exists for +// any element kind (str/slice 24B header, tagged box, struct body) — +// one word-copy loop serves all kinds, unlike append's value-store +// dispatch (#34) which boxes from a foreign source. Ascending j keeps +// src (j+1) ahead of dst (j), the safe memmove-down direction. Mirrors +// cmd/w6c/cgen.c's N_CALL delete arm instruction-for-instruction +// (rule-10 byte-id). +fn cgdelete(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + // esz off the STAMPED base tinfo (#34/#48 discipline — never the + // value node). Peel TY_NAMED on indexable AND element, mirroring + // cstage's type_chase_named on both (#8 family). + let sti: *tinfo = base.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + let esub: *tinfo = nil; + if (sti != nil) { esub = sti.sub; }; + for (esub != nil && esub.kind == tykind.TY_NAMED) { esub = esub.under; }; + let esz: i32 = 0; + if (esub != nil) { esz = esub.size: i32; }; + if (esz <= 0) { + let m35a: str = "#35: delete() element size unresolved (rule-7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; + let hdr_lea: bool = false; + let hdr_off: i32 = 0; + let hdr_ok: bool = false; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + hdr_lea = true; + hdr_off = lc.off; + hdr_ok = true; + }; + }; + // (*p)[i]: the header lives behind a local ptr-to-slice — the + // regex fold-2b delete_thread shape (threads: *[]thread). + if (!hdr_ok && base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR && base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let pc: *local = localfindnode(c, base.lhs.str); + if (pc != nil) { + hdr_off = pc.off; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + cgexpr(c, d.rhs); // AX = i + emitline("\tPUSHQ\tAX\n"); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + let dll: str = mklabel(c, "del_l"); + let dle: str = mklabel(c, "del_e"); + emitlabel(dll); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\t$1, BX\n"); + emitline("\tCMPQ\tBX, CX\n"); + emitline("\tJGE\t"); emitline(dle); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + let dk: i32 = 0; + for (dk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 8; + }; + if (dk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 4; + }; + if (dk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 2; + }; + if (dk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(dll); emitline("\n"); + emitlabel(dle); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tSUBQ\t$1, 8(DX)\n"); + emitline("\tADDQ\t$16, SP\n"); +}; + fn cgcall(c: *cgen, n: *node) void = { // Hare-style `append(s, v)` / `append(s, items...)` builtin — // special-cased before pushargsrev so the spread variant can run @@ -4780,6 +4909,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // delete(xs[i]) — #35 delete-half; mirror of cstage + // cgen.c's N_CALL delete arm. + if (streq(callee.str, "delete")) { + if (n.list != nil) { + if (n.list.next == nil) { + cgdelete(c, n); + return; + }; + }; + }; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 870ac4e1..64c8131e 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -97,12 +97,14 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "nomem", skind.SK_TYPE, nil, nomemdecl); // `nil`, `true`, `false` are keywords — handled at the lex/parser // level, no symbol needed. - // `len`, `alloc`, `free`, `append` are pseudo-builtins; scopedefine - // them so their use sites resolve. The actual semantics live in cgen. + // `len`, `alloc`, `free`, `append`, `delete` are pseudo-builtins; + // scopedefine them so their use sites resolve. The actual semantics + // live in cgen. scopedefine(c.top, "len", skind.SK_FN, nil, nil); scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + scopedefine(c.top, "delete", skind.SK_FN, nil, nil); // #42: typed builtins folded to integer literals at check time — // `size(T)` / `align(T)` (arg is a type-expression planted by the // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is @@ -2562,6 +2564,44 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // delete(xs[i]) — single-element slice removal, the + // delete-half of #35 (insert() deferred). Mirrors + // cstage cmd/wcc/check.c's delete arm. harec + // ref/harec/src/check.c:1981-2027 also accepts the + // range form delete(xs[i..j]) (EXPR_SLICE) — + // loud-rejected here, filed on #35 (regex fold-2b's + // consumers are all single-element). + if (streq(callee.str, "delete")) { + let d: *node = e.list; + if (d == nil || d.next != nil) { + cerr("delete: takes exactly one argument\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("delete: range form delete(xs[i..j]) unimplemented (task #35)\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("delete: operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + let basetn: *node = exprtype(c, d.lhs, nil); + let u: *node = resolvealias(c, unwrapbang(basetn)); + // harec check.c:2024 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; }; let nm: str; nm.ptr = nil; nm.len = 0; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 97a05b82..90225cf7 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10424,12 +10424,14 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "nomem", skind.SK_TYPE, nil, nomemdecl); // `nil`, `true`, `false` are keywords — handled at the lex/parser // level, no symbol needed. - // `len`, `alloc`, `free`, `append` are pseudo-builtins; scopedefine - // them so their use sites resolve. The actual semantics live in cgen. + // `len`, `alloc`, `free`, `append`, `delete` are pseudo-builtins; + // scopedefine them so their use sites resolve. The actual semantics + // live in cgen. scopedefine(c.top, "len", skind.SK_FN, nil, nil); scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + scopedefine(c.top, "delete", skind.SK_FN, nil, nil); // #42: typed builtins folded to integer literals at check time — // `size(T)` / `align(T)` (arg is a type-expression planted by the // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is @@ -12889,6 +12891,44 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // delete(xs[i]) — single-element slice removal, the + // delete-half of #35 (insert() deferred). Mirrors + // cstage cmd/wcc/check.c's delete arm. harec + // ref/harec/src/check.c:1981-2027 also accepts the + // range form delete(xs[i..j]) (EXPR_SLICE) — + // loud-rejected here, filed on #35 (regex fold-2b's + // consumers are all single-element). + if (streq(callee.str, "delete")) { + let d: *node = e.list; + if (d == nil || d.next != nil) { + cerr("delete: takes exactly one argument\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("delete: range form delete(xs[i..j]) unimplemented (task #35)\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("delete: operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + let basetn: *node = exprtype(c, d.lhs, nil); + let u: *node = resolvealias(c, unwrapbang(basetn)); + // harec check.c:2024 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; }; let nm: str; nm.ptr = nil; nm.len = 0; @@ -24478,6 +24518,135 @@ fn cgappend(c: *cgen, n: *node) void = { return; }; +// cgdelete — Hare `delete(xs[i])`: single-element slice removal, the +// delete-half of #35. Shift [i+1..len) down one stride, len -= 1, cap +// unchanged. The move is a same-type whole-stride byte copy: src and +// dst are elements of the SAME slice, so no boxing/coercion exists for +// any element kind (str/slice 24B header, tagged box, struct body) — +// one word-copy loop serves all kinds, unlike append's value-store +// dispatch (#34) which boxes from a foreign source. Ascending j keeps +// src (j+1) ahead of dst (j), the safe memmove-down direction. Mirrors +// cmd/w6c/cgen.c's N_CALL delete arm instruction-for-instruction +// (rule-10 byte-id). +fn cgdelete(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + // esz off the STAMPED base tinfo (#34/#48 discipline — never the + // value node). Peel TY_NAMED on indexable AND element, mirroring + // cstage's type_chase_named on both (#8 family). + let sti: *tinfo = base.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + let esub: *tinfo = nil; + if (sti != nil) { esub = sti.sub; }; + for (esub != nil && esub.kind == tykind.TY_NAMED) { esub = esub.under; }; + let esz: i32 = 0; + if (esub != nil) { esz = esub.size: i32; }; + if (esz <= 0) { + let m35a: str = "#35: delete() element size unresolved (rule-7)\n"; + os.write(2, m35a.ptr, m35a.len: u64); + os.exit(1); + }; + let hdr_lea: bool = false; + let hdr_off: i32 = 0; + let hdr_ok: bool = false; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + hdr_lea = true; + hdr_off = lc.off; + hdr_ok = true; + }; + }; + // (*p)[i]: the header lives behind a local ptr-to-slice — the + // regex fold-2b delete_thread shape (threads: *[]thread). + if (!hdr_ok && base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR && base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let pc: *local = localfindnode(c, base.lhs.str); + if (pc != nil) { + hdr_off = pc.off; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + cgexpr(c, d.rhs); // AX = i + emitline("\tPUSHQ\tAX\n"); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + let dll: str = mklabel(c, "del_l"); + let dle: str = mklabel(c, "del_e"); + emitlabel(dll); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\t$1, BX\n"); + emitline("\tCMPQ\tBX, CX\n"); + emitline("\tJGE\t"); emitline(dle); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + let dk: i32 = 0; + for (dk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 8; + }; + if (dk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 4; + }; + if (dk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 2; + }; + if (dk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg((esz + dk): i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(dk: i64, "BX"); + emitline("\n"); + dk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(dll); emitline("\n"); + emitlabel(dle); + emitline("\tMOVQ\t(SP), DX\n"); + emitline("\tSUBQ\t$1, 8(DX)\n"); + emitline("\tADDQ\t$16, SP\n"); +}; + fn cgcall(c: *cgen, n: *node) void = { // Hare-style `append(s, v)` / `append(s, items...)` builtin — // special-cased before pushargsrev so the spread variant can run @@ -24650,6 +24819,16 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // delete(xs[i]) — #35 delete-half; mirror of cstage + // cgen.c's N_CALL delete arm. + if (streq(callee.str, "delete")) { + if (n.list != nil) { + if (n.list.next == nil) { + cgdelete(c, n); + return; + }; + }; + }; }; }; diff --git a/test/wcc/804_delete_elem.c b/test/wcc/804_delete_elem.c new file mode 100644 index 00000000..27e1e306 --- /dev/null +++ b/test/wcc/804_delete_elem.c @@ -0,0 +1,422 @@ +/* + * 804_delete_elem — cstage and wwstage agree, byte-for-byte and at + * runtime, that `delete(xs[i])` removes element i from a slice: the + * tail [i+1..len) shifts down one stride, len -= 1, cap unchanged + * (the delete-half of task #35; insert() stays deferred; regex + * fold-2b's delete_thread/failed-thread/PC-dedup consumers). + * + * Lowering (BOTH stages, converged byte-identical by construction — + * there was no pre-existing reference side): push i and &hdr, then an + * ascending word-copy loop moves element j+1 onto element j until + * j >= len-1, then hdr.len -= 1. The element move is a same-type + * whole-stride byte copy — src and dst are elements of the SAME + * slice, so no boxing/coercion exists for any element kind; the rows + * below prove that across scalar (8B), narrow (4B/1B — the MOVL/MOVB + * copy tails), str (24B header), struct (16B) and tagged (56B) esz. + * esz comes off the STAMPED base type (the #34/#48 discipline), so a + * named-alias element cannot mis-stride. + * + * Hare's delete also accepts the range form delete(xs[i..j]) + * (ref/harec/src/check.c:1981-2027 EXPR_SLICE). Out of scope here — + * checker loud-rejects it (reject_range row); filed on #35. + * + * row | shape | want + * ----------------+----------------------------------------+------ + * i64_first | [7,11,13], delete(xs[0]) | 44 + * i64_middle | [7,11,13], delete(xs[1]) | 40 + * i64_last | [7,11,13], delete(xs[2]) (no copy, | 38 + * | loop body never runs) | + * i32_narrow | []i32 esz=4 — the MOVL copy tail | 229 + * u8_narrow | []u8 esz=1 — the MOVB copy tail | 28 + * cap_unchanged | manual {ptr,len=3,cap=8} header over a | 91 + * | stack backing; delete middle; cap must | + * | still read 8, tail value intact | + * delete_to_empty | 1-element slice, delete(xs[0]), len=0 | 7 + * str_elem | []str esz=24 — 3-qword header moves | 234 + * | whole | + * tagged_56b | [](s6|bool) esz=56 — 7-qword raw move, | 231 + * | tag+payload survive; match head + | + * | raw-byte tail readback (#43 caveat) | + * regex_shape | delete((*threads)[i]) behind *[]thread | + * | — the fold-2b delete_thread shape | 24 + * | (deref-of-local base, struct element) | + * reject_array | delete(t[0]) on [3]i64 — "delete must | BUILD_FAIL + * | operate on a slice" | + * reject_nonindex | delete(xs) — operand must be xs[i] | BUILD_FAIL + * reject_range | delete(xs[0:2]) — range form deferred | BUILD_FAIL + * reject_arity | delete(xs[0], xs[1]) | BUILD_FAIL + * + * Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id, + * which subsumes the frame canary (TEXT main,$N) and the del_l/del_e + * label-counter symmetry. + */ +#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; +} + +/* want == BUILD_FAIL: the row must FAIL to build on both stages (the + * checker reject set is part of the contract — rule 7, never a silent + * acceptance). */ +#define BUILD_FAIL (-2147483647 - 1) + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + { "i64_first", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tappend(xs, 13);\n" + "\tdelete(xs[0]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 44 }, + + { "i64_middle", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tappend(xs, 13);\n" + "\tdelete(xs[1]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 40 }, + + { "i64_last", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tappend(xs, 13);\n" + "\tdelete(xs[2]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 38 }, + + { "i32_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i32 = [];\n" + "\tappend(xs, 4i32);\n" + "\tappend(xs, 9i32);\n" + "\tappend(xs, 2i32);\n" + "\tdelete(xs[0]);\n" + "\treturn xs[0] + xs[1]*10 + len(xs)*100;\n" + "};\n", + 229 }, + + { "u8_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tappend(xs, 9u8);\n" + "\tappend(xs, 3u8);\n" + "\tdelete(xs[1]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 28 }, + + { "cap_unchanged", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet hb: [8]u8; hb[0] = 4u8; hb[1] = 6u8; hb[2] = 9u8;\n" + "\tlet xs: []u8; xs.ptr = &hb[0]; xs.len = 3; xs.cap = 8;\n" + "\tdelete(xs[1]);\n" + "\treturn (xs.cap*10 + xs.len): i32 + xs[1]: i32;\n" + "};\n", + 91 }, + + { "delete_to_empty", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tdelete(xs[0]);\n" + "\treturn len(xs) + 7;\n" + "};\n", + 7 }, + + { "str_elem", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []str = [];\n" + "\tlet a: str = \"abc\";\n" + "\tlet b: str = \"de\";\n" + "\tlet g: str = \"fghi\";\n" + "\tappend(xs, a);\n" + "\tappend(xs, b);\n" + "\tappend(xs, g);\n" + "\tdelete(xs[1]);\n" + "\treturn (xs[0].len*10 + xs[1].len): i32 + len(xs)*100;\n" + "};\n", + 234 }, + + /* 56B element: tag qword + 48B struct payload — seven whole-qword + * moves; tag AND payload must both survive the raw move (a tagged + * element at rest is just bytes; no boxing on a same-slice move). + * Readback is split: match proves tag + head (s.a), and the tail + * qword (f, element byte 48 -> absolute byte 104 of element 1) is + * read RAW via a *u8 over xs.ptr — the indexed-match payload + * cursor itself truncates past 32B (pre-existing, task #43), so a + * match on s.f would mis-fail regardless of the move. Verified + * truncation-free against a no-delete control. */ + { "tagged_56b", + "package main;\n" + "type s6 = struct { a: i64, b: i64, c: i64, d: i64, e: i64, f: i64 };\n" + "type cell = (s6 | bool);\n" + "export fn main() i32 = {\n" + "\tlet xs: []cell = [];\n" + "\tlet p0: s6 = s6 { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 };\n" + "\tlet p1: s6 = s6 { a = 7, b = 8, c = 9, d = 10, e = 11, f = 12 };\n" + "\tlet p2: s6 = s6 { a = 13, b = 14, c = 15, d = 16, e = 17, f = 18 };\n" + "\tappend(xs, p0);\n" + "\tappend(xs, p1);\n" + "\tappend(xs, p2);\n" + "\tdelete(xs[1]);\n" + "\tlet r: i32 = 0;\n" + "\tmatch (xs[1]) {\n" + "\tcase let s: s6 => r = s.a: i32;\n" + "\tcase bool => r = 99;\n" + "\t};\n" + "\tlet bp: *u8 = xs.ptr: *u8;\n" + "\tr += bp[104]: i32;\n" + "\treturn r + len(xs)*100;\n" + "};\n", + 231 }, + + /* The fold-2b consumer shape: regex.ha:547-551 delete_thread takes + * threads: *[]thread and deletes through the pointer. ww spells the + * autoderef explicitly: delete((*threads)[i]). */ + { "regex_shape", + "package main;\n" + "type thread = struct { pc: i64, matched: bool };\n" + "fn delete_thread(i: i64, threads: *[]thread) void = {\n" + "\tdelete((*threads)[i]);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet xs: []thread = [];\n" + "\tappend(xs, thread { pc = 1, matched = false });\n" + "\tappend(xs, thread { pc = 2, matched = false });\n" + "\tappend(xs, thread { pc = 3, matched = false });\n" + "\tdelete_thread(1, &xs);\n" + "\tlet r: i32 = (xs[0].pc + xs[1].pc): i32;\n" + "\treturn r + len(xs)*10;\n" + "};\n", + 24 }, + + { "reject_array", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: [3]i64 = [1, 2, 3];\n" + "\tdelete(t[0]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL }, + + { "reject_nonindex", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tdelete(xs);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL }, + + { "reject_range", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tappend(xs, 6u8);\n" + "\tdelete(xs[0:2]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL }, + + { "reject_arity", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tappend(xs, 6u8);\n" + "\tdelete(xs[0], xs[1]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL }, +}; + +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/dele_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dele_%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 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + if (r->want != BUILD_FAIL) + 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; +} + +/* asm_byte_identical — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff. Both delete lowerings are written fresh, so this is + * the converged-by-construction gate: any drift in the shift loop, the + * del_l/del_e label sequence, or the esz word-copy shows here. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dele_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/dele_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/dele_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +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, "delete_elem: 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++; + int bad = rows[i].want == BUILD_FAIL + ? (got != -1) : (got != rows[i].want); + if (bad) { + fprintf(stderr, + "delete_elem[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (rows[i].want == BUILD_FAIL) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "delete_elem: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("delete_elem: %d fixtures passed\n", total); + return 0; +}