From 9861f73bbb794b50bd1ba23dbdee3931ef95225c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 16:50:22 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c+w6c=5Fww:=20insert()=20builtin=20?= =?UTF-8?q?=E2=80=94=20single-element=20slice=20insertion=20(part=20of=20#?= =?UTF-8?q?35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hare's insert(xs[idx], v) (ref/harec/src/check.c:745 check_expr_append_insert — append/insert share the checker arm, "insert" at :786): checker accepts an INDEX place over a slice plus one value, stamps void; idx == len is a legal end-insert (the ref/hare os/exec/platform_cmd.ha:86 idiom). Loud-rejects with exact texts: spread form insert(xs[i], vs...) (filed, #35 — also covers harec's with-length form via the arity check), range place (not Hare; harec only parses ACCESS_INDEX, :784), non-index operands, array bases, wrong arity. delete()-parity throughout. Lowering (both stages, converged byte-identical by construction) is a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) and the entire #34 value-store dispatch (scalar / str-slice header / tagged widen / struct fill) verbatim, one boxing choke-point — lands v at slot len-1; then a rotate-right of [idx, len) moves it home through a fresh per-site esz frame scratch (@insscr). The rotate is delete's shift loop in reverse (descending j, the safe memmove-up direction) and is a same-slice whole-stride raw byte move — no boxing exists for any element kind. idx evaluates BEFORE the grow (Hare's left-to-right operand order — pinned by the pregrow_len_idx row, insert(xs[len(xs)-1], v): pre-grow [7,13,11] vs post-grow [7,11,13]; an idx==len(xs) end-insert cannot discriminate, the rotate degenerates either way). Base shapes: local slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ); others rule-7 loud-stop, like delete. test/807: 57 fixtures — front/middle/end + idx==len via len(xs) + the pre-grow eval-order pin, esz 1/2/4/8/16/24/56 (MOVB/MOVW/MOVL tails, struct body, str header, 7-qword tagged from a typed local [the regex fold-3 ha:347 newinst shape] and from a cast rvalue [ha:419/441]), empty-slice grow, (*p)[i] deref base, front-insert loop, 6 checker reject rows with diagnostic-text checks; every accept row cs==ww asm byte-id. --- Makefile | 14 + cmd/w6c/cgen.c | 195 +++++++++ cmd/wcc/check.c | 46 +- selfhost/cmd/w6c/main.combined.ww | 315 +++++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 256 +++++++++++ selfhost/cmd/wcc/check.ww | 59 ++- selfhost/cmd/wwdump/main.combined.ww | 315 +++++++++++++- test/wcc/807_insert_elem.c | 615 +++++++++++++++++++++++++++ 8 files changed, 1802 insertions(+), 13 deletions(-) create mode 100644 test/wcc/807_insert_elem.c diff --git a/Makefile b/Makefile index 29e3a1fd..3a05d3db 100644 --- a/Makefile +++ b/Makefile @@ -390,6 +390,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_insert_elem \ $(BIN)/test_placeaddr_store \ $(BIN)/test_tryprop_multisuccess \ $(BIN)/test_append_place \ @@ -1015,6 +1016,19 @@ $(BIN)/test_delete_elem: test/wcc/804_delete_elem.c \ $(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. +# the regex fold-3 typed-local + cast-rvalue consumer shapes) + idx==len +# end-insert + pre-grow idx evaluation + the deref-of-local base + +# checker reject rows w/ exact diagnostic text + cs==ww asm byte-id +# (the converged-by-construction rotate loop + @insscr frame slot). +$(BIN)/test_insert_elem: test/wcc/807_insert_elem.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # F6 (task #4, regex fold-2b): (*ts)[i].field = v / OP= v through the # cgplaceaddr resolver — runtime rows across widths/signedness/str + # compound ops + loud-tail reject rows w/ exact diagnostic text + cs==ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 30f00a9b..1adb9a8c 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6839,6 +6839,201 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_ADDQ, aimm(16), areg(D_SP)); break; } + if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && + strcmp(n->lhs->str, "insert") == 0 && n->list && + n->list->next && n->list->next->next == NULL) { + /* 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): + * the append arm below contributes grow (rt_ensure) + * and the whole #34 value-store dispatch (scalar / + * str-slice header / tagged widen / struct fill) + * verbatim — one boxing choke-point, byte-id by + * construction — landing v at slot len-1; the + * rotate then moves it home through an esz frame + * scratch. The rotate is delete's shift loop in + * reverse (descending j keeps src j behind dst j+1, + * the safe memmove-up direction) and, like + * delete's, is a same-slice whole-stride raw byte + * move — no boxing exists for any element kind. + * idx evaluates BEFORE the grow (Hare's + * left-to-right operand order: insert(xs[len(xs)], + * v) sees the pre-grow len); v's evaluation point + * inherits append's per-kind rules. Bounds are + * implicit (no index check, matching delete). + * + * ; AX = idx (cgexpr) ; PUSHQ AX + * ; ...append(xs, v) body (grow + store at end, + * ; push-balanced)... + * ; LEAQ/MOVQ off(BP), AX ; &hdr (ident / *p) + * ; PUSHQ AX ; (SP)=&hdr 8(SP)=idx + * ; save elem[len-1] -> @insscr (word copy) + * ; MOVQ 8(DX), AX ; SUBQ $2, AX ; PUSHQ AX + * ; ; (SP)=j=len-2 + * ; ins_l: + * ; MOVQ (SP), CX ; MOVQ 16(SP), DX + * ; CMPQ DX, CX ; JL ins_e ; j < idx + * ; [IMULQ esz, CX] + * ; MOVQ 8(SP), DX ; MOVQ (DX), BX ; ADDQ CX, BX + * ; word-copy esz bytes (BX) -> esz(BX) + * ; SUBQ $1, (SP) ; JMP ins_l + * ; ins_e: + * ; store @insscr -> elem[idx] (word copy) + * ; ADDQ $24, SP */ + Node *d = n->list; /* N_INDEX, checker-validated */ + Node *base = d->lhs; + Node *v = d->next; + 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: insert() 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]: header behind a local ptr-to-slice — + * delete'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; + } + if (!hdr_ok) + fatal("#35: insert() base shape unsupported " + "(rule-7: local slice ident or " + "deref-of-local only)"); + /* Fresh esz-sized slot per SITE (esz varies; an + * @-name dedup would mis-share across element + * types). */ + int ins_scr = local_alloc(c, &locals, "@insscr", + esz, cg_frame); + cgexpr(c, d->rhs, locals); /* AX = idx */ + ins1(c, A_PUSHQ, areg(D_AX)); + /* Desugar in place and re-dispatch into the append + * arm: cgen is single-pass, base is an lhs node + * (never on a sibling chain), and the checker has + * already validated this call — the mutation is + * dead after this emission. */ + n->lhs->str = "append"; + n->list = base; + base->next = v; + cgexpr(c, n, locals); + 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)); + ins2(c, A_MOVQ, areg(D_AX), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_CX)); + ins2(c, A_SUBQ, aimm(1), 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_MOVQ, amem(D_DX, 0), areg(D_BX)); + ins2(c, A_ADDQ, areg(D_CX), areg(D_BX)); + int ik = 0; + for (; ik + 8 <= esz; ik += 8) { + ins2(c, A_MOVQ, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, ins_scr + ik)); + } + if (ik + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BP, ins_scr + ik)); + ik += 4; + } + if (ik + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BP, ins_scr + ik)); + ik += 2; + } + if (ik + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BP, ins_scr + ik)); + ik += 1; + } + ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_AX)); + ins2(c, A_SUBQ, aimm(2), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + char *ill = mklabel(c, "ins_l"); + char *ile = mklabel(c, "ins_e"); + label(c, ill); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX)); + ins2(c, A_CMPQ, areg(D_DX), areg(D_CX)); + ins1(c, A_JL, abranch(ile)); + 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_SP, 8), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX)); + ins2(c, A_ADDQ, areg(D_CX), areg(D_BX)); + ik = 0; + for (; ik + 8 <= esz; ik += 8) { + ins2(c, A_MOVQ, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, esz + ik)); + } + if (ik + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BX, esz + ik)); + ik += 4; + } + if (ik + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BX, esz + ik)); + ik += 2; + } + if (ik + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BX, ik), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BX, esz + ik)); + ik += 1; + } + ins2(c, A_SUBQ, aimm(1), amem(D_SP, 0)); + ins1(c, A_JMP, abranch(ill)); + label(c, ile); + ins2(c, A_MOVQ, amem(D_SP, 16), 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_MOVQ, amem(D_SP, 8), areg(D_DX)); + ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX)); + ins2(c, A_ADDQ, areg(D_CX), areg(D_BX)); + ik = 0; + for (; ik + 8 <= esz; ik += 8) { + ins2(c, A_MOVQ, amem(D_BP, ins_scr + ik), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ik)); + } + if (ik + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BP, ins_scr + ik), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ik)); + ik += 4; + } + if (ik + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BP, ins_scr + ik), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ik)); + ik += 2; + } + if (ik + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BP, ins_scr + ik), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ik)); + ik += 1; + } + ins2(c, A_ADDQ, aimm(24), 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 37daceb8..531b1cde 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1436,7 +1436,7 @@ cexpr(Checker *c, Node *n) return n->type; } /* delete(xs[i]) — single-element slice removal, the - * delete-half of #35 (insert() stays deferred). harec + * 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); @@ -1466,6 +1466,50 @@ cexpr(Checker *c, Node *n) n->lhs->type = ty_err; return n->type; } + /* insert(xs[idx], v) — single-element slice insertion + * before idx, delete()'s twin (the insert-half of #35). + * harec models append/insert in ONE checker arm + * (ref/harec/src/check.c:745 check_expr_append_insert; + * "insert" at :786): operand 1 must be an indexing place + * over a slice; idx == len is a legal end-insert (the + * ref/hare os/exec platform_cmd.ha:86 idiom). The spread + * form insert(xs[i], vs...) and the with-length form + * (harec :821/:837) stay filed on #35 — regex fold-3's + * consumers are all single-value. A range PLACE is not + * Hare (harec asserts ACCESS_INDEX at :784; the form + * never parses there) — rejected, no task cite. */ + if (n->lhs && n->lhs->kind == N_IDENT && + n->lhs->str && strcmp(n->lhs->str, "insert") == 0) { + Node *d = n->list; + if (d == NULL || d->next == NULL || + d->next->next != NULL) + err(c, n->pos, "insert: takes exactly two arguments"); + if (d != NULL) { + (void)cexpr(c, d); + if (d->kind == N_SLICE) + err(c, n->pos, "insert: range place is invalid; operand must be an indexing expression xs[i]"); + else if (d->kind != N_INDEX) + err(c, n->pos, "insert: 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:807 wording; a + * fixed-size [N]T base lands here. */ + if (bt == NULL || bt->kind != TY_SLICE) + err(c, n->pos, "insert must operate on a slice"); + } + if (d->next != NULL) { + if (d->next->kind == N_SPREAD) + err(c, n->pos, "insert: spread form insert(xs[i], vs...) unimplemented (task #35)"); + else + (void)cexpr(c, d->next); + } + } + 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 1993d6b3..214842ae 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10424,14 +10424,15 @@ 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`, `delete` are pseudo-builtins; - // scopedefine them so their use sites resolve. The actual semantics - // live in cgen. + // `len`, `alloc`, `free`, `append`, `delete`, `insert` 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); + scopedefine(c.top, "insert", 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 @@ -12919,7 +12920,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { return tn; }; // delete(xs[i]) — single-element slice removal, the - // delete-half of #35 (insert() deferred). Mirrors + // 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) — @@ -12956,6 +12957,56 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // insert(xs[idx], v) — single-element slice insertion + // before idx, delete()'s twin (the insert-half of + // #35). Mirrors cstage cmd/wcc/check.c's insert arm. + // harec models append/insert in ONE checker arm + // (ref/harec/src/check.c:745 check_expr_append_insert; + // "insert" at :786): operand 1 must be an indexing + // place over a slice; idx == len is a legal + // end-insert. The spread form and the with-length + // form (harec :821/:837) stay filed on #35; a range + // PLACE is not Hare (harec asserts ACCESS_INDEX at + // :784) — rejected, no task cite. + if (streq(callee.str, "insert")) { + let d: *node = e.list; + if (d == nil || d.next == nil || d.next.next != nil) { + cerr("insert: takes exactly two arguments\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("insert: range place is invalid; operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("insert: 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:807 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("insert must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + if (d.next != nil) { + if (d.next.kind == nkind.N_SPREAD) { + cerr("insert: spread form insert(xs[i], vs...) unimplemented (task #35)\n"); + c.errs += 1; + } else { + exprtype(c, d.next, nil); + }; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; }; let nm: str; nm.ptr = nil; nm.len = 0; @@ -25439,6 +25490,250 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, 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 +// contributes grow (rt_ensure) and the whole #34 value-store dispatch +// (scalar / str-slice header / tagged widen / struct fill) verbatim — +// one boxing choke-point, byte-id by construction — landing v at slot +// len-1; the rotate then moves it home through an esz frame scratch. +// The rotate is cgdelete's shift loop in reverse (descending j keeps +// src j behind dst j+1, the safe memmove-up direction) and, like +// delete's, is a same-slice whole-stride raw byte move — no boxing +// exists for any element kind. idx evaluates BEFORE the grow (Hare's +// left-to-right operand order: insert(xs[len(xs)], v) sees the pre-grow +// len); v's evaluation point inherits append's per-kind rules. Bounds +// are implicit (no index check, matching delete). Mirrors cmd/w6c/ +// cgen.c's N_CALL insert arm instruction-for-instruction (rule-10 +// byte-id). +fn cginsert(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + let v: *node = d.next; + // 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 m35c: str = "#35: insert() element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.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 — + // delete'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; + }; + }; + }; + }; + if (!hdr_ok) { + let m35d: str = "#35: insert() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35d.ptr, m35d.len: u64); + os.exit(1); + }; + // Fresh esz-sized slot per SITE (esz varies; an @-name dedup + // would mis-share across element types). Allocated BEFORE the + // cgappend body's own scratch allocs — cstage order. + let insscr: i32 = localalloc(c, "@insscr", esz, nil); + cgexpr(c, d.rhs); // AX = idx + emitline("\tPUSHQ\tAX\n"); + // Desugar in place and route through cgappend: cgen is + // single-pass, base is an lhs node (never on a sibling chain), + // and the checker has already validated this call — the mutation + // is dead after this emission. The callee rename keeps the AST + // consistent with cstage's re-dispatch. + n.lhs.str = "append"; + n.list = base; + base.next = v; + cgappend(c, n); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\tAX, DX\n"); + emitline("\tMOVQ\t8(DX), CX\n"); + emitline("\tSUBQ\t$1, CX\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 ik: i32 = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 1; + }; + emitline("\tMOVQ\t8(DX), AX\n"); + emitline("\tSUBQ\t$2, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let ill: str = mklabel(c, "ins_l"); + let ile: str = mklabel(c, "ins_e"); + emitlabel(ill); + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tCMPQ\tDX, CX\n"); + emitline("\tJL\t"); emitline(ile); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tSUBQ\t$1, (SP)\n"); + emitline("\tJMP\t"); emitline(ill); emitline("\n"); + emitlabel(ile); + emitline("\tMOVQ\t16(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tADDQ\t$24, 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 @@ -25644,6 +25939,18 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // insert(xs[idx], v) — #35 insert-half; mirror of + // cstage cgen.c's N_CALL insert arm. + if (streq(callee.str, "insert")) { + if (n.list != nil) { + if (n.list.next != nil) { + if (n.list.next.next == nil) { + cginsert(c, n); + return; + }; + }; + }; + }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1c01c6d0..c0ab77ee 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5272,6 +5272,250 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, 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 +// contributes grow (rt_ensure) and the whole #34 value-store dispatch +// (scalar / str-slice header / tagged widen / struct fill) verbatim — +// one boxing choke-point, byte-id by construction — landing v at slot +// len-1; the rotate then moves it home through an esz frame scratch. +// The rotate is cgdelete's shift loop in reverse (descending j keeps +// src j behind dst j+1, the safe memmove-up direction) and, like +// delete's, is a same-slice whole-stride raw byte move — no boxing +// exists for any element kind. idx evaluates BEFORE the grow (Hare's +// left-to-right operand order: insert(xs[len(xs)], v) sees the pre-grow +// len); v's evaluation point inherits append's per-kind rules. Bounds +// are implicit (no index check, matching delete). Mirrors cmd/w6c/ +// cgen.c's N_CALL insert arm instruction-for-instruction (rule-10 +// byte-id). +fn cginsert(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + let v: *node = d.next; + // 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 m35c: str = "#35: insert() element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.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 — + // delete'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; + }; + }; + }; + }; + if (!hdr_ok) { + let m35d: str = "#35: insert() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35d.ptr, m35d.len: u64); + os.exit(1); + }; + // Fresh esz-sized slot per SITE (esz varies; an @-name dedup + // would mis-share across element types). Allocated BEFORE the + // cgappend body's own scratch allocs — cstage order. + let insscr: i32 = localalloc(c, "@insscr", esz, nil); + cgexpr(c, d.rhs); // AX = idx + emitline("\tPUSHQ\tAX\n"); + // Desugar in place and route through cgappend: cgen is + // single-pass, base is an lhs node (never on a sibling chain), + // and the checker has already validated this call — the mutation + // is dead after this emission. The callee rename keeps the AST + // consistent with cstage's re-dispatch. + n.lhs.str = "append"; + n.list = base; + base.next = v; + cgappend(c, n); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\tAX, DX\n"); + emitline("\tMOVQ\t8(DX), CX\n"); + emitline("\tSUBQ\t$1, CX\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 ik: i32 = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 1; + }; + emitline("\tMOVQ\t8(DX), AX\n"); + emitline("\tSUBQ\t$2, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let ill: str = mklabel(c, "ins_l"); + let ile: str = mklabel(c, "ins_e"); + emitlabel(ill); + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tCMPQ\tDX, CX\n"); + emitline("\tJL\t"); emitline(ile); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tSUBQ\t$1, (SP)\n"); + emitline("\tJMP\t"); emitline(ill); emitline("\n"); + emitlabel(ile); + emitline("\tMOVQ\t16(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tADDQ\t$24, 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 @@ -5477,6 +5721,18 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // insert(xs[idx], v) — #35 insert-half; mirror of + // cstage cgen.c's N_CALL insert arm. + if (streq(callee.str, "insert")) { + if (n.list != nil) { + if (n.list.next != nil) { + if (n.list.next.next == nil) { + cginsert(c, n); + return; + }; + }; + }; + }; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 8f111660..f7c8fe59 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -97,14 +97,15 @@ 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`, `delete` are pseudo-builtins; - // scopedefine them so their use sites resolve. The actual semantics - // live in cgen. + // `len`, `alloc`, `free`, `append`, `delete`, `insert` 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); + scopedefine(c.top, "insert", 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 @@ -2592,7 +2593,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { return tn; }; // delete(xs[i]) — single-element slice removal, the - // delete-half of #35 (insert() deferred). Mirrors + // 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) — @@ -2629,6 +2630,56 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // insert(xs[idx], v) — single-element slice insertion + // before idx, delete()'s twin (the insert-half of + // #35). Mirrors cstage cmd/wcc/check.c's insert arm. + // harec models append/insert in ONE checker arm + // (ref/harec/src/check.c:745 check_expr_append_insert; + // "insert" at :786): operand 1 must be an indexing + // place over a slice; idx == len is a legal + // end-insert. The spread form and the with-length + // form (harec :821/:837) stay filed on #35; a range + // PLACE is not Hare (harec asserts ACCESS_INDEX at + // :784) — rejected, no task cite. + if (streq(callee.str, "insert")) { + let d: *node = e.list; + if (d == nil || d.next == nil || d.next.next != nil) { + cerr("insert: takes exactly two arguments\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("insert: range place is invalid; operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("insert: 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:807 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("insert must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + if (d.next != nil) { + if (d.next.kind == nkind.N_SPREAD) { + cerr("insert: spread form insert(xs[i], vs...) unimplemented (task #35)\n"); + c.errs += 1; + } else { + exprtype(c, d.next, nil); + }; + }; + }; + 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 c77476d7..3e364a6f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10424,14 +10424,15 @@ 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`, `delete` are pseudo-builtins; - // scopedefine them so their use sites resolve. The actual semantics - // live in cgen. + // `len`, `alloc`, `free`, `append`, `delete`, `insert` 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); + scopedefine(c.top, "insert", 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 @@ -12919,7 +12920,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { return tn; }; // delete(xs[i]) — single-element slice removal, the - // delete-half of #35 (insert() deferred). Mirrors + // 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) — @@ -12956,6 +12957,56 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, tn): *void; return tn; }; + // insert(xs[idx], v) — single-element slice insertion + // before idx, delete()'s twin (the insert-half of + // #35). Mirrors cstage cmd/wcc/check.c's insert arm. + // harec models append/insert in ONE checker arm + // (ref/harec/src/check.c:745 check_expr_append_insert; + // "insert" at :786): operand 1 must be an indexing + // place over a slice; idx == len is a legal + // end-insert. The spread form and the with-length + // form (harec :821/:837) stay filed on #35; a range + // PLACE is not Hare (harec asserts ACCESS_INDEX at + // :784) — rejected, no task cite. + if (streq(callee.str, "insert")) { + let d: *node = e.list; + if (d == nil || d.next == nil || d.next.next != nil) { + cerr("insert: takes exactly two arguments\n"); + c.errs += 1; + }; + if (d != nil) { + exprtype(c, d, nil); + if (d.kind == nkind.N_SLICE) { + cerr("insert: range place is invalid; operand must be an indexing expression xs[i]\n"); + c.errs += 1; + } else { + if (d.kind != nkind.N_INDEX) { + cerr("insert: 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:807 wording; a + // fixed-size [N]T base lands here. + if (u == nil || u.kind != nkind.N_TSLICE) { + cerr("insert must operate on a slice\n"); + c.errs += 1; + }; + }; + }; + if (d.next != nil) { + if (d.next.kind == nkind.N_SPREAD) { + cerr("insert: spread form insert(xs[i], vs...) unimplemented (task #35)\n"); + c.errs += 1; + } else { + exprtype(c, d.next, nil); + }; + }; + }; + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; }; let nm: str; nm.ptr = nil; nm.len = 0; @@ -25439,6 +25490,250 @@ fn cgdelete(c: *cgen, n: *node) void = { emitline("\tADDQ\t$16, 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 +// contributes grow (rt_ensure) and the whole #34 value-store dispatch +// (scalar / str-slice header / tagged widen / struct fill) verbatim — +// one boxing choke-point, byte-id by construction — landing v at slot +// len-1; the rotate then moves it home through an esz frame scratch. +// The rotate is cgdelete's shift loop in reverse (descending j keeps +// src j behind dst j+1, the safe memmove-up direction) and, like +// delete's, is a same-slice whole-stride raw byte move — no boxing +// exists for any element kind. idx evaluates BEFORE the grow (Hare's +// left-to-right operand order: insert(xs[len(xs)], v) sees the pre-grow +// len); v's evaluation point inherits append's per-kind rules. Bounds +// are implicit (no index check, matching delete). Mirrors cmd/w6c/ +// cgen.c's N_CALL insert arm instruction-for-instruction (rule-10 +// byte-id). +fn cginsert(c: *cgen, n: *node) void = { + let d: *node = n.list; // N_INDEX, checker-validated + let base: *node = d.lhs; + let v: *node = d.next; + // 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 m35c: str = "#35: insert() element size unresolved (rule-7)\n"; + os.write(2, m35c.ptr, m35c.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 — + // delete'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; + }; + }; + }; + }; + if (!hdr_ok) { + let m35d: str = "#35: insert() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; + os.write(2, m35d.ptr, m35d.len: u64); + os.exit(1); + }; + // Fresh esz-sized slot per SITE (esz varies; an @-name dedup + // would mis-share across element types). Allocated BEFORE the + // cgappend body's own scratch allocs — cstage order. + let insscr: i32 = localalloc(c, "@insscr", esz, nil); + cgexpr(c, d.rhs); // AX = idx + emitline("\tPUSHQ\tAX\n"); + // Desugar in place and route through cgappend: cgen is + // single-pass, base is an lhs node (never on a sibling chain), + // and the checker has already validated this call — the mutation + // is dead after this emission. The callee rename keeps the AST + // consistent with cstage's re-dispatch. + n.lhs.str = "append"; + n.list = base; + base.next = v; + cgappend(c, n); + if (hdr_lea) { + emitline("\tLEAQ\t"); + } else { + emitline("\tMOVQ\t"); + }; + emitoff(hdr_off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\tAX, DX\n"); + emitline("\tMOVQ\t8(DX), CX\n"); + emitline("\tSUBQ\t$1, CX\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 ik: i32 = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((insscr + ik): i64); + emitline("(BP)\n"); + ik += 1; + }; + emitline("\tMOVQ\t8(DX), AX\n"); + emitline("\tSUBQ\t$2, AX\n"); + emitline("\tPUSHQ\tAX\n"); + let ill: str = mklabel(c, "ins_l"); + let ile: str = mklabel(c, "ins_e"); + emitlabel(ill); + emitline("\tMOVQ\t(SP), CX\n"); + emitline("\tMOVQ\t16(SP), DX\n"); + emitline("\tCMPQ\tDX, CX\n"); + emitline("\tJL\t"); emitline(ile); emitline("\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitdispreg(ik: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg((esz + ik): i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tSUBQ\t$1, (SP)\n"); + emitline("\tJMP\t"); emitline(ill); emitline("\n"); + emitlabel(ile); + emitline("\tMOVQ\t16(SP), CX\n"); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", AX\n"); + emitline("\tIMULQ\tAX, CX\n"); + }; + emitline("\tMOVQ\t8(SP), DX\n"); + emitline("\tMOVQ\t(DX), BX\n"); + emitline("\tADDQ\tCX, BX\n"); + ik = 0; + for (ik + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 8; + }; + if (ik + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 4; + }; + if (ik + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 2; + }; + if (ik + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((insscr + ik): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ik: i64, "BX"); + emitline("\n"); + ik += 1; + }; + emitline("\tADDQ\t$24, 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 @@ -25644,6 +25939,18 @@ fn cgcall(c: *cgen, n: *node) void = { }; }; }; + // insert(xs[idx], v) — #35 insert-half; mirror of + // cstage cgen.c's N_CALL insert arm. + if (streq(callee.str, "insert")) { + if (n.list != nil) { + if (n.list.next != nil) { + if (n.list.next.next == nil) { + cginsert(c, n); + return; + }; + }; + }; + }; }; }; diff --git a/test/wcc/807_insert_elem.c b/test/wcc/807_insert_elem.c new file mode 100644 index 00000000..d6c70a6f --- /dev/null +++ b/test/wcc/807_insert_elem.c @@ -0,0 +1,615 @@ +/* + * 807_insert_elem — cstage and wwstage agree, byte-for-byte and at + * runtime, that `insert(xs[idx], v)` inserts v BEFORE idx: len += 1, + * the tail [idx..oldlen) shifts up one stride, v lands at idx + * (the insert-half of task #35, delete()'s twin; regex fold-3's + * `|`/`?`/`*` compile-arm consumers, regex.ha:347/419/441). + * + * Lowering (BOTH stages, converged byte-identical by construction) + * is a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) + * and the whole #34 value-store dispatch (scalar / str-slice header / + * tagged widen / struct fill) verbatim, one boxing choke-point — + * lands v at slot len-1; then a rotate-right of [idx, len) moves it + * home through an esz frame scratch (@insscr). The rotate is + * delete's shift loop in reverse (descending j, the safe memmove-up + * direction) and is a same-slice whole-stride raw byte move — no + * boxing exists for any element kind. idx evaluates BEFORE the grow + * (Hare's left-to-right operand order — the pregrow_len_idx row pins + * insert(xs[len(xs)-1], v) reading the pre-grow len; an end-insert + * via len(xs) cannot discriminate, see the end_insert_len row). + * + * idx == len is a legal end-insert per harec's shared append/insert + * checker arm (ref/harec/src/check.c:745, "insert" at :786; the + * ref/hare os/exec/platform_cmd.ha:86 `insert(cmd.env[len(...)...]` + * idiom) — pinned by the i64_end / end_insert_len rows. + * + * row | shape | want + * ----------------+----------------------------------------+------ + * i64_front | [7,11,13], insert(xs[0], 5) | 115 + * i64_middle | [7,11,13], insert(xs[1], 5) | 97 + * i64_end | [7,11,13], insert(xs[3], 5) (idx==len, | 157 + * | rotate loop never runs) | + * end_insert_len | insert(xs[len(xs)], v) — idx==len | 13 + * | spelled through a dynamic len read | + * pregrow_len_idx | insert(xs[len(xs)-1], v) — the eval- | 48 + * | order pin: pre-grow idx=1 -> [7,13,11],| + * | post-grow idx=2 -> [7,11,13] | + * i32_narrow | []i32 esz=4 — the MOVL copy tail | 42 + * u16_narrow | []u16 esz=2 — the MOVW copy tail | 43 + * u8_narrow | []u8 esz=1 — the MOVB copy tail | 44 + * empty_insert | insert(xs[0], v) on an empty slice | 15 + * | (grow 0->1; rotate degenerates) | + * str_elem | []str esz=24 — 3-qword headers move | 45 + * | whole | + * struct_elem | []p2t esz=16 — struct body insert + | 46 + * | survivor shift | + * tagged_56b | [](s6|bool) esz=56, value from a TYPED | + * | LOCAL (the regex newinst shape, | + * | ha:347); tag+payload survive the | 217 + * | rotate; match head + raw tail byte | + * tagged_cast | insert(insts[k], (7: inst_split)) — | + * | CAST-rvalue value boxed by append's | 27 + * | widen choke-point (ha:419/441 shape) | + * regex_shape | insert((*p)[i], v) behind *[]i64 — | + * | deref-of-local base, delete's | 171 + * | regex_shape twin | + * insert_in_loop | front-insert 1,2,3,4 -> [4,3,2,1] — | 47 + * | len bookkeeping under iteration + | + * | per-position checks | + * + * Wants stay under 256 (the exit-status byte); the if-ladder rows + * return a distinct small failure code per check, so a wrong element + * pinpoints itself. + * reject_array | insert(t[0], v) on [3]i64 | BUILD_FAIL + * reject_nonindex | insert(xs, v) | BUILD_FAIL + * reject_range | insert(xs[0:1], v) — not Hare (harec | BUILD_FAIL + * | only parses an index place) | + * reject_arity1 | insert(xs[0]) | BUILD_FAIL + * reject_arity3 | insert(xs[0], a, b) — also covers the | BUILD_FAIL + * | harec with-length form (#35) | + * reject_spread | insert(xs[0], vs...) — multi form | BUILD_FAIL + * | deferred, message cites task #35 | + * + * 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 — @insscr sizing) and + * the ins_l/ins_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, esp. the #35 cite on the deferred spread 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). */ +#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[] = { + { "i64_front", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tappend(xs, 13);\n" + "\tinsert(xs[0], 5);\n" + "\treturn (xs[0] + xs[1]*10): i32 + len(xs)*10;\n" + "};\n", + 115, NULL }, + + { "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" + "\tinsert(xs[1], 5);\n" + "\treturn (xs[0] + xs[1]*10): i32 + len(xs)*10;\n" + "};\n", + 97, NULL }, + + { "i64_end", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tappend(xs, 13);\n" + "\tinsert(xs[3], 5);\n" + "\treturn (xs[0] + xs[1]*10): i32 + len(xs)*10;\n" + "};\n", + 157, NULL }, + + /* idx == len spelled THROUGH a dynamic len(xs) read (the + * ref/hare os/exec idiom). NOTE: this row does NOT discriminate + * idx eval order — under the desugar, post-grow idx=newlen + * degenerates the rotate and lands v at the end too; the + * pregrow_len_idx row below is the eval-order pin. */ + { "end_insert_len", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tinsert(xs[len(xs)], 13);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\treturn xs[2]: i32;\n" + "};\n", + 13, NULL }, + + /* THE pre-grow eval-order pin (Hare's left-to-right operand + * order): idx = len(xs)-1 reads the PRE-grow len -> idx=1 -> + * [7,13,11]; a post-grow evaluation would compute idx=2 and + * produce a plain end-insert [7,11,13] — every position is + * checked, so the orders are distinguishable. */ + { "pregrow_len_idx", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 11);\n" + "\tinsert(xs[len(xs) - 1], 13);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0] != 7) { return 2; };\n" + "\tif (xs[1] != 13) { return 3; };\n" + "\tif (xs[2] != 11) { return 4; };\n" + "\treturn 48;\n" + "};\n", + 48, NULL }, + + { "i32_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i32 = [];\n" + "\tappend(xs, 4i32);\n" + "\tappend(xs, 2i32);\n" + "\tinsert(xs[1], 9i32);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0] != 4i32) { return 2; };\n" + "\tif (xs[1] != 9i32) { return 3; };\n" + "\tif (xs[2] != 2i32) { return 4; };\n" + "\treturn 42;\n" + "};\n", + 42, NULL }, + + { "u16_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u16 = [];\n" + "\tappend(xs, 4u16);\n" + "\tappend(xs, 2u16);\n" + "\tinsert(xs[1], 9u16);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0] != 4u16) { return 2; };\n" + "\tif (xs[1] != 9u16) { return 3; };\n" + "\tif (xs[2] != 2u16) { return 4; };\n" + "\treturn 43;\n" + "};\n", + 43, NULL }, + + { "u8_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tappend(xs, 3u8);\n" + "\tinsert(xs[0], 2u8);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0] != 2u8) { return 2; };\n" + "\tif (xs[1] != 5u8) { return 3; };\n" + "\tif (xs[2] != 3u8) { return 4; };\n" + "\treturn 44;\n" + "};\n", + 44, NULL }, + + { "empty_insert", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tinsert(xs[0], 5);\n" + "\treturn xs[0]: i32 + len(xs)*10;\n" + "};\n", + 15, NULL }, + + { "str_elem", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []str = [];\n" + "\tlet a: str = \"abc\";\n" + "\tlet g: str = \"fghi\";\n" + "\tappend(xs, a);\n" + "\tappend(xs, g);\n" + "\tlet b: str = \"de\";\n" + "\tinsert(xs[1], b);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0].len != 3) { return 2; };\n" + "\tif (xs[1].len != 2) { return 3; };\n" + "\tif (xs[2].len != 4) { return 4; };\n" + "\treturn 45;\n" + "};\n", + 45, NULL }, + + { "struct_elem", + "package main;\n" + "type p2t = struct { x: i64, y: i64 };\n" + "export fn main() i32 = {\n" + "\tlet xs: []p2t = [];\n" + "\tappend(xs, p2t { x = 1, y = 2 });\n" + "\tappend(xs, p2t { x = 5, y = 6 });\n" + "\tinsert(xs[1], p2t { x = 3, y = 4 });\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\tif (xs[0].x != 1 || xs[0].y != 2) { return 2; };\n" + "\tif (xs[1].x != 3 || xs[1].y != 4) { return 3; };\n" + "\tif (xs[2].x != 5 || xs[2].y != 6) { return 4; };\n" + "\treturn 46;\n" + "};\n", + 46, NULL }, + + /* 56B element from a TYPED LOCAL — the regex fold-3 newinst + * shape (regex.ha:347: `insert(insts[split_idx], newinst)`). + * Tag qword + 48B payload: seven whole-qword moves through + * @insscr; tag AND payload must survive both the append store + * and the rotate. Readback is split like 804's tagged_56b row: + * match proves tag + head (s.a), and the tail qword of the + * SHIFTED element (f = 18, element 2 byte 48 -> absolute byte + * 160) is read RAW via a *u8 over xs.ptr — the indexed-match + * payload cursor truncates past 32B (pre-existing, task #43). */ + { "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 p2: s6 = s6 { a = 13, b = 14, c = 15, d = 16, e = 17, f = 18 };\n" + "\tappend(xs, p0);\n" + "\tappend(xs, p2);\n" + "\tlet p1: cell = (s6 { a = 7, b = 8, c = 9, d = 10, e = 11, f = 12 });\n" + "\tinsert(xs[1], p1);\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[160]: i32) * 10;\n" + "\treturn r + len(xs)*10;\n" + "};\n", + 217, NULL }, + + /* CAST-rvalue value into a tagged slice — the regex ha:419/441 + * shape `insert(insts[term_start_idx], after_idx: inst_split)`; + * append's widen choke-point boxes it post-grow. */ + { "tagged_cast", + "package main;\n" + "type inst_lit = rune;\n" + "type inst_split = i64;\n" + "type inst = (inst_lit | inst_split);\n" + "export fn main() i32 = {\n" + "\tlet insts: []inst = [];\n" + "\tappend(insts, ('a': inst_lit));\n" + "\tappend(insts, ('b': inst_lit));\n" + "\tinsert(insts[1], (7: inst_split));\n" + "\tif (len(insts) != 3) { return 1; };\n" + "\tlet r: i32 = 0;\n" + "\tmatch (insts[1]) {\n" + "\tcase let z: inst_split => r += (z: i32);\n" + "\tcase => return 2;\n" + "\t};\n" + "\tmatch (insts[2]) {\n" + "\tcase let l: inst_lit => { if ((l: rune) == 'b') { r += 20; }; };\n" + "\tcase => return 3;\n" + "\t};\n" + "\treturn r;\n" + "};\n", + 27, NULL }, + + /* The deref-of-local base — delete's regex_shape twin: the + * header lives behind a *[]i64 param. */ + { "regex_shape", + "package main;\n" + "fn ins_at(i: i64, p: *[]i64, v: i64) void = {\n" + "\tinsert((*p)[i], v);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1);\n" + "\tappend(xs, 3);\n" + "\tins_at(1, &xs, 2);\n" + "\tif (len(xs) != 3) { return 1; };\n" + "\treturn (xs[0] + xs[1]*10 + xs[2]*50): i32;\n" + "};\n", + 171, NULL }, + + /* Pins len bookkeeping under iteration: each front-insert must + * see the grown len and shift the whole accumulated tail; + * per-position checks of [4,3,2,1] make any wrong order, missed + * shift, or stale len visible. */ + { "insert_in_loop", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tlet v: i64 = 1;\n" + "\tfor (v <= 4) {\n" + "\t\tinsert(xs[0], v);\n" + "\t\tv += 1;\n" + "\t};\n" + "\tif (len(xs) != 4) { return 1; };\n" + "\tlet k: i32 = 0;\n" + "\tfor (k < 4) {\n" + "\t\tif (xs[k] != (4 - k): i64) { return 2 + k; };\n" + "\t\tk += 1;\n" + "\t};\n" + "\treturn 47;\n" + "};\n", + 47, NULL }, + + { "reject_array", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: [3]i64 = [1, 2, 3];\n" + "\tinsert(t[0], 9);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert must operate on a slice" }, + + { "reject_nonindex", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tinsert(xs, 6u8);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert: 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" + "\tinsert(xs[0:1], 7u8);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert: range place is invalid" }, + + { "reject_arity1", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tinsert(xs[0]);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert: takes exactly two arguments" }, + + { "reject_arity3", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tappend(xs, 5u8);\n" + "\tinsert(xs[0], 6u8, 7u8);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert: takes exactly two arguments" }, + + { "reject_spread", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet xs: []u8 = [];\n" + "\tlet vs: []u8 = [];\n" + "\tappend(vs, 1u8);\n" + "\tinsert(xs[0], vs...);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "insert: spread form insert(xs[i], vs...) unimplemented (task #35)" }, +}; + +/* 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/inse_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/inse_%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 — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff. Both insert lowerings are written fresh, so this is + * the converged-by-construction gate: any drift in the rotate loop, + * the ins_l/ins_e label sequence, the @insscr frame slot, or the + * reused append body 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/inse_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/inse_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/inse_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, "insert_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, + "insert_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, + "insert_elem: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("insert_elem: %d fixtures passed\n", total); + return 0; +}