From 90479fed68117035b7694bbdc3b1173a464dae89 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 18:54:35 +0900 Subject: [PATCH] =?UTF-8?q?w6c+wwstage:=20reject=20untyped=20empty-[]=20al?= =?UTF-8?q?loc=20=E2=80=94=20require=20context,=20loud=20cannot-infer=20(#?= =?UTF-8?q?3=20B',=20subsumes=20#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty `[]` carries no element type; ww gets it only from a let annotation (the #45 retype). Both stages used to silently default the element to u8, and in value-form positions (return / call-arg) the lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B slice was expected (#5). Now every empty alloc that isn't a let-annotated binding fails to infer with a loud error, aligning ww DOWN to harec (ref/harec/src/check.c:1801-1802). Mechanism: clet / checkletassign flags the single alloc call node that a `let x: []T =` rescues (save/restore around the init walk); the alloc branch errors on any empty alloc that isn't that node. The #45 wide-T retype path is kept. wwstage needs an extra not-yet-stamped guard because resolvewalk re-types value nodes context-free after checkletassign. Tests: negative cstage-driver 729 (table-driven: bare-let, return, call-arg, assignment) + positive @test in attest_pass.ww exercising the u8 and the wide-i32 (#45) paths at runtime. Both stages reject symmetrically; byte-id verified on []u8 and []i32. --- Makefile | 6 + cmd/wcc/check.c | 37 +++++ cmd/wcc/ww.h | 5 + selfhost/cmd/w6c/main.combined.ww | 57 ++++++++ selfhost/cmd/wcc/check.ww | 57 ++++++++ selfhost/cmd/wwdump/main.combined.ww | 57 ++++++++ test/wcc/729_empty_alloc_infer.c | 198 +++++++++++++++++++++++++++ test/wcc/data/attest_pass.ww | 23 ++++ 8 files changed, 440 insertions(+) create mode 100644 test/wcc/729_empty_alloc_infer.c diff --git a/Makefile b/Makefile index e6344e50..c3fe4f73 100644 --- a/Makefile +++ b/Makefile @@ -256,6 +256,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_cgreturn_variant_zero \ $(BIN)/test_arrlit_str_full \ $(BIN)/test_redecl \ + $(BIN)/test_empty_alloc_infer \ $(BIN)/test_struct_field_index \ $(BIN)/test_tagged_return_scratch \ $(BIN)/test_tagged_widen_f64 \ @@ -979,6 +980,11 @@ $(BIN)/test_redecl: test/wcc/712_redecl.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_empty_alloc_infer: test/wcc/729_empty_alloc_infer.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_struct_field_index: test/wcc/713_struct_field_index.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 0260c0d6..416933c9 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1450,6 +1450,22 @@ cexpr(Checker *c, Node *n) !(c->cur_mod && scope_lookup_in_module(c->cur, c->cur_mod, "alloc"))) { (void)cexpr(c, n->list->next); + /* B' (#3): an empty `[]` carries no element type. ww + * gets that type only from a let annotation (the + * #45 retype below). Any other empty alloc — return, + * call-arg, bare `let b = alloc([],n)` — has no hint, + * so refuse to guess instead of defaulting to u8 (was + * a silent u8-default + #5 value-form miscompile). + * Aligns ww DOWN to harec, which errors the same way: + * ref/harec/src/check.c:1801-1802. */ + if (n != c->alloc_octx) { + err(c, n->pos, "cannot infer slice element " + "type for alloc([], n) without a type " + "hint; annotate the binding, e.g. " + "`let x: []T = alloc([], n)`"); + n->lhs->type = ty_err; + return n->type = ty_err; + } Type *st = type_slice(c->a, ty_u8); /* Task #30 — slice form graduates the same way: * `alloc([], n)` now returns `([]T | nomem)`. Slot is @@ -1862,7 +1878,28 @@ clet(Checker *c, Node *n) { Type *declared = n->lhs ? resolve_type(c, n->lhs) : NULL; Type *initt = NULL; + /* B' (#3): a `let x: []T = alloc([], n)` is the one context that + * lets the empty alloc infer its element type (the #45 retype runs + * AFTER cexpr, so flag the exact call node up front; cexpr errors on + * any empty alloc that isn't this one). Peel the same ?/! wrapper + * #45 peels so the flagged node matches. */ + Node *octx = NULL; + if (declared && declared->kind == TY_SLICE && n->rhs) { + Node *call = n->rhs; + if (call->kind == N_TRYPROP || call->kind == N_TRYUNW) + call = call->lhs; + if (call && call->kind == N_CALL && call->lhs + && call->lhs->kind == N_IDENT && call->lhs->str + && strcmp(call->lhs->str, "alloc") == 0 + && call->list && call->list->kind == N_ARRLIT + && call->list->list == NULL + && call->list->next && call->list->next->next == NULL) + octx = call; + } + Node *saved_octx = c->alloc_octx; + c->alloc_octx = octx; if (n->rhs) initt = cexpr(c, n->rhs); + c->alloc_octx = saved_octx; /* `let xs: [_]T = arrlit;` — fill in the inferred length from the * initialiser. `resolve_type` left alen=0 as a sentinel. */ if (declared && declared->kind == TY_ARRAY && declared->alen == 0 && diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index d061c3f9..8fa15776 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -547,6 +547,11 @@ struct Checker { * would shadow an imported module bareword. */ int loops; /* nesting count for break/continue */ int errs; + Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node + * that has let-declared slice context this walk; + * any OTHER empty alloc has no element-type hint + * and must fail to infer (harec check.c:1801). + * Set by clet around its cexpr, NULL elsewhere. */ }; void check_init(Checker*, Arena*); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 39176c46..ed531e09 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10444,6 +10444,12 @@ type checker = struct { file: *node, // N_FILE root; used by checkmoduleshadow // to consult the declaring source's own // `use` directives. + allococtx: *node, // #3/B': the one empty alloc([], n) call node + // with let-declared slice context this walk; + // any other empty alloc has no element hint + // and must fail to infer (harec + // check.c:1801). Set by checkletassign + // around its exprtype, nil elsewhere. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -12810,6 +12816,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (e.list.list == nil) { if (e.list.next != nil) { if (e.list.next.next == nil) { + // B' (#3): an empty `[]` has no element type; + // ww gets it only from a let annotation (the + // #45 retype). Any other empty alloc has no + // hint, so refuse to guess rather than default + // to u8 (was a silent u8-default + #5 value-form + // miscompile). Align DOWN to harec, which errors + // the same way: ref/harec/src/check.c:1801-1802. + // The e.type_ != nil guard is the wwstage-only half: + // resolvewalk re-types every value node context-free + // (L648) AFTER checkletassign already rescued+stamped + // this node, so a stamped node is a rescued one — do + // not re-error it. cstage cexpr is single-visit (clet + // only) so it needs only the allococtx check. + if (e != c.allococtx && e.type_ == nil) { + deffolderr(c, e, "cannot infer slice element type for alloc([], n) without a type hint; annotate the binding, e.g. let x: []T = alloc([], n)"); + return nil; + }; let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = mktname(c, "u8"); let nome: *node = mktname(c, "nomem"); @@ -14322,7 +14345,40 @@ fn checkletassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; // no init // hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT // arms consume it. Plumbing-only at this point. + // B' (#3): a `let x: []T = alloc([], n)` is the one context that lets + // the empty alloc infer its element type (the #45 retype runs AFTER + // exprtype, so flag the exact call node up front; exprtype errors on + // any empty alloc that isn't this one). Peel the same ?/! wrapper #45 + // peels so the flagged node matches. + let octx: *node = nil; + if (n.lhs != nil && n.lhs.kind == nkind.N_TSLICE) { + let inner: *node = n.rhs; + if (inner.kind == nkind.N_TRYPROP) { + inner = inner.lhs; + } else { if (inner.kind == nkind.N_TRYUNW) { + inner = inner.lhs; + }; }; + if (inner != nil && inner.kind == nkind.N_CALL) { + let callee: *node = inner.lhs; + let a0: *node = inner.list; + let a1: *node = nil; + let a2: *node = nil; + if (a0 != nil) { a1 = a0.next; }; + if (a1 != nil) { a2 = a1.next; }; + if (callee != nil + && callee.kind == nkind.N_IDENT + && streq(callee.str, "alloc") + && a0 != nil && a0.kind == nkind.N_ARRLIT + && a0.list == nil + && a1 != nil && a2 == nil) { + octx = inner; + }; + }; + }; + let savedoctx: *node = c.allococtx; + c.allococtx = octx; let src: *node = exprtype(c, n.rhs, nil); + c.allococtx = savedoctx; // Inferred binding (`let r = expr;`, no type annotation). Mirror // cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt) t = // type_default(initt);` and ref/harec/src/check.c:1422 @@ -14779,6 +14835,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = { let empty: str; c.curmod = empty; c.file = nil; + c.allococtx = nil; seedprimitives(c); }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 166da5e6..63ceff1f 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -39,6 +39,12 @@ type checker = struct { file: *node, // N_FILE root; used by checkmoduleshadow // to consult the declaring source's own // `use` directives. + allococtx: *node, // #3/B': the one empty alloc([], n) call node + // with let-declared slice context this walk; + // any other empty alloc has no element hint + // and must fail to infer (harec + // check.c:1801). Set by checkletassign + // around its exprtype, nil elsewhere. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -2405,6 +2411,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (e.list.list == nil) { if (e.list.next != nil) { if (e.list.next.next == nil) { + // B' (#3): an empty `[]` has no element type; + // ww gets it only from a let annotation (the + // #45 retype). Any other empty alloc has no + // hint, so refuse to guess rather than default + // to u8 (was a silent u8-default + #5 value-form + // miscompile). Align DOWN to harec, which errors + // the same way: ref/harec/src/check.c:1801-1802. + // The e.type_ != nil guard is the wwstage-only half: + // resolvewalk re-types every value node context-free + // (L648) AFTER checkletassign already rescued+stamped + // this node, so a stamped node is a rescued one — do + // not re-error it. cstage cexpr is single-visit (clet + // only) so it needs only the allococtx check. + if (e != c.allococtx && e.type_ == nil) { + deffolderr(c, e, "cannot infer slice element type for alloc([], n) without a type hint; annotate the binding, e.g. let x: []T = alloc([], n)"); + return nil; + }; let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = mktname(c, "u8"); let nome: *node = mktname(c, "nomem"); @@ -3917,7 +3940,40 @@ fn checkletassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; // no init // hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT // arms consume it. Plumbing-only at this point. + // B' (#3): a `let x: []T = alloc([], n)` is the one context that lets + // the empty alloc infer its element type (the #45 retype runs AFTER + // exprtype, so flag the exact call node up front; exprtype errors on + // any empty alloc that isn't this one). Peel the same ?/! wrapper #45 + // peels so the flagged node matches. + let octx: *node = nil; + if (n.lhs != nil && n.lhs.kind == nkind.N_TSLICE) { + let inner: *node = n.rhs; + if (inner.kind == nkind.N_TRYPROP) { + inner = inner.lhs; + } else { if (inner.kind == nkind.N_TRYUNW) { + inner = inner.lhs; + }; }; + if (inner != nil && inner.kind == nkind.N_CALL) { + let callee: *node = inner.lhs; + let a0: *node = inner.list; + let a1: *node = nil; + let a2: *node = nil; + if (a0 != nil) { a1 = a0.next; }; + if (a1 != nil) { a2 = a1.next; }; + if (callee != nil + && callee.kind == nkind.N_IDENT + && streq(callee.str, "alloc") + && a0 != nil && a0.kind == nkind.N_ARRLIT + && a0.list == nil + && a1 != nil && a2 == nil) { + octx = inner; + }; + }; + }; + let savedoctx: *node = c.allococtx; + c.allococtx = octx; let src: *node = exprtype(c, n.rhs, nil); + c.allococtx = savedoctx; // Inferred binding (`let r = expr;`, no type annotation). Mirror // cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt) t = // type_default(initt);` and ref/harec/src/check.c:1422 @@ -4374,6 +4430,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = { let empty: str; c.curmod = empty; c.file = nil; + c.allococtx = nil; seedprimitives(c); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 21345c63..59718946 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10444,6 +10444,12 @@ type checker = struct { file: *node, // N_FILE root; used by checkmoduleshadow // to consult the declaring source's own // `use` directives. + allococtx: *node, // #3/B': the one empty alloc([], n) call node + // with let-declared slice context this walk; + // any other empty alloc has no element hint + // and must fail to infer (harec + // check.c:1801). Set by checkletassign + // around its exprtype, nil elsewhere. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -12810,6 +12816,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (e.list.list == nil) { if (e.list.next != nil) { if (e.list.next.next == nil) { + // B' (#3): an empty `[]` has no element type; + // ww gets it only from a let annotation (the + // #45 retype). Any other empty alloc has no + // hint, so refuse to guess rather than default + // to u8 (was a silent u8-default + #5 value-form + // miscompile). Align DOWN to harec, which errors + // the same way: ref/harec/src/check.c:1801-1802. + // The e.type_ != nil guard is the wwstage-only half: + // resolvewalk re-types every value node context-free + // (L648) AFTER checkletassign already rescued+stamped + // this node, so a stamped node is a rescued one — do + // not re-error it. cstage cexpr is single-visit (clet + // only) so it needs only the allococtx check. + if (e != c.allococtx && e.type_ == nil) { + deffolderr(c, e, "cannot infer slice element type for alloc([], n) without a type hint; annotate the binding, e.g. let x: []T = alloc([], n)"); + return nil; + }; let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = mktname(c, "u8"); let nome: *node = mktname(c, "nomem"); @@ -14322,7 +14345,40 @@ fn checkletassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; // no init // hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT // arms consume it. Plumbing-only at this point. + // B' (#3): a `let x: []T = alloc([], n)` is the one context that lets + // the empty alloc infer its element type (the #45 retype runs AFTER + // exprtype, so flag the exact call node up front; exprtype errors on + // any empty alloc that isn't this one). Peel the same ?/! wrapper #45 + // peels so the flagged node matches. + let octx: *node = nil; + if (n.lhs != nil && n.lhs.kind == nkind.N_TSLICE) { + let inner: *node = n.rhs; + if (inner.kind == nkind.N_TRYPROP) { + inner = inner.lhs; + } else { if (inner.kind == nkind.N_TRYUNW) { + inner = inner.lhs; + }; }; + if (inner != nil && inner.kind == nkind.N_CALL) { + let callee: *node = inner.lhs; + let a0: *node = inner.list; + let a1: *node = nil; + let a2: *node = nil; + if (a0 != nil) { a1 = a0.next; }; + if (a1 != nil) { a2 = a1.next; }; + if (callee != nil + && callee.kind == nkind.N_IDENT + && streq(callee.str, "alloc") + && a0 != nil && a0.kind == nkind.N_ARRLIT + && a0.list == nil + && a1 != nil && a2 == nil) { + octx = inner; + }; + }; + }; + let savedoctx: *node = c.allococtx; + c.allococtx = octx; let src: *node = exprtype(c, n.rhs, nil); + c.allococtx = savedoctx; // Inferred binding (`let r = expr;`, no type annotation). Mirror // cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt) t = // type_default(initt);` and ref/harec/src/check.c:1422 @@ -14779,6 +14835,7 @@ export fn checkinit(c: *checker, tc: *tctx) void = { let empty: str; c.curmod = empty; c.file = nil; + c.allococtx = nil; seedprimitives(c); }; diff --git a/test/wcc/729_empty_alloc_infer.c b/test/wcc/729_empty_alloc_infer.c new file mode 100644 index 00000000..31465119 --- /dev/null +++ b/test/wcc/729_empty_alloc_infer.c @@ -0,0 +1,198 @@ +/* + * 729_empty_alloc_infer — check: an untyped empty `alloc([], n)` must + * loudly fail to infer its slice element type instead of silently + * defaulting to []u8 (task #3 / B', subsumes #5). + * + * Pre-fix: cmd/wcc/check.c's alloc-slice branch pinned the element type + * to u8 with no context. `let b = alloc([], n)!` (no annotation) became + * []u8, and the value-form lowerings `return alloc([], n)!` / + * `f(alloc([], n))` SILENTLY MISCOMPILED (malloc(8) ignoring n; a 16B + * *u8|nomem where a 24B slice was expected) — that was bug #5. + * + * Post-fix: ww aligns DOWN to harec, which refuses to guess — + * "Cannot infer array type from context" (ref/harec/src/check.c:1801). + * The ONLY context that supplies the element type is the let annotation + * (the #45 retype), so an annotated alloc of ANY element type still + * compiles; every other empty alloc errors at check time. + * + * Cstage-driver negative test, same shape as 712_redecl. The wwstage + * twin (selfhost/cmd/wcc/check.ww, exprtype alloc-slice branch + + * checkletassign context-flag) rejects symmetrically; it is validated by + * the 990-997 byte-id gates rebuilding the *_ww tools from check.ww. The + * positive @test (annotated []u8 / []i32 alloc) rides attest_pass.ww + * (910/997, dual-stage). + * + * row | kind | what it pins + * ---------------------+-------------+------------------------------ + * neg_bare_let | build fails | no-annotation u8 default gone + * neg_return | build fails | #5 value-form return + * neg_call_arg | build fails | #5 value-form call-arg + * neg_assign | build fails | re-bind has no annotation hint + * pos_annotated_u8 | exit=16 | let []u8 still infers + * pos_annotated_wide | exit=5 | let []i32 (#45 retype) infers + */ +#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; +} + +/* + * kind == 0: negative — build must fail (any nonzero exit). + * kind == 1: positive — build must succeed AND binary exits with `want`. + */ +struct row { const char *label; int kind; const char *src; int want; }; + +static const struct row rows[] = { + /* neg: bare let, no annotation — was a silent []u8 default. */ + { "neg_bare_let", 0, + "fn main() i32 = {\n" + " let b = alloc([], 8u64)!;\n" + " return b.len: i32;\n" + "};\n", + 0 }, + + /* neg: value-form return — #5 silent miscompile. */ + { "neg_return", 0, + "fn mk() []u8 = { return alloc([], 8u64)!; };\n" + "fn main() i32 = { let b: []u8 = mk(); return b.len: i32; };\n", + 0 }, + + /* neg: value-form call-arg — #5 silent miscompile. */ + { "neg_call_arg", 0, + "fn g(x: []u8) i32 = { return x.len: i32; };\n" + "fn main() i32 = { return g(alloc([], 8u64)!); };\n", + 0 }, + + /* neg: assignment target supplies NO context (only the let + * annotation does; #45). A re-bind `x = alloc([], n)` must error + * too — top-down hint threading to assign is A', out of scope. */ + { "neg_assign", 0, + "fn main() i32 = {\n" + " let x: []u8 = alloc([], 4u64)!;\n" + " x = alloc([], 8u64)!;\n" + " return x.len: i32;\n" + "};\n", + 0 }, + + /* pos: let annotation supplies the element type (u8). alloc([], n) + * is Hare's len=0 / cap=n empty slice, so write into the cap-backed + * memory and read back — also proves the u8 element stride. */ + { "pos_annotated_u8", 1, + "import rt;\n" + "fn main() i32 = {\n" + " let b: []u8 = alloc([], 8u64)!;\n" + " b[0] = 7u8;\n" + " b[3] = 9u8;\n" + " return (b[0]: i32) + (b[3]: i32);\n" + "};\n", + 16 }, + + /* pos: let annotation with a wide element — the #45 retype. The i32 + * stride (4B) must drive indexing, not the u8 default. */ + { "pos_annotated_wide", 1, + "import rt;\n" + "fn main() i32 = {\n" + " let w: []i32 = alloc([], 4u64)!;\n" + " w[2] = 5i32;\n" + " return w[2];\n" + "};\n", + 5 }, +}; + +static int +run_row(const char *driver, const struct row *r, int i) +{ + char src[128], tmpdir[128], cmd[2048]; + snprintf(src, sizeof src, "/tmp/wcealloc_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcealloc_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + char combined[256]; + snprintf(combined, sizeof combined, "/tmp/wcealloc_%d_%d.combined.ww", + getpid(), i); + + if (r->kind == 0) { + /* Negative — build must fail. */ + snprintf(cmd, sizeof cmd, + "cd %s && %s build %s >/dev/null 2>&1", tmpdir, driver, src); + int rc = runwait(cmd); + if (rc == 0) { + fprintf(stderr, + "ealloc[%s]: build unexpectedly succeeded\n", + r->label); + unlink(outbin); + } + unlink(src); + unlink(combined); + rmdir(tmpdir); + return rc == 0 ? -1 : 0; + } + + /* Positive — build (with rt linked via `ww run`) then check exit. */ + snprintf(cmd, sizeof cmd, "%s run %s >/dev/null 2>&1", driver, src); + int got = runwait(cmd); + unlink(src); + unlink(outbin); + unlink(combined); + rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "ealloc[%s]: exit=%d want=%d\n", + r->label, got, r->want); + return -1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int fail = 0; + for (int i = 0; i < n; i++) { + if (run_row(cdrv, &rows[i], i) != 0) fail++; + } + + if (fail) { + fprintf(stderr, "ealloc: %d/%d row(s) failed\n", fail, n); + return 1; + } + printf("ealloc: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wcc/data/attest_pass.ww b/test/wcc/data/attest_pass.ww index fc602be3..2541c1e4 100644 --- a/test/wcc/data/attest_pass.ww +++ b/test/wcc/data/attest_pass.ww @@ -2,6 +2,8 @@ package data; +import rt; // #3/B': check_empty_alloc_annotated calls alloc → rt_malloc. + type point = struct { x: i32, y: i32 }; @test fn check_add() void = { @@ -104,3 +106,24 @@ type point = struct { x: i32, y: i32 }; let _: i32 = 1 / 0; }; }; + +// #3/B': the let-annotation context path keeps `alloc([], n)` inferring +// its element type for ANY T (u8 default + non-u8 #45 retype). The bare / +// return / arg empty allocs that lost the silent u8 default are covered by +// the negative test (test/wcc/729). Here we pin that annotated allocs of +// both an 8-bit and a wide element still compile and index correctly. +// alloc([], n) is Hare's len=0 / cap=n empty slice, so we write into the +// cap-backed memory and read it back (ref/hare expects len 0, not n). +@test fn check_empty_alloc_annotated() void = { + let b: []u8 = alloc([], 8u64)!; + b[0] = 7u8; + b[3] = 9u8; + if (b[0] != 7 || b[3] != 9) { + let _: i32 = 1 / 0; + }; + let w: []i32 = alloc([], 4u64)!; + w[2] = 5i32; + if (w[2] != 5) { + let _: i32 = 1 / 0; + }; +};