From 1bcf2726cff32965082c81633676ca03bd70e0e6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 23:18:38 +0900 Subject: [PATCH] wcc+w6c+w6c_ww: delete() range form delete(xs[lo:hi]) (fold-5a P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hare's delete also takes a slicing place (harec check.c:1981-2027 EXPR_SLICE; Hare spells it delete(xs[i..j])): remove [lo, hi) — shift [hi..len) down count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, hi defaults len, so delete(xs[:]) clears the slice with storage retained. Checker accepts N_SLICE next to N_INDEX (object must chase to a slice, harec :2024); the old range-unimplemented reject and its #35 cite drop. Lowering (both stages, converged byte-identical by construction) is the single-element arm's same-slice whole-stride word-copy loop with a DYNAMIC src offset (count*esz via a src register) instead of the constant one-stride. Base shapes: local slice ident, deref-of-local, plus NEW indexed local-slice base xs[g][lo:hi] — the fold-5a consumer shape (regex.ha:333 delete(jump_idxs[group_level][..]); outer stride off the type table). Bounds stay implicit, inheriting the documented single-element posture (no index checks anywhere in cgen). Operands evaluate left-to-right, exactly once, before the shift (harec order); only the header ADDRESS is taken before operand eval, so a bound expression's writes through the slice land before the copy. test/809: 64 fixtures — full/explicit/re-clear/head/mid/tail/empty a:a/end-boundary len:len/explicit 0:0 on a never-appended (nil-ptr) slice, single-vs-range equivalence, cap preservation, esz 1/2/4/8/16/24 copy tails against the dynamic src, operand order-of-eval (lo/hi CALLs fire once each, in order) + aliasing-visibility pins, the EXACT [][]size regex consumer shape, deref base, 2 reject rows w/ diagnostic text; every accept row cs==ww asm byte-id. test/804: reject_range row retired (form now accepted), reject_nonindex text follows the widened message. --- Makefile | 15 + cmd/w6c/cgen.c | 193 ++++++++- cmd/wcc/check.c | 25 +- selfhost/cmd/w6c/main.combined.ww | 245 ++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 205 ++++++++- selfhost/cmd/wcc/check.ww | 40 +- selfhost/cmd/wwdump/main.combined.ww | 245 ++++++++++- test/wcc/804_delete_elem.c | 28 +- test/wcc/809_delete_range.c | 607 +++++++++++++++++++++++++++ 9 files changed, 1505 insertions(+), 98 deletions(-) create mode 100644 test/wcc/809_delete_range.c diff --git a/Makefile b/Makefile index e8a715b9..a490ceb5 100644 --- a/Makefile +++ b/Makefile @@ -393,6 +393,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_sret_receive_run \ $(BIN)/test_append_wide_elem \ $(BIN)/test_delete_elem \ + $(BIN)/test_delete_range \ $(BIN)/test_insert_elem \ $(BIN)/test_arrlit_overlong \ $(BIN)/test_placeaddr_store \ @@ -1024,6 +1025,20 @@ $(BIN)/test_delete_elem: test/wcc/804_delete_elem.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #35 (range-half, fold-5a prereq P2): delete(xs[lo:hi]) range slice +# removal — runtime rows across defaults (full/head/tail/empty/end +# boundary), the esz copy-tail spread against the dynamic src offset +# (1/2/4/8/16/24), the single-elem equivalence, cap preservation, +# operand order-of-eval + aliasing pins, the deref-of-local base, the +# indexed-base regex.ha:333 consumer shape EXACT + checker reject rows +# w/ diagnostic text + cs==ww asm byte-id (the converged-by-construction +# rdl_l/rdl_e shift loop). +$(BIN)/test_delete_range: test/wcc/809_delete_range.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #35 (insert-half): insert(xs[idx], v) single-element slice insertion — # delete()'s twin, lowered as append-desugar + rotate-right. Runtime rows # across every element kind (scalar/narrow/str/struct/56B tagged incl. diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 07f7fd52..461cbd75 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6853,7 +6853,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * ; del_e: * ; MOVQ (SP), DX ; SUBQ $1, 8(DX) * ; ADDQ $16, SP */ - Node *d = n->list; /* N_INDEX, checker-validated */ + Node *d = n->list; /* N_INDEX or N_SLICE, + * checker-validated */ Node *base = d->lhs; Type *su = type_chase_named(base->type); Type *esub = (su && su->sub) @@ -6862,6 +6863,196 @@ cgexpr(Cg *c, Node *n, Local *locals) if (esz <= 0) fatal("#35: delete() element size unresolved " "(rule-7)"); + if (d->kind == N_SLICE) { + /* delete(xs[lo:hi]) — range slice removal + * (fold-5a prereq P2; harec check.c:1994 + * EXPR_SLICE). Shift [hi..len) down count = + * hi-lo strides, len -= count, cap + * unchanged; lo defaults 0, hi defaults + * len. delete(xs[:]) never enters the copy + * loop (lo+count == len at entry) and + * zeroes len. The per-element move is the + * single-element arm's same-slice + * whole-stride word copy with a DYNAMIC + * src offset (count*esz, via a src + * register) instead of the constant + * one-stride. Ascending j keeps src >= + * dst, the safe memmove-down direction. + * Bounds are implicit (no range check, + * matching the single-element arm and the + * rest of cgen). + * + * ; &hdr -> AX (ident / *p / xs[g]) + * ; PUSHQ AX ; 16(SP)=&hdr + * ; lo -> AX ($0 default) ; PUSHQ AX + * ; ; 8(SP)=j + * ; count = hi - lo (hi: cgexpr or len) + * ; PUSHQ AX ; (SP)=count + * ; rdl_l: + * ; MOVQ 16(SP), DX ; MOVQ 8(SP), CX + * ; MOVQ (SP), AX ; ADDQ CX, AX + * ; MOVQ 8(DX), BX + * ; CMPQ BX, AX ; JGE rdl_e + * ; ; j+count>=len + * ; [IMULQ esz, CX] + * ; MOVQ (DX), BX ; ADDQ CX, BX ; dst + * ; MOVQ (SP), CX ; [IMULQ esz, CX] + * ; ADDQ BX, CX ; src + * ; word-copy esz bytes (CX) -> (BX) + * ; ADDQ $1, 8(SP) ; JMP rdl_l + * ; rdl_e: + * ; MOVQ 16(SP), DX ; MOVQ (SP), AX + * ; MOVQ 8(DX), BX ; SUBQ AX, BX + * ; MOVQ BX, 8(DX) ; len-=count + * ; ADDQ $24, SP */ + int hdr_lea = 0; + int hdr_off = 0; + int hdr_ok = 0; + int hdr_idx = 0; + int osz = 0; + if (base->kind == N_IDENT && + localfind(locals, base->str) != 0) { + hdr_lea = 1; + hdr_off = localfind(locals, base->str); + hdr_ok = 1; + } + /* (*p)[lo:hi]: header behind a local + * ptr-to-slice — the single-element arm's + * regex_shape twin. */ + 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; + } + /* xs[g][lo:hi]: the header IS element g of + * an outer local slice — the fold-5a + * consumer shape (ref/hare/regex/regex.ha:333 + * delete(jump_idxs[group_level][..])). + * Outer stride = the inner header type's + * own table size (su). */ + if (!hdr_ok && base->kind == N_INDEX && + base->lhs && + base->lhs->kind == N_IDENT && + localfind(locals, base->lhs->str) != 0) { + hdr_idx = 1; + hdr_off = localfind(locals, + base->lhs->str); + osz = su ? (int)su->size : 0; + if (osz <= 0) + fatal("#35: delete() outer " + "element size unresolved " + "(rule-7)"); + hdr_ok = 1; + } + if (!hdr_ok) + fatal("#35: delete() range base " + "shape unsupported (rule-7: " + "local slice ident, " + "deref-of-local, or indexed " + "local slice only)"); + if (hdr_idx) { + cgexpr(c, base->rhs, locals); + if (osz > 1) { + ins2(c, A_MOVQ, aimm(osz), + areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), + areg(D_AX)); + } + ins2(c, A_MOVQ, + amem(D_BP, hdr_off), areg(D_CX)); + ins2(c, A_ADDQ, areg(D_CX), + areg(D_AX)); + } else 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)); + if (d->rhs) + cgexpr(c, d->rhs, locals); + else + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + if (d->cond) + cgexpr(c, d->cond, locals); + else { + ins2(c, A_MOVQ, amem(D_SP, 8), + areg(D_CX)); + ins2(c, A_MOVQ, amem(D_CX, 8), + areg(D_AX)); + } + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX)); + ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + char *rll = mklabel(c, "rdl_l"); + char *rle = mklabel(c, "rdl_e"); + label(c, rll); + ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_AX)); + ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX)); + ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); + ins1(c, A_JGE, abranch(rle)); + 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)); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX)); + 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_ADDQ, areg(D_BX), areg(D_CX)); + int rk = 0; + for (; rk + 8 <= esz; rk += 8) { + ins2(c, A_MOVQ, amem(D_CX, rk), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, rk)); + } + if (rk + 4 <= esz) { + ins2(c, A_MOVL, amem(D_CX, rk), + areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), + amem(D_BX, rk)); + rk += 4; + } + if (rk + 2 <= esz) { + ins2(c, A_MOVW, amem(D_CX, rk), + areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), + amem(D_BX, rk)); + rk += 2; + } + if (rk + 1 <= esz) { + ins2(c, A_MOVB, amem(D_CX, rk), + areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), + amem(D_BX, rk)); + rk += 1; + } + ins2(c, A_ADDQ, aimm(1), amem(D_SP, 8)); + ins1(c, A_JMP, abranch(rll)); + label(c, rle); + ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX)); + ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, 8)); + ins2(c, A_ADDQ, aimm(24), areg(D_SP)); + break; + } int hdr_lea = 0; int hdr_off = 0; int hdr_ok = 0; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 5841ed43..1a33db8e 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1479,12 +1479,14 @@ 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() is the twin arm below). 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. */ + /* delete(xs[i]) / delete(xs[lo:hi]) — slice removal, the + * delete-half of #35 (insert() is the twin arm below). + * harec ref/harec/src/check.c:1981-2027 accepts both an + * indexing place (EXPR_ACCESS/ACCESS_INDEX) and a slicing + * place (EXPR_SLICE — Hare spells it delete(xs[i..j])); + * either way the OBJECT must be a slice. The range form + * is the fold-5a prereq P2 (regex.ha:333 + * delete(jump_idxs[group_level][..])). */ if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "delete") == 0) { Node *d = n->list; @@ -1492,16 +1494,17 @@ cexpr(Checker *c, Node *n) 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]"); + /* harec check.c:2016's reject; wording + * adapted to ww's delete: prefix. */ + if (d->kind != N_INDEX && d->kind != N_SLICE) + err(c, n->pos, "delete: operand must be an indexing or slicing expression"); 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. */ + * fixed-size [N]T base and a str + * base land here. */ if (bt == NULL || bt->kind != TY_SLICE) err(c, n->pos, "delete must operate on a slice"); } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 2966ab84..9cd26442 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12939,13 +12939,15 @@ 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() is the twin arm below). 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). + // delete(xs[i]) / delete(xs[lo:hi]) — slice removal, + // the delete-half of #35 (insert() is the twin arm + // below). Mirrors cstage cmd/wcc/check.c's delete + // arm. harec ref/harec/src/check.c:1981-2027 accepts + // both an indexing place (EXPR_ACCESS/ACCESS_INDEX) + // and a slicing place (EXPR_SLICE — Hare spells it + // delete(xs[i..j])); either way the OBJECT must be a + // slice. The range form is the fold-5a prereq P2 + // (regex.ha:333 delete(jump_idxs[group_level][..])). if (streq(callee.str, "delete")) { let d: *node = e.list; if (d == nil || d.next != nil) { @@ -12954,22 +12956,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; 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"); + // harec check.c:2016's reject; wording + // adapted to ww's delete: prefix. + if (d.kind != nkind.N_INDEX && d.kind != nkind.N_SLICE) { + cerr("delete: operand must be an indexing or slicing expression\n"); c.errs += 1; } else { - if (d.kind != nkind.N_INDEX) { - cerr("delete: operand must be an indexing expression xs[i]\n"); + 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 and a str + // base land here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\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; - }; }; }; }; @@ -25817,6 +25817,201 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, SP\n"); }; +// cgdeleterange — Hare `delete(xs[lo..hi])` (ww spells the range +// `xs[lo:hi]`): range slice removal, the fold-5a prereq P2 +// (ref/hare/regex/regex.ha:333 delete(jump_idxs[group_level][..]); +// harec ref/harec/src/check.c:1994 EXPR_SLICE). Shift [hi..len) down +// count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, +// hi defaults len. delete(xs[:]) never enters the copy loop (lo+count +// == len at entry) and zeroes len. The per-element move is cgdelete's +// same-slice whole-stride word copy with a DYNAMIC src offset +// (count*esz via a src register) instead of the constant one-stride. +// Ascending j keeps src >= dst, the safe memmove-down direction. +// Bounds are implicit (no range check, matching cgdelete and the rest +// of cgen). Mirrors cmd/w6c/cgen.c's N_CALL delete range arm +// instruction-for-instruction (rule-10 byte-id). +fn cgdeleterange(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_SLICE, 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; + let hdr_idx: bool = false; + let osz: i32 = 0; + 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)[lo:hi]: header behind a local ptr-to-slice — cgdelete's + // regex_shape twin. + 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; + }; + }; + }; + }; + // xs[g][lo:hi]: the header IS element g of an outer local slice — + // the fold-5a consumer shape (ref/hare/regex/regex.ha:333 + // delete(jump_idxs[group_level][..])). Outer stride = the inner + // header type's own table size (sti). + if (!hdr_ok && base.kind == nkind.N_INDEX) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let oc: *local = localfindnode(c, base.lhs.str); + if (oc != nil) { + hdr_idx = true; + hdr_off = oc.off; + if (sti != nil) { osz = sti.size: i32; }; + if (osz <= 0) { + let m35c: str = "#35: delete() outer element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.len: u64); + os.exit(1); + }; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() range base shape unsupported (rule-7: local slice ident, deref-of-local, or indexed local slice only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + if (hdr_idx) { + cgexpr(c, base.rhs); + if (osz > 1) { + emitline("\tMOVQ\t$"); + emitint(osz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(hdr_off: i64); + emitline("(BP), CX\n"); + emitline("\tADDQ\tCX, AX\n"); + } else { + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.rhs != nil) { + cgexpr(c, d.rhs); + } else { + emitline("\tMOVQ\t$0, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.cond != nil) { + cgexpr(c, d.cond); + } else { + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + }; + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tSUBQ\tCX, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let rll: str = mklabel(c, "rdl_l"); + let rle: str = mklabel(c, "rdl_e"); + emitlabel(rll); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tADDQ\tCX, AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(rle); 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"); + emitline("\tMOVQ\t(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tADDQ\tBX, CX\n"); + let rk: i32 = 0; + for (rk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 8; + }; + if (rk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 4; + }; + if (rk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 2; + }; + if (rk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(rll); emitline("\n"); + emitlabel(rle); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tADDQ\t$24, SP\n"); +}; + // cginsert — Hare `insert(xs[idx], v)`: delete()'s twin, the insert-half // of #35. Insert v BEFORE idx; idx==len is a legal end-insert. Lowered // as a DESUGAR to append(xs, v) + a rotate-right of [idx, len): cgappend @@ -26335,11 +26530,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // delete(xs[i]) — #35 delete-half; mirror of cstage - // cgen.c's N_CALL delete arm. + // delete(xs[i]) / delete(xs[lo:hi]) — #35 + // delete-half + fold-5a P2 range form; mirror of + // cstage cgen.c's N_CALL delete arm (which branches + // on d->kind == N_SLICE internally). if (streq(callee.str, "delete")) { if (n.list != nil) { if (n.list.next == nil) { + if (n.list.kind == nkind.N_SLICE) { + cgdeleterange(c, n); + return; + }; cgdelete(c, n); return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 0a2ce29e..0c3b1682 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5383,6 +5383,201 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, SP\n"); }; +// cgdeleterange — Hare `delete(xs[lo..hi])` (ww spells the range +// `xs[lo:hi]`): range slice removal, the fold-5a prereq P2 +// (ref/hare/regex/regex.ha:333 delete(jump_idxs[group_level][..]); +// harec ref/harec/src/check.c:1994 EXPR_SLICE). Shift [hi..len) down +// count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, +// hi defaults len. delete(xs[:]) never enters the copy loop (lo+count +// == len at entry) and zeroes len. The per-element move is cgdelete's +// same-slice whole-stride word copy with a DYNAMIC src offset +// (count*esz via a src register) instead of the constant one-stride. +// Ascending j keeps src >= dst, the safe memmove-down direction. +// Bounds are implicit (no range check, matching cgdelete and the rest +// of cgen). Mirrors cmd/w6c/cgen.c's N_CALL delete range arm +// instruction-for-instruction (rule-10 byte-id). +fn cgdeleterange(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_SLICE, 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; + let hdr_idx: bool = false; + let osz: i32 = 0; + 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)[lo:hi]: header behind a local ptr-to-slice — cgdelete's + // regex_shape twin. + 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; + }; + }; + }; + }; + // xs[g][lo:hi]: the header IS element g of an outer local slice — + // the fold-5a consumer shape (ref/hare/regex/regex.ha:333 + // delete(jump_idxs[group_level][..])). Outer stride = the inner + // header type's own table size (sti). + if (!hdr_ok && base.kind == nkind.N_INDEX) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let oc: *local = localfindnode(c, base.lhs.str); + if (oc != nil) { + hdr_idx = true; + hdr_off = oc.off; + if (sti != nil) { osz = sti.size: i32; }; + if (osz <= 0) { + let m35c: str = "#35: delete() outer element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.len: u64); + os.exit(1); + }; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() range base shape unsupported (rule-7: local slice ident, deref-of-local, or indexed local slice only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + if (hdr_idx) { + cgexpr(c, base.rhs); + if (osz > 1) { + emitline("\tMOVQ\t$"); + emitint(osz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(hdr_off: i64); + emitline("(BP), CX\n"); + emitline("\tADDQ\tCX, AX\n"); + } else { + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.rhs != nil) { + cgexpr(c, d.rhs); + } else { + emitline("\tMOVQ\t$0, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.cond != nil) { + cgexpr(c, d.cond); + } else { + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + }; + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tSUBQ\tCX, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let rll: str = mklabel(c, "rdl_l"); + let rle: str = mklabel(c, "rdl_e"); + emitlabel(rll); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tADDQ\tCX, AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(rle); 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"); + emitline("\tMOVQ\t(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tADDQ\tBX, CX\n"); + let rk: i32 = 0; + for (rk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 8; + }; + if (rk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 4; + }; + if (rk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 2; + }; + if (rk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(rll); emitline("\n"); + emitlabel(rle); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tADDQ\t$24, SP\n"); +}; + // cginsert — Hare `insert(xs[idx], v)`: delete()'s twin, the insert-half // of #35. Insert v BEFORE idx; idx==len is a legal end-insert. Lowered // as a DESUGAR to append(xs, v) + a rotate-right of [idx, len): cgappend @@ -5901,11 +6096,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // delete(xs[i]) — #35 delete-half; mirror of cstage - // cgen.c's N_CALL delete arm. + // delete(xs[i]) / delete(xs[lo:hi]) — #35 + // delete-half + fold-5a P2 range form; mirror of + // cstage cgen.c's N_CALL delete arm (which branches + // on d->kind == N_SLICE internally). if (streq(callee.str, "delete")) { if (n.list != nil) { if (n.list.next == nil) { + if (n.list.kind == nkind.N_SLICE) { + cgdeleterange(c, n); + return; + }; cgdelete(c, n); return; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 48bc908e..4424529c 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2675,13 +2675,15 @@ 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() is the twin arm below). 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). + // delete(xs[i]) / delete(xs[lo:hi]) — slice removal, + // the delete-half of #35 (insert() is the twin arm + // below). Mirrors cstage cmd/wcc/check.c's delete + // arm. harec ref/harec/src/check.c:1981-2027 accepts + // both an indexing place (EXPR_ACCESS/ACCESS_INDEX) + // and a slicing place (EXPR_SLICE — Hare spells it + // delete(xs[i..j])); either way the OBJECT must be a + // slice. The range form is the fold-5a prereq P2 + // (regex.ha:333 delete(jump_idxs[group_level][..])). if (streq(callee.str, "delete")) { let d: *node = e.list; if (d == nil || d.next != nil) { @@ -2690,22 +2692,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; 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"); + // harec check.c:2016's reject; wording + // adapted to ww's delete: prefix. + if (d.kind != nkind.N_INDEX && d.kind != nkind.N_SLICE) { + cerr("delete: operand must be an indexing or slicing expression\n"); c.errs += 1; } else { - if (d.kind != nkind.N_INDEX) { - cerr("delete: operand must be an indexing expression xs[i]\n"); + 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 and a str + // base land here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\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; - }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bd322de4..f73d779e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12939,13 +12939,15 @@ 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() is the twin arm below). 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). + // delete(xs[i]) / delete(xs[lo:hi]) — slice removal, + // the delete-half of #35 (insert() is the twin arm + // below). Mirrors cstage cmd/wcc/check.c's delete + // arm. harec ref/harec/src/check.c:1981-2027 accepts + // both an indexing place (EXPR_ACCESS/ACCESS_INDEX) + // and a slicing place (EXPR_SLICE — Hare spells it + // delete(xs[i..j])); either way the OBJECT must be a + // slice. The range form is the fold-5a prereq P2 + // (regex.ha:333 delete(jump_idxs[group_level][..])). if (streq(callee.str, "delete")) { let d: *node = e.list; if (d == nil || d.next != nil) { @@ -12954,22 +12956,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; 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"); + // harec check.c:2016's reject; wording + // adapted to ww's delete: prefix. + if (d.kind != nkind.N_INDEX && d.kind != nkind.N_SLICE) { + cerr("delete: operand must be an indexing or slicing expression\n"); c.errs += 1; } else { - if (d.kind != nkind.N_INDEX) { - cerr("delete: operand must be an indexing expression xs[i]\n"); + 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 and a str + // base land here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("delete must operate on a slice\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; - }; }; }; }; @@ -25817,6 +25817,201 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, SP\n"); }; +// cgdeleterange — Hare `delete(xs[lo..hi])` (ww spells the range +// `xs[lo:hi]`): range slice removal, the fold-5a prereq P2 +// (ref/hare/regex/regex.ha:333 delete(jump_idxs[group_level][..]); +// harec ref/harec/src/check.c:1994 EXPR_SLICE). Shift [hi..len) down +// count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, +// hi defaults len. delete(xs[:]) never enters the copy loop (lo+count +// == len at entry) and zeroes len. The per-element move is cgdelete's +// same-slice whole-stride word copy with a DYNAMIC src offset +// (count*esz via a src register) instead of the constant one-stride. +// Ascending j keeps src >= dst, the safe memmove-down direction. +// Bounds are implicit (no range check, matching cgdelete and the rest +// of cgen). Mirrors cmd/w6c/cgen.c's N_CALL delete range arm +// instruction-for-instruction (rule-10 byte-id). +fn cgdeleterange(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_SLICE, 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; + let hdr_idx: bool = false; + let osz: i32 = 0; + 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)[lo:hi]: header behind a local ptr-to-slice — cgdelete's + // regex_shape twin. + 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; + }; + }; + }; + }; + // xs[g][lo:hi]: the header IS element g of an outer local slice — + // the fold-5a consumer shape (ref/hare/regex/regex.ha:333 + // delete(jump_idxs[group_level][..])). Outer stride = the inner + // header type's own table size (sti). + if (!hdr_ok && base.kind == nkind.N_INDEX) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + let oc: *local = localfindnode(c, base.lhs.str); + if (oc != nil) { + hdr_idx = true; + hdr_off = oc.off; + if (sti != nil) { osz = sti.size: i32; }; + if (osz <= 0) { + let m35c: str = "#35: delete() outer element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.len: u64); + os.exit(1); + }; + hdr_ok = true; + }; + }; + }; + }; + if (!hdr_ok) { + let m35b: str = "#35: delete() range base shape unsupported (rule-7: local slice ident, deref-of-local, or indexed local slice only)\n"; + os.write(2, m35b.ptr, m35b.len: u64); + os.exit(1); + }; + if (hdr_idx) { + cgexpr(c, base.rhs); + if (osz > 1) { + emitline("\tMOVQ\t$"); + emitint(osz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tMOVQ\t"); + emitoff(hdr_off: i64); + emitline("(BP), CX\n"); + emitline("\tADDQ\tCX, AX\n"); + } else { + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.rhs != nil) { + cgexpr(c, d.rhs); + } else { + emitline("\tMOVQ\t$0, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (d.cond != nil) { + cgexpr(c, d.cond); + } else { + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t8(CX), AX\n"); + }; + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tSUBQ\tCX, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let rll: str = mklabel(c, "rdl_l"); + let rle: str = mklabel(c, "rdl_e"); + emitlabel(rll); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t8(SP), CX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tADDQ\tCX, AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(rle); 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"); + emitline("\tMOVQ\t(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tADDQ\tBX, CX\n"); + let rk: i32 = 0; + for (rk + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 8; + }; + if (rk + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 4; + }; + if (rk + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 2; + }; + if (rk + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(rk: i64, "CX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(rk: i64, "BX"); + emitline("\n"); + rk += 1; + }; + emitline("\tADDQ\t$1, 8(SP)\n"); + emitline("\tJMP\t"); emitline(rll); emitline("\n"); + emitlabel(rle); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tMOVQ\t(SP), AX\n"); + emitline("\tMOVQ\t8(DX), BX\n"); + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tADDQ\t$24, SP\n"); +}; + // cginsert — Hare `insert(xs[idx], v)`: delete()'s twin, the insert-half // of #35. Insert v BEFORE idx; idx==len is a legal end-insert. Lowered // as a DESUGAR to append(xs, v) + a rotate-right of [idx, len): cgappend @@ -26335,11 +26530,17 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; - // delete(xs[i]) — #35 delete-half; mirror of cstage - // cgen.c's N_CALL delete arm. + // delete(xs[i]) / delete(xs[lo:hi]) — #35 + // delete-half + fold-5a P2 range form; mirror of + // cstage cgen.c's N_CALL delete arm (which branches + // on d->kind == N_SLICE internally). if (streq(callee.str, "delete")) { if (n.list != nil) { if (n.list.next == nil) { + if (n.list.kind == nkind.N_SLICE) { + cgdeleterange(c, n); + return; + }; cgdelete(c, n); return; }; diff --git a/test/wcc/804_delete_elem.c b/test/wcc/804_delete_elem.c index b24efea1..e85a185a 100644 --- a/test/wcc/804_delete_elem.c +++ b/test/wcc/804_delete_elem.c @@ -17,8 +17,8 @@ * 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. + * (ref/harec/src/check.c:1981-2027 EXPR_SLICE) — implemented as the + * fold-5a prereq P2; covered by 809_delete_range. * * row | shape | want * ----------------+----------------------------------------+------ @@ -47,9 +47,8 @@ * | iteration, base-4 order accumulation | * 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 - * | message must cite task #35 | + * reject_nonindex | delete(xs) — operand must be an index- | BUILD_FAIL + * | ing or slicing expression | * reject_arity | delete(xs[0], xs[1]) | BUILD_FAIL * * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, @@ -78,9 +77,9 @@ runwait(const char *cmd) /* want == BUILD_FAIL: the row must FAIL to build on both stages AND * emit expect_err on stderr (the checker reject set — message text - * included, esp. the #35 cite on the deferred range form — is part of - * the contract: rule 7, never a silent acceptance; without the message - * check a row would pass vacuously on any unrelated build failure). */ + * included — is part of the contract: rule 7, never a silent + * acceptance; without the message check a row would pass vacuously on + * any unrelated build failure). */ #define BUILD_FAIL (-2147483647 - 1) struct row { @@ -290,18 +289,7 @@ static const struct row rows[] = { "\tdelete(xs);\n" "\treturn 0;\n" "};\n", - BUILD_FAIL, "delete: operand must be an indexing expression" }, - - { "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, "range form delete(xs[i..j]) unimplemented (task #35)" }, + BUILD_FAIL, "delete: operand must be an indexing or slicing expression" }, { "reject_arity", "package main;\n" diff --git a/test/wcc/809_delete_range.c b/test/wcc/809_delete_range.c new file mode 100644 index 00000000..73fdb86b --- /dev/null +++ b/test/wcc/809_delete_range.c @@ -0,0 +1,607 @@ +/* + * 809_delete_range — cstage and wwstage agree, byte-for-byte and at + * runtime, that the range form `delete(xs[lo:hi])` removes elements + * [lo, hi) from a slice: the tail [hi..len) shifts down count = hi-lo + * strides, len -= count, cap unchanged; lo defaults 0, hi defaults + * len (so delete(xs[:]) clears the slice, storage retained). The + * range half of the delete() builtin — fold-5a prereq P2 for the + * regex.ha:333 consumer `delete(jump_idxs[group_level][..])` (harec + * ref/harec/src/check.c:1981-2027 accepts EXPR_SLICE alongside the + * 804-covered ACCESS_INDEX form). + * + * Lowering (BOTH stages, converged byte-identical by construction): + * push &hdr, lo and count = hi-lo, then an ascending word-copy loop + * moves element j+count onto element j until j+count >= len, then + * hdr.len -= count. The per-element move is 804's same-slice + * whole-stride word copy with a DYNAMIC src offset (count*esz via a + * src register) instead of the constant one-stride; the esz spread + * below (1/2/4/8/16/24) re-proves every copy-tail arm against the + * dynamic src. Base shapes: local slice ident, deref-of-local + * ptr-to-slice, and — new for the range arm — an INDEXED local slice + * base xs[g][lo:hi] (the regex fold-5a consumer shape). Bounds are + * implicit (no range check), matching the 804 single-element arm and + * the rest of cgen — so no out-of-bounds abort rows here; a + * violating range is as undefined as a violating index. + * + * row | shape | want + * -----------------+----------------------------------------+------ + * full_range | [7,11,13], delete(xs[:]) -> len 0 | 7 + * full_explicit | [7,11,13], delete(xs[0:3]) -> len 0 | 8 + * full_twice | delete(xs[:]) twice — second is a | 9 + * | no-op on the emptied slice (count 0) | + * head | [1,2,3], delete(xs[:2]) — lo default 0 | 13 + * mid | [1..5], delete(xs[1:3]) | 148 + * tail_open | [1,2,3], delete(xs[1:]) — hi default | 11 + * | len, copy loop never entered | + * empty_range | [1,2], delete(xs[1:1]) — count 0, | 23 + * | self-copy loop, len unchanged | + * end_boundary | [1,2], delete(xs[len:len]) — lo==hi== | 212 + * | len, loop exits at entry, no deref | + * | past the end | + * empty_explicit | delete(xs[0:0]) on a never-appended | 31 + * | empty slice (ptr nil) — count 0 and | + * | len 0, nothing dereferenced | + * single_via_range | delete(xs[1:2]) == delete(ys[1]) | 42 + * | element-for-element | + * cap_preserved | manual {ptr,len=3,cap=8} header, | 80 + * | delete(xs[:]); cap must still read 8 | + * u8_narrow | []u8 esz=1 — MOVB tail, dynamic src | 215 + * u16_narrow | []u16 esz=2 — MOVW tail, dynamic src | 25 + * i32_narrow | []i32 esz=4 — MOVL tail, dynamic src | 50 + * struct16 | []pair esz=16 — 2 whole qwords, no | 216 + * | sub-word tail (804's struct esz twin) | + * str_elem | []str esz=24 — 3-qword headers move | 235 + * | whole across a 2-stride shift | + * regex_exact | [][]size, delete(jj[lvl][:]) — the | 10 + * | EXACT regex.ha:333 consumer shape | + * | (indexed base, size-typed index) | + * deref_shape | delete((*p)[:]) behind *[]u8 — the | 17 + * | 804 regex_shape twin | + * order_of_eval | delete(xs[lof():hif()]) — lo then hi, | 16 + * | each CALLED exactly once (trace must | + * | read 12; harec evaluates the slicing | + * | operands left-to-right, once) | + * alias_visible | delete(xs[poke(&xs):]) — lo's write | 91 + * | through &xs lands BEFORE the shift | + * | (loop reads ptr/len/elems fresh from | + * | the header, after operand eval) | + * reject_array | delete(t[0:2]) on [3]i64 — "delete | BUILD_FAIL + * | must operate on a slice" | + * reject_str | delete(s[:]) on str — slicing a str | BUILD_FAIL + * | yields str, not a slice | + * + * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, + * both stages) — a build that fails for any other reason (parse error, + * crash) is a vacuous reject and fails the row. + * + * Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id, + * which subsumes the frame canary (TEXT main,$N) and the rdl_l/rdl_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 AND + * emit expect_err on stderr (the checker reject set — message text + * included — is part of the contract: rule 7, never a silent + * acceptance; without the message check a row would pass vacuously on + * any unrelated build failure). */ +#define BUILD_FAIL (-2147483647 - 1) + +struct row { + const char *label; + const char *src; + int want; + const char *expect_err; /* BUILD_FAIL rows: required stderr substring */ +}; + +static const struct row rows[] = { + { "full_range", + "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[:]);\n" + "\treturn len(xs): i32 + 7;\n" + "};\n", + 7, NULL }, + + { "full_explicit", + "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:3]);\n" + "\treturn len(xs): i32 + 8;\n" + "};\n", + 8, NULL }, + + /* Second full-range delete sees len == 0: count = len - 0 = 0, + * the copy loop exits at entry, len -= 0. Pins that an emptied + * slice survives a re-clear (the regex `)` arm clears + * jump_idxs[group_level] whether or not `|` populated it). */ + { "full_twice", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tdelete(xs[:]);\n" + "\tdelete(xs[:]);\n" + "\treturn len(xs): i32 + 9;\n" + "};\n", + 9, NULL }, + + { "head", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tappend(xs, 3);\n" + "\tdelete(xs[:2]);\n" + "\treturn xs[0]: i32 + len(xs)*10;\n" + "};\n", + 13, NULL }, + + { "mid", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tappend(xs, 3);\n" + "\tappend(xs, 4);\n" + "\tappend(xs, 5);\n" + "\tdelete(xs[1:3]);\n" + "\treturn (xs[0]*100 + xs[1]*10 + xs[2]): i32 + len(xs);\n" + "};\n", + 148, NULL }, + + { "tail_open", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tappend(xs, 3);\n" + "\tdelete(xs[1:]);\n" + "\treturn xs[0]: i32 + len(xs)*10;\n" + "};\n", + 11, NULL }, + + { "empty_range", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tdelete(xs[1:1]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 23, NULL }, + + /* lo == hi == len: the loop guard j+count >= len holds at entry + * with j = len, so nothing past the end is ever addressed. */ + { "end_boundary", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tdelete(xs[len(xs):len(xs)]);\n" + "\treturn (xs[0]*10 + xs[1]): i32 + len(xs)*100;\n" + "};\n", + 212, NULL }, + + /* Never-appended empty slice: ptr is nil. count 0 and len 0 mean + * the loop body (the only dereferencing code) never runs. */ + { "empty_explicit", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tdelete(xs[0:0]);\n" + "\treturn len(xs): i32 + 31;\n" + "};\n", + 31, NULL }, + + { "single_via_range", + "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:2]);\n" + "\tlet ys: []i64 = [];\n" + "\tappend(ys, 7);\n" + "\tappend(ys, 11);\n" + "\tappend(ys, 13);\n" + "\tdelete(ys[1]);\n" + "\tif (len(xs) != len(ys)) { return 99; };\n" + "\tif (xs[0] != ys[0]) { return 98; };\n" + "\tif (xs[1] != ys[1]) { return 97; };\n" + "\treturn 42;\n" + "};\n", + 42, NULL }, + + { "cap_preserved", + "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[:]);\n" + "\treturn (xs.cap*10 + xs.len): i32;\n" + "};\n", + 80, NULL }, + + { "u8_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 1u8);\n" + "\tappend(xs, 2u8);\n" + "\tappend(xs, 3u8);\n" + "\tappend(xs, 4u8);\n" + "\tappend(xs, 5u8);\n" + "\tdelete(xs[1:4]);\n" + "\treturn (xs[0]*10u8 + xs[1]): i32 + len(xs)*100;\n" + "};\n", + 215, NULL }, + + { "u16_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u16 = [];\n" + "\tappend(xs, 1u16);\n" + "\tappend(xs, 2u16);\n" + "\tappend(xs, 3u16);\n" + "\tappend(xs, 4u16);\n" + "\tdelete(xs[1:3]);\n" + "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" + "};\n", + 25, NULL }, + + { "i32_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i32 = [];\n" + "\tappend(xs, 10i32);\n" + "\tappend(xs, 20i32);\n" + "\tappend(xs, 30i32);\n" + "\tappend(xs, 40i32);\n" + "\tdelete(xs[2:4]);\n" + "\treturn xs[0] + xs[1] + len(xs)*10;\n" + "};\n", + 50, NULL }, + + /* esz 16: exactly two whole qwords per element — the only spread + * point with multiple MOVQs and NO sub-word tail. */ + { "struct16", + "package main;\n" + "type pair = struct { a: i64, b: i64 };\n" + "export fn main() i32 = {\n" + "\tlet xs: []pair = [];\n" + "\tappend(xs, pair{ a = 1, b = 2 });\n" + "\tappend(xs, pair{ a = 3, b = 4 });\n" + "\tappend(xs, pair{ a = 5, b = 6 });\n" + "\tdelete(xs[1:2]);\n" + "\treturn (xs[0].a*10 + xs[1].b): i32 + len(xs)*100;\n" + "};\n", + 216, NULL }, + + { "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" + "\tlet h: str = \"jklmn\";\n" + "\tappend(xs, a);\n" + "\tappend(xs, b);\n" + "\tappend(xs, g);\n" + "\tappend(xs, h);\n" + "\tdelete(xs[1:3]);\n" + "\treturn (xs[0].len*10 + xs[1].len): i32 + len(xs)*100;\n" + "};\n", + 235, NULL }, + + /* The fold-5a consumer, shape-EXACT: indexed local-slice base, + * size-typed outer index, defaulted full range (regex.ha:333 + * `delete(jump_idxs[group_level][..])`; ww spells `[..]` as + * `[:]`, D7). The header lives at jj.ptr + g*size([]size) — + * outer stride off the type table, never a literal. */ + { "regex_exact", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet jump_idxs: [][]size = [];\n" + "\tlet lvl0: []size = [];\n" + "\tlet a: size = 3;\n" + "\tlet b: size = 9;\n" + "\tappend(lvl0, a);\n" + "\tappend(lvl0, b);\n" + "\tappend(jump_idxs, lvl0);\n" + "\tlet group_level: size = 0;\n" + "\tdelete(jump_idxs[group_level][:]);\n" + "\treturn len(jump_idxs[0]): i32 + len(jump_idxs): i32 * 10;\n" + "};\n", + 10, NULL }, + + { "deref_shape", + "package main;\n" + "fn clear(p: *[]u8) void = {\n" + "\tdelete((*p)[:]);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tappend(xs, 6u8);\n" + "\tclear(&xs);\n" + "\treturn len(xs): i32 + 17;\n" + "};\n", + 17, NULL }, + + /* lo and hi as CALLs: each side effect fires exactly once, lo + * before hi (trace 12, never 21/121/122) — pins that neither + * bound expression is re-evaluated by the count computation or + * the copy loop. */ + { "order_of_eval", + "package main;\n" + "let trace: i64 = 0;\n" + "fn lof() size = {\n" + "\ttrace = trace*10 + 1;\n" + "\treturn 1;\n" + "};\n" + "fn hif() size = {\n" + "\ttrace = trace*10 + 2;\n" + "\treturn 3;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tappend(xs, 3);\n" + "\tappend(xs, 4);\n" + "\tdelete(xs[lof():hif()]);\n" + "\tif (trace != 12) { return 90; };\n" + "\treturn (xs[0]*10 + xs[1]): i32 + len(xs);\n" + "};\n", + 16, NULL }, + + /* lo mutates the slice it is deleting from: only the header + * ADDRESS is taken before operand eval; ptr/len/elements are read + * after, so poke's write to xs[0] survives into the result. */ + { "alias_visible", + "package main;\n" + "fn poke(p: *[]i64) size = {\n" + "\t(*p)[0] = 9;\n" + "\treturn 1;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 2);\n" + "\tappend(xs, 3);\n" + "\tdelete(xs[poke(&xs):]);\n" + "\treturn xs[0]: i32 * 10 + len(xs);\n" + "};\n", + 91, NULL }, + + { "reject_array", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: [3]i64 = [1, 2, 3];\n" + "\tdelete(t[0:2]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "delete must operate on a slice" }, + + { "reject_str", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet s: str = \"abc\";\n" + "\tdelete(s[:]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "delete must operate on a slice" }, +}; + +/* errlog_has — the build-failure stderr must carry the row's expected + * diagnostic; any other failure (parse error, crash) is a vacuous + * reject and must not pass. */ +static int +errlog_has(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); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], errlog[80], cmd[1200]; + snprintf(src, sizeof src, "/tmp/delr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/delr_%d_d_%d", getpid(), i); + snprintf(errlog, sizeof errlog, "%s.err", src); + + 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>%s", + tmpdir, driver, src, errlog); + if (runwait(cmd) != 0) { + int rc = -1; + if (r->want != BUILD_FAIL) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + } else if (r->expect_err && + !errlog_has(errlog, r->expect_err)) { + fprintf(stderr, "row[%s]: %s build failed without " + "expected diagnostic \"%s\"\n", + r->label, driver, r->expect_err); + rc = -3; /* failed, but for the wrong reason */ + } + unlink(src); unlink(errlog); rmdir(tmpdir); + return rc; + } + + 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(errlog); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — compile the row's source standalone with w6c and + * w6c_ww and diff. Both range-delete lowerings are written fresh, so + * this is the converged-by-construction gate: any drift in the shift + * loop, the rdl_l/rdl_e label sequence, the dynamic-src copy, or the + * indexed-base header address 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/delr_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/delr_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/delr_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_range: 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_range[%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_range: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("delete_range: %d fixtures passed\n", total); + return 0; +}