From c6bec0914ddba57fede6e29b69ad460349e0d884 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 19:02:35 +0900 Subject: [PATCH] ww: implement package initialization --- cmd/w6c/cgen.c | 283 ++++- cmd/w6c/gc.h | 3 + cmd/w6c/main.c | 262 ++++- cmd/w6c/wwi.c | 11 +- cmd/wcc/check.c | 744 ++++++++++++- cmd/wcc/parse.c | 10 +- cmd/wcc/ww.h | 9 + cmd/ww/main.c | 1273 ++++++++++++++++----- lib/bufio/stream.ww | 2 +- lib/math/random/random.ww | 2 +- lib/ww/syntax/ast.ww | 10 +- lib/ww/syntax/decl.ww | 16 +- selfhost/cmd/w6a/main.ww | 2 +- selfhost/cmd/w6a/parse.ww | 2 +- selfhost/cmd/w6c/main.ww | 327 +++++- selfhost/cmd/wcc/api.ww | 16 +- selfhost/cmd/wcc/cgen.ww | 59 +- selfhost/cmd/wcc/cgendecl.ww | 19 +- selfhost/cmd/wcc/cgenexpr.ww | 8 +- selfhost/cmd/wcc/cgenstmt.ww | 292 +++++ selfhost/cmd/wcc/check.ww | 882 ++++++++++++++- selfhost/cmd/wcc/wwi.ww | 49 +- selfhost/cmd/ww/main.ww | 2004 ++++++++++++++++++++++++---------- selfhost/cmd/wwdump/main.ww | 3 +- 24 files changed, 5279 insertions(+), 1009 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 7198025b..a474bc94 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1189,6 +1189,7 @@ ffi_collect(Cg *c, Node *file) if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; + if (d->initfn || d->initsynthetic) continue; for (Node *a = d->attr; a; a = a->next) { if (a->kind != N_ATTR) continue; if (strcmp(a->str, "symbol") != 0) continue; @@ -1487,6 +1488,7 @@ mod_collect(Cg *c, Node *file) use_map = u; continue; } + if (d->initfn || d->initsynthetic) continue; int isfn = (d->kind == N_FNDECL); int track = isfn || (d->kind == N_TYPEDECL) || (d->kind == N_DEF) || (d->kind == N_LET); @@ -4196,6 +4198,238 @@ cg_arrlit_fill_bp(Cg *c, Local **locals, Type *lu, Node *arrlit, int off) } } +/* Package-variable helpers publish their local only after the complete value + * has been evaluated. Composite literals need a memory-directed path here: + * the ordinary tuple cursor has finite register capacity, nested aggregates + * are not scalar expressions, and an ordinary slice literal points at stack + * storage that dies when the helper returns. This path is gated by the + * checker-owned N_LET.initsynthetic bit and therefore cannot perturb normal + * user locals or any statically emitted initializer. */ +static void cg_init_value_bp(Cg*, Local**, Type*, Node*, int); + +static void +cg_init_zero_bp(Cg *c, int off, int sz) +{ + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + int k = 0; + for (; k + 8 <= sz; k += 8) + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + k)); + if (k + 4 <= sz) { + ins2(c, A_MOVL, areg(D_AX), amem(D_BP, off + k)); + k += 4; + } + if (k + 2 <= sz) { + ins2(c, A_MOVW, areg(D_AX), amem(D_BP, off + k)); + k += 2; + } + if (k + 1 <= sz) + ins2(c, A_MOVB, areg(D_AX), amem(D_BP, off + k)); +} + +static void +cg_init_copy_bp(Cg *c, int src, int dst, int sz) +{ + ins2(c, A_LEAQ, amem(D_BP, src), areg(D_SI)); + ins2(c, A_LEAQ, amem(D_BP, dst), areg(D_BX)); + cg_aggcopy(c, sz); +} + +static Node * +cg_init_strip_cast(Node *n) +{ + while (n != NULL && n->kind == N_CAST) n = n->lhs; + return n; +} + +static Tfield * +cg_init_field(Type *u, const char *name) +{ + for (Tfield *f = u ? u->fields : NULL; f; f = f->next) + if (name != NULL && f->name != NULL + && strcmp(name, f->name) == 0) + return f; + return NULL; +} + +static void +cg_init_array_bp(Cg *c, Local **locals, Type *u, Node *lit, int off) +{ + Type *et = u->sub; + int esz = et ? (int)type_chase_named(et)->size : 1; + int total = (int)u->alen; + cg_init_zero_bp(c, off, (int)u->size); + int idx = 0, lastoff = 0; + for (Node *e = lit->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str != NULL + && strcmp(e->str, "...") == 0) { + if (idx == 0) + fatal("package initializer array repeat has no value"); + while (idx < total) { + cg_init_copy_bp(c, lastoff, off + idx * esz, esz); + idx++; + } + return; + } + if (idx >= total) + fatal("package initializer array literal exceeds destination"); + lastoff = off + idx * esz; + cg_init_value_bp(c, locals, et, e, lastoff); + idx++; + } +} + +static void +cg_init_struct_bp(Cg *c, Local **locals, Type *u, Node *lit, int off) +{ + cg_init_zero_bp(c, off, (int)u->size); + for (Node *e = lit->list; e; e = e->next) { + Tfield *f = cg_init_field(u, e->str); + if (f == NULL) continue; /* checker owns unknown/ellipsis errors */ + cg_init_value_bp(c, locals, f->type, e->lhs, + off + (int)f->offset); + } +} + +static void +cg_init_tuple_bp(Cg *c, Local **locals, Type *u, Node *lit, int off) +{ + cg_init_zero_bp(c, off, (int)u->size); + Node *e = lit->list; + int eoff = 0; + for (Tparam *p = u->params; p && e; p = p->next, e = e->next) { + cg_init_value_bp(c, locals, p->type, e, off + eoff); + eoff += tuple_eslot(p->type); + } +} + +static void +cg_init_slice_bp(Cg *c, Local **locals, Type *u, Node *lit, int off) +{ + if (lit->linksym == NULL || lit->linksym[0] == '\0') + fatal("runtime package slice literal has no canonical backing"); + Type *at = type_chase_named(lit->type); + if (at == NULL || at->kind != TY_ARRAY || at->sub == NULL) + fatal("runtime package slice literal has no backing type"); + int count = (int)at->alen; + int bsz = (int)at->size; + if (bsz != 0) { + int scr = local_alloc(c, locals, "@initbacking", bsz, cg_frame); + cg_init_array_bp(c, locals, at, lit, scr); + ins2(c, A_LEAQ, amem(D_BP, scr), areg(D_SI)); + ins2(c, A_LEAQ, asym(lit->linksym), areg(D_BX)); + cg_aggcopy(c, bsz); + } + ins2(c, A_LEAQ, asym(lit->linksym), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + ins2(c, A_MOVQ, aimm(count), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, aimm(count), amem(D_BP, off + 16)); +} + +static void +cg_init_aggregate_expr_bp(Cg *c, Local **locals, Type *t, Type *u, + Node *expr, int off) +{ + int sz = (int)u->size; + if (expr->kind == N_CALL && cg_sret_retsize(t) > 0) { + cg_sret_dest_off = off; + cgexpr(c, expr, *locals); + cg_sret_dest_off = 0; + return; + } + if (u->kind == TY_TUPLE && cg_sret_retsize(t) == 0) { + cgexpr(c, expr, *locals); + int gpcur = 0, ssecur = 0, eoff = 0, f32; + for (Tparam *p = u->params; p; p = p->next) { + int isfloat = fld_isfloat(p->type, &f32); + tuple_store(c, p->type, gpcur, ssecur, off + eoff); + if (isfloat) ssecur++; + else gpcur += tuple_eslot(p->type) / 8; + eoff += tuple_eslot(p->type); + } + return; + } + if (expr->kind == N_CALL && u->kind == TY_STRUCT) { + int cls[2], nb = struct_float_class(u, cls); + if (nb > 0) { + cgexpr(c, expr, *locals); + int gp = 0, sse = 0; + for (int i = 0; i < nb; i++) { + if (cls[i]) { + ins2(c, A_MOVSD, areg(tuple_sse_seq[sse++]), + amem(D_BP, off + i * 8)); + } else { + ins2(c, A_MOVQ, areg(tuple_rseq[gp++]), + amem(D_BP, off + i * 8)); + } + } + return; + } + } + if (expr->kind == N_CALL && sz <= 24) { + cgexpr(c, expr, *locals); + cg_agg_reg_store(c, locals, D_BP, off, sz, 1); + return; + } + if (aggarg_srcaddr(c, expr, D_SI, *locals)) { + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); + cg_aggcopy(c, sz); + return; + } + fatal("package initializer aggregate expression shape unsupported"); +} + +static void +cg_init_value_bp(Cg *c, Local **locals, Type *t, Node *expr, int off) +{ + Type *u = type_chase_named(t); + Node *r = cg_init_strip_cast(expr); + if (u == NULL || r == NULL) + fatal("package initializer value has no type or expression"); + if (u->kind == TY_ARRAY && r->kind == N_ARRLIT) { + cg_init_array_bp(c, locals, u, r, off); + return; + } + if (u->kind == TY_STRUCT && r->kind == N_STRUCTLIT) { + cg_init_struct_bp(c, locals, u, r, off); + return; + } + if (u->kind == TY_TUPLE && r->kind == N_TUPLE) { + cg_init_tuple_bp(c, locals, u, r, off); + return; + } + if (u->kind == TY_SLICE && r->kind == N_ARRLIT) { + cg_init_slice_bp(c, locals, u, r, off); + return; + } + if (u->kind == TY_TAGGED) { + cg_widen_tagged_store(c, locals, u, expr, D_BP, off, + (int)u->size); + return; + } + if (type_isstr(t) || u->kind == TY_SLICE) { + cgexpr(c, expr, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); + return; + } + if (u->kind == TY_ARRAY || u->kind == TY_STRUCT + || u->kind == TY_TUPLE) { + cg_init_aggregate_expr_bp(c, locals, t, u, r, off); + return; + } + cgexpr(c, expr, *locals); + int f32 = 0; + if (fld_isfloat(t, &f32)) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), + amem(D_BP, off)); + return; + } + int sz = (int)u->size; + if (sz != 1 && sz != 2 && sz != 4) sz = 8; + ins2(c, fldstoreop(t, sz), areg(D_AX), amem(D_BP, off)); +} + /* cg_dotfield_combine — single-dot field compound combine. The old * field value is in BX, the rhs in AX; the result is left in AX. * PLUSEQ/MINUSEQ preserve the pre-#34 emission (byte-id); the other 8 @@ -10941,7 +11175,10 @@ cgexpr(Cg *c, Node *n, Local *locals) else ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); } - if (n->lhs->kind == N_IDENT) { + if (n->lhs->kind == N_IDENT && n->lhs->refdecl != NULL + && n->lhs->refdecl->linksym != NULL) { + ins1(c, A_CALL, asym(n->lhs->refdecl->linksym)); + } else if (n->lhs->kind == N_IDENT) { /* If the callee names a local variable holding a * function pointer, load it and call indirect. Without * this check `CALL fp(SB)` is emitted as if `fp` were @@ -13506,6 +13743,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int off = letloc->off; int isf = cg_isfloat(lt); int isf32 = type_isf32(lt); + Node *initlit = cg_init_strip_cast(n->rhs); + if (n->initsynthetic && initlit != NULL + && (initlit->kind == N_ARRLIT + || initlit->kind == N_STRUCTLIT + || initlit->kind == N_TUPLE)) { + cg_init_value_bp(c, locals, lt, n->rhs, off); + goto letlink; + } /* alloc([], n) initialiser for a slice local: allocate * n*esize bytes, build the {ptr, 0, n} header in the slot. * Element size comes from the declared slice type. @@ -15843,7 +16088,9 @@ cgfn(Cg *c, FILE *out, Node *fn) * !sep_isdep — under sep a dep unit's main is imported==0 (path- * carrying `//ww:module-reset`, #57); only the root/link-entry unit * (wwiout==NULL, #69) keeps the bare label. */ - if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported + if (fn->linksym != NULL) + text->to = asym(fn->linksym); + else if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported && !c->sep_isdep) text->to = asym("main"); else @@ -16098,6 +16345,15 @@ cgfn(Cg *c, FILE *out, Node *fn) fatal("#38b: >48B tagged param mixed with stack-spilled " "params unwired"); + /* The package coordinator owns one complete dependency-first schedule. + * Only the selected source/generated main invokes it; package tasks never + * recurse through dependencies, so independent-package tie order cannot + * accidentally become linker or DFS order. */ + if (c->init_dispatch_symbol != NULL && fn->str != NULL + && strcmp(fn->str, "main") == 0 && !fn->imported + && !c->sep_isdep) + ins1(c, A_CALL, asym(c->init_dispatch_symbol)); + /* Iterate the fn body's statements directly rather than dispatching * the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores * the locals head to scope inner shadows, but the function body is @@ -17674,6 +17930,28 @@ let_pre_intern(Cg *c, Node *file) c->cur_source = save_source; } +static void +emit_init_backings(FILE *out, Node *n) +{ + for (; n; n = n->next) { + if (n->kind == N_ARRLIT && n->linksym != NULL + && n->linksym[0] != '\0') { + Type *u = type_chase_named(n->type); + if (u == NULL || u->kind != TY_ARRAY) + fatal("runtime package slice backing has no array type"); + emit_data_row_zero(out, "DATAW", n->linksym, + u->size == 0 ? 1 : (int)u->size); + } + emit_init_backings(out, n->attr); + emit_init_backings(out, n->lhs); + emit_init_backings(out, n->rhs); + emit_init_backings(out, n->cond); + emit_init_backings(out, n->body); + emit_init_backings(out, n->els); + emit_init_backings(out, n->list); + } +} + void cg_file(Cg *c, FILE *out, Node *file) { @@ -17697,5 +17975,6 @@ cg_file(Cg *c, FILE *out, Node *file) let_pre_intern(c, file); emit_data(c, out); emit_defs(c, out, file); + emit_init_backings(out, file->list); emit_lets(c, out, file); } diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index d603108b..d7795c6a 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -60,6 +60,9 @@ struct Cg { * the bare-`main` carve-out: a dep's `fn main` must * mangle on its path like any decl; only the root * entry stays bare. */ + const char *init_dispatch_symbol; /* package-mode entry hook; the + * root-owned dispatcher runs the complete reachable + * package-init schedule before source main/test main. */ }; /* cgen.c */ diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index c549d31d..451bd3a3 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -1,7 +1,167 @@ #include "gc.h" +#include +#include #include #include #include +#include +#include + +struct publication { + const char *dst; + char *stage; + char *backup; + int had_old; + int installed; +}; + +static char * +publish_path(const char *dst, const char *kind) +{ + int n = snprintf(NULL, 0, "%s.w6c.%ld.%s", dst, (long)getpid(), kind); + if (n < 0 || (size_t)n == (size_t)-1) return NULL; + char *p = malloc((size_t)n + 1); + if (p == NULL) return NULL; + if (snprintf(p, (size_t)n + 1, "%s.w6c.%ld.%s", dst, + (long)getpid(), kind) != n) { + free(p); + return NULL; + } + return p; +} + +static int +materialize_stream(FILE *src, struct publication *p) +{ + p->stage = publish_path(p->dst, "new"); + p->backup = publish_path(p->dst, "old"); + if (p->stage == NULL || p->backup == NULL) { + fputs("w6c: out of memory\n", stderr); + return -1; + } + int fd = open(p->stage, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd < 0) { + fprintf(stderr, "w6c: cannot stage %s\n", p->dst); + return -1; + } + FILE *out = fdopen(fd, "wb"); + if (out == NULL) { + close(fd); + unlink(p->stage); + return -1; + } + int bad = fflush(src) != 0 || fseek(src, 0, SEEK_SET) != 0; + unsigned char buf[65536]; + while (!bad) { + size_t n = fread(buf, 1, sizeof buf, src); + if (n != 0 && fwrite(buf, 1, n, out) != n) bad = 1; + if (n < sizeof buf) { + if (ferror(src)) bad = 1; + break; + } + } + if (fclose(out) != 0) bad = 1; + if (bad) { + unlink(p->stage); + fprintf(stderr, "w6c: cannot stage %s\n", p->dst); + return -1; + } + return 0; +} + +static void +publication_free(struct publication *p) +{ + free(p->backup); + free(p->stage); +} + +/* A publication rollback name is occupied by any terminal directory entry, + * including a dangling symlink. Never follow it while deciding whether the + * compiler may park an existing destination there. */ +static int +path_exists_nofollow(const char *path) +{ + struct stat st; + if (lstat(path, &st) == 0) return 1; + return errno == ENOENT ? 0 : -1; +} + +static int +path_is_regular_nofollow(const char *path) +{ + struct stat st; + return lstat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +/* Publish assembly and interface as one rollback group. Named output bytes + * never change before checking and lowering have both succeeded. */ +static int +publish_all(struct publication *p, int n) +{ + for (int i = 0; i < n; i++) { + if (!path_is_regular_nofollow(p[i].stage)) { + fprintf(stderr, "w6c: publication stage is not a regular file for %s\n", + p[i].dst); + goto rollback; + } + struct stat st; + if (lstat(p[i].dst, &st) == 0) { + if (!S_ISREG(st.st_mode)) { + fprintf(stderr, + "w6c: publication destination is not a regular file: %s\n", + p[i].dst); + goto rollback; + } + } else if (errno != ENOENT) { + fprintf(stderr, "w6c: cannot inspect publication destination %s\n", + p[i].dst); + goto rollback; + } + } + for (int i = 0; i < n; i++) { + int exists = path_exists_nofollow(p[i].backup); + if (exists != 0) { + if (exists < 0) + fprintf(stderr, "w6c: cannot inspect publication backup for %s\n", + p[i].dst); + else + fprintf(stderr, "w6c: publication backup exists for %s\n", + p[i].dst); + goto rollback; + } + } + for (int i = 0; i < n; i++) { + if (rename(p[i].dst, p[i].backup) == 0) + p[i].had_old = 1; + else if (errno != ENOENT) { + fprintf(stderr, "w6c: cannot preserve %s\n", p[i].dst); + goto rollback; + } + } + for (int i = 0; i < n; i++) { + if (rename(p[i].stage, p[i].dst) != 0) { + fprintf(stderr, "w6c: cannot publish %s\n", p[i].dst); + goto rollback; + } + p[i].installed = 1; + } + /* Installation is the commit point. Backup cleanup cannot truthfully + * turn a completely installed pair into a rejected compilation. */ + for (int i = 0; i < n; i++) + if (p[i].had_old && unlink(p[i].backup) != 0) { + fprintf(stderr, "w6c: cannot remove backup for %s\n", p[i].dst); + } + return 0; + +rollback: + for (int i = n - 1; i >= 0; i--) { + if (p[i].installed) (void)unlink(p[i].dst); + if (p[i].had_old) (void)rename(p[i].backup, p[i].dst); + if (p[i].stage != NULL) (void)unlink(p[i].stage); + } + return -1; +} static int slurp(const char *path, char **outbuf, u64 *outlen) @@ -148,8 +308,10 @@ bind_import_names(Node *list, struct importin *imports, int nimports, } if (name != NULL) { u->usepkgname = name; - u->str = u->usealias ? u->usealias : name; - u->strlen = strlen(u->str); + if (!u->useblank) { + u->str = u->usealias ? u->usealias : name; + u->strlen = strlen(u->str); + } } else if (!u->imported) { fprintf(stderr, "w6c: import %s has no declared package name in direct export data\n", @@ -159,8 +321,10 @@ bind_import_names(Node *list, struct importin *imports, int nimports, /* A closure-only import need not contribute declarations to this * interface. Keep it canonical-path keyed without reinstalling * the historical path-leaf qualifier. */ - u->str = u->usealias ? u->usealias : u->usepath; - u->strlen = strlen(u->str); + if (!u->useblank) { + u->str = u->usealias ? u->usealias : u->usepath; + u->strlen = strlen(u->str); + } } } return 0; @@ -184,6 +348,8 @@ main(int argc, char **argv) const char *wwiout = NULL; /* -I : M2 export-data producer */ const char *testsupport = NULL; const char *testtarget = NULL; + const char *packageinit = NULL; + const char *initdispatch = NULL; int testmode = 0; int testpackage = 0; int commandpackage = 0; @@ -215,6 +381,18 @@ main(int argc, char **argv) commandpackage = 1; } else if (strcmp(a, "--entry") == 0) { entrymode = 1; + } else if (strcmp(a, "--package-init-symbol") == 0) { + if (i + 1 >= argc) { + fputs("w6c: --package-init-symbol requires arg\n", stderr); + return 2; + } + packageinit = argv[++i]; + } else if (strcmp(a, "--init-dispatch-symbol") == 0) { + if (i + 1 >= argc) { + fputs("w6c: --init-dispatch-symbol requires arg\n", stderr); + return 2; + } + initdispatch = argv[++i]; } else if (strcmp(a, "--test-support-module") == 0) { if (i + 1 >= argc) { fputs("w6c: --test-support-module requires arg\n", stderr); @@ -256,10 +434,14 @@ main(int argc, char **argv) } } if (src == NULL) { - fputs("usage: w6c [-T|--test-package] [--command-package] [--entry] [--test-target-package path] [-c] [-I out.wwi] " + fputs("usage: w6c [-T|--test-package] [--command-package] [--entry] [--package-init-symbol symbol] [--init-dispatch-symbol symbol] [--test-target-package path] [-c] [-I out.wwi] " "[--import path dep.wwi]... [--import-map source path]... [-o out.s] file.ww\n", stderr); return 2; } + if (out != NULL && wwiout != NULL && strcmp(out, wwiout) == 0) { + fputs("w6c: assembly and interface outputs must be distinct\n", stderr); + return 2; + } if (nimports > 0 && !sepmode) { fputs("w6c: --import requires -c\n", stderr); return 2; @@ -272,6 +454,19 @@ main(int argc, char **argv) fputs("w6c: --entry, --test-package, and --command-package require -c\n", stderr); return 2; } + if ((packageinit != NULL || initdispatch != NULL) && !sepmode) { + fputs("w6c: package initialization symbols require -c\n", stderr); + return 2; + } + if (packageinit != NULL && packageinit[0] == '\0') { + fputs("w6c: --package-init-symbol is empty\n", stderr); + return 2; + } + if (initdispatch != NULL && (initdispatch[0] == '\0' || !entrymode)) { + fputs("w6c: --init-dispatch-symbol requires --entry and a non-empty symbol\n", + stderr); + return 2; + } if (testmode && testpackage) { fputs("w6c: -T and --test-package are mutually exclusive\n", stderr); return 2; @@ -427,27 +622,33 @@ main(int argc, char **argv) if (testsupport != NULL) c.test_module = testsupport; c.test_target = testtarget; c.sep_mode = sepmode; + c.package_init_symbol = packageinit; check_file(&c, file); if (c.errs) return 1; - /* M2 export-data: write the `.wwi` after a clean check, before cgen. - * Dead on the live path (no existing invocation passes -I); the - * producer's check_exported_type may reject a dangling export. */ + /* Anonymous streams keep every named destination untouched until checking, + * export writing, and lowering have all succeeded. A cgen fatal exits with + * only kernel-owned anonymous files open. */ + FILE *wf = NULL; if (wwiout) { - FILE *wf = fopen(wwiout, "wb"); + wf = tmpfile(); if (wf == NULL) { - fprintf(stderr, "w6c: cannot open %s\n", wwiout); + fprintf(stderr, "w6c: cannot stage %s\n", wwiout); + return 1; + } + if (wwi_emit(&c, wf, file) != 0 || fflush(wf) != 0 + || ferror(wf)) { + fclose(wf); return 1; } - if (wwi_emit(&c, wf, file) != 0) { fclose(wf); return 1; } - fclose(wf); } FILE *of = stdout; if (out) { - of = fopen(out, "wb"); + of = tmpfile(); if (of == NULL) { - fprintf(stderr, "w6c: cannot open %s\n", out); + fprintf(stderr, "w6c: cannot stage %s\n", out); + if (wf != NULL) fclose(wf); return 1; } } @@ -458,13 +659,46 @@ main(int argc, char **argv) * properties. Legacy raw invocations without -I remain entry-like; every * driver package now supplies -I, and only link roots add --entry. */ cg.sep_isdep = (wwiout != NULL && !entrymode); + cg.init_dispatch_symbol = initdispatch; cg_file(&cg, of, file); + if (of != stdout && (fflush(of) != 0 || ferror(of))) { + fclose(of); + if (wf != NULL) fclose(wf); + return 1; + } + struct publication pub[2] = {0}; + int npub = 0; + if (wwiout != NULL) { + pub[npub].dst = wwiout; + if (materialize_stream(wf, &pub[npub]) < 0) goto publish_fail; + npub++; + } + if (out != NULL) { + pub[npub].dst = out; + if (materialize_stream(of, &pub[npub]) < 0) goto publish_fail; + npub++; + } + if (wf != NULL) fclose(wf); if (of != stdout) fclose(of); + if (publish_all(pub, npub) < 0) goto publish_free_fail; + for (int i = 0; i < npub; i++) publication_free(&pub[i]); freearena(a); for (int i = 0; i < nimports; i++) free(imports[i].buf); free(imports); free(maps); free(buf); return 0; + +publish_fail: + if (wf != NULL) fclose(wf); + if (of != stdout) fclose(of); + for (int i = 0; i < 2; i++) { + if (pub[i].stage != NULL) (void)unlink(pub[i].stage); + publication_free(&pub[i]); + } + return 1; +publish_free_fail: + for (int i = 0; i < npub; i++) publication_free(&pub[i]); + return 1; } diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 4db11dcd..9fa9e4cc 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -57,7 +57,7 @@ static const char * wwi_use_path(Checker *c, const char *owner, int source, const char *alias) { for (Node *u = c->file->list; u; u = u->next) { - if (u->kind != N_USE || u->str == NULL + if (u->kind != N_USE || u->useblank || u->str == NULL || u->sourceid != source || strcmp(u->str, alias) != 0) continue; int same = owner == NULL ? u->imported == 0 @@ -592,6 +592,7 @@ factcmp(const void *a, const void *b) static int wwi_is_decl(Node *d) { + if (d->initfn || d->initsynthetic) return 0; return d->kind == N_FNDECL || d->kind == N_TYPEDECL || d->kind == N_DEF || d->kind == N_LET; } @@ -791,7 +792,7 @@ wwi_collect_decl(struct factset *fs, const char *owner, Node *d) static int wwi_use_owned(Node *u, const char *owner, int source) { - return u->kind == N_USE && u->imported != 0 + return u->kind == N_USE && !u->useblank && u->imported != 0 && u->sourceid == source && wwi_mod_eq(u->module, owner); } @@ -803,7 +804,7 @@ wwi_emit_imports(FILE *of, Node *file, const char *owner, int source, for (Node *u = file->list; u; u = u->next) { int owned = imported ? wwi_use_owned(u, owner, source) : u->kind == N_USE && u->imported == 0 - && u->sourceid == source; + && !u->useblank && u->sourceid == source; if (owned) nuse++; } if (nuse == 0) return; @@ -813,7 +814,7 @@ wwi_emit_imports(FILE *of, Node *file, const char *owner, int source, for (Node *u = file->list; u; u = u->next) { int owned = imported ? wwi_use_owned(u, owner, source) : u->kind == N_USE && u->imported == 0 - && u->sourceid == source; + && !u->useblank && u->sourceid == source; if (!owned) continue; us[k].path = u->usepath ? u->usepath : u->str; us[k].idx = k; @@ -836,7 +837,7 @@ wwi_primary_section_has(Checker *c, Node *file, struct factset *fs, struct declent *exports, int nexports, int source) { for (Node *u = file->list; u; u = u->next) - if (u->kind == N_USE && u->imported == 0 + if (u->kind == N_USE && !u->useblank && u->imported == 0 && u->sourceid == source) return 1; for (int i = 0; i < fs->nprivate; i++) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 601cbc53..a578b035 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -7,6 +7,8 @@ * diagnostics from one run. Nodes get their resolved Type attached. */ #include "ww.h" +#include +#include #include static void cstmt(Checker*, Node*); @@ -1448,6 +1450,7 @@ cexpr(Checker *c, Node *n) * here; the DOT case below resolves the qualified symbol. */ if (s->kind == SK_USE) return n->type = ty_err; + n->refdecl = s->decl; n->type = s->type; return s->type; } @@ -1499,8 +1502,10 @@ cexpr(Checker *c, Node *n) return n->type = err(c, n->pos, "package '%s' has no exported declaration '%s'", n->lhs->str, n->str); - if (fs) + if (fs) { + n->refdecl = fs->decl; return n->type = fs->type; + } /* A bare `w6c -T` intentionally leaves the * compiler-generated support.run hook external; the * ordinary driver supplies lib/test. This is the only @@ -3006,7 +3011,8 @@ src_imports(Node *file, const char *modtag, int source, const char *name) { if (file == NULL || name == NULL || name[0] == '\0') return 0; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->sourceid != source) continue; + if (u->kind != N_USE || u->useblank + || u->sourceid != source) continue; /* Skip self-imports: lib/fmt/fmt_test.ww carries `use fmt;` * even though its module tag is also "fmt"; that directive * doesn't introduce a foreign module bareword and lib/fmt's @@ -3132,6 +3138,680 @@ top_decl_kind(Node *d) || d->kind == N_FNDECL || d->kind == N_LET); } +static int +init_private_symbol(Node *d) +{ + for (Node *a = d ? d->attr : NULL; a; a = a->next) { + if (a->kind != N_ATTR || a->str == NULL + || strcmp(a->str, "symbol") != 0 || a->list == NULL + || a->list->kind != N_STRLIT || a->list->str == NULL) + continue; + if (strncmp(a->list->str, "__ww..", 6) == 0) + return 1; + } + return 0; +} + +/* Mutable module lets are WW's Go-variable analogue. Literal data stays in + * the archive's static image; every surviving value computation is moved to + * the package task. `def` and `const` deliberately remain outside this path. */ +static Node * +init_strip_cast(Node *n) +{ + while (n != NULL && n->kind == N_CAST) n = n->lhs; + return n; +} + +static int init_expr_static(Type *, Node *); +static int init_array_static(Type *, Node *, unsigned); + +static int +init_fnptr_static(Node *n) +{ + Node *r = init_strip_cast(n); + if (r == NULL || r->kind != N_UN || r->op != TK_AMP) return 0; + Node *v = init_strip_cast(r->lhs); + Type *vt = type_chase_named(v ? v->type : NULL); + if (v == NULL || vt == NULL || vt->kind != TY_FN) return 0; + if (v->kind == N_IDENT) return 1; + return v->kind == N_DOT && v->lhs != NULL + && v->lhs->kind == N_IDENT + && (v->lhs->type == NULL || v->lhs->type == ty_err); +} + +static int +init_float_static(Node *n) +{ + Node *r = init_strip_cast(n); + if (r != NULL && r->kind == N_UN + && (r->op == TK_PLUS || r->op == TK_MINUS)) + r = init_strip_cast(r->lhs); + return r != NULL && r->kind == N_FLOATLIT; +} + +static int +init_tagged_raw_static(Type *u, Node *n) +{ + Node *r = init_strip_cast(n); + Type *ru = type_chase_named(r ? r->type : NULL); + u64 ignored; + return u != NULL && u->kind == TY_TAGGED && !u->nullable + && r != NULL && variant_present(u->params, r->type) + && (ru == NULL || (ru->kind != TY_STR && ru->kind != TY_SLICE)) + && fold_int_literal(r, &ignored); +} + +static int +init_tuple_static(Type *u, Node *n) +{ + Node *r = init_strip_cast(n); + if (u == NULL || u->kind != TY_TUPLE + || r == NULL || r->kind != N_TUPLE) + return 0; + Tparam *tp = u->params; + for (Node *e = r->list; e; e = e->next) { + if (tp == NULL) return 0; + Node *v = init_strip_cast(e); + Type *et = type_chase_named(tp->type); + if (v == NULL || (et != NULL && et->kind == TY_TAGGED)) return 0; + if (et != NULL && (et->kind == TY_STR || et->kind == TY_SLICE)) { + if (v->kind != N_STRLIT) return 0; + } else if (!init_fnptr_static(v)) { + u64 ignored; + if (!fold_int_literal(v, &ignored)) return 0; + } + tp = tp->next; + } + return tp == NULL; +} + +static int +init_struct_static(Type *u, Node *n) +{ + Node *r = init_strip_cast(n); + if (u == NULL || u->kind != TY_STRUCT + || r == NULL || r->kind != N_STRUCTLIT) + return 0; + for (Tfield *f = u->fields; f; f = f->next) { + Node *value = NULL; + for (Node *e = r->list; e; e = e->next) + if (e->str != NULL && f->name != NULL + && strcmp(e->str, f->name) == 0) { + value = e->lhs; + break; + } + if (value != NULL) { + Type *fu = type_chase_named(f->type); + int ok = 0; + if (fu != NULL && fu->kind == TY_TAGGED && !fu->nullable) + ok = init_tagged_raw_static(fu, value); + else if (fu != NULL && fu->kind == TY_STRUCT) + ok = init_struct_static(fu, value); + else if (fu != NULL && fu->kind == TY_ARRAY) + ok = init_array_static(fu, value, 1); + else if (type_isfloat(f->type)) + ok = init_float_static(value); + else { + u64 ignored; + ok = fold_int_literal(init_strip_cast(value), &ignored); + } + if (!ok) return 0; + } + } + return 1; +} + +#define INIT_ARR_REPEAT 1u +#define INIT_ARR_STR_RELOC 2u +#define INIT_ARR_TUPLE_ROWS 4u + +static int +init_array_static(Type *u, Node *n, unsigned flags) +{ + Node *r = init_strip_cast(n); + if (u == NULL || (u->kind != TY_ARRAY && u->kind != TY_SLICE) + || r == NULL || r->kind != N_ARRLIT) + return 0; + Type *et = u->sub; + Type *eu = type_chase_named(et); + int seen = 0; + for (Node *e = r->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str != NULL + && strcmp(e->str, "...") == 0) + return (flags & INIT_ARR_REPEAT) != 0 && seen > 0 + && e->next == NULL + && (eu == NULL || eu->kind != TY_ARRAY); + Node *v = init_strip_cast(e); + if (v == NULL) return 0; + if (eu != NULL && eu->kind == TY_STR) { + if ((flags & INIT_ARR_STR_RELOC) == 0 + || v->kind != N_STRLIT) return 0; + } else if (eu != NULL && eu->kind == TY_STRUCT) { + if (!init_struct_static(eu, v)) return 0; + } else if (eu != NULL && eu->kind == TY_ARRAY) { + if (!init_array_static(eu, v, INIT_ARR_REPEAT)) return 0; + } else if (eu != NULL && eu->kind == TY_TUPLE) { + if ((flags & INIT_ARR_TUPLE_ROWS) == 0 + || !init_tuple_static(eu, v)) return 0; + } else if (eu != NULL && eu->kind == TY_TAGGED) { + if (!init_tagged_raw_static(eu, v)) return 0; + } else if (eu != NULL && (eu->kind == TY_SLICE + || eu->kind == TY_PTR + || eu->kind == TY_FN)) { + return 0; + } else if (type_isfloat(et)) { + if (!init_float_static(v)) return 0; + } else { + u64 ignored; + if (!fold_int_literal(v, &ignored)) return 0; + } + seen++; + } + return 1; +} + +/* This predicate is the checker's validate-only twin of cgen's static-data + * arms. A true result must be safe to emit; every other valid mutable value + * is zero-backed and evaluated exactly once by the package task. */ +static int +init_expr_static(Type *t, Node *n) +{ + Node *r = init_strip_cast(n); + if (r == NULL) return 1; + Type *u = type_chase_named(t); + if (type_isfloat(t)) return init_float_static(r); + if (u != NULL && (u->kind == TY_STR || u->kind == TY_UNTYPED_STR)) + return r->kind == N_STRLIT || r->kind == N_NIL; + if (u != NULL && u->kind == TY_ARRAY) + return init_array_static(u, r, + INIT_ARR_REPEAT | INIT_ARR_STR_RELOC); + if (u != NULL && u->kind == TY_STRUCT) + return init_struct_static(u, r); + if (u != NULL && u->kind == TY_TUPLE) + return init_tuple_static(u, r); + if (u != NULL && u->kind == TY_SLICE) { + if (r->kind == N_NIL) return 1; + return init_array_static(u, r, INIT_ARR_TUPLE_ROWS); + } + if (u != NULL && u->kind == TY_TAGGED && !u->nullable) { + Type *ru = type_chase_named(r->type); + if (!variant_present(u->params, r->type)) return 0; + if (ru != NULL && (ru->kind == TY_STR || ru->kind == TY_SLICE)) + return r->kind == N_STRLIT; + return init_tagged_raw_static(u, r); + } + if (init_fnptr_static(r)) return 1; + u64 ignored; + return fold_int_literal(r, &ignored); +} + +struct initwalkitem { + Node *node; + struct initwalkitem *next; +}; + +struct initwalk { + struct initwalkitem *stack; + struct initwalkitem *seenfn; +}; + +static int +initwalk_push(struct initwalkitem **head, Node *x) +{ + if (x == NULL) return 0; + struct initwalkitem *p = malloc(sizeof *p); + if (p == NULL) return -1; + p->node = x; + p->next = *head; + *head = p; + return 0; +} + +static int +initwalk_seen_fn(struct initwalk *w, Node *fn) +{ + for (struct initwalkitem *p = w->seenfn; p; p = p->next) + if (p->node == fn) return 1; + if (initwalk_push(&w->seenfn, fn) < 0) return -1; + return 0; +} + +static void +initwalk_free(struct initwalkitem *p) +{ + while (p != NULL) { + struct initwalkitem *next = p->next; + free(p); + p = next; + } +} + +/* Does variable `from` depend on `target`? Function nodes are transparent, + * as in go/types initOrder: references in their bodies become variable edges. */ +static int +init_refers(Node *from, Node *target) +{ + struct initwalk w = {0}; + int result = 0; + if (initwalk_push(&w.stack, from->rhs) < 0) + result = -1; + while (result == 0 && w.stack != NULL) { + struct initwalkitem *top = w.stack; + Node *n = top->node; + w.stack = top->next; + free(top); + if (n->refdecl == target) { + result = 1; + break; + } + Node *r = n->refdecl; + if (r != NULL && r->kind == N_FNDECL && !r->imported + && !r->initfn && r->body != NULL) { + int seen = initwalk_seen_fn(&w, r); + if (seen < 0) { result = -1; break; } + if (!seen && initwalk_push(&w.stack, r->body) < 0) { + result = -1; + break; + } + } + Node *child[] = { n->attr, n->lhs, n->rhs, n->cond, + n->body, n->els, n->list, n->next }; + for (size_t i = 0; result == 0 && i < nelem(child); i++) + if (initwalk_push(&w.stack, child[i]) < 0) + result = -1; + } + initwalk_free(w.stack); + initwalk_free(w.seenfn); + return result; +} + +static void +init_cycle_note(Node *from, Node *to) +{ + FILE *f = errout ? errout : stderr; + fprintf(f, "\t%s:%d:%d: %s refers to %s\n", + from->pos.file ? from->pos.file : "?", from->pos.line, + from->pos.col, from->str, to->str); +} + +/* Go's findPath, iteratively: dependencies are visited in source order and a + * global seen set prevents a side cycle from consuming the native stack. */ +static int +init_find_cycle(Checker *c, Node **vars, size_t nvar, size_t start) +{ + unsigned char *seen = calloc(nvar, 1); + size_t *path = malloc(nvar * sizeof *path); + size_t *next = calloc(nvar, sizeof *next); + if (seen == NULL || path == NULL || next == NULL) { + free(next); free(path); free(seen); + err(c, vars[start]->pos, + "out of memory while ordering package initialization"); + return -1; + } + size_t depth = 1; + path[0] = start; + seen[start] = 1; + while (depth > 0) { + size_t from = path[depth - 1]; + int descended = 0; + while (next[depth - 1] < nvar) { + size_t to = next[depth - 1]++; + int dep = init_refers(vars[from], vars[to]); + if (dep < 0) { + err(c, vars[start]->pos, + "out of memory while ordering package initialization"); + free(next); free(path); free(seen); + return -1; + } + if (!dep) continue; + if (to == start) { + if (depth == 1) { + err(c, vars[start]->pos, + "initialization cycle: %s refers to itself", + vars[start]->str); + } else { + err(c, vars[start]->pos, + "initialization cycle for %s", + vars[start]->str); + for (size_t i = 1; i < depth; i++) + init_cycle_note(vars[path[i - 1]], + vars[path[i]]); + init_cycle_note(vars[path[depth - 1]], + vars[start]); + } + free(next); free(path); free(seen); + return 1; + } + if (seen[to]) continue; + seen[to] = 1; + path[depth] = to; + next[depth] = 0; + depth++; + descended = 1; + break; + } + if (!descended) depth--; + } + free(next); free(path); free(seen); + return 0; +} + +static Node * +init_make_call(Checker *c, Node *fn, Pos pos) +{ + Node *id = newnode(c->a, N_IDENT, pos); + id->str = fn->str; + id->strlen = strlen(fn->str); + id->type = fn->type; + id->refdecl = fn; + Node *call = newnode(c->a, N_CALL, pos); + call->lhs = id; + call->type = ty_void; + Node *stmt = newnode(c->a, N_EXPRSTMT, pos); + stmt->lhs = call; + return stmt; +} + +static u64 +init_arrlit_count(Node *lit) +{ + u64 count = 0; + for (Node *e = lit ? lit->list : NULL; e; e = e->next) { + if (e->kind == N_FIELD && e->str != NULL + && strcmp(e->str, "...") == 0) + continue; + count++; + } + return count; +} + +/* Runtime slice literals cannot use the ordinary local-literal backing: that + * storage dies when the compiler-generated variable helper returns. Attach a + * canonical package-owned backing symbol to every slice literal contained in + * the value being published. The cgen emits zeroed writable storage for the + * symbol and fills it at the literal's exact evaluation point. + * + * The name is derived only from the action-owned package-init symbol, the + * Go-ordered variable ordinal, and literal preorder. Source names, import + * aliases, declared package names, and path leaves never enter the identity. */ +static void +init_mark_slice_backings(Checker *c, Type *want, Node *expr, + const char *base, u64 order, u64 *preorder) +{ + Node *r = init_strip_cast(expr); + Type *u = type_chase_named(want); + if (r == NULL || u == NULL) return; + if (u->kind == TY_SLICE && r->kind == N_ARRLIT) { + u64 count = init_arrlit_count(r); + (*preorder)++; + r->linksym = aprintf(c->a, "%s.v.%llu.b.%llu", base, + (unsigned long long)order, + (unsigned long long)*preorder); + r->type = type_array(c->a, u->sub, count); + for (Node *e = r->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str != NULL + && strcmp(e->str, "...") == 0) + continue; + init_mark_slice_backings(c, u->sub, e, base, order, + preorder); + } + return; + } + if (u->kind == TY_ARRAY && r->kind == N_ARRLIT) { + for (Node *e = r->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str != NULL + && strcmp(e->str, "...") == 0) + continue; + init_mark_slice_backings(c, u->sub, e, base, order, + preorder); + } + return; + } + if (u->kind == TY_STRUCT && r->kind == N_STRUCTLIT) { + for (Node *e = r->list; e; e = e->next) { + Tfield *field = NULL; + for (Tfield *f = u->fields; f; f = f->next) + if (e->str != NULL && f->name != NULL + && strcmp(e->str, f->name) == 0) { + field = f; + break; + } + if (field != NULL) + init_mark_slice_backings(c, field->type, e->lhs, + base, order, preorder); + } + return; + } + if (u->kind == TY_TUPLE && r->kind == N_TUPLE) { + Tparam *p = u->params; + for (Node *e = r->list; e && p; e = e->next, p = p->next) + init_mark_slice_backings(c, p->type, e, base, order, + preorder); + } +} + +static Node * +init_make_helper(Checker *c, Node *d, const char *base) +{ + Node *fn = newnode(c->a, N_FNDECL, d->pos); + fn->str = aprintf(c->a, "__ww_init_var_%llu", + (unsigned long long)d->initorder); + fn->strlen = strlen(fn->str); + fn->module = d->module; + fn->pkgname = d->pkgname; + fn->sourceid = d->sourceid; + fn->initsynthetic = 1; + fn->linksym = aprintf(c->a, "%s.v.%llu", base, + (unsigned long long)d->initorder); + Type *ft = newtype(c->a, TY_FN); + ft->size = 8; ft->align = 8; ft->ret = ty_void; + fn->type = ft; + + /* Evaluate into an addressable local first. The established local-let and + * full-value assignment paths cover arrays, structs, tuples, calls, and + * allocation without asking the static-data emitter to understand them. */ + Node *tmp = newnode(c->a, N_LET, d->pos); + tmp->str = aprintf(c->a, "__ww_init_tmp_%llu", + (unsigned long long)d->initorder); + tmp->strlen = strlen(tmp->str); + tmp->lhs = d->lhs; + tmp->rhs = d->rhs; + tmp->type = d->type; + tmp->initsynthetic = 1; + + Node *id = newnode(c->a, N_IDENT, d->pos); + id->str = d->str; + id->strlen = strlen(d->str); + id->type = d->type; + id->refdecl = d; + Node *value = newnode(c->a, N_IDENT, d->pos); + value->str = tmp->str; + value->strlen = tmp->strlen; + value->type = d->type; + value->refdecl = tmp; + Node *assign = newnode(c->a, N_ASSIGN, d->pos); + assign->op = TK_ASSIGN; + assign->lhs = id; + assign->rhs = value; + assign->type = d->type; + Node *stmt = newnode(c->a, N_EXPRSTMT, d->pos); + stmt->lhs = assign; + Node *body = newnode(c->a, N_BLOCK, d->pos); + body->list = tmp; + tmp->next = stmt; + fn->body = body; + d->rhs = NULL; + return fn; +} + +static int +init_lower_package(Checker *c, Node *file) +{ + int nvar = 0; + u64 nruntime = 0; + u64 ninit = 0; + Node *firstinit = NULL; + for (Node *d = file->list; d; d = d->next) { + if (d->initfn && !d->imported) { + ninit++; + if (firstinit == NULL) firstinit = d; + } + if (d->kind != N_LET || d->imported || d->rhs == NULL) + continue; + if (d->op == TK_CONST) continue; + if (nvar == INT_MAX) { + err(c, d->pos, + "out of memory while ordering package initialization"); + return -1; + } + nvar++; + if (!init_expr_static(d->type, d->rhs)) { + d->runtimeinit = 1; + nruntime++; + if (firstinit == NULL) firstinit = d; + } + } + if (c->errs) return -1; + /* A separate package with executable initialization must have the exact + * action-owned symbol supplied by its driver. Legacy non-separate mode + * retains its historical private fallback. */ + if (c->sep_mode && (c->package_init_symbol == NULL + || c->package_init_symbol[0] == '\0') + && (nruntime != 0 || ninit != 0)) { + err(c, firstinit ? firstinit->pos : file->pos, + "package initialization requires --package-init-symbol under -c"); + return -1; + } + if ((c->package_init_symbol == NULL + || c->package_init_symbol[0] == '\0') && nruntime == 0 && ninit == 0) + return 0; + + Node **vars = NULL; + if (nvar != 0) { + if ((size_t)nvar > (size_t)-1 / sizeof *vars + || (vars = malloc((size_t)nvar * sizeof *vars)) == NULL) { + err(c, file->pos, + "out of memory while ordering package initialization"); + return -1; + } + int vi = 0; + for (Node *d = file->list; d; d = d->next) + if (d->kind == N_LET && !d->imported && d->rhs != NULL + && d->op != TK_CONST) + vars[vi++] = d; + } + int done = 0; + while (done < nvar) { + size_t best = (size_t)-1; + size_t bestdeps = (size_t)-1; + for (int i = 0; i < nvar; i++) { + if (vars[i]->initorder != 0) continue; + size_t ndeps = 0; + for (int j = 0; j < nvar; j++) { + if (vars[j]->initorder != 0) continue; + int dep = init_refers(vars[i], vars[j]); + if (dep < 0) { + err(c, vars[i]->pos, + "out of memory while ordering package initialization"); + free(vars); + return -1; + } + if (dep) ndeps++; + } + if (best == (size_t)-1 || ndeps < bestdeps) { + best = (size_t)i; + bestdeps = ndeps; + } + } + if (best == (size_t)-1) break; + if (bestdeps != 0) { + int cycle = init_find_cycle(c, vars, (size_t)nvar, best); + if (cycle < 0) { free(vars); return -1; } + /* A reported cycle is broken by removing this node, exactly + * like go/types' priority-queue walk. Continue so disjoint or + * overlapping later cycles retain their deterministic errors. */ + } + vars[best]->initorder = (u64)++done; + } + free(vars); + if (c->errs) return -1; + + const char *base = c->package_init_symbol; + if (base == NULL || base[0] == '\0') + base = "__ww..pkg.v0.r0.e.init"; + Node *tail = file->list; + while (tail && tail->next) tail = tail->next; + Node *helpers = NULL, *helpertail = NULL; + for (u64 order = 1; order <= (u64)nvar; order++) + for (Node *d = file->list; d; d = d->next) + if (d->runtimeinit && d->initorder == order) { + u64 preorder = 0; + init_mark_slice_backings(c, d->type, d->rhs, + base, order, &preorder); + Node *fn = init_make_helper(c, d, base); + if (helpers == NULL) helpers = fn; + else helpertail->next = fn; + helpertail = fn; + break; + } + if (tail) tail->next = helpers; + else file->list = helpers; + if (helpertail) tail = helpertail; + + Node *task = newnode(c->a, N_FNDECL, file->pos); + task->str = "__ww_init_task"; + task->strlen = strlen(task->str); + task->initsynthetic = 1; + task->linksym = base; + Type *tt = newtype(c->a, TY_FN); + tt->size = 8; tt->align = 8; tt->ret = ty_void; + task->type = tt; + Node *body = newnode(c->a, N_BLOCK, file->pos); + Node *stail = NULL; + for (Node *fn = helpers; fn; fn = fn->next) { + Node *s = init_make_call(c, fn, fn->pos); + if (body->list == NULL) body->list = s; + else stail->next = s; + stail = s; + } + for (Node *d = file->list; d; d = d->next) { + if (!d->initfn || d->imported) continue; + Node *s = init_make_call(c, d, d->pos); + if (body->list == NULL) body->list = s; + else stail->next = s; + stail = s; + } + task->body = body; + if (tail) tail->next = task; + else file->list = task; + return 0; +} + +static void +classify_init_decls(Checker *c, Node *file) +{ + u64 ordinal = 0; + for (Node *d = file->list; d; d = d->next) { + if (init_private_symbol(d)) + err(c, d->pos, "@symbol name uses reserved prefix __ww.."); + if (d->str == NULL || strcmp(d->str, "init") != 0 + || !top_decl_kind(d)) + continue; + if (d->kind != N_FNDECL) { + err(c, d->pos, "cannot declare init - must be func"); + continue; + } + d->initfn = 1; + d->initorder = ++ordinal; + if (d->export) + err(c, d->pos, "func init cannot be exported"); + if (d->body == NULL) + err(c, d->pos, "func init must have a body"); + if (d->attr != NULL) + err(c, d->pos, "func init cannot have attributes"); + } +} + /* Import usage is a property of the file-local qualifier occurrence. Record * qualified syntax before resolving declaration bodies so import diagnostics * retain production Go's source order without making a failed bare lookup a @@ -3186,10 +3866,12 @@ check_import_redeclarations(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->imported || u->str == NULL) + if (u->kind != N_USE || u->useblank + || u->imported || u->str == NULL) continue; for (Node *v = file->list; v != u; v = v->next) { - if (v->kind != N_USE || v->imported || v->str == NULL + if (v->kind != N_USE || v->useblank + || v->imported || v->str == NULL || v->sourceid != u->sourceid) continue; if (strcmp(v->str, u->str) == 0) { @@ -3210,7 +3892,8 @@ check_import_usage_and_collisions(Checker *c, Node *file) { if (!c->sep_mode) return; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->imported || u->used || u->str == NULL) + if (u->kind != N_USE || u->useblank + || u->imported || u->used || u->str == NULL) continue; const char *path = u->usesource ? u->usesource : u->usepath ? u->usepath : u->str; @@ -3226,7 +3909,8 @@ check_import_usage_and_collisions(Checker *c, Node *file) if (d->imported || !top_decl_kind(d) || d->str == NULL) continue; for (Node *u = file->list; u; u = u->next) { - if (u->kind != N_USE || u->imported || u->str == NULL + if (u->kind != N_USE || u->useblank + || u->imported || u->str == NULL || strcmp(d->str, u->str) != 0) continue; const char *path = u->usesource ? u->usesource @@ -3287,6 +3971,7 @@ check_file(Checker *c, Node *file) mark_import_uses(c, file); check_import_redeclarations(c, file); check_import_usage_and_collisions(c, file); + classify_init_decls(c, file); /* pass 1: install names (types first, then defs/fns). * For self-referential types we install the named-type placeholder @@ -3308,6 +3993,12 @@ check_file(Checker *c, Node *file) && strcmp(d->usepath, owner) == 0) err(c, d->pos, "self-import: package " "'%s' cannot import itself", owner); + if (d->useblank) + continue; + if (d->str != NULL && strcmp(d->str, "init") == 0) { + err(c, d->pos, "cannot import package as init - init must be a func"); + continue; + } Sym *prev = scope_lookup_local(c->cur, d->str); if (prev != NULL) { /* Self-import: the driver concatenates the @@ -3323,6 +4014,7 @@ check_file(Checker *c, Node *file) continue; } if (d->kind != N_TYPEDECL) continue; + if (d->str != NULL && strcmp(d->str, "init") == 0) continue; const char *mod = decl_mod(file, d); Sym *fact = same_import_fact(c, d, mod, SK_TYPE); if (fact != NULL) { @@ -3357,6 +4049,7 @@ check_file(Checker *c, Node *file) c->cur_source = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF) continue; + if (d->str != NULL && strcmp(d->str, "init") == 0) continue; c->cur_mod = decl_mod(file, d); c->cur_source = d->sourceid; const char *mod = decl_mod(file, d); @@ -3390,6 +4083,10 @@ check_file(Checker *c, Node *file) break; case N_DEF: { Type *t = resolve_type(c, d->lhs); + if (d->str != NULL && strcmp(d->str, "init") == 0) { + d->type = t; + break; + } const char *mod = decl_mod(file, d); Sym *fact = same_import_fact(c, d, mod, SK_DEF); if (fact != NULL) { @@ -3419,6 +4116,18 @@ check_file(Checker *c, Node *file) case N_FNDECL: { Type *t = build_fn_type(c, d); d->type = t; + if (d->initfn) { + Type *ret = type_chase_named(t->ret); + if (d->list != NULL || ret != ty_void) + err(c, d->pos, "func init must have no arguments and no return values"); + const char *base = c->package_init_symbol; + if ((base == NULL || base[0] == '\0') && !c->sep_mode) + base = "__ww..pkg.v0.r0.e.init"; + if (base != NULL && base[0] != '\0') + d->linksym = aprintf(c->a, "%s.f.%llu", base, + (unsigned long long)d->initorder); + break; + } Sym *prev = scope_lookup_local(c->cur, d->str); const char *mod = decl_mod(file, d); if (prev && prev->kind == SK_USE) { @@ -3444,26 +4153,8 @@ check_file(Checker *c, Node *file) if (t) require_sized(c, t, d->pos, "a variable"); d->type = t; - /* A module-scope initializer must be link-time data: - * an alloc/call rhs runs code, emit_lets' fold-fail - * skipped the DATAW slot silently, and every - * reference died at LINK time ("undefined reference") - * — reject at the declaration instead (rule 7). - * Hare rejects at check time too (ref/harec/src/ - * check.c:4360 "Unable to evaluate initializer at - * compile time"); ww has no @init path. */ - { - Node *r = d->rhs; - while (r != NULL && (r->kind == N_CAST - || r->kind == N_TRYPROP - || r->kind == N_TRYUNW)) - r = r->lhs; - if (r != NULL && (r->kind == N_ALLOC - || r->kind == N_CALL)) - err(c, d->pos, "module-scope let %s: " - "runtime initializer unsupported " - "(alloc/call; rule 7)", d->str); - } + if (d->str != NULL && strcmp(d->str, "init") == 0) + break; if (d->str && d->str[0]) { Sym *prev = scope_lookup_local(c->cur, d->str); const char *mod = decl_mod(file, d); @@ -3929,6 +4620,7 @@ check_file(Checker *c, Node *file) } c->cur_mod = NULL; c->cur_source = 0; + (void)init_lower_package(c, file); /* * #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is * fully checked above — pass 2 walked its body like every fn — but diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index fc853c88..70de3e32 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1326,9 +1326,7 @@ parseuse(Parser *p) const char *alias = NULL; const char *first; if (p->cur.kind == TK_UNDER) { - errorf(p->cur.pos, "blank import alias _ is not implemented"); - p->errs++; - alias = "_"; + n->useblank = 1; advance(p); first = expectident(p); } else { @@ -1344,8 +1342,10 @@ parseuse(Parser *p) leaf = expectident(p); path = aprintf(p->a, "%s.%s", path, leaf); } - n->str = alias ? alias : leaf; - n->strlen = strlen(n->str); + if (!n->useblank) { + n->str = alias ? alias : leaf; + n->strlen = strlen(n->str); + } n->usesource = path; n->usepath = path; n->usealias = alias; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index f4de16d0..a8e12f72 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -349,12 +349,20 @@ struct Node { const char *usealias; /* N_USE: explicit file-local alias, or NULL. */ const char *usepkgname; /* N_USE: imported declared package name, * independent of the visible binding in `str`. */ + int useblank; /* N_USE: `_` spelling; no source binding. */ const char *pkgname; /* declared package name for this source/export * section; independent of canonical `module`. */ int sourceid; /* lexical source-file scope within the parsed * owner unit; module-reset/module boundaries * advance it deterministically. */ int used; /* N_USE: checker observed this file-local binding. */ + int initfn; /* special source `fn init`, absent from scope/API. */ + int initsynthetic; /* compiler-owned variable helper/package task. */ + int runtimeinit; /* module let lowered through an init helper. */ + u64 initorder; /* 1-based variable or init-function order. */ + const char *linksym; /* raw compiler-private assembler symbol. */ + Node *refdecl; /* checker-resolved value declaration. */ + u64 initmark; /* checker-private initializer walk mark. */ int imported; /* M1 #22: decl reached through an * `//ww:module ` import boundary * (vs root/primary). Gates the root-only @@ -616,6 +624,7 @@ struct Checker { * 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. */ + const char *package_init_symbol; /* canonical action-owned hidden task. */ }; void check_init(Checker*, Arena*); diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 48735106..64adc23b 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -745,6 +745,7 @@ struct seppkg { char *canon; /* canonical location; never compiler identity */ char *artifact; /* legacy short artifact key, when one exists */ char *storage; /* internal storage basename; never package identity */ + char *init_symbol; /* canonical package/variant-owned hidden task */ int storage_hashed; /* storage is the bounded complete-action locator */ char *name; /* validated declared name; directory packages only */ char *test_package; /* selected test package; root variants only */ @@ -761,6 +762,9 @@ struct seppkg { int test_support; /* compiler-generated -T support package */ int loaded; /* directory membership/name loaded exactly once */ int export_changed; /* staged export differs from committed export */ + int source_staged; /* warm request owns complete source `.new` set */ + int init_staged; /* warm request owns complete dispatcher `.new` set */ + int archive_staged; /* warm request owns complete archive `.new` */ int emit_context; /* first verified resolution context */ unsigned char *context_state; /* 0 new, 1 active, 2 checked */ int context_cap; @@ -801,6 +805,9 @@ struct sepproduct { int root; int variant_root; /* production-plus-test or external test package */ int support; /* direct generated-main support action, or -1 */ + char *stage_out; /* request-private linked/published output */ + char *stage_iface; /* request-private published package interface */ + char *stage_status; /* request-private completion marker */ }; static void sep_pkg_free_fields(struct seppkg *); @@ -1569,6 +1576,7 @@ sep_pkg_free_fields(struct seppkg *p) free(p->canon); free(p->artifact); free(p->storage); + free(p->init_symbol); free(p->name); free(p->test_package); memset(p, 0, sizeof *p); @@ -1904,9 +1912,11 @@ sep_legacy_artifact(const struct seppkg *p) static int sep_validate_storage_path(const struct seppkg *p, const char *scratch) { + static const char tail[] = + ".init.unit.ww.wwtxn.9223372036854775807.old"; size_t need = strlen(scratch) + 1 + strlen(p->storage) - + strlen(".unit.new") + 1; - if (strlen(p->storage) + strlen(".unit.new") > SEP_NAME_MAX + + strlen(tail) + 1; + if (strlen(p->storage) + strlen(tail) > SEP_NAME_MAX || need > SEP_ARTIFACT_MAX) { fprintf(stderr, "ww: package artifact path is too long\n"); return -1; @@ -1917,10 +1927,12 @@ sep_validate_storage_path(const struct seppkg *p, const char *scratch) static int sep_assign_storage(struct seppkg *p, const char *scratch) { + static const char tail[] = + ".init.unit.ww.wwtxn.9223372036854775807.old"; const char *base = sep_legacy_artifact(p); size_t need = strlen(scratch) + 1 + strlen(base) - + strlen(".unit.new") + 1; - if (strlen(base) + strlen(".unit.new") <= SEP_NAME_MAX + + strlen(tail) + 1; + if (strlen(base) + strlen(tail) <= SEP_NAME_MAX && need <= SEP_ARTIFACT_MAX) { p->storage = strdup(base); if (p->storage == NULL) sep_fail_nomem(); @@ -2706,6 +2718,16 @@ sep_dep_cmp(const struct sepgraph *g, int a, int b) return strcmp(g->pkg[a].canon, g->pkg[b].canon); } +static char * +sep_package_init_symbol(const struct seppkg *p) +{ + if (p->path[0] == '\0') + return sep_sprintf("__ww..pkg.e.v%d.r%d.init", + p->variant, p->role); + return sep_sprintf("__ww..pkg.p.%s.v%d.r%d.init", + p->path, p->variant, p->role); +} + /* Add the compiler-owned test main as a real package action. Its identity is a * pure function of the selected variant (and its one command-global support * edge), never a product ordinal. Equivalent products therefore reuse it. */ @@ -3347,6 +3369,151 @@ sep_internal_replaces_production(const struct sepgraph *g, int a, int b) && strcmp(internal->canon, production->canon) == 0; } +/* A production action is physically omitted from an internal-test product + * because the augmented variant owns those same production sources. Map every + * edge through that replacement before scheduling initialization, exactly as + * the link-closure filter does. */ +static int +sep_init_effective(const struct sepgraph *g, int variant_root, int pi) +{ + if (variant_root >= 0 && variant_root < g->n + && sep_internal_replaces_production(g, variant_root, pi)) + return variant_root; + return pi; +} + +static int +sep_init_cmp(const struct sepgraph *g, int a, int b) +{ + int r = strcmp(g->pkg[a].path, g->pkg[b].path); + if (r != 0) return r; + if (g->pkg[a].variant != g->pkg[b].variant) + return g->pkg[a].variant - g->pkg[b].variant; + return g->pkg[a].role - g->pkg[b].role; +} + +/* Go's linker uses a lexical ready queue over the reachable init-task DAG. + * Compute that schedule explicitly: dependencies become ready first; among + * otherwise independent actions canonical package identity breaks ties. */ +static int +sep_init_order(const struct sepgraph *g, int root, int variant_root, + int **out, int *nout) +{ + unsigned char *active = calloc((size_t)g->n, 1); + unsigned char *done = calloc((size_t)g->n, 1); + int *todo = calloc((size_t)g->n, sizeof *todo); + int *order = calloc((size_t)g->n, sizeof *order); + if (active == NULL || done == NULL || todo == NULL || order == NULL) { + if (!sep_fatal_allocation) (void)sep_fail_nomem(); + free(order); free(todo); free(done); free(active); + return -1; + } + int ntodo = 0; + int effective_root = sep_init_effective(g, variant_root, root); + active[effective_root] = 1; + todo[ntodo++] = effective_root; + while (ntodo > 0) { + int pi = todo[--ntodo]; + for (int k = 0; k < g->pkg[pi].ndeps; k++) { + int dep = sep_init_effective(g, variant_root, + g->pkg[pi].deps[k]); + if (!active[dep]) { + active[dep] = 1; + todo[ntodo++] = dep; + } + } + } + int nactive = 0; + for (int pi = 0; pi < g->n; pi++) if (active[pi]) nactive++; + int no = 0; + while (no < nactive) { + int best = -1; + for (int pi = 0; pi < g->n; pi++) { + if (!active[pi] || done[pi]) continue; + int blocked = 0; + for (int k = 0; k < g->pkg[pi].ndeps; k++) { + int dep = sep_init_effective(g, variant_root, + g->pkg[pi].deps[k]); + if (dep != pi && active[dep] && !done[dep]) { + blocked = 1; + break; + } + } + if (!blocked && (best < 0 || sep_init_cmp(g, pi, best) < 0)) + best = pi; + } + if (best < 0) { + fprintf(stderr, "ww: dependency cycle in initialization closure\n"); + free(order); free(todo); free(done); free(active); + return -1; + } + done[best] = 1; + order[no++] = best; + } + free(todo); free(done); free(active); + *out = order; + *nout = no; + return 0; +} + +static int +sep_compose_init_dispatch(const struct sepgraph *g, + const struct sepproduct *product, const char *unitpath, + const char *asmpath) +{ + int *order = NULL, norder = 0; + if (sep_init_order(g, product->root, product->variant_root, + &order, &norder) < 0) + return -1; + FILE *unit = fopen(unitpath, "wb"); + if (unit == NULL) { + fprintf(stderr, "ww: cannot open %s\n", unitpath); + free(order); + return -1; + } + int bad = fprintf(unit, "//ww:init-root %s\n", + g->pkg[product->root].init_symbol) < 0; + for (int i = 0; i < norder && !bad; i++) + if (fprintf(unit, "//ww:init-call %s\n", + g->pkg[order[i]].init_symbol) < 0) + bad = 1; + if (fclose(unit) != 0) bad = 1; + if (bad) { + fprintf(stderr, "ww: cannot write initialization unit\n"); + free(order); + return -1; + } + FILE *out = fopen(asmpath, "wb"); + if (out == NULL) { + fprintf(stderr, "ww: cannot open %s\n", asmpath); + (void)unlink(unitpath); + free(order); + return -1; + } + bad = fputs("TEXT __ww..dispatch,$0\n" + "\tPUSHQ\tBP\n" + "\tMOVQ\tSP, BP\n" + "\tSUBQ\t$0, SP\n", out) == EOF; + for (int i = 0; i < norder && !bad; i++) + if (fprintf(out, "\tCALL\t%s(SB)\n", + g->pkg[order[i]].init_symbol) < 0) + bad = 1; + if (!bad && fputs("\tMOVQ\t$0, AX\n" + "\tMOVQ\tBP, SP\n" + "\tPOPQ\tBP\n" + "\tRET\n", out) == EOF) + bad = 1; + if (fclose(out) != 0) bad = 1; + free(order); + if (bad) { + fprintf(stderr, "ww: cannot write initialization assembly\n"); + (void)unlink(unitpath); + (void)unlink(asmpath); + return -1; + } + return 0; +} + static int sep_validate_module_closure(struct sepgraph *g, const int *order, int n, int include_root) @@ -3416,11 +3583,37 @@ sep_emit_hex(FILE *out, const char *value) return 0; } +static int +sep_emit_file_hex(FILE *out, const char *path) +{ + FILE *in = fopen(path, "rb"); + if (in == NULL) return -1; + static const char hex[] = "0123456789abcdef"; + unsigned char buf[65536]; + int bad = 0; + for (;;) { + size_t n = fread(buf, 1, sizeof buf, in); + for (size_t i = 0; i < n; i++) + if (fputc(hex[buf[i] >> 4], out) == EOF + || fputc(hex[buf[i] & 15], out) == EOF) { + bad = 1; + break; + } + if (bad || n < sizeof buf) { + if (ferror(in)) bad = 1; + break; + } + } + if (fclose(in) != 0) bad = 1; + return bad ? -1 : 0; +} + /* Compose pi's sep-unit at `unitf` from only pi's byte-sorted sources. * Direct exports are separate compiler inputs; the linker separately retains * the reachable archive closure. */ static int -sep_compose_unit(struct sepgraph *g, int pi, const char *unitf) +sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, + const char *unitf) { if (g->pkg[pi].emit_context < 0 || g->pkg[pi].emit_context >= g->ncontext) @@ -3469,6 +3662,22 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *unitf) || fputc('\n', u) == EOF) bodyrc = -1; } + /* Direct export bytes complete the source-action voucher. A compiler + * rejection can therefore retain the previous committed unit safely: if a + * dependency export changed, the next request still sees a unit mismatch. */ + for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++) { + int dep = g->pkg[pi].deps[i]; + char interface[SEP_ARTIFACT_MAX]; + const char *suffix = g->pkg[dep].source_staged + ? ".wwi.new" : ".wwi"; + if (sep_fname(g, dep, scratch, suffix, interface, + sizeof interface) < 0 + || fprintf(u, "//ww:direct-export %s ", + g->pkg[dep].path) < 0 + || sep_emit_file_hex(u, interface) < 0 + || fputc('\n', u) == EOF) + bodyrc = -1; + } if (fclose(u) != 0) { fprintf(stderr, "ww: cannot close package unit %s\n", unitf); return -1; @@ -3476,17 +3685,19 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *unitf) return bodyrc; } -/* archive_o — write a deterministic single-member SysV ar archive at - * `apath` wrapping the object at `objpath`. No armap / long-name table: +/* archive_o — write a deterministic one- or two-member SysV ar archive at + * `apath` wrapping `objpath` and the optional `initpath`. No armap / long-name table: * w6l reads each member's ELF .symtab directly (obj.c elf_globals) and * skips '/'-named members, so a package `.a` needs only the global magic, - * one 60-byte member header, and the `.o` bytes (newline-padded to even). - * Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes - * a pure function of the `.o` content → cstage `.a` == wwstage `.a` + * fixed 60-byte member headers, and the `.o` bytes (newline-padded to even). + * Zeroed mtime/uid/gid + fixed mode + fixed member names make the bytes + * a pure function of the object content → cstage `.a` == wwstage `.a` * (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */ static int -archive_o(const char *objpath, const char *apath) +archive_member(FILE *out, const char *objpath, const char *member) { + if (strlen(member) > 16) + return -1; FILE *in = fopen(objpath, "rb"); if (in == NULL) { fprintf(stderr, "ww: cannot read %s\n", objpath); @@ -3498,41 +3709,54 @@ archive_o(const char *objpath, const char *apath) fclose(in); return -1; } - unsigned char *buf = malloc((size_t)n); - if (buf == NULL) { fclose(in); return -1; } - if (fread(buf, 1, (size_t)n, in) != (size_t)n) { - free(buf); fclose(in); return -1; - } - if (fclose(in) != 0) { free(buf); return -1; } - - FILE *out = fopen(apath, "wb"); - if (out == NULL) { - fprintf(stderr, "ww: cannot open %s\n", apath); - free(buf); - return -1; - } - int bad = fwrite("!\n", 1, 8, out) != 8; /* ar(5) fixes each member header at 60 bytes; the offsets below * address fields in that serialized header. */ char hdr[60]; memset(hdr, ' ', sizeof hdr); - memcpy(hdr + 0, "pkg.o/", 6); /* GNU short-name '/' terminator */ + memcpy(hdr + 0, member, strlen(member)); hdr[16] = '0'; /* mtime (zeroed → determinism) */ hdr[28] = '0'; /* uid (zeroed) */ hdr[34] = '0'; /* gid (zeroed) */ memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */ char sz[12]; + int bad = 0; int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n); if (szn <= 0 || szn > 10) bad = 1; else memcpy(hdr + 48, sz, (size_t)szn); hdr[58] = 0x60; /* member-header magic byte */ hdr[59] = 0x0a; - if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr - || fwrite(buf, 1, (size_t)n, out) != (size_t)n) - bad = 1; + if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr) bad = 1; + unsigned char buf[8192]; + long remaining = n; + while (!bad && remaining > 0) { + size_t want = remaining > (long)sizeof buf + ? sizeof buf : (size_t)remaining; + size_t got = fread(buf, 1, want, in); + if (got != want || fwrite(buf, 1, got, out) != got) { + bad = 1; + break; + } + remaining -= (long)got; + } + if (fclose(in) != 0) bad = 1; if ((n & 1) && fputc('\n', out) == EOF) bad = 1; + return bad ? -1 : 0; +} + +static int +archive_o(const char *objpath, const char *initpath, const char *apath) +{ + FILE *out = fopen(apath, "wb"); + if (out == NULL) { + fprintf(stderr, "ww: cannot open %s\n", apath); + return -1; + } + int bad = fwrite("!\n", 1, 8, out) != 8; + if (!bad && archive_member(out, objpath, "pkg.o/") < 0) bad = 1; + if (!bad && initpath != NULL + && archive_member(out, initpath, "init.o/") < 0) + bad = 1; if (fclose(out) != 0) bad = 1; - free(buf); if (bad) { fprintf(stderr, "ww: cannot write archive %s\n", apath); return -1; @@ -3546,12 +3770,12 @@ archive_o(const char *objpath, const char *apath) * when its freshly composed unit byte-equals the committed unit, no direct * dependency emitted a changed export, AND the driver/tool copies recorded in * the dir byte-equal the live executables — every decision is reproducible by - * hand with cmp(1) against plain files. Artifacts commit - * via temp + rename with the unit renamed last, so a killed build can - * never leave a committed unit vouching for uncommitted artifacts. The - * caller serializes invocations per workdir (Make target = one workdir) - * and `make clean` reclaims the state; the wwstage twin is the - * fileequal/copyfileatomic/workdirstamp group in selfhost/cmd/ww/main.ww. */ + * hand with cmp(1) against plain files. Artifacts, units, tool records, stamp, + * products, and statuses stage together and publish through one rollback- + * capable request transaction, so a killed or rejected build cannot expose a + * mixed generation. The caller serializes invocations per workdir (Make target + * = one workdir), and `make clean` reclaims the state; the wwstage twin is the + * fileequal/workdirstamp/transaction group in selfhost/cmd/ww/main.ww. */ /* `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt * emits no text), so committed presence is their freshness test; the @@ -3561,14 +3785,25 @@ static int file_is_reg(const char *path) { struct stat st; - return stat(path, &st) == 0 && S_ISREG(st.st_mode); + return lstat(path, &st) == 0 && S_ISREG(st.st_mode); } static int file_size_nonzero(const char *path) { struct stat st; - return stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0; + return lstat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0; +} + +/* Existence checks for caller-visible staging and rollback paths must never + * follow a terminal symlink. A dangling OUT.new is occupied, not permission + * to truncate its target through a later fopen(3). */ +static int +path_exists_nofollow(const char *path) +{ + struct stat st; + if (lstat(path, &st) == 0) return 1; + return errno == ENOENT ? 0 : -1; } /* Byte equality of two files; absence or IO error is inequality. */ @@ -3579,7 +3814,7 @@ file_equal(const char *a, const char *b) if (fa == NULL) return 0; FILE *fb = fopen(b, "rb"); if (fb == NULL) { fclose(fa); return 0; } - static char ba[65536], bb[65536]; + static char ba[8192], bb[8192]; int eq = 1; for (;;) { size_t na = fread(ba, 1, sizeof ba, fa); @@ -3594,28 +3829,301 @@ file_equal(const char *a, const char *b) return eq; } -/* Replace dst with src's bytes via temp + rename, so a torn write can - * never masquerade as a committed tool copy. */ static int -copy_file_atomic(const char *src, const char *dst) +copy_file_stage(const char *src, const char *dst) { - char tmp[PATH_MAX]; - int tn = snprintf(tmp, sizeof tmp, "%s.new", dst); - if (tn < 0 || (size_t)tn >= sizeof tmp) return -1; FILE *in = fopen(src, "rb"); if (in == NULL) return -1; - FILE *out = fopen(tmp, "wb"); + FILE *out = fopen(dst, "wb"); if (out == NULL) { fclose(in); return -1; } - static char buf[65536]; - size_t n; - while ((n = fread(buf, 1, sizeof buf, in)) > 0) - if (fwrite(buf, 1, n, out) != n) { - fclose(in); fclose(out); return -1; + unsigned char buf[65536]; + int bad = 0; + for (;;) { + size_t n = fread(buf, 1, sizeof buf, in); + if (n != 0 && fwrite(buf, 1, n, out) != n) bad = 1; + if (bad || n < sizeof buf) { + if (ferror(in)) bad = 1; + break; } - int bad = ferror(in); - fclose(in); - if (fclose(out) != 0 || bad) return -1; - return rename(tmp, dst); + } + if (fclose(in) != 0) bad = 1; + if (fclose(out) != 0) bad = 1; + if (bad) (void)unlink(dst); + return bad ? -1 : 0; +} + +struct septxnentry { + char *stage; + char *dst; + char *backup; + int had_old; + int installed; +}; + +struct septxn { + struct septxnentry *v; + int n, cap; +}; + +static int +sep_txn_add(struct septxn *tx, const char *stage, const char *dst) +{ + if (strcmp(stage, dst) == 0) { + fprintf(stderr, "ww: transaction path collision: %s\n", dst); + return -1; + } + for (int i = 0; i < tx->n; i++) + if (strcmp(tx->v[i].dst, dst) == 0 + || strcmp(tx->v[i].stage, stage) == 0 + || strcmp(tx->v[i].dst, stage) == 0 + || strcmp(tx->v[i].stage, dst) == 0) { + fprintf(stderr, "ww: transaction path collision: %s\n", dst); + return -1; + } + if (tx->n == INT_MAX || sep_reserve((void **)&tx->v, &tx->cap, + tx->n + 1, sizeof *tx->v) < 0) + return -1; + struct septxnentry *e = &tx->v[tx->n]; + memset(e, 0, sizeof *e); + e->stage = strdup(stage); + e->dst = strdup(dst); + e->backup = sep_sprintf("%s.wwtxn.%ld.old", dst, (long)getpid()); + if (e->stage == NULL || e->dst == NULL || e->backup == NULL) { + sep_fail_nomem(); + free(e->backup); free(e->dst); free(e->stage); + memset(e, 0, sizeof *e); + return -1; + } + if (strlen(e->backup) + 1 > (size_t)PATH_MAX) { + fprintf(stderr, "ww: transaction path is too long\n"); + free(e->backup); free(e->dst); free(e->stage); + memset(e, 0, sizeof *e); + return -1; + } + tx->n++; + return 0; +} + +static void +sep_txn_discard(struct septxn *tx) +{ + for (int i = 0; i < tx->n; i++) + if (tx->v[i].stage != NULL) + (void)unlink(tx->v[i].stage); +} + +static void +sep_txn_free(struct septxn *tx) +{ + for (int i = 0; i < tx->n; i++) { + free(tx->v[i].backup); + free(tx->v[i].dst); + free(tx->v[i].stage); + } + free(tx->v); + memset(tx, 0, sizeof *tx); +} + +/* One request-wide rollback group: producers and linkers finish first; only + * then are old destinations parked and all staged files installed. */ +static int +sep_txn_commit(struct septxn *tx) +{ + for (int i = 0; i < tx->n; i++) + if (!file_is_reg(tx->v[i].stage)) { + fprintf(stderr, "ww: transaction stage is not a regular file: %s\n", + tx->v[i].stage); + goto rollback; + } + for (int i = 0; i < tx->n; i++) + if (path_exists_nofollow(tx->v[i].backup) != 0) { + fprintf(stderr, "ww: transaction backup already exists: %s\n", + tx->v[i].backup); + goto rollback; + } + for (int i = 0; i < tx->n; i++) { + if (rename(tx->v[i].dst, tx->v[i].backup) == 0) + tx->v[i].had_old = 1; + else if (errno != ENOENT) { + fprintf(stderr, "ww: cannot preserve transaction destination %s\n", + tx->v[i].dst); + goto rollback; + } + } + for (int i = 0; i < tx->n; i++) { + if (rename(tx->v[i].stage, tx->v[i].dst) != 0) { + fprintf(stderr, "ww: cannot install transaction destination %s\n", + tx->v[i].dst); + goto rollback; + } + tx->v[i].installed = 1; + } + /* Installation is the commit point. Backup cleanup cannot truthfully turn + * a fully installed generation into a rejected one; retain a recoverable + * old copy and report the cleanup failure instead of claiming rollback. */ + for (int i = 0; i < tx->n; i++) + if (tx->v[i].had_old && unlink(tx->v[i].backup) != 0) + fprintf(stderr, "ww: cannot remove transaction backup %s\n", + tx->v[i].backup); + return 0; + +rollback: + for (int i = tx->n - 1; i >= 0; i--) { + if (tx->v[i].installed + && unlink(tx->v[i].dst) != 0 && errno != ENOENT) + fprintf(stderr, "ww: cannot roll back %s\n", tx->v[i].dst); + if (tx->v[i].had_old + && rename(tx->v[i].backup, tx->v[i].dst) != 0) + fprintf(stderr, "ww: cannot restore %s\n", tx->v[i].dst); + (void)unlink(tx->v[i].stage); + } + return -1; +} + +static int +sep_txn_add_pkg_suffix(struct septxn *tx, struct sepgraph *g, int pi, + const char *scratch, const char *stage_suffix, const char *dst_suffix) +{ + char stage[SEP_ARTIFACT_MAX], dst[SEP_ARTIFACT_MAX]; + if (sep_fname(g, pi, scratch, stage_suffix, stage, sizeof stage) < 0 + || sep_fname(g, pi, scratch, dst_suffix, dst, sizeof dst) < 0) + return -1; + return sep_txn_add(tx, stage, dst); +} + +static char * +sep_product_stage_path(const char *dst) +{ + char *path = sep_sprintf("%s.new", dst); + if (path != NULL && strlen(path) + 1 > (size_t)PATH_MAX) { + fprintf(stderr, "ww: product staging path is too long\n"); + free(path); + return NULL; + } + return path; +} + +static int +sep_write_text_stage(const char *path, const char *text) +{ + FILE *f = fopen(path, "wb"); + if (f == NULL) return -1; + int bad = fputs(text, f) == EOF; + if (fclose(f) != 0) bad = 1; + if (bad) (void)unlink(path); + return bad ? -1 : 0; +} + +static int +sep_stage_product_status(struct sepproduct *p) +{ + if (p->status == NULL) return 0; + if (p->stage_status == NULL) + p->stage_status = sep_product_stage_path(p->status); + if (p->stage_status == NULL) return -1; + if (path_exists_nofollow(p->stage_status) != 0) { + fprintf(stderr, "ww: product staging path already exists: %s\n", + p->stage_status); + free(p->stage_status); + p->stage_status = NULL; + return -1; + } + return sep_write_text_stage(p->stage_status, "ok\n"); +} + +static void +sep_free_product_staging(struct sepproduct *products, int nproducts) +{ + for (int i = 0; i < nproducts; i++) { + free(products[i].stage_status); + free(products[i].stage_iface); + free(products[i].stage_out); + products[i].stage_status = NULL; + products[i].stage_iface = NULL; + products[i].stage_out = NULL; + } +} + +static int +sep_prepare_product_stage(char **slot, const char *dst) +{ + if (*slot == NULL) *slot = sep_product_stage_path(dst); + if (*slot == NULL) return -1; + if (path_exists_nofollow(*slot) != 0) { + fprintf(stderr, "ww: product staging path already exists: %s\n", + *slot); + free(*slot); + *slot = NULL; + return -1; + } + return 0; +} + +/* Loader/coordinator-owned staging names are structural request inputs. Check + * every one before scratch acquisition or producer execution, and never treat + * a dangling symlink as an absent path. */ +static int +sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm, + struct sepproduct *products, int nproducts, int root_package, + int publish_package, int emit_asm, int is_test) +{ + if (warm) { + const char *suffix[] = { ".unit.new", ".wwi.new", ".s.new", + ".o.new", ".a.new", ".init.unit.new", ".init.s.new", + ".init.o.new" }; + for (int pi = 0; pi < g->n; pi++) { + if (g->pkg[pi].failed || !g->pkg[pi].loaded) continue; + for (size_t si = 0; si < nelem(suffix); si++) { + char path[SEP_ARTIFACT_MAX]; + if (sep_fname(g, pi, scratch, suffix[si], path, + sizeof path) < 0) + return -1; + if (path_exists_nofollow(path) != 0) { + fprintf(stderr, + "ww: package staging path already exists: %s\n", + path); + return -1; + } + } + } + const char *tool_suffix[] = { "/.wwtool.ww.new", + "/.wwtool.w6c.new", "/.wwtool.w6a.new", + "/.wwtool.stamp.new" }; + for (size_t i = 0; i < nelem(tool_suffix); i++) { + char path[PATH_MAX]; + int n = snprintf(path, sizeof path, "%s%s", scratch, + tool_suffix[i]); + if (n < 0 || (size_t)n >= sizeof path) return -1; + if (path_exists_nofollow(path) != 0) { + fprintf(stderr, + "ww: tool staging path already exists: %s\n", path); + return -1; + } + } + } + for (int i = 0; i < nproducts; i++) { + if (products[i].status != NULL + && sep_prepare_product_stage(&products[i].stage_status, + products[i].status) < 0) + return -1; + if (emit_asm) continue; + int owns_output = root_package ? publish_package + : is_test || sep_root_is_command(&g->pkg[products[i].root]); + if (!owns_output) continue; + if (sep_prepare_product_stage(&products[i].stage_out, + products[i].out) < 0) + return -1; + if (root_package) { + char iface[PATH_MAX]; + int n = snprintf(iface, sizeof iface, "%s.wwi", + products[i].out); + if (n < 0 || (size_t)n >= sizeof iface + || sep_prepare_product_stage(&products[i].stage_iface, + iface) < 0) + return -1; + } + } + return 0; } /* A package publication writes OUT, OUT.new, OUT.wwi, and OUT.wwi.new. @@ -3624,8 +4132,9 @@ static int validate_package_output_path(const char *out) { size_t n = strlen(out); - if ((size_t)PATH_MAX < sizeof ".wwi.new" - || n > (size_t)PATH_MAX - sizeof ".wwi.new") { + if ((size_t)PATH_MAX < sizeof ".wwi.wwtxn.9223372036854775807.old" + || n > (size_t)PATH_MAX + - sizeof ".wwi.wwtxn.9223372036854775807.old") { fprintf(stderr, "ww: package output path is too long\n"); return -1; } @@ -3635,30 +4144,16 @@ validate_package_output_path(const char *out) static int validate_command_output_path(const char *out) { - if (out == NULL || strlen(out) + 1 > (size_t)PATH_MAX) { + if (out == NULL + || (size_t)PATH_MAX < sizeof ".wwtxn.9223372036854775807.old" + || strlen(out) > (size_t)PATH_MAX + - sizeof ".wwtxn.9223372036854775807.old") { fprintf(stderr, "ww: command output path is too long\n"); return -1; } return 0; } -/* A coordinator-private completion marker distinguishes a newly linked - * product from a caller-owned binary left behind by an earlier invocation. */ -static int -record_product_status(const char *path) -{ - if (path == NULL) return 0; - char tmp[PATH_MAX]; - int tn = snprintf(tmp, sizeof tmp, "%s.new", path); - if (tn < 0 || (size_t)tn >= sizeof tmp) return -1; - FILE *f = fopen(tmp, "wb"); - if (f == NULL) return -1; - int bad = fputs("ok\n", f) == EOF; - if (fclose(f) != 0) bad = 1; - if (bad) return -1; - return rename(tmp, path); -} - /* The stamp pins the non-content build inputs a unit compare cannot see: * the -T/-S/root-action shape of the producer pass and the artifact protocol * revision (bump "fmt" when the unit/archive/commit format changes). */ @@ -3666,45 +4161,14 @@ static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", - is_test ? 14 : 15, is_test ? "test" : "build", emit_asm); -} - -/* A stale global builder identity invalidates every committed unit voucher in - * this driver-owned workdir before compilation starts. Artifacts may remain, - * but without their unit they cannot be reused. That makes it safe to record - * the new identity after a partial multi-root pass: successful actions have - * current units, while failed and no-longer-requested actions do not. */ -static int -invalidate_workdir_units(const char *scratch) -{ - DIR *d = opendir(scratch); - if (d == NULL) return -1; - struct dirent *de; - int rc = 0; - while ((de = readdir(d)) != NULL) { - size_t n = strlen(de->d_name); - if (n < 8 || strcmp(de->d_name + n - 8, ".unit.ww") != 0) - continue; - char path[SEP_ARTIFACT_MAX]; - int pn = snprintf(path, sizeof path, "%s/%s", scratch, - de->d_name); - if (pn < 0 || (size_t)pn >= sizeof path - || (unlink(path) != 0 && errno != ENOENT)) { - rc = -1; - break; - } - } - if (closedir(d) != 0) rc = -1; - if (rc != 0) - fprintf(stderr, "ww: cannot invalidate stale package units\n"); - return rc; + is_test ? 16 : 17, is_test ? "test" : "build", emit_asm); } static int sep_discard_action_staging(int warm, const char *unit, const char *wwi, const char *assembly, const char *object, const char *archive) { - if (!warm) return 0; + (void)warm; const char *paths[] = { unit, wwi, assembly, object, archive }; for (size_t i = 0; i < sizeof paths / sizeof paths[0]; i++) { if (unlink(paths[i]) == 0 || errno == ENOENT) continue; @@ -3714,6 +4178,66 @@ sep_discard_action_staging(int warm, const char *unit, const char *wwi, return 0; } +static int +sep_discard_init_staging(int warm, const char *unit, const char *assembly, + const char *object) +{ + (void)warm; + const char *paths[] = { unit, assembly, object }; + for (size_t i = 0; i < sizeof paths / sizeof paths[0]; i++) { + if (paths[i][0] == '\0' + || unlink(paths[i]) == 0 || errno == ENOENT) + continue; + fprintf(stderr, "ww: cannot remove staged initialization artifacts\n"); + return -1; + } + return 0; +} + +static int +sep_discard_request_staging(struct sepgraph *g, const char *scratch, int warm, + struct sepproduct *products, int nproducts) +{ + const char *warm_suffix[] = { ".unit.new", ".wwi.new", ".s.new", + ".o.new", ".a.new", ".init.unit.new", ".init.s.new", + ".init.o.new" }; + const char *cold_suffix[] = { ".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o" }; + const char **suffix = warm ? warm_suffix : cold_suffix; + int rc = 0; + for (int pi = 0; pi < g->n; pi++) + for (size_t i = 0; i < nelem(warm_suffix); i++) { + char path[SEP_ARTIFACT_MAX]; + if (sep_fname(g, pi, scratch, suffix[i], path, + sizeof path) < 0 + || (unlink(path) != 0 && errno != ENOENT)) + rc = -1; + } + for (int i = 0; i < nproducts; i++) { + const char *path[] = { products[i].stage_out, + products[i].stage_iface, products[i].stage_status }; + for (size_t j = 0; j < nelem(path); j++) + if (path[j] != NULL + && unlink(path[j]) != 0 && errno != ENOENT) + rc = -1; + } + if (warm) { + const char *tool_suffix[] = { "/.wwtool.ww.new", + "/.wwtool.w6c.new", "/.wwtool.w6a.new", + "/.wwtool.stamp.new" }; + for (size_t i = 0; i < nelem(tool_suffix); i++) { + char path[PATH_MAX]; + int n = snprintf(path, sizeof path, "%s%s", scratch, + tool_suffix[i]); + if (n < 0 || (size_t)n >= sizeof path + || (unlink(path) != 0 && errno != ENOENT)) + rc = -1; + } + } + if (rc != 0) fprintf(stderr, "ww: cannot discard rejected request staging\n"); + return rc; +} + struct sep_created_dirs { char path[PATH_MAX]; unsigned short offset[(PATH_MAX + 1) / 2]; @@ -3913,7 +4437,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, } g->support_context = -1; if (graphout) *graphout = g; - for (int i = 0; i < nproducts; i++) products[i].support = -1; + for (int i = 0; i < nproducts; i++) { + products[i].support = -1; + products[i].stage_out = NULL; + products[i].stage_iface = NULL; + products[i].stage_status = NULL; + } for (int i = 0; i < nproducts; i++) { const char *entry = products[i].dir != NULL ? products[i].dir : src; @@ -4101,17 +4630,29 @@ build_one_sep_impl(const char *src, int entry_is_dir, products[i].root = mainpkg; } } + for (int pi = 0; pi < g->n; pi++) { + g->pkg[pi].init_symbol = sep_package_init_symbol(&g->pkg[pi]); + if (g->pkg[pi].init_symbol == NULL) return 1; + } for (int i = 0; i < nproducts; i++) { int root = products[i].root; if (!g->pkg[root].failed && sep_root_is_command(&g->pkg[root]) && validate_command_output_path(products[i].out) < 0) return 1; + if (products[i].status != NULL + && validate_command_output_path(products[i].status) < 0) + return 1; } int root_package = !is_test && nproducts == 1 && !g->pkg[products[0].root].failed && !sep_root_is_command(&g->pkg[products[0].root]); if (sep_validate_artifact_paths(g, scratch) < 0) return 1; + if (sep_validate_request_staging(g, scratch, warm, products, nproducts, + root_package, publish_package, emit_asm, is_test) < 0) { + sep_free_product_staging(products, nproducts); + return 1; + } if (warm && workdir_exists && sep_validate_workdir_owners(g, scratch) < 0) return 1; int *order = calloc((size_t)g->n, sizeof *order); @@ -4122,7 +4663,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, free(stack); free(order); return 1; } /* Diagnose cycles per product before constructing the shared union. A - * variant-local cycle must not suppress an independent sibling root. */ + * variant-local cycle does not erase another root's attribution, although + * any failure still rejects the request-wide publication transaction. */ for (int i = 0; i < nproducts; i++) { int root = products[i].root; if (g->pkg[root].failed) continue; @@ -4136,6 +4678,22 @@ build_one_sep_impl(const char *src, int entry_is_dir, || sep_validate_module_closure(g, order, ignored, 1) < 0) g->pkg[root].failed = 1; } + /* Internal-test substitution can create a cycle that is absent from the + * ordinary production graph (I -> X -> P becomes I -> X -> I). Go rejects + * that effective test graph during loading, before any producer action. */ + for (int i = 0; i < nproducts; i++) { + int root = products[i].root; + if (g->pkg[root].failed) continue; + int *initcheck = NULL, ninitcheck = 0; + if (sep_init_order(g, root, products[i].variant_root, + &initcheck, &ninitcheck) < 0) + g->pkg[root].failed = 1; + free(initcheck); + } + if (sep_fatal_allocation) { + free(stack); free(order); + return 1; + } if (!is_test && require_command) { int root = products[0].root; if (!g->pkg[root].failed @@ -4163,8 +4721,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, } free(stack); /* Propagate already-known package-load failures through the union before - * acquiring scratch or completion state. Independent sibling roots remain - * viable, but an entirely rejected cold request leaves no empty tree. */ + * acquiring scratch or completion state. Independent sibling roots may + * remain viable for deterministic staging/diagnosis, but any failed product + * rejects publication; an entirely rejected cold request leaves no tree. */ for (int oi = 0; oi < norder; oi++) { int pi = order[oi]; for (int k = 0; k < g->pkg[pi].ndeps; k++) @@ -4212,19 +4771,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (scratchout != NULL) memcpy(scratchout, scratch, strlen(scratch) + 1); } - for (int i = 0; i < nproducts; i++) - if (products[i].status != NULL - && unlink(products[i].status) != 0 && errno != ENOENT) { - free(order); - return 1; - } - if (warm && stale_all && invalidate_workdir_units(scratch) != 0) { - free(order); - return 1; - } + struct septxn tx = {0}; int any_failed = 0; - int commit_open = 0; - int commit_integrity_failed = 0; for (int i = 0; i < nproducts; i++) if (g->pkg[products[i].root].failed) any_failed = 1; for (int oi = 0; oi < norder; oi++) { @@ -4241,6 +4789,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, char apath[SEP_ARTIFACT_MAX], unitnew[SEP_ARTIFACT_MAX]; char wwinew[SEP_ARTIFACT_MAX], asmnew[SEP_ARTIFACT_MAX]; char objnew[SEP_ARTIFACT_MAX], anew[SEP_ARTIFACT_MAX]; + char initunitf[SEP_ARTIFACT_MAX] = ""; + char initasmf[SEP_ARTIFACT_MAX] = ""; + char initobj[SEP_ARTIFACT_MAX] = ""; + char initunitnew[SEP_ARTIFACT_MAX] = ""; + char initasmnew[SEP_ARTIFACT_MAX] = ""; + char initobjnew[SEP_ARTIFACT_MAX] = ""; sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf); sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi); sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf); @@ -4251,6 +4805,48 @@ build_one_sep_impl(const char *src, int entry_is_dir, sep_fname(g, pi, scratch, ".s.new", asmnew, sizeof asmnew); sep_fname(g, pi, scratch, ".o.new", objnew, sizeof objnew); sep_fname(g, pi, scratch, ".a.new", anew, sizeof anew); + int product_index = -1; + if (g->pkg[pi].link_entry) { + for (int i = 0; i < nproducts; i++) + if (products[i].root == pi) { + product_index = i; + break; + } + if (product_index < 0) { + fprintf(stderr, + "ww: executable action has no owning product\n"); + g->pkg[pi].failed = 1; + any_failed = 1; + continue; + } + sep_fname(g, pi, scratch, ".init.unit.ww", initunitf, + sizeof initunitf); + sep_fname(g, pi, scratch, ".init.s", initasmf, + sizeof initasmf); + sep_fname(g, pi, scratch, ".init.o", initobj, + sizeof initobj); + sep_fname(g, pi, scratch, ".init.unit.new", initunitnew, + sizeof initunitnew); + sep_fname(g, pi, scratch, ".init.s.new", initasmnew, + sizeof initasmnew); + sep_fname(g, pi, scratch, ".init.o.new", initobjnew, + sizeof initobjnew); + } + /* Classic scratch has no committed generation to preserve. Alias the + * cleanup paths to its in-place outputs so a rejected producer leaves + * no partial action or dispatcher artifacts. */ + if (!warm) { + memcpy(unitnew, unitf, strlen(unitf) + 1); + memcpy(wwinew, wwi, strlen(wwi) + 1); + memcpy(asmnew, asmf, strlen(asmf) + 1); + memcpy(objnew, obj, strlen(obj) + 1); + memcpy(anew, apath, strlen(apath) + 1); + if (product_index >= 0) { + memcpy(initunitnew, initunitf, strlen(initunitf) + 1); + memcpy(initasmnew, initasmf, strlen(initasmf) + 1); + memcpy(initobjnew, initobj, strlen(initobj) + 1); + } + } /* Warm mode compiles from staged `.new` paths and commits by * rename; classic mode keeps its exact in-place paths. */ const char *cu = warm ? unitnew : unitf; @@ -4258,19 +4854,33 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *cs = warm ? asmnew : asmf; const char *co = warm ? objnew : obj; const char *ca = warm ? anew : apath; + const char *ciu = warm ? initunitnew : initunitf; + const char *cis = warm ? initasmnew : initasmf; + const char *cio = warm ? initobjnew : initobj; if (sep_discard_action_staging(warm, unitnew, wwinew, asmnew, - objnew, anew) < 0) { - if (warm && unlink(unitf) != 0 && errno != ENOENT) - commit_integrity_failed = 1; + objnew, anew) < 0 + || sep_discard_init_staging(warm, initunitnew, initasmnew, + initobjnew) < 0) { g->pkg[pi].failed = 1; any_failed = 1; continue; } - if (sep_compose_unit(g, pi, cu) < 0) { + if (sep_compose_unit(g, pi, scratch, cu) < 0) { (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); - if (warm && unlink(unitf) != 0 && errno != ENOENT) - commit_integrity_failed = 1; + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + g->pkg[pi].failed = 1; + any_failed = 1; + continue; + } + if (product_index >= 0 + && sep_compose_init_dispatch(g, &products[product_index], + ciu, cis) < 0) { + (void)sep_discard_action_staging(warm, unitnew, wwinew, + asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); g->pkg[pi].failed = 1; any_failed = 1; continue; @@ -4279,33 +4889,71 @@ build_one_sep_impl(const char *src, int entry_is_dir, for (int k = 0; k < g->pkg[pi].ndeps; k++) if (g->pkg[g->pkg[pi].deps[k]].export_changed) deps_changed = 1; - if (warm && !stale_all && !deps_changed - && !commit_integrity_failed + int source_reusable = warm && !stale_all && !deps_changed && file_equal(unitnew, unitf) && file_is_reg(asmf) && file_is_reg(wwi) && (emit_asm || (file_size_nonzero(obj) - && file_size_nonzero(apath)))) { - if (unlink(unitnew) != 0) { - fprintf(stderr, "ww: cannot remove %s\n", - unitnew); + && (product_index >= 0 || file_size_nonzero(apath)))); + int init_reusable = product_index < 0 + || (warm && !stale_all + && file_is_reg(initunitf) + && file_equal(initunitnew, initunitf) + && file_is_reg(initasmf) + && (emit_asm || file_size_nonzero(initobj))); + if (source_reusable && init_reusable + && (emit_asm || file_size_nonzero(apath))) { + if (sep_discard_action_staging(warm, unitnew, wwinew, + asmnew, objnew, anew) < 0 + || sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew) < 0) { g->pkg[pi].failed = 1; any_failed = 1; } continue; } - /* Once an action is known not to be reusable, its old unit must no - * longer vouch for artifacts if any later producer or commit step - * fails. This is especially important when a dependency already - * committed a changed export during the same request. */ - if (warm && unlink(unitf) != 0 && errno != ENOENT) { - fprintf(stderr, "ww: cannot invalidate package unit %s\n", - unitf); + /* A changed closure is owned by the root dispatcher, not the source + * compile action. Rebuild that member and the existing root archive + * without recompiling an otherwise reusable root package. */ + if (source_reusable && product_index >= 0) { + (void)unlink(unitnew); + if (!emit_asm) { + char *iaargv[] = {"w6a", "-o", (char *)cio, + (char *)cis, NULL}; + if (run_argv(a6, iaargv) != 0 + || archive_o(obj, cio, ca) != 0) { + fprintf(stderr, + "ww: initialization archive failed for %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); + g->pkg[pi].failed = 1; + any_failed = 1; + (void)sep_discard_action_staging(warm, unitnew, + wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + continue; + } + } + if (warm) { + g->pkg[pi].init_staged = 1; + if (!emit_asm) g->pkg[pi].archive_staged = 1; + } + continue; + } + /* Staged producers cannot disturb the previous generation. Its source + * and dispatcher vouchers remain committed until commit actually opens; + * direct-export bytes in the source voucher prevent stale later reuse. */ + if (product_index >= 0 && init_reusable && warm + && sep_discard_init_staging(warm, initunitnew, initasmnew, + initobjnew) < 0) { + fprintf(stderr, + "ww: cannot remove staged initialization assembly\n"); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); g->pkg[pi].failed = 1; any_failed = 1; - commit_integrity_failed = 1; continue; } int nmaps = 0; @@ -4316,26 +4964,29 @@ build_one_sep_impl(const char *src, int entry_is_dir, sep_fail_size(); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); - free(order); - return 1; + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + goto request_fail; } nmaps++; } - size_t cargvcap = 14; + size_t cargvcap = 18; if ((size_t)g->pkg[pi].ndeps > ((size_t)-1 - cargvcap) / 3) { fprintf(stderr, "ww: package graph is too large\n"); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); - free(order); - return 1; + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + goto request_fail; } cargvcap += 3 * (size_t)g->pkg[pi].ndeps; if ((size_t)nmaps > ((size_t)-1 - cargvcap) / 3) { fprintf(stderr, "ww: package graph is too large\n"); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); - free(order); - return 1; + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + goto request_fail; } cargvcap += 3 * (size_t)nmaps; char **cargv = calloc(cargvcap, sizeof *cargv); @@ -4348,10 +4999,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, fprintf(stderr, "ww: out of memory\n"); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); free(importfiles); free(cargv); - free(order); - return 1; + goto request_fail; } int cpos = 0; cargv[cpos++] = "w6c"; @@ -4378,10 +5030,18 @@ build_one_sep_impl(const char *src, int entry_is_dir, cargv[cpos++] = (char *)test_support_module; } } + cargv[cpos++] = "--package-init-symbol"; + cargv[cpos++] = g->pkg[pi].init_symbol; + if (product_index >= 0) { + cargv[cpos++] = "--init-dispatch-symbol"; + cargv[cpos++] = "__ww..dispatch"; + } cargv[cpos++] = "-c"; for (int k = 0; k < g->pkg[pi].ndeps; k++) { int dj = g->pkg[pi].deps[k]; - sep_fname(g, dj, scratch, ".wwi", importfiles[k], + const char *suffix = g->pkg[dj].source_staged + ? ".wwi.new" : ".wwi"; + sep_fname(g, dj, scratch, suffix, importfiles[k], sizeof importfiles[k]); cargv[cpos++] = "--import"; cargv[cpos++] = g->pkg[dj].path; @@ -4411,6 +5071,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, any_failed = 1; (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); continue; } g->pkg[pi].export_changed = !warm @@ -4425,141 +5087,92 @@ build_one_sep_impl(const char *src, int entry_is_dir, any_failed = 1; (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); continue; } } - /* Every package action, including executable and generated-test roots, - * produces the existing deterministic single-member archive. */ + if (!emit_asm && product_index >= 0 && !init_reusable) { + char *iaargv[] = {"w6a", "-o", (char *)cio, + (char *)cis, NULL}; + if (run_argv(a6, iaargv) != 0) { + fprintf(stderr, "ww: w6a failed for initialization of %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); + g->pkg[pi].failed = 1; + any_failed = 1; + (void)sep_discard_action_staging(warm, unitnew, wwinew, + asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + continue; + } + } + /* Executable and generated-test roots add their dispatcher as a fixed + * second archive member. All other package archives remain one-member. */ if (!emit_asm) { - if (archive_o(co, ca) != 0) { + const char *archive_init = product_index < 0 ? NULL + : init_reusable ? initobj : cio; + if (archive_o(co, archive_init, ca) != 0) { fprintf(stderr, "ww: archive failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].failed = 1; any_failed = 1; (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); continue; } } - /* Commit order: artifacts before the unit that vouches for - * them, unit strictly last. Remove the workdir identity before the - * first artifact rename; failure to do so is a pre-commit rejection - * that leaves all committed artifacts untouched. */ if (warm) { - if (!commit_open) { - if (unlink(stampf) != 0 && errno != ENOENT) { - fprintf(stderr, - "ww: cannot invalidate package workdir\n"); - g->pkg[pi].failed = 1; - any_failed = 1; - (void)sep_discard_action_staging(warm, unitnew, - wwinew, asmnew, objnew, anew); - continue; - } - commit_open = 1; - } - if (rename(wwinew, wwi) != 0 - || rename(asmnew, asmf) != 0 - || (!emit_asm && rename(objnew, obj) != 0) - || (!emit_asm && rename(anew, apath) != 0) - || rename(unitnew, unitf) != 0) { - fprintf(stderr, "ww: cannot commit %s\n", - g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); - g->pkg[pi].failed = 1; - any_failed = 1; - /* A failed rename sequence may already have replaced the - * interface or another artifact. The stamp is already absent; - * also invalidate every unit voucher and force later actions in - * this invocation through their producers. */ - commit_integrity_failed = 1; - (void)invalidate_workdir_units(scratch); - (void)sep_discard_action_staging(warm, unitnew, wwinew, - asmnew, objnew, anew); - continue; - } - } - } - /* Stale passes removed every old unit voucher before compiling. Current - * successful units are therefore safe to vouch for even when a sibling - * compiler rejects. A partial artifact commit invalidates all vouchers - * and suppresses the workdir identity so the next pass starts stale. */ - if (warm && !commit_integrity_failed) { - if (!file_equal(toolw, self_path) - && copy_file_atomic(self_path, toolw) != 0) { - fprintf(stderr, "ww: cannot record %s\n", toolw); - free(order); return 1; - } - if (!file_equal(toolc, c6) - && copy_file_atomic(c6, toolc) != 0) { - fprintf(stderr, "ww: cannot record %s\n", toolc); - free(order); return 1; - } - if (!emit_asm && !file_equal(toola, a6) - && copy_file_atomic(a6, toola) != 0) { - fprintf(stderr, "ww: cannot record %s\n", toola); - free(order); return 1; - } - if (!stampok || commit_open) { - char stampnew[PATH_MAX]; - int sn = snprintf(stampnew, sizeof stampnew, "%s.new", stampf); - if (sn < 0 || (size_t)sn >= sizeof stampnew) { - free(order); return 1; - } - FILE *sf = fopen(stampnew, "wb"); - int bad = sf == NULL || fputs(stampwant, sf) == EOF; - if (sf != NULL && fclose(sf) != 0) bad = 1; - if (bad || rename(stampnew, stampf) != 0) { - fprintf(stderr, "ww: cannot record %s\n", - stampf); - free(order); return 1; - } + g->pkg[pi].source_staged = 1; + if (!emit_asm) g->pkg[pi].archive_staged = 1; + if (product_index >= 0 && !init_reusable) + g->pkg[pi].init_staged = 1; } } + if (any_failed) goto request_fail; if (emit_asm) { - for (int i = 0; i < nproducts; i++) { - int root = products[i].root; - if (g->pkg[root].failed) { any_failed = 1; continue; } - if (record_product_status(products[i].status) != 0) { - fprintf(stderr, "ww: cannot record package-build product\n"); - g->pkg[root].failed = 1; - any_failed = 1; + for (int i = 0; i < nproducts; i++) + if (sep_stage_product_status(&products[i]) != 0) { + fprintf(stderr, "ww: cannot stage package-build product\n"); + goto request_fail; } - } - free(order); - return any_failed ? 1 : 0; + goto prepare_transaction; } if (root_package) { int root = products[0].root; - if (g->pkg[root].failed) { free(order); return 1; } if (publish_package) { char archive[SEP_ARTIFACT_MAX], iface[SEP_ARTIFACT_MAX]; char outiface[SEP_ARTIFACT_MAX]; - sep_fname(g, root, scratch, ".a", archive, sizeof archive); - sep_fname(g, root, scratch, ".wwi", iface, sizeof iface); + const char *asuffix = warm && g->pkg[root].archive_staged + ? ".a.new" : ".a"; + const char *isuffix = warm && g->pkg[root].source_staged + ? ".wwi.new" : ".wwi"; + sep_fname(g, root, scratch, asuffix, archive, sizeof archive); + sep_fname(g, root, scratch, isuffix, iface, sizeof iface); int on = snprintf(outiface, sizeof outiface, "%s.wwi", out); if (on < 0 || (size_t)on >= sizeof outiface) { fprintf(stderr, "ww: package output path is too long\n"); - free(order); - return 1; + goto request_fail; } - if (copy_file_atomic(archive, out) != 0 - || copy_file_atomic(iface, outiface) != 0) { + if (sep_prepare_product_stage(&products[0].stage_out, out) < 0 + || sep_prepare_product_stage(&products[0].stage_iface, + outiface) < 0 + || copy_file_stage(archive, products[0].stage_out) != 0 + || copy_file_stage(iface, products[0].stage_iface) != 0) { fprintf(stderr, - "ww: cannot write package artifact %s\n", out); - free(order); - return 1; + "ww: cannot stage package artifact %s\n", out); + goto request_fail; } } - if (record_product_status(products[0].status) != 0) { - fprintf(stderr, "ww: cannot record package-build product\n"); - free(order); - return 1; + if (sep_stage_product_status(&products[0]) != 0) { + fprintf(stderr, "ww: cannot stage package-build product\n"); + goto request_fail; } - free(order); - return 0; + goto prepare_transaction; } - free(order); /* Each product gets its own reverse-topological archive closure: root `.a` * first, then every transitively reachable package `.a`, then libwwrt.a. An * internal test variant already contains production sources, so its @@ -4573,37 +5186,37 @@ build_one_sep_impl(const char *src, int entry_is_dir, nrt = 2; rn = snprintf(rtpaths[0], sizeof rtpaths[0], "%s/../obj/rt/start.o", self_dir); - if (rn < 0 || (size_t)rn >= sizeof rtpaths[0]) return 1; + if (rn < 0 || (size_t)rn >= sizeof rtpaths[0]) goto request_fail; rn = snprintf(rtpaths[1], sizeof rtpaths[1], "%s/../obj/rt/syscall.o", self_dir); - if (rn < 0 || (size_t)rn >= sizeof rtpaths[1]) return 1; + if (rn < 0 || (size_t)rn >= sizeof rtpaths[1]) goto request_fail; } int nlibdirs = linkflags ? linkflags->nlibdirs : 0; int nlibs = linkflags ? linkflags->nlibs : 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; int variant_root = products[i].variant_root; - if (g->pkg[root].failed) { any_failed = 1; continue; } if (!is_test && !sep_root_is_command(&g->pkg[root])) { - if (record_product_status(products[i].status) != 0) { - fprintf(stderr, - "ww: cannot record package-build product\n"); - g->pkg[root].failed = 1; - any_failed = 1; + if (sep_stage_product_status(&products[i]) != 0) { + fprintf(stderr, "ww: cannot stage package-build product\n"); + goto request_fail; } continue; } + if (sep_prepare_product_stage(&products[i].stage_out, + products[i].out) < 0) + goto request_fail; for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; int *linkorder = calloc((size_t)g->n, sizeof *linkorder); int *linkstack = calloc((size_t)g->n, sizeof *linkstack); int nlink = 0; if (linkorder == NULL || linkstack == NULL) { fprintf(stderr, "ww: out of memory\n"); - free(linkstack); free(linkorder); return 1; + free(linkstack); free(linkorder); goto request_fail; } if (sep_topo_visit(g, root, linkorder, &nlink, linkstack, 0) < 0) { - free(linkstack); free(linkorder); return 1; + free(linkstack); free(linkorder); goto request_fail; } free(linkstack); size_t largvcap = 4; @@ -4616,7 +5229,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, - 2 * (size_t)nlibdirs) / 2) { fprintf(stderr, "ww: package graph is too large\n"); free(linkorder); - return 1; + goto request_fail; } largvcap += (size_t)nlink + (size_t)nrt + 2 * (size_t)nlibdirs + 2 * (size_t)nlibs; @@ -4626,27 +5239,20 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (largv == NULL || linkpaths == NULL) { fprintf(stderr, "ww: out of memory\n"); free(linkpaths); free(largv); free(linkorder); - return 1; + goto request_fail; } int pos = 0, npath = 0; largv[pos++] = "w6l"; largv[pos++] = "-o"; - largv[pos++] = (char *)products[i].out; + largv[pos++] = products[i].stage_out; for (int oi = nlink - 1; oi >= 0; oi--) { int pi = linkorder[oi]; if (variant_root >= 0 - && g->pkg[variant_root].variant == SEP_VARIANT_SAME_TEST - && pi != variant_root - && g->pkg[pi].variant == SEP_VARIANT_PRODUCTION - && g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT - && g->pkg[pi].import_base != NULL - && g->pkg[variant_root].import_base != NULL - && strcmp(g->pkg[pi].import_base, - g->pkg[variant_root].import_base) == 0 - && strcmp(g->pkg[pi].canon, - g->pkg[variant_root].canon) == 0) + && sep_internal_replaces_production(g, variant_root, pi)) continue; - sep_fname(g, pi, scratch, ".a", linkpaths[npath], + const char *suffix = warm && g->pkg[pi].archive_staged + ? ".a.new" : ".a"; + sep_fname(g, pi, scratch, suffix, linkpaths[npath], sizeof linkpaths[npath]); largv[pos++] = linkpaths[npath++]; } @@ -4667,16 +5273,111 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (linkrc != 0) { fprintf(stderr, "ww: w6l failed\n"); g->pkg[root].failed = 1; - any_failed = 1; - continue; + goto request_fail; } - if (record_product_status(products[i].status) != 0) { - fprintf(stderr, "ww: cannot record package-test product\n"); - g->pkg[root].failed = 1; - any_failed = 1; + if (sep_stage_product_status(&products[i]) != 0) { + fprintf(stderr, "ww: cannot stage package-test product\n"); + goto request_fail; } } - return any_failed ? 1 : 0; + +prepare_transaction: + if (warm) { + for (int oi = 0; oi < norder; oi++) { + int pi = order[oi]; + if (g->pkg[pi].source_staged + && (sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".wwi.new", ".wwi") < 0 + || sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".s.new", ".s") < 0 + || (!emit_asm && sep_txn_add_pkg_suffix(&tx, g, pi, + scratch, ".o.new", ".o") < 0))) + goto request_fail; + if (g->pkg[pi].init_staged + && (sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".init.s.new", ".init.s") < 0 + || (!emit_asm && sep_txn_add_pkg_suffix(&tx, g, pi, + scratch, ".init.o.new", ".init.o") < 0))) + goto request_fail; + if (g->pkg[pi].archive_staged + && sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".a.new", ".a") < 0) + goto request_fail; + if (g->pkg[pi].source_staged + && sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".unit.new", ".unit.ww") < 0) + goto request_fail; + if (g->pkg[pi].init_staged + && sep_txn_add_pkg_suffix(&tx, g, pi, scratch, + ".init.unit.new", ".init.unit.ww") < 0) + goto request_fail; + } + const char *toolsrc[] = { self_path, c6, a6 }; + const char *tooldst[] = { toolw, toolc, toola }; + int ntools = emit_asm ? 2 : 3; + for (int i = 0; i < ntools; i++) { + if (file_equal(tooldst[i], toolsrc[i])) continue; + char stage[PATH_MAX]; + int sn = snprintf(stage, sizeof stage, "%s.new", tooldst[i]); + if (sn < 0 || (size_t)sn >= sizeof stage + || copy_file_stage(toolsrc[i], stage) != 0 + || sep_txn_add(&tx, stage, tooldst[i]) < 0) { + fprintf(stderr, "ww: cannot stage workdir tool identity\n"); + goto request_fail; + } + } + if (!stampok) { + char stage[PATH_MAX]; + int sn = snprintf(stage, sizeof stage, "%s.new", stampf); + if (sn < 0 || (size_t)sn >= sizeof stage + || sep_write_text_stage(stage, stampwant) != 0 + || sep_txn_add(&tx, stage, stampf) < 0) { + fprintf(stderr, "ww: cannot stage workdir stamp\n"); + goto request_fail; + } + } + } + for (int i = 0; i < nproducts; i++) { + if (products[i].stage_out != NULL + && sep_txn_add(&tx, products[i].stage_out, + products[i].out) < 0) + goto request_fail; + if (products[i].stage_iface != NULL) { + char outiface[SEP_ARTIFACT_MAX]; + int on = snprintf(outiface, sizeof outiface, "%s.wwi", + products[i].out); + if (on < 0 || (size_t)on >= sizeof outiface + || sep_txn_add(&tx, products[i].stage_iface, + outiface) < 0) + goto request_fail; + } + } + for (int i = 0; i < nproducts; i++) + if (products[i].stage_status != NULL + && sep_txn_add(&tx, products[i].stage_status, + products[i].status) < 0) + goto request_fail; + if (sep_txn_commit(&tx) != 0) goto request_fail; + sep_txn_free(&tx); + sep_free_product_staging(products, nproducts); + free(order); + return 0; + +request_fail: + sep_txn_discard(&tx); + (void)sep_discard_request_staging(g, scratch, warm, products, nproducts); + sep_txn_free(&tx); + sep_free_product_staging(products, nproducts); + if (!warm) { + if (rmdir(scratch) != 0 && errno != ENOENT) + fprintf(stderr, "ww: cannot remove rejected scratch %s\n", scratch); + else if (scratchout != NULL) scratchout[0] = '\0'; + } else { + sep_rollback_dirs(&created_work); + } + sep_rollback_dirs(&created_output); + free(order); + return 1; } /* build_one_sep — thin wrapper over build_one_sep_impl. `ww build` and an diff --git a/lib/bufio/stream.ww b/lib/bufio/stream.ww index a1eafe3d..ae2ef85a 100644 --- a/lib/bufio/stream.ww +++ b/lib/bufio/stream.ww @@ -75,7 +75,7 @@ export type stream = struct { // // Returns the stream BY VALUE; the caller passes `&b.vt` to the io // dispatchers. Mirrors ref/hare/bufio/stream.ha:69. -export fn init(src: io.stream, rbuf: []u8, wbuf: []u8) stream = { +export fn newstream(src: io.stream, rbuf: []u8, wbuf: []u8) stream = { let r: stream; r.vt.reader = (&bread): *io.reader; r.vt.writer = (&bwrite): *io.writer; diff --git a/lib/math/random/random.ww b/lib/math/random/random.ww index 0991cd58..c96f6e90 100644 --- a/lib/math/random/random.ww +++ b/lib/math/random/random.ww @@ -10,7 +10,7 @@ package random; export type random = u64; // Mirrors Hare's random::init. -export fn init(seed: u64) random = { return seed: random; }; +export fn fromseed(seed: u64) random = { return seed: random; }; // SplitMix64, per Hare's random::next. export fn next(r: *random) u64 = { diff --git a/lib/ww/syntax/ast.ww b/lib/ww/syntax/ast.ww index 3981a8c0..536c9932 100644 --- a/lib/ww/syntax/ast.ww +++ b/lib/ww/syntax/ast.ww @@ -131,9 +131,17 @@ export type node = struct { usepath: str, // N_USE: canonical vendor-expanded identity usealias: str, // N_USE: explicit file-local alias, or empty usepkgname: str,// N_USE: imported declared package name + useblank: i32, // N_USE: `_` spelling; no source binding pkgname: str, // declared package name; independent of canonical nmod sourceid: i32, // lexical source-file scope in an owner/export unit used: i32, // N_USE: checker observed this file-local binding + initfn: i32, // special source `fn init`, absent from scope/API + initsynthetic: i32,// compiler-owned variable helper/package task + runtimeinit: i32,// module let lowered through an init helper + initorder: u64,// 1-based variable or init-function order + linksym: str, // raw compiler-private assembler symbol + refdecl: *node,// checker-resolved value declaration + initmark: u64,// checker-private initializer dependency walk mark imported: i32, // M1 #22: decl reached via `//ww:module ` import // boundary (vs root/primary); gates root-only bare main }; @@ -142,7 +150,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). - let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usepkgname="", pkgname="", sourceid=0, used=0, imported=0})!; + let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, packed=0, type_=nil, tsuffix="", nmod="", usesource="", usepath="", usealias="", usepkgname="", useblank=0, pkgname="", sourceid=0, used=0, initfn=0, initsynthetic=0, runtimeinit=0, initorder=0u64, linksym="", refdecl=nil, initmark=0u64, imported=0})!; return n; }; diff --git a/lib/ww/syntax/decl.ww b/lib/ww/syntax/decl.ww index f07a1bd0..a2437b62 100644 --- a/lib/ww/syntax/decl.ww +++ b/lib/ww/syntax/decl.ww @@ -16,8 +16,7 @@ fn parseuse(p: *parser) *node = { let alias: str; let first: str; if (p.curkind == tkind.TK_UNDER) { - errmsg(p, "blank import alias _ is not implemented"); - alias = "_"; + n.useblank = 1; advance(p); expectident(p, &first); } else { @@ -34,7 +33,10 @@ fn parseuse(p: *parser) *node = { expectident(p, &leaf); path = strings.concat(path, ".", leaf); }; - if (alias.len > 0) { n.str = alias; } else { n.str = leaf; }; + if (n.useblank != 0) { + let empty: str; + n.str = empty; + } else { if (alias.len > 0) { n.str = alias; } else { n.str = leaf; }; }; n.usesource = path; n.usepath = path; n.usealias = alias; @@ -68,14 +70,16 @@ fn parsedef(p: *parser, exported: i32) *node = { }; fn parselet(p: *parser, exported: i32) *node = { - let pf: str = p.curfile; - let pl: i32 = p.curline; - let pc: i32 = p.curcol; // Accept `let` or `const`. Const-bound bindings are marked via // n.op = tkind.TK_CONST so the checker can reject reassignment. let is_const: i32 = 0; if (p.curkind == tkind.TK_CONST) { is_const = 1; }; advance(p); + // The declaration position is the binding identifier, matching the C + // parser and the source anchor used by package-initialization diagnostics. + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; let n: *node = newnode(nkind.N_LET, pf, pl, pc); n.nmod = p.curmod; let id: str; diff --git a/selfhost/cmd/w6a/main.ww b/selfhost/cmd/w6a/main.ww index b75db3c3..21936309 100644 --- a/selfhost/cmd/w6a/main.ww +++ b/selfhost/cmd/w6a/main.ww @@ -113,7 +113,7 @@ export fn main(argc: i32, argv: **u8) i32 = { view.ptr = src; view.len = nlen: i32; let fname: str = strings.dup(view); - init(&s, fname, buf, blen); + parserinit(&s, fname, buf, blen); if (parse(&s) != 0) { return 1; }; if (encode(&s) != 0) { return 1; }; diff --git a/selfhost/cmd/w6a/parse.ww b/selfhost/cmd/w6a/parse.ww index 663df0ea..16241ef1 100644 --- a/selfhost/cmd/w6a/parse.ww +++ b/selfhost/cmd/w6a/parse.ww @@ -136,7 +136,7 @@ fn reglookup(p: *u8, n: u64) i32 = { return D_NONE; }; -export fn init(a: *asm_, file: str, src: *u8, len: u64) void = { +export fn parserinit(a: *asm_, file: str, src: *u8, len: u64) void = { a.file = file; a.src = src; a.srclen = len; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 02bc9382..ca2f3757 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -3,10 +3,204 @@ package main; import os; +import strconv; import strings; import syntax; import wcc; +type publication = struct { + dst: *u8, + stage: str, + backup: str, + hadold: bool, + installed: bool, +}; + +fn publicationpath(dst: *u8, kind: str) str = { + return strings.concat(pathstr(dst), ".w6c.", + strconv.i32tos(os.getpid(), strconv.base.DEC), ".", kind); +}; + +// Open a destination-adjacent file and immediately unlink its name. Cgen's +// fatal exit then leaves only a kernel-owned fd, never a partial destination +// or named staging artifact. +fn anonymousfd(dst: *u8, kind: str) i32 = { + let path: str = publicationpath(dst, kind); + let fd: i32 = os.open(path, + os.flag.RDWR | os.flag.CREATE | os.flag.EXCL, 384i32); + if (fd < 0) { + os.write(2, "w6c: cannot create anonymous output\n".ptr, + "w6c: cannot create anonymous output\n".len: u64); + return -1; + }; + if (os.remove(path) != 0) { + os.close(fd); + os.write(2, "w6c: cannot unlink anonymous output\n".ptr, + "w6c: cannot unlink anonymous output\n".len: u64); + return -1; + }; + return fd; +}; + +fn copystream(src: i32, dst: i32) bool = { + if (os.lseek(src, 0i64, os.whence.SET) < 0) { return false; }; + let buf: [65536]u8; + for (true) { + let n: i64 = os.read(src, buf.ptr, 65536u64); + if (n < 0) { return false; }; + if (n == 0) { return true; }; + let wr: (i64 | os.oserror) = os.writeall(dst, buf.ptr, n: u64); + match (wr) { + case let wrote: i64 => { if (wrote != n) { return false; }; }; + case let e: os.oserror => return false; + }; + }; + return false; +}; + +fn materializestream(src: i32, dst: *u8, p: *publication) bool = { + p.dst = dst; + p.stage = publicationpath(dst, "new"); + p.backup = publicationpath(dst, "old"); + p.hadold = false; + p.installed = false; + let fd: i32 = os.open(p.stage, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 420i32); + if (fd < 0) { return false; }; + let ok: bool = copystream(src, fd); + if (os.close(fd) != 0) { ok = false; }; + if (!ok) { os.remove(p.stage); }; + return ok; +}; + +// A publication rollback name is occupied by any terminal directory entry, +// including a dangling symlink. Never follow it while deciding whether the +// compiler may park an existing destination there. +fn pathexistsnofollow(path: str) i32 = { + let fi: os.filestat; + match (os.lstat(&fi, path)) { + case void => return 1; + case let e: os.oserror => { + if ((e: i64) == -2i64) { return 0; }; + return -1; + }; + }; + return -1; +}; + +fn pathisregularnofollow(path: str) bool = { + let fi: os.filestat; + match (os.lstat(&fi, path)) { + case void => { + return (((fi.mode: u32) & 61440u32) == (os.mode.REG: u32)); + }; + case let e: os.oserror => return false; + }; + return false; +}; + +fn publishdiag(prefix: str, dst: *u8) void = { + os.write(2, prefix.ptr, prefix.len: u64); + let path: str = pathstr(dst); + os.write(2, path.ptr, path.len: u64); + os.write(2, "\n".ptr, 1u64); +}; + +fn publishall(p: *publication, n: i32) bool = { + let i: i32 = 0; + let ok: bool = true; + for (i < n) { + if (!pathisregularnofollow(p[i].stage)) { + publishdiag("w6c: publication stage is not a regular file for ", + p[i].dst); + ok = false; + break; + }; + let fi: os.filestat; + match (os.lstat(&fi, pathstr(p[i].dst))) { + case void => { + if (((fi.mode: u32) & 61440u32) != (os.mode.REG: u32)) { + publishdiag( + "w6c: publication destination is not a regular file: ", + p[i].dst); + ok = false; + }; + }; + case let e: os.oserror => { + if ((e: i64) != -2i64) { + publishdiag("w6c: cannot inspect publication destination ", + p[i].dst); + ok = false; + }; + }; + }; + if (!ok) { break; }; + i += 1; + }; + if (ok) { + i = 0; + for (i < n) { + let exists: i32 = pathexistsnofollow(p[i].backup); + if (exists != 0) { + if (exists < 0) { + publishdiag("w6c: cannot inspect publication backup for ", + p[i].dst); + } else { + publishdiag("w6c: publication backup exists for ", p[i].dst); + }; + ok = false; + break; + }; + i += 1; + }; + }; + if (ok) { + i = 0; + for (i < n) { + let rc: i32 = os.rename(pathstr(p[i].dst), p[i].backup); + if (rc == 0) { p[i].hadold = true; } + else { if (rc != -2) { + publishdiag("w6c: cannot preserve ", p[i].dst); + ok = false; + break; + }; }; + i += 1; + }; + }; + if (ok) { + i = 0; + for (i < n) { + if (os.rename(p[i].stage, pathstr(p[i].dst)) != 0) { + publishdiag("w6c: cannot publish ", p[i].dst); + ok = false; + break; + }; + p[i].installed = true; + i += 1; + }; + if (ok) { + // Installation is the commit point. Backup cleanup cannot + // truthfully turn a completely installed pair into rejection. + i = 0; + for (i < n) { + if (p[i].hadold && os.remove(p[i].backup) != 0) { + publishdiag("w6c: cannot remove backup for ", p[i].dst); + }; + i += 1; + }; + return true; + }; + }; + i = n - 1; + for (i >= 0) { + if (p[i].installed) { os.remove(pathstr(p[i].dst)); }; + if (p[i].hadold) { os.rename(p[i].backup, pathstr(p[i].dst)); }; + if (p[i].stage.len != 0) { os.remove(p[i].stage); }; + i -= 1; + }; + return false; +}; + fn cstreq(a: *u8, lit: str) bool = { let n: u64 = lit.len: u64; let i: u64 = 0u64; @@ -192,8 +386,10 @@ fn bindimportnames(list: *syntax.node, asts: []*syntax.node, paths: []*u8, }; if (name.len > 0) { u.usepkgname = name; - if (u.usealias.len > 0) { u.str = u.usealias; } - else { u.str = name; }; + if (u.useblank == 0) { + if (u.usealias.len > 0) { u.str = u.usealias; } + else { u.str = name; }; + }; } else { if (u.imported == 0) { let pre: str = "w6c: import "; let post: str = " has no declared package name in direct export data\n"; @@ -205,8 +401,10 @@ fn bindimportnames(list: *syntax.node, asts: []*syntax.node, paths: []*u8, // A closure-only import may have no declarations in this // interface. Its canonical path must never become a leaf // qualifier by fallback. - if (u.usealias.len > 0) { u.str = u.usealias; } - else { u.str = u.usepath; }; + if (u.useblank == 0) { + if (u.usealias.len > 0) { u.str = u.usealias; } + else { u.str = u.usepath; }; + }; }; }; }; }; @@ -225,6 +423,8 @@ export fn main(argc: i32, argv: **u8) i32 = { let testpackage: i32 = 0i32; let commandpackage: i32 = 0i32; let entrymode: i32 = 0i32; + let packageinit: *u8 = nil; + let initdispatch: *u8 = nil; let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- // only codegen (emit imported==0 decls // only; treat `.wwi` deps as external) @@ -302,6 +502,22 @@ export fn main(argc: i32, argv: **u8) i32 = { commandpackage = 1i32; } else { if (cstreq(a, "--entry")) { entrymode = 1i32; + } else { if (cstreq(a, "--package-init-symbol")) { + i += 1; + if (i >= argc) { + let m: str = "w6c: --package-init-symbol requires arg\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + packageinit = argv[i]; + } else { if (cstreq(a, "--init-dispatch-symbol")) { + i += 1; + if (i >= argc) { + let m: str = "w6c: --init-dispatch-symbol requires arg\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + initdispatch = argv[i]; } else { if (cstreq(a, "--test-support-module")) { i += 1; if (i >= argc) { @@ -352,12 +568,17 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; }; }; }; }; }; }; }; }; + }; }; }; }; }; }; }; }; }; }; }; }; }; }; i += 1; }; if (src == nil) { - let m: str = "usage: w6c_ww [-T|--test-package] [--command-package] [--entry] [--test-target-package path] [-c] [-I out.wwi] [--import path dep.wwi]... [--import-map source path]... [-o out.s] file.ww\n"; + let m: str = "usage: w6c_ww [-T|--test-package] [--command-package] [--entry] [--package-init-symbol symbol] [--init-dispatch-symbol symbol] [--test-target-package path] [-c] [-I out.wwi] [--import path dep.wwi]... [--import-map source path]... [-o out.s] file.ww\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (out != nil && wwiout != nil && cstreq(out, pathstr(wwiout))) { + let m: str = "w6c: assembly and interface outputs must be distinct\n"; os.write(2, m.ptr, m.len: u64); return 2; }; @@ -377,6 +598,22 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + if ((packageinit != nil || initdispatch != nil) && sepmode == 0) { + let m: str = "w6c: package initialization symbols require -c\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (packageinit != nil && packageinit[0u64] == 0u8) { + let m: str = "w6c: --package-init-symbol is empty\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (initdispatch != nil + && (initdispatch[0u64] == 0u8 || entrymode == 0)) { + let m: str = "w6c: --init-dispatch-symbol requires --entry and a non-empty symbol\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; if (testmode != 0 && testpackage != 0) { let m: str = "w6c: -T and --test-package are mutually exclusive\n"; os.write(2, m.ptr, m.len: u64); @@ -595,30 +832,82 @@ export fn main(argc: i32, argv: **u8) i32 = { f.list = importhead; }; - // cgen writes directly to fd 1; dup2 keeps that implementation private. + let wwifd: i32 = -1; + if (wwiout != nil) { + wwifd = anonymousfd(wwiout, "anon.wwi"); + if (wwifd < 0) { return 1; }; + }; + // cgen writes directly to fd 1. Redirect it only to an already-unlinked + // anonymous file, never to the named destination. + let asmfd: i32 = -1; if (out != nil) { - let ofd: i32 = os.open(pathstr(out), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (ofd < 0) { - let m: str = "w6c: cannot open output\n"; - os.write(2, m.ptr, m.len: u64); + asmfd = anonymousfd(out, "anon.asm"); + if (asmfd < 0) { + if (wwifd >= 0) { os.close(wwifd); }; return 1; }; - if (os.dup2(ofd, 1i32) < 0) { + if (os.dup2(asmfd, 1i32) < 0) { let m: str = "w6c: dup2 failed\n"; os.write(2, m.ptr, m.len: u64); - os.close(ofd); + os.close(asmfd); + if (wwifd >= 0) { os.close(wwifd); }; return 1; }; - os.close(ofd); }; let testmodule: str; if (testsupport != nil) { testmodule = pathstr(testsupport); }; let testtargetmodule: str; if (testtarget != nil) { testtargetmodule = pathstr(testtarget); }; - let interfaceout: str; - if (wwiout != nil) { interfaceout = pathstr(wwiout); }; - return wcc.compilefile(f, testmode, testpackage, testmodule, - testtargetmodule, sepmode, interfaceout, entrymode); + let packageinitsymbol: str; + if (packageinit != nil) { packageinitsymbol = pathstr(packageinit); }; + let initdispatchsymbol: str; + if (initdispatch != nil) { initdispatchsymbol = pathstr(initdispatch); }; + let haswwi: i32 = 0; + if (wwiout != nil) { haswwi = 1; }; + let compilerc: i32 = wcc.compilefile(f, testmode, testpackage, testmodule, + testtargetmodule, sepmode, wwifd, haswwi, entrymode, + packageinitsymbol, initdispatchsymbol); + if (out != nil) { os.close(1i32); }; + if (compilerc != 0) { + if (asmfd >= 0) { os.close(asmfd); }; + if (wwifd >= 0) { os.close(wwifd); }; + return compilerc; + }; + let pubs: [2]publication; + let pi: i32 = 0; + for (pi < 2) { + pubs[pi].dst = nil; + pubs[pi].stage = ""; + pubs[pi].backup = ""; + pubs[pi].hadold = false; + pubs[pi].installed = false; + pi += 1; + }; + let npub: i32 = 0; + let stagedok: bool = true; + if (wwiout != nil) { + stagedok = materializestream(wwifd, wwiout, &pubs[npub]); + if (stagedok) { npub += 1; }; + }; + if (stagedok && out != nil) { + stagedok = materializestream(asmfd, out, &pubs[npub]); + if (stagedok) { npub += 1; }; + }; + if (asmfd >= 0) { os.close(asmfd); }; + if (wwifd >= 0) { os.close(wwifd); }; + if (!stagedok) { + pi = 0; + for (pi < 2) { + if (pubs[pi].stage.len != 0) { os.remove(pubs[pi].stage); }; + pi += 1; + }; + let m: str = "w6c: cannot stage compiler output\n"; + os.write(2, m.ptr, m.len: u64); + return 1; + }; + if (!publishall(&pubs[0], npub)) { + return 1; + }; + return 0; }; diff --git a/selfhost/cmd/wcc/api.ww b/selfhost/cmd/wcc/api.ww index 04579ece..401d0583 100644 --- a/selfhost/cmd/wcc/api.ww +++ b/selfhost/cmd/wcc/api.ww @@ -3,8 +3,9 @@ package wcc; import syntax; export fn compilefile(file: *syntax.node, testmode: i32, testpackage: i32, - testmodule: str, testtarget: str, sepmode: i32, wwiout: str, - entrymode: i32) i32 = { + testmodule: str, testtarget: str, sepmode: i32, wwifd: i32, + haswwi: i32, + entrymode: i32, packageinitsymbol: str, initdispatchsymbol: str) i32 = { let tc: syntax.tctx; syntax.typesinit(&tc); let ck: checker; @@ -14,18 +15,19 @@ export fn compilefile(file: *syntax.node, testmode: i32, testpackage: i32, if (testmodule.len > 0) { ck.testmodule = testmodule; }; ck.testtarget = testtarget; ck.sepmode = sepmode; + ck.packageinitsymbol = packageinitsymbol; checkfile(&ck, file); if (ck.errs > 0) { return 1; }; - if (wwiout.len > 0) { - if (wwiemit(&ck, file, wwiout) != 0) { return 1; }; + if (haswwi != 0) { + if (wwiemitfd(&ck, file, wwifd) != 0) { return 1; }; }; let cg: cgen; cgeninit(&cg); cg.sepmode = sepmode; - if (wwiout.len > 0 && entrymode == 0) { cg.sepisdep = 1i32; }; - cgfile(&cg, file); - return 0; + if (haswwi != 0 && entrymode == 0) { cg.sepisdep = 1i32; }; + cg.initdispatchsymbol = initdispatchsymbol; + return cgfile(&cg, file); }; export fn resolvefile(file: *syntax.node, verbose: i32, diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index e28bcab5..95085908 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -509,6 +509,7 @@ type cgen = struct { // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). // Symmetric with cstage Cg.sep_isdep. sepisdep: i32, + initdispatchsymbol: str, // root-owned complete package-init schedule }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -741,6 +742,17 @@ fn localfind(c: *cgen, name: str) i32 = { let cgoutstream: memio.stream; let cgoutmode: i32 = 0; let cgoutinit: i32 = 0; +let cgwritefailed: i32 = 0; + +fn cgwrite(p: *u8, n: u64) void = { + if (cgwritefailed != 0) { return; }; + match (os.writeall(1i32, p, n)) { + case let wrote: i64 => { + if (wrote < 0 || (wrote: u64) != n) { cgwritefailed = 1; }; + }; + case let e: os.oserror => cgwritefailed = 1; + }; +}; fn cgout_enable() void = { if (cgoutinit == 0) { @@ -754,7 +766,7 @@ fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { if (cgoutstream.pos > 0) { - os.write(1, cgoutstream.ptr, cgoutstream.pos: u64); + cgwrite(cgoutstream.ptr, cgoutstream.pos: u64); memio.reset(&cgoutstream); }; }; @@ -769,7 +781,7 @@ fn emitbytes(p: *u8, n: u64) void = { // lib/log/log.ww stdprintln. #94 fold-eFinal. io.write(&cgoutstream.vt, buf); } else { - os.write(1, p, n); + cgwrite(p, n); }; }; @@ -2894,6 +2906,35 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *syntax.node, rhs: *synta return true; }; +fn emitinitbackings(n: *syntax.node) void = { + for (n != nil) { + if (n.kind == syntax.nkind.N_ARRLIT && n.linksym.len > 0) { + let u: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); + if (u == nil || u.kind != syntax.tykind.TY_ARRAY) { + let msg: str = "runtime package slice backing has no array type\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + emitline("DATAW "); + emitbytes(n.linksym.ptr, n.linksym.len: u64); + emitline("(SB),\""); + let count: u64 = u.size; + if (count == 0u64) { count = 1u64; }; + let i: u64 = 0u64; + for (i < count) { emitdatawbyte(0u8); i += 1u64; }; + emitline("\"\n"); + }; + emitinitbackings(n.attr); + emitinitbackings(n.lhs); + emitinitbackings(n.rhs); + emitinitbackings(n.cond); + emitinitbackings(n.body); + emitinitbackings(n.els); + emitinitbackings(n.list); + n = n.next; + }; +}; + fn emitletdataw(c: *cgen, file: *syntax.node) void = { let savedmod: str = c.curmod; let savedsource: i32 = c.cursource; @@ -3458,6 +3499,10 @@ fn collectfnrets(c: *cgen, file: *syntax.node) void = { let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_FNDECL) { + if (d.initfn != 0 || d.initsynthetic != 0) { + d = d.next; + continue; + }; let f: *fnret = alloc(fnret{fname=d.str, fmod=d.nmod, rtype=d.lhs, params=d.list, frnext=c.fnrets})!; c.fnrets = f; }; @@ -3788,12 +3833,16 @@ fn collectmods(c: *cgen, file: *syntax.node) void = { let d: *syntax.node = file.list; for (d != nil) { // M1 #22: record alias→path for the qualified-ref hint. - if (d.kind == syntax.nkind.N_USE) { + if (d.kind == syntax.nkind.N_USE && d.useblank == 0) { if (d.usepath.len > 0) { let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, sourceid=d.sourceid, mnext=c.uses})!; c.uses = um; }; }; + if (d.initfn != 0 || d.initsynthetic != 0) { + d = d.next; + continue; + }; // Mirror collectfnrets' shape exactly (plain prepend in one // branch). Earlier nested-if/early-return variants tickled a // wwstage cgen bug that dropped most prepends. @@ -3988,6 +4037,10 @@ fn fficollect(c: *cgen, file: *syntax.node) void = { let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_FNDECL) { + if (d.initfn != 0 || d.initsynthetic != 0) { + d = d.next; + continue; + }; let a: *syntax.node = d.attr; for (a != nil) { if (a.kind == syntax.nkind.N_ATTR) { diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index dda5116c..e1f3f082 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -544,6 +544,12 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { }; cgfnparams(c, fn_.list); + if (c.initdispatchsymbol.len > 0 && syntax.streq(fn_.str, "main") + && fn_.imported == 0 && c.sepisdep == 0) { + emitline("\tCALL\t"); + emitbytes(c.initdispatchsymbol.ptr, c.initdispatchsymbol.len: u64); + emitline("(SB)\n"); + }; c.lastwasreturn = 0; // Iterate the fn body's statements directly rather than dispatching // the outermost N_BLOCK through cgstmt — cgblock now save/restores @@ -594,11 +600,13 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { // (path-carrying `//ww:module-reset`, #57); only the root/link-entry // unit (wwiout==nil, #69) keeps the bare label. emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { + if (fn_.linksym.len > 0) { + emitbytes(fn_.linksym.ptr, fn_.linksym.len: u64); + } else { if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { emitbytes(fn_.str.ptr, fn_.str.len: u64); } else { emitfnname(c, fn_.str, fn_.nmod); - }; + }; }; emitline(",$"); emitint(frame: i64); emitline("\n"); @@ -612,8 +620,9 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = { cgout_flush(); }; -fn cgfile(c: *cgen, file: *syntax.node) void = { - if (file == nil) { return; }; +fn cgfile(c: *cgen, file: *syntax.node) i32 = { + cgwritefailed = 0; + if (file == nil) { return 0; }; c.strlits = nil; c.strlitseq = 0; collectaliases(c, file); @@ -647,5 +656,7 @@ fn cgfile(c: *cgen, file: *syntax.node) void = { letpreintern(c, file); emitdatasection(c); emitdefconstants(c, file); + emitinitbackings(file.list); emitletdataw(c, file); + return cgwritefailed; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index b030bfe2..9e6900ce 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -8698,7 +8698,11 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { emitline("\tCALL\tAX\n"); } else { emitline("\tCALL\t"); - if (callee != nil) { + if (callee != nil && callee.refdecl != nil + && callee.refdecl.linksym.len > 0) { + emitbytes(callee.refdecl.linksym.ptr, + callee.refdecl.linksym.len: u64); + } else { if (callee != nil) { if (callee.kind == syntax.nkind.N_IDENT) { // Bare `f()` — same-module by ww's resolver, // so c.curmod is the disambiguation hint. @@ -8718,7 +8722,7 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { }; emitfnname(c, calleename, hint); };}; - }; + }; }; emitline("(SB)\n"); }; // Caller cleanup for stack-passed args (args 7+, or any diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 61ed82bd..45e158cd 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2173,6 +2173,284 @@ fn cgarrlitfillbp(c: *cgen, arrtn: *syntax.node, rhs: *syntax.node, off: i32) vo }; }; +// Compiler-generated package-variable helpers need a memory-directed literal +// path. It bypasses the finite tuple cursor, recursively fills nested +// aggregates, and moves runtime slice-literal backing into canonical writable +// package storage before the helper returns. The hook is gated solely by the +// checker-owned N_LET.initsynthetic bit, so ordinary locals are unchanged. +fn cginitfatal(msg: str) void = { + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + os.exit(1); +}; + +fn cginitzerobp(c: *cgen, off: i32, sz: i32) void = { + emitline("\tXORQ\tAX, AX\n"); + let k: i32 = 0; + for (k + 8 <= sz) { + emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); + emitline("(BP)\n"); k += 8; + }; + if (k + 4 <= sz) { + emitline("\tMOVL\tAX, "); emitoff((off + k): i64); + emitline("(BP)\n"); k += 4; + }; + if (k + 2 <= sz) { + emitline("\tMOVW\tAX, "); emitoff((off + k): i64); + emitline("(BP)\n"); k += 2; + }; + if (k + 1 <= sz) { + emitline("\tMOVB\tAX, "); emitoff((off + k): i64); + emitline("(BP)\n"); + }; +}; + +fn cginitcopybp(c: *cgen, src: i32, dst: i32, sz: i32) void = { + emitline("\tLEAQ\t"); emitoff(src: i64); emitline("(BP), SI\n"); + emitline("\tLEAQ\t"); emitoff(dst: i64); emitline("(BP), BX\n"); + aggcopy(c, sz); +}; + +fn cginitstripcast(n: *syntax.node) *syntax.node = { + for (n != nil && n.kind == syntax.nkind.N_CAST) { n = n.lhs; }; + return n; +}; + +fn cginitfield(u: *syntax.tinfo, name: str) *syntax.tfield = { + let f: *syntax.tfield = nil; + if (u != nil) { f = u.fields; }; + for (f != nil) { + if (syntax.streq(name, f.name)) { return f; }; + f = f.tnext; + }; + return nil; +}; + +fn cginitarraybp(c: *cgen, u: *syntax.tinfo, lit: *syntax.node, + off: i32) void = { + let et: *syntax.tinfo = u.sub; + let eu: *syntax.tinfo = tichase(et); + let esz: i32 = 1; + if (eu != nil) { esz = eu.size: i32; }; + let total: i32 = u.alen: i32; + cginitzerobp(c, off, u.size: i32); + let idx: i32 = 0; + let lastoff: i32 = 0; + let e: *syntax.node = lit.list; + for (e != nil) { + if (e.kind == syntax.nkind.N_FIELD + && syntax.streq(e.str, "...")) { + if (idx == 0) { + cginitfatal("package initializer array repeat has no value"); + }; + for (idx < total) { + cginitcopybp(c, lastoff, off + idx * esz, esz); + idx += 1; + }; + return; + }; + if (idx >= total) { + cginitfatal("package initializer array literal exceeds destination"); + }; + lastoff = off + idx * esz; + cginitvaluebp(c, et, e, lastoff); + idx += 1; + e = e.next; + }; +}; + +fn cginitstructbp(c: *cgen, u: *syntax.tinfo, lit: *syntax.node, + off: i32) void = { + cginitzerobp(c, off, u.size: i32); + let e: *syntax.node = lit.list; + for (e != nil) { + let f: *syntax.tfield = cginitfield(u, e.str); + if (f != nil) { + cginitvaluebp(c, f.type_, e.lhs, off + (f.offset: i32)); + }; + e = e.next; + }; +}; + +fn cginittuplebp(c: *cgen, u: *syntax.tinfo, lit: *syntax.node, + off: i32) void = { + cginitzerobp(c, off, u.size: i32); + let te: *syntax.ttupleelem = u.tupleelems; + let e: *syntax.node = lit.list; + for (te != nil && e != nil) { + cginitvaluebp(c, te.type_, e, off + (te.offset: i32)); + te = te.tnext; + e = e.next; + }; +}; + +fn cginitslicebp(c: *cgen, u: *syntax.tinfo, lit: *syntax.node, + off: i32) void = { + if (lit.linksym.len == 0) { + cginitfatal("runtime package slice literal has no canonical backing"); + }; + let at: *syntax.tinfo = tichase(lit.type_: *syntax.tinfo); + if (at == nil || at.kind != syntax.tykind.TY_ARRAY || at.sub == nil) { + cginitfatal("runtime package slice literal has no backing type"); + }; + let count: i32 = at.alen: i32; + let bsz: i32 = at.size: i32; + if (bsz != 0) { + let scr: i32 = localalloc(c, "@initbacking", bsz, nil); + cginitarraybp(c, at, lit, scr); + emitline("\tLEAQ\t"); emitoff(scr: i64); emitline("(BP), SI\n"); + emitline("\tLEAQ\t"); emitbytes(lit.linksym.ptr, + lit.linksym.len: u64); emitline("(SB), BX\n"); + aggcopy(c, bsz); + }; + emitline("\tLEAQ\t"); emitbytes(lit.linksym.ptr, + lit.linksym.len: u64); emitline("(SB), AX\n"); + emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); + emitline("\tMOVQ\t$"); emitint(count: i64); emitline(", "); + emitoff((off + 8): i64); emitline("(BP)\n"); + emitline("\tMOVQ\t$"); emitint(count: i64); emitline(", "); + emitoff((off + 16): i64); emitline("(BP)\n"); +}; + +fn cginitcursorstore(c: *cgen, ti: *syntax.tinfo, gpcur: i32, + ssecur: i32, off: i32) void = { + let eslot: i32 = tupeslot(ti); + if (eslot == 0) { return; }; + if (eslot > 8) { + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); emitline(tupreg(gpcur + k)); + emitline(", "); emitoff((off + k * 8): i64); + emitline("(BP)\n"); k += 1; + }; + return; + }; + if (syntax.typeisfloat(ti)) { + let op: str = "MOVSD"; + if (syntax.typeisf32(ti)) { op = "MOVSS"; }; + emitline("\t"); emitline(op); emitline("\t"); + emitline(tupsse(ssecur)); emitline(", "); + emitoff(off: i64); emitline("(BP)\n"); + return; + }; + emitline("\tMOVQ\t"); emitline(tupreg(gpcur)); emitline(", "); + emitoff(off: i64); emitline("(BP)\n"); +}; + +fn cginitaggregatebp(c: *cgen, ti: *syntax.tinfo, u: *syntax.tinfo, + expr: *syntax.node, off: i32) void = { + let sz: i32 = u.size: i32; + if (expr.kind == syntax.nkind.N_CALL && sretretsizetn(c, ti) > 0) { + c.sretdestoff = off; + cgexpr(c, expr); + c.sretdestoff = 0; + return; + }; + if (u.kind == syntax.tykind.TY_TUPLE && sretretsizetn(c, ti) == 0) { + cgexpr(c, expr); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let te: *syntax.ttupleelem = u.tupleelems; + for (te != nil) { + cginitcursorstore(c, te.type_, gpcur, ssecur, + off + (te.offset: i32)); + if (syntax.typeisfloat(te.type_)) { ssecur += 1; } + else { gpcur += tupeslot(te.type_) / 8; }; + te = te.tnext; + }; + return; + }; + if (expr.kind == syntax.nkind.N_CALL + && u.kind == syntax.tykind.TY_STRUCT) { + let fc: i32 = structfloatclassti(u); + if (fc != 0) { + cgexpr(c, expr); + let nb: i32 = fc & 15; + let gp: i32 = 0; + let sse: i32 = 0; + let i: i32 = 0; + for (i < nb) { + let issse: bool = false; + if (i == 0 && (fc & 16) != 0) { issse = true; }; + if (i == 1 && (fc & 32) != 0) { issse = true; }; + if (issse) { + emitline("\tMOVSD\t"); emitline(tupsse(sse)); + emitline(", "); sse += 1; + } else { + emitline("\tMOVQ\t"); emitline(tupreg(gp)); + emitline(", "); gp += 1; + }; + emitoff((off + i * 8): i64); emitline("(BP)\n"); + i += 1; + }; + return; + }; + }; + if (expr.kind == syntax.nkind.N_CALL && sz <= 24) { + cgexpr(c, expr); + cgaggregstore(c, "BP", off, sz, true); + return; + }; + if (aggargsrcaddr(c, expr, "SI")) { + emitline("\tLEAQ\t"); emitoff(off: i64); emitline("(BP), BX\n"); + aggcopy(c, sz); + return; + }; + cginitfatal("package initializer aggregate expression shape unsupported"); +}; + +fn cginitvaluebp(c: *cgen, ti: *syntax.tinfo, expr: *syntax.node, + off: i32) void = { + let u: *syntax.tinfo = tichase(ti); + let r: *syntax.node = cginitstripcast(expr); + if (u == nil || r == nil) { + cginitfatal("package initializer value has no type or expression"); + }; + if (u.kind == syntax.tykind.TY_ARRAY + && r.kind == syntax.nkind.N_ARRLIT) { + cginitarraybp(c, u, r, off); return; + }; + if (u.kind == syntax.tykind.TY_STRUCT + && r.kind == syntax.nkind.N_STRUCTLIT) { + cginitstructbp(c, u, r, off); return; + }; + if (u.kind == syntax.tykind.TY_TUPLE + && r.kind == syntax.nkind.N_TUPLE) { + cginittuplebp(c, u, r, off); return; + }; + if (u.kind == syntax.tykind.TY_SLICE + && r.kind == syntax.nkind.N_ARRLIT) { + cginitslicebp(c, u, r, off); return; + }; + if (u.kind == syntax.tykind.TY_TAGGED) { + cgwidentaggedstore(c, u, expr, "BP", off, u.size: i32); + return; + }; + if (syntax.typeisstr(ti) || u.kind == syntax.tykind.TY_SLICE) { + cgexpr(c, expr); + emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); emitoff((off + 8): i64); emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); emitoff((off + 16): i64); emitline("(BP)\n"); + return; + }; + if (u.kind == syntax.tykind.TY_ARRAY + || u.kind == syntax.tykind.TY_STRUCT + || u.kind == syntax.tykind.TY_TUPLE) { + cginitaggregatebp(c, ti, u, r, off); return; + }; + cgexpr(c, expr); + if (syntax.typeisfloat(ti)) { + let op: str = "MOVSD"; + if (syntax.typeisf32(ti)) { op = "MOVSS"; }; + emitline("\t"); emitline(op); emitline("\tX0, "); + emitoff(off: i64); emitline("(BP)\n"); return; + }; + let sz: i32 = u.size: i32; + if (sz != 1 && sz != 2 && sz != 4) { sz = 8; }; + emitline("\t"); emitline(storeopsz(sz)); emitline("\tAX, "); + emitoff(off: i64); emitline("(BP)\n"); +}; + // #152: reserve the let's frame slot, emit its initializer against the // PRE-binding locals chain, then link the binding. A self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in @@ -2205,6 +2483,20 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = { if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; if (n.rhs != nil) { let rhs: *syntax.node = n.rhs; + let initlit: *syntax.node = cginitstripcast(rhs); + if (n.initsynthetic != 0 && initlit != nil + && (initlit.kind == syntax.nkind.N_ARRLIT + || initlit.kind == syntax.nkind.N_STRUCTLIT + || initlit.kind == syntax.nkind.N_TUPLE)) { + let iti: *syntax.tinfo = nil; + if (tn != nil) { iti = tn.type_: *syntax.tinfo; }; + if (iti == nil && n.type_ != nil) { + iti = n.type_: *syntax.tinfo; + }; + cginitvaluebp(c, iti, rhs, off); + c.lastwasreturn = 0; + return; + }; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). // Mirror of cstage cgen.c N_LET arrlit-empty branch: allocate // n*esz bytes via rt_malloc, then build the {ptr, 0, n} slice diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index dc2bd351..73e3b485 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -46,6 +46,8 @@ type checker = struct { // and must fail to infer (harec // check.c:1801). Set by checkletassign // around its exprtype, nil elsewhere. + packageinitsymbol: str, // canonical package/variant-owned init task + initwalkmark: u64, // generation for dependency walks through fn bodies }; // cerr — bare stderr fragment writer for the checker's piecewise @@ -189,7 +191,8 @@ fn findusepath(file: *syntax.node, modtag: str, source: i32, alias: str, }; let u: *syntax.node = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.sourceid == source) { + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.sourceid == source) { if (syntax.streq(u.str, alias)) { let um: str = declmod(file, u); let same: bool = false; @@ -227,7 +230,8 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = { if (name.len == 0) { return false; }; let u: *syntax.node = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.sourceid == source) { + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.sourceid == source) { // Skip self-imports: lib/fmt/fmt_test.ww carries // `use fmt;` while its module tag is also "fmt". // That directive doesn't introduce a foreign @@ -420,6 +424,13 @@ fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = { cerr("self-import: package '"); cerr(mod); cerr("' cannot import itself\n"); c.errs += 1i32; }; + if (d.useblank != 0) { return; }; + if (syntax.streq(nm, "init")) { + importdiagprefix(d); + cerr("cannot import package as init - init must be a func\n"); + c.errs += 1; + return; + }; // #30 value-before-use: a same-leaf VALUE/type decl is already // installed (source order placed `fn aa` before `import aa`). // Promote it in place with use_alias instead of installing a @@ -441,32 +452,17 @@ fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = { }; }; syntax.scopedefine(c.top, nm, syntax.skind.SK_USE, nil, d); return; }; + // Every package-block declaration named init is reserved. Invalid + // non-functions are diagnosed by classifyinitdecls and, like Go's + // resolver, are never inserted into package scope. + if (topdeclkind(d) && syntax.streq(nm, "init")) { return; }; if (k == syntax.nkind.N_DEF) { installtop(c, d, nm, mod, syntax.skind.SK_DEF, "def"); return; }; if (k == syntax.nkind.N_TYPEDECL) { installtop(c, d, nm, mod, syntax.skind.SK_TYPE, "type"); return; }; - if (k == syntax.nkind.N_FNDECL) { installtop(c, d, nm, mod, syntax.skind.SK_FN, "fn"); return; }; + if (k == syntax.nkind.N_FNDECL) { + if (d.initfn != 0) { return; }; + installtop(c, d, nm, mod, syntax.skind.SK_FN, "fn"); return; + }; if (k == syntax.nkind.N_LET) { - // A module-scope initializer must be link-time data: an - // alloc/call rhs runs code, emitletdataw's fold-fail - // skipped the DATAW slot silently, and every reference - // died at LINK time ("undefined reference") — reject at - // the declaration instead (rule 7). Hare rejects at check - // time too (ref/harec/src/check.c:4360); ww has no @init - // path. Mirrors cstage check_file N_LET. - let rr: *syntax.node = d.rhs; - for (rr != nil) { - if (rr.kind == syntax.nkind.N_CAST) { rr = rr.lhs; continue; }; - if (rr.kind == syntax.nkind.N_TRYPROP) { rr = rr.lhs; continue; }; - if (rr.kind == syntax.nkind.N_TRYUNW) { rr = rr.lhs; continue; }; - break; - }; - if (rr != nil) { - if (rr.kind == syntax.nkind.N_ALLOC || rr.kind == syntax.nkind.N_CALL) { - cerr(d.file); cerr(": error: module-scope let "); - cerr(nm); - cerr(": runtime initializer unsupported (alloc/call; rule 7)\n"); - c.errs += 1; - }; - }; installtop(c, d, nm, mod, syntax.skind.SK_VAR, "let"); return; }; @@ -1440,6 +1436,30 @@ fn varianterr(c: *checker, v: *syntax.node) bool = { return false; }; +// initretiserror preserves the `!` bit which tinfofornode deliberately +// erases. Chase named declaration bodies so `fn init() errvoid`, where +// `type errvoid = !void`, is rejected exactly like the explicit spelling. +// Resolved alias cycles terminate at TY_ERR before this declaration pass. +fn initretiserror(c: *checker, n: *syntax.node) bool = { + let cur: *syntax.node = n; + for (cur != nil) { + if (cur.kind == syntax.nkind.N_TBANG) { return true; }; + if (cur.kind != syntax.nkind.N_TNAME) { return false; }; + let s: *syntax.sym = aliassym(c, cur); + if (s == nil || s.decl == nil || s.decl.lhs == nil) { + return false; + }; + if (s.type_ != nil) { + let base: *syntax.tinfo = tichase(s.type_); + if (base == nil || base.kind == syntax.tykind.TY_ERR) { + return false; + }; + }; + cur = s.decl.lhs; + }; + return false; +}; + // taggedhaserr — true iff any variant of `n` (assumed // nkind.N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over // the legacy "first variant = success" rule. @@ -1478,6 +1498,7 @@ fn scruttype(c: *checker, e: *syntax.node) *syntax.node = { let s: *syntax.sym = syntax.scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; + e.refdecl = s.decl; // For nkind.N_LET / nkind.N_PARAM: declared type is decl.lhs. return s.decl.lhs; }; @@ -3826,6 +3847,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return nil; }; if (s.decl == nil) { return nil; }; + e.refdecl = s.decl; // #34: a bare fn-name rvalue types as its FN TYPE, not its return // type. decl.lhs is the RETURN type for an N_FNDECL, so synthesize // the N_TFN over (ret=decl.lhs, params=decl.list) — the shape @@ -3922,6 +3944,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { if (e.type_ == c.tc.tyerr: *void) { return nil; }; let callee: *syntax.node = e.lhs; if (callee == nil) { return nil; }; + // A module-qualified leaf may already have been rejected while the + // N_DOT callee was checked on an earlier resolve walk. cstage caches + // that failure on the call; mirror its once-only diagnostic here rather + // than resolving the same missing package declaration a second time. + if (callee.type_ == c.tc.tyerr: *void) { + e.type_ = c.tc.tyerr: *void; + return nil; + }; // #31: synthesize the `alloc(value)` / `alloc([], n)` builtin // return shape so checkletassign sees the same `(*T | nomem)` / // `([]T | nomem)` cstage's check.c stamps at L981-1006. Without @@ -4366,6 +4396,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { }; }; if (s != nil) { if (s.skind == syntax.skind.SK_FN) { if (s.decl != nil) { + callee.refdecl = s.decl; // fn-decl's lhs is the return-type AST node. Mirrors cstage // cmd/wcc/check.c:984+ regular-CALL `n->type = // build_fn_type(c, s->decl)->ret` shape. @@ -4465,6 +4496,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return nil; }; if (fs != nil) { if (fs.decl != nil) { + e.refdecl = fs.decl; // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is // the RETURN type for an N_FNDECL. Pins 706 (`let p1: fn()i32 @@ -7548,10 +7580,12 @@ fn checkimportredeclarations(c: *checker, file: *syntax.node) void = { if (c.sepmode == 0) { return; }; let u: *syntax.node = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.imported == 0) { + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.imported == 0) { let v: *syntax.node = file.list; for (v != u) { - if (v.kind == syntax.nkind.N_USE && v.imported == 0 + if (v.kind == syntax.nkind.N_USE && v.useblank == 0 + && v.imported == 0 && v.sourceid == u.sourceid && syntax.streq(v.str, u.str)) { importdiagprefix(u); cerr(u.str); cerr(" redeclared in this block\n"); @@ -7570,7 +7604,8 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = { if (c.sepmode == 0) { return; }; let u: *syntax.node = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.imported == 0 && u.used == 0) { + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.imported == 0 && u.used == 0) { let path: str = u.usesource; if (path.len == 0) { path = u.usepath; }; if (path.len == 0) { path = u.str; }; @@ -7593,7 +7628,8 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = { if (d.imported == 0 && topdeclkind(d)) { u = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.imported == 0 + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.imported == 0 && syntax.streq(d.str, u.str)) { let path: str = u.usesource; if (path.len == 0) { path = u.usepath; }; @@ -7611,6 +7647,771 @@ fn checkimportusageandcollisions(c: *checker, file: *syntax.node) void = { }; }; +fn initprivatesymbol(d: *syntax.node) bool = { + let a: *syntax.node = d.attr; + for (a != nil) { + if (a.kind == syntax.nkind.N_ATTR && syntax.streq(a.str, "symbol") + && a.list != nil && a.list.kind == syntax.nkind.N_STRLIT + && strings.hasprefix(a.list.str, "__ww..")) { + return true; + }; + a = a.next; + }; + return false; +}; + +fn classifyinitdecls(c: *checker, file: *syntax.node) void = { + let ordinal: u64 = 0u64; + let d: *syntax.node = file.list; + for (d != nil) { + if (initprivatesymbol(d)) { + importdiagprefix(d); + cerr("@symbol name uses reserved prefix __ww..\n"); + c.errs += 1; + }; + if (topdeclkind(d) && syntax.streq(d.str, "init")) { + if (d.kind != syntax.nkind.N_FNDECL) { + importdiagprefix(d); + cerr("cannot declare init - must be func\n"); + c.errs += 1; + } else { + d.initfn = 1; + ordinal += 1u64; + d.initorder = ordinal; + if (d.exported != 0) { + importdiagprefix(d); cerr("func init cannot be exported\n"); + c.errs += 1; + }; + if (d.body == nil) { + importdiagprefix(d); cerr("func init must have a body\n"); + c.errs += 1; + }; + if (d.attr != nil) { + importdiagprefix(d); cerr("func init cannot have attributes\n"); + c.errs += 1; + }; + }; + }; + d = d.next; + }; +}; + +fn initstripcast(n: *syntax.node) *syntax.node = { + for (n != nil && n.kind == syntax.nkind.N_CAST) { n = n.lhs; }; + return n; +}; + +fn initfnptrstatic(n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (r == nil || r.kind != syntax.nkind.N_UN + || r.op != syntax.tkind.TK_AMP) { return false; }; + let v: *syntax.node = initstripcast(r.lhs); + let vt: *syntax.tinfo = nil; + if (v != nil && v.type_ != nil) { vt = tichase(v.type_: *syntax.tinfo); }; + if (v == nil || vt == nil || vt.kind != syntax.tykind.TY_FN) { + return false; + }; + if (v.kind == syntax.nkind.N_IDENT) { return true; }; + if (v.kind != syntax.nkind.N_DOT || v.lhs == nil + || v.lhs.kind != syntax.nkind.N_IDENT) { return false; }; + if (v.lhs.type_ == nil) { return true; }; + let bt: *syntax.tinfo = tichase(v.lhs.type_: *syntax.tinfo); + return bt != nil && bt.kind == syntax.tykind.TY_ERR; +}; + +fn initfloatstatic(n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (r != nil && r.kind == syntax.nkind.N_UN + && (r.op == syntax.tkind.TK_PLUS + || r.op == syntax.tkind.TK_MINUS)) { + r = initstripcast(r.lhs); + }; + return r != nil && r.kind == syntax.nkind.N_FLOATLIT; +}; + +fn inittaggedrawstatic(u: *syntax.tinfo, n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (u == nil || u.kind != syntax.tykind.TY_TAGGED || u.nullable != 0 + || r == nil || !variantpresent(u.params, r.type_: *syntax.tinfo) + || syntax.typeisstr(r.type_: *syntax.tinfo) + || syntax.typeisslice(r.type_: *syntax.tinfo)) { return false; }; + let ignored: u64; + return foldintliteral(r, &ignored); +}; + +fn inittuplestatic(u: *syntax.tinfo, n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (u == nil || u.kind != syntax.tykind.TY_TUPLE + || r == nil || r.kind != syntax.nkind.N_TUPLE) { return false; }; + let te: *syntax.ttupleelem = u.tupleelems; + let e: *syntax.node = r.list; + for (e != nil) { + if (te == nil) { return false; }; + let v: *syntax.node = initstripcast(e); + let et: *syntax.tinfo = tichase(te.type_); + if (v == nil || (et != nil && et.kind == syntax.tykind.TY_TAGGED)) { + return false; + }; + if (syntax.typeisstr(te.type_) || syntax.typeisslice(te.type_)) { + if (v.kind != syntax.nkind.N_STRLIT) { return false; }; + } else if (!initfnptrstatic(v)) { + let ignored: u64; + if (!foldintliteral(v, &ignored)) { return false; }; + }; + te = te.tnext; + e = e.next; + }; + return te == nil; +}; + +fn initstructstatic(u: *syntax.tinfo, n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (u == nil || u.kind != syntax.tykind.TY_STRUCT + || r == nil || r.kind != syntax.nkind.N_STRUCTLIT) { return false; }; + let f: *syntax.tfield = u.fields; + for (f != nil) { + let value: *syntax.node = nil; + let e: *syntax.node = r.list; + for (e != nil) { + if (syntax.streq(e.str, f.name)) { value = e.lhs; break; }; + e = e.next; + }; + if (value != nil) { + let fu: *syntax.tinfo = tichase(f.type_); + let ok: bool = false; + if (fu != nil && fu.kind == syntax.tykind.TY_TAGGED + && fu.nullable == 0) { + ok = inittaggedrawstatic(fu, value); + } else { if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { + ok = initstructstatic(fu, value); + } else { if (fu != nil && fu.kind == syntax.tykind.TY_ARRAY) { + ok = initarraystatic(fu, value, true, false, false); + } else { if (syntax.typeisfloat(f.type_)) { + ok = initfloatstatic(value); + } else { + let ignored: u64; + ok = foldintliteral(initstripcast(value), &ignored); + }; }; }; }; + if (!ok) { return false; }; + }; + f = f.tnext; + }; + return true; +}; + +fn initarraystatic(u: *syntax.tinfo, n: *syntax.node, allowrepeat: bool, + strreloc: bool, tuplerows: bool) bool = { + let r: *syntax.node = initstripcast(n); + if (u == nil + || (u.kind != syntax.tykind.TY_ARRAY && u.kind != syntax.tykind.TY_SLICE) + || r == nil || r.kind != syntax.nkind.N_ARRLIT) { return false; }; + let et: *syntax.tinfo = u.sub; + let eu: *syntax.tinfo = tichase(et); + let seen: u64 = 0u64; + let e: *syntax.node = r.list; + for (e != nil) { + if (e.kind == syntax.nkind.N_FIELD && syntax.streq(e.str, "...")) { + return allowrepeat && seen > 0u64 && e.next == nil + && (eu == nil || eu.kind != syntax.tykind.TY_ARRAY); + }; + let v: *syntax.node = initstripcast(e); + if (v == nil) { return false; }; + if (eu != nil && eu.kind == syntax.tykind.TY_STR) { + if (!strreloc || v.kind != syntax.nkind.N_STRLIT) { return false; }; + } else if (eu != nil && eu.kind == syntax.tykind.TY_STRUCT) { + if (!initstructstatic(eu, v)) { return false; }; + } else if (eu != nil && eu.kind == syntax.tykind.TY_ARRAY) { + if (!initarraystatic(eu, v, true, false, false)) { return false; }; + } else if (eu != nil && eu.kind == syntax.tykind.TY_TUPLE) { + if (!tuplerows || !inittuplestatic(eu, v)) { return false; }; + } else if (eu != nil && eu.kind == syntax.tykind.TY_TAGGED) { + if (!inittaggedrawstatic(eu, v)) { return false; }; + } else if (eu != nil && (eu.kind == syntax.tykind.TY_SLICE + || eu.kind == syntax.tykind.TY_PTR + || eu.kind == syntax.tykind.TY_FN)) { + return false; + } else if (syntax.typeisfloat(et)) { + if (!initfloatstatic(v)) { return false; }; + } else { + let ignored: u64; + if (!foldintliteral(v, &ignored)) { return false; }; + }; + seen += 1u64; + e = e.next; + }; + return true; +}; + +// This predicate is the checker's validate-only twin of cgen's static-data +// arms. A true result must be safe to emit; every other valid mutable value +// is zero-backed and evaluated exactly once by the package task. +fn initexprstatic(t: *syntax.tinfo, n: *syntax.node) bool = { + let r: *syntax.node = initstripcast(n); + if (r == nil) { return true; }; + let u: *syntax.tinfo = tichase(t); + if (syntax.typeisfloat(t)) { return initfloatstatic(r); }; + if (u != nil && (u.kind == syntax.tykind.TY_STR + || u.kind == syntax.tykind.TY_UNTYPED_STR)) { + return r.kind == syntax.nkind.N_STRLIT || r.kind == syntax.nkind.N_NIL; + }; + if (u != nil && u.kind == syntax.tykind.TY_ARRAY) { + return initarraystatic(u, r, true, true, false); + }; + if (u != nil && u.kind == syntax.tykind.TY_STRUCT) { + return initstructstatic(u, r); + }; + if (u != nil && u.kind == syntax.tykind.TY_TUPLE) { + return inittuplestatic(u, r); + }; + if (u != nil && u.kind == syntax.tykind.TY_SLICE) { + if (r.kind == syntax.nkind.N_NIL) { return true; }; + return initarraystatic(u, r, false, false, true); + }; + if (u != nil && u.kind == syntax.tykind.TY_TAGGED + && u.nullable == 0) { + if (!variantpresent(u.params, r.type_: *syntax.tinfo)) { return false; }; + let ru: *syntax.tinfo = tichase(r.type_: *syntax.tinfo); + if (ru != nil && (ru.kind == syntax.tykind.TY_STR + || ru.kind == syntax.tykind.TY_SLICE)) { + return r.kind == syntax.nkind.N_STRLIT; + }; + return inittaggedrawstatic(u, r); + }; + if (initfnptrstatic(r)) { return true; }; + let ignored: u64; + return foldintliteral(r, &ignored); +}; + +type initwalkitem = struct { + node: *syntax.node, + next: *initwalkitem, +}; + +fn initwalkalloc(n: *syntax.node, next: *initwalkitem) + (*initwalkitem | nomem) = { + let p: *initwalkitem = alloc(initwalkitem{node=n, next=next})?; + return p; +}; + +fn initwalkpush(head: **initwalkitem, n: *syntax.node) bool = { + if (n == nil) { return true; }; + let allocation: (*initwalkitem | nomem) = initwalkalloc(n, *head); + match (allocation) { + case let p: *initwalkitem => { *head = p; return true; }; + case nomem => return false; + }; + return false; +}; + +fn initwalkfree(p: *initwalkitem) void = { + for (p != nil) { + let next: *initwalkitem = p.next; + os.free(p: *void, size(initwalkitem): u64); + p = next; + }; +}; + +fn initwalkseenfn(seen: **initwalkitem, fn_: *syntax.node) i32 = { + let p: *initwalkitem = *seen; + for (p != nil) { + if (p.node == fn_) { return 1; }; + p = p.next; + }; + if (!initwalkpush(seen, fn_)) { return -1; }; + return 0; +}; + +// Does variable from depend on target? Function declarations are transparent, +// matching go/types' graph reduction. The explicit heap stack is symmetric +// with cstage and never ties valid source depth to the native call stack. +fn initrefers(from: *syntax.node, target: *syntax.node) i32 = { + let stack: *initwalkitem = nil; + let seenfn: *initwalkitem = nil; + let result: i32 = 0; + if (!initwalkpush(&stack, from.rhs)) { result = -1; }; + for (result == 0 && stack != nil) { + let top: *initwalkitem = stack; + let n: *syntax.node = top.node; + stack = top.next; + os.free(top: *void, size(initwalkitem): u64); + if (n.refdecl == target) { result = 1; break; }; + let r: *syntax.node = n.refdecl; + if (r != nil && r.kind == syntax.nkind.N_FNDECL + && r.imported == 0 && r.initfn == 0 && r.body != nil) { + let wasseen: i32 = initwalkseenfn(&seenfn, r); + if (wasseen < 0) { result = -1; break; }; + if (wasseen == 0 && !initwalkpush(&stack, r.body)) { + result = -1; break; + }; + }; + if (!initwalkpush(&stack, n.attr) + || !initwalkpush(&stack, n.lhs) + || !initwalkpush(&stack, n.rhs) + || !initwalkpush(&stack, n.cond) + || !initwalkpush(&stack, n.body) + || !initwalkpush(&stack, n.els) + || !initwalkpush(&stack, n.list) + || !initwalkpush(&stack, n.next)) { + result = -1; + }; + }; + initwalkfree(stack); + initwalkfree(seenfn); + return result; +}; + +fn initmakecall(c: *checker, fn_: *syntax.node, owner: *syntax.node) *syntax.node = { + let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, + owner.file, owner.line, owner.col); + id.str = fn_.str; + id.refdecl = fn_; + let call: *syntax.node = syntax.newnode(syntax.nkind.N_CALL, + owner.file, owner.line, owner.col); + call.lhs = id; + call.type_ = c.tc.tyvoid: *void; + let stmt: *syntax.node = syntax.newnode(syntax.nkind.N_EXPRSTMT, + owner.file, owner.line, owner.col); + stmt.lhs = call; + return stmt; +}; + +fn initarrlitcount(lit: *syntax.node) u64 = { + let count: u64 = 0u64; + let e: *syntax.node = nil; + if (lit != nil) { e = lit.list; }; + for (e != nil) { + if (!(e.kind == syntax.nkind.N_FIELD + && syntax.streq(e.str, "..."))) { + count += 1u64; + }; + e = e.next; + }; + return count; +}; + +// Runtime slice literals need package-lifetime backing: the ordinary local +// literal backing dies when the compiler-generated variable helper returns. +// Attach a canonical raw symbol to each slice literal contained in the value +// being published. The cgen owns the zeroed storage and fills it at the +// literal's exact evaluation point. +fn initmarkslicebackings(c: *checker, want: *syntax.tinfo, + expr: *syntax.node, base: str, order: u64, preorder: *u64) void = { + let r: *syntax.node = initstripcast(expr); + let u: *syntax.tinfo = tichase(want); + if (r == nil || u == nil) { return; }; + if (u.kind == syntax.tykind.TY_SLICE + && r.kind == syntax.nkind.N_ARRLIT) { + let count: u64 = initarrlitcount(r); + *preorder += 1u64; + // strconv's decimal formatter owns reusable scratch; materialize + // the variable-order prefix before formatting the literal ordinal. + let prefix: str = strings.concat(base, ".v.", + strconv.u64tos(order, strconv.base.DEC)); + r.linksym = strings.concat(prefix, ".b.", + strconv.u64tos(*preorder, strconv.base.DEC)); + r.type_ = syntax.typearray(u.sub, count): *void; + let e: *syntax.node = r.list; + for (e != nil) { + if (!(e.kind == syntax.nkind.N_FIELD + && syntax.streq(e.str, "..."))) { + initmarkslicebackings(c, u.sub, e, base, order, + preorder); + }; + e = e.next; + }; + return; + }; + if (u.kind == syntax.tykind.TY_ARRAY + && r.kind == syntax.nkind.N_ARRLIT) { + let e: *syntax.node = r.list; + for (e != nil) { + if (!(e.kind == syntax.nkind.N_FIELD + && syntax.streq(e.str, "..."))) { + initmarkslicebackings(c, u.sub, e, base, order, + preorder); + }; + e = e.next; + }; + return; + }; + if (u.kind == syntax.tykind.TY_STRUCT + && r.kind == syntax.nkind.N_STRUCTLIT) { + let e: *syntax.node = r.list; + for (e != nil) { + let field: *syntax.tfield = u.fields; + for (field != nil) { + if (syntax.streq(e.str, field.name)) { break; }; + field = field.tnext; + }; + if (field != nil) { + initmarkslicebackings(c, field.type_, e.lhs, + base, order, preorder); + }; + e = e.next; + }; + return; + }; + if (u.kind == syntax.tykind.TY_TUPLE + && r.kind == syntax.nkind.N_TUPLE) { + let te: *syntax.ttupleelem = u.tupleelems; + let e: *syntax.node = r.list; + for (e != nil && te != nil) { + initmarkslicebackings(c, te.type_, e, base, order, + preorder); + e = e.next; + te = te.tnext; + }; + }; +}; + +fn initmakehelper(c: *checker, d: *syntax.node, base: str) *syntax.node = { + let fn_: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, + d.file, d.line, d.col); + fn_.str = strings.concat("__ww_init_var_", + strconv.u64tos(d.initorder, strconv.base.DEC)); + fn_.nmod = d.nmod; + fn_.pkgname = d.pkgname; + fn_.sourceid = d.sourceid; + fn_.initsynthetic = 1; + fn_.linksym = strings.concat(base, ".v.", + strconv.u64tos(d.initorder, strconv.base.DEC)); + let rt: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, + d.file, d.line, d.col); + rt.str = "void"; + rt.type_ = c.tc.tyvoid: *void; + fn_.lhs = rt; + + // Evaluate into an addressable local first. Local-let lowering plus the + // full-value assignment path covers aggregate literals, calls, and alloc. + let tmp: *syntax.node = syntax.newnode(syntax.nkind.N_LET, + d.file, d.line, d.col); + tmp.str = strings.concat("__ww_init_tmp_", + strconv.u64tos(d.initorder, strconv.base.DEC)); + tmp.lhs = d.lhs; + tmp.rhs = d.rhs; + let dtype: *void = nil; + if (d.lhs != nil) { dtype = d.lhs.type_; } + else { if (d.rhs != nil) { dtype = d.rhs.type_; }; }; + tmp.type_ = dtype; + tmp.initsynthetic = 1; + + let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, + d.file, d.line, d.col); + id.str = d.str; + id.type_ = dtype; + id.refdecl = d; + let value: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, + d.file, d.line, d.col); + value.str = tmp.str; + value.type_ = dtype; + value.refdecl = tmp; + let assign: *syntax.node = syntax.newnode(syntax.nkind.N_ASSIGN, + d.file, d.line, d.col); + assign.op = syntax.tkind.TK_ASSIGN; + assign.lhs = id; + assign.rhs = value; + assign.type_ = dtype; + let stmt: *syntax.node = syntax.newnode(syntax.nkind.N_EXPRSTMT, + d.file, d.line, d.col); + stmt.lhs = assign; + let body: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, + d.file, d.line, d.col); + body.list = tmp; + tmp.next = stmt; + fn_.body = body; + d.rhs = nil; + return fn_; +}; + +fn initcyclenote(from: *syntax.node, to: *syntax.node) void = { + cerr("\t"); cerr(from.file); cerr(":"); + cerr(strconv.i32tos(from.line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(from.col, strconv.base.DEC)); cerr(": "); + cerr(from.str); cerr(" refers to "); cerr(to.str); cerr("\n"); +}; + +fn initordernomem(c: *checker, d: *syntax.node) void = { + importdiagprefix(d); + cerr("out of memory while ordering package initialization\n"); + c.errs += 1; +}; + +fn initallocnodes(n: i32) ([]*syntax.node | nomem) = { + let value: []*syntax.node = alloc([], n: u64)?; + return value; +}; + +fn initallocints(n: i32) ([]i32 | nomem) = { + let value: []i32 = alloc([], n: u64)?; + return value; +}; + +fn initallocbytes(n: i32) ([]u8 | nomem) = { + let value: []u8 = alloc([], n: u64)?; + return value; +}; + +fn initfreenodes(v: []*syntax.node) void = { + if (v.ptr != nil) { + os.free(v.ptr: *void, + (v.cap: u64) * (size(*syntax.node): u64)); + }; +}; + +fn initfreeints(v: []i32) void = { + if (v.ptr != nil) { + os.free(v.ptr: *void, (v.cap: u64) * (size(i32): u64)); + }; +}; + +fn initfreebytes(v: []u8) void = { + if (v.ptr != nil) { os.free(v.ptr: *void, v.cap: u64); }; +}; + +// Iterative source-ordered equivalent of go/types findPath(start, start). +fn initfindcycle(c: *checker, vars: []*syntax.node, nvar: i32, + start: i32) i32 = { + let seen: []u8; + let path: []i32; + let next: []i32; + let ba: ([]u8 | nomem) = initallocbytes(nvar); + match (ba) { + case let value: []u8 => { seen = value; seen.len = nvar; }; + case nomem => { initordernomem(c, vars[start]); return -1; }; + }; + let pa: ([]i32 | nomem) = initallocints(nvar); + match (pa) { + case let value: []i32 => { path = value; path.len = nvar; }; + case nomem => { + initfreebytes(seen); initordernomem(c, vars[start]); return -1; + }; + }; + let na: ([]i32 | nomem) = initallocints(nvar); + match (na) { + case let value: []i32 => { next = value; next.len = nvar; }; + case nomem => { + initfreeints(path); initfreebytes(seen); + initordernomem(c, vars[start]); return -1; + }; + }; + let i: i32 = 0; + for (i < nvar) { seen[i] = 0u8; next[i] = 0; i += 1; }; + let depth: i32 = 1; + path[0] = start; + seen[start] = 1u8; + for (depth > 0) { + let from: i32 = path[depth - 1]; + let descended: bool = false; + for (next[depth - 1] < nvar) { + let to: i32 = next[depth - 1]; + next[depth - 1] += 1; + let dep: i32 = initrefers(vars[from], vars[to]); + if (dep < 0) { + initfreeints(next); initfreeints(path); initfreebytes(seen); + initordernomem(c, vars[start]); return -1; + }; + if (dep == 0) { continue; }; + if (to == start) { + importdiagprefix(vars[start]); + if (depth == 1) { + cerr("initialization cycle: "); cerr(vars[start].str); + cerr(" refers to itself\n"); + } else { + cerr("initialization cycle for "); + cerr(vars[start].str); cerr("\n"); + let pi: i32 = 1; + for (pi < depth) { + initcyclenote(vars[path[pi - 1]], vars[path[pi]]); + pi += 1; + }; + initcyclenote(vars[path[depth - 1]], vars[start]); + }; + c.errs += 1; + initfreeints(next); initfreeints(path); initfreebytes(seen); + return 1; + }; + if (seen[to] != 0u8) { continue; }; + seen[to] = 1u8; + path[depth] = to; + next[depth] = 0; + depth += 1; + descended = true; + break; + }; + if (!descended) { depth -= 1; }; + }; + initfreeints(next); initfreeints(path); initfreebytes(seen); + return 0; +}; + +fn initlowerpackage(c: *checker, file: *syntax.node) void = { + let nvar: i32 = 0; + let nruntime: u64 = 0u64; + let ninit: u64 = 0u64; + let firstinit: *syntax.node = nil; + let d: *syntax.node = file.list; + for (d != nil) { + if (d.initfn != 0 && d.imported == 0) { + ninit += 1u64; + if (firstinit == nil) { firstinit = d; }; + }; + if (d.kind == syntax.nkind.N_LET && d.imported == 0 && d.rhs != nil) { + if (d.op != syntax.tkind.TK_CONST) { + if (nvar == 2147483647) { + initordernomem(c, d); return; + }; + nvar += 1; + let dt: *syntax.tinfo = nil; + if (d.lhs != nil) { dt = d.lhs.type_: *syntax.tinfo; } + else { if (d.rhs != nil) { + dt = d.rhs.type_: *syntax.tinfo; + }; }; + if (!initexprstatic(dt, d.rhs)) { + d.runtimeinit = 1; + nruntime += 1u64; + if (firstinit == nil) { firstinit = d; }; + }; + }; + }; + d = d.next; + }; + if (c.errs != 0) { return; }; + if (c.sepmode != 0 && c.packageinitsymbol.len == 0 + && (nruntime != 0u64 || ninit != 0u64)) { + if (firstinit == nil) { firstinit = file; }; + importdiagprefix(firstinit); + cerr("package initialization requires --package-init-symbol under -c\n"); + c.errs += 1; + return; + }; + // Legacy non-separate callers need no empty task. + if (c.packageinitsymbol.len == 0 && nruntime == 0u64 && ninit == 0u64) { + return; + }; + + let vars: []*syntax.node; + if (nvar > 0) { + let allocation: ([]*syntax.node | nomem) = initallocnodes(nvar); + match (allocation) { + case let value: []*syntax.node => { vars = value; vars.len = nvar; }; + case nomem => { initordernomem(c, file); return; }; + }; + let vi: i32 = 0; + d = file.list; + for (d != nil) { + if (d.kind == syntax.nkind.N_LET && d.imported == 0 + && d.rhs != nil && d.op != syntax.tkind.TK_CONST) { + vars[vi] = d; + vi += 1; + }; + d = d.next; + }; + }; + let done: i32 = 0; + for (done < nvar) { + let best: i32 = -1; + let bestdeps: i32 = 2147483647; + let vi: i32 = 0; + for (vi < nvar) { + if (vars[vi].initorder == 0u64) { + let ndeps: i32 = 0; + let qi: i32 = 0; + for (qi < nvar) { + if (vars[qi].initorder == 0u64) { + let dep: i32 = initrefers(vars[vi], vars[qi]); + if (dep < 0) { + let badvar: *syntax.node = vars[vi]; + initfreenodes(vars); initordernomem(c, badvar); + return; + }; + if (dep != 0) { ndeps += 1; }; + }; + qi += 1; + }; + if (best < 0 || ndeps < bestdeps) { + best = vi; bestdeps = ndeps; + }; + }; + vi += 1; + }; + if (best < 0) { break; }; + if (bestdeps != 0) { + let cycle: i32 = initfindcycle(c, vars, nvar, best); + if (cycle < 0) { initfreenodes(vars); return; }; + // A reported cycle is broken by removing this node, matching + // go/types; continue so later cycles retain deterministic errors. + }; + done += 1; + vars[best].initorder = done: u64; + }; + if (nvar > 0) { initfreenodes(vars); }; + if (c.errs != 0) { return; }; + + let base: str = c.packageinitsymbol; + if (base.len == 0) { base = "__ww..pkg.v0.r0.e.init"; }; + let tail: *syntax.node = file.list; + if (tail != nil) { for (tail.next != nil) { tail = tail.next; }; }; + let helpers: *syntax.node = nil; + let helpertail: *syntax.node = nil; + let order: u64 = 1u64; + for (order <= nvar: u64) { + d = file.list; + for (d != nil) { + if (d.runtimeinit != 0 && d.initorder == order) { + let preorder: u64 = 0u64; + let dt: *syntax.tinfo = nil; + if (d.lhs != nil) { dt = d.lhs.type_: *syntax.tinfo; } + else { if (d.rhs != nil) { + dt = d.rhs.type_: *syntax.tinfo; + }; }; + initmarkslicebackings(c, dt, d.rhs, base, order, + &preorder); + let fn_: *syntax.node = initmakehelper(c, d, base); + if (helpers == nil) { helpers = fn_; } + else { helpertail.next = fn_; }; + helpertail = fn_; + break; + }; + d = d.next; + }; + order += 1u64; + }; + if (tail != nil) { tail.next = helpers; } else { file.list = helpers; }; + if (helpertail != nil) { tail = helpertail; }; + + let task: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, + file.file, file.line, file.col); + task.str = "__ww_init_task"; + task.initsynthetic = 1; + task.linksym = base; + let rt: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, + file.file, file.line, file.col); + rt.str = "void"; + rt.type_ = c.tc.tyvoid: *void; + task.lhs = rt; + let body: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, + file.file, file.line, file.col); + let stail: *syntax.node = nil; + let fn_: *syntax.node = helpers; + for (fn_ != nil) { + let s: *syntax.node = initmakecall(c, fn_, fn_); + if (body.list == nil) { body.list = s; } else { stail.next = s; }; + stail = s; + fn_ = fn_.next; + }; + d = file.list; + for (d != nil) { + if (d.initfn != 0 && d.imported == 0) { + let s: *syntax.node = initmakecall(c, d, d); + if (body.list == nil) { body.list = s; } else { stail.next = s; }; + stail = s; + }; + d = d.next; + }; + task.body = body; + if (tail != nil) { tail.next = task; } else { file.list = task; }; +}; + fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.tc = tc; c.top = syntax.newscope(nil); @@ -7632,6 +8433,9 @@ fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.cursource = 0; c.file = nil; c.allococtx = nil; + let emptyinitsymbol: str; + c.packageinitsymbol = emptyinitsymbol; + c.initwalkmark = 0u64; seedprimitives(c); }; @@ -7684,6 +8488,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { markimportuses(c, file); checkimportredeclarations(c, file); checkimportusageandcollisions(c, file); + classifyinitdecls(c, file); // Pass 1: install all top-level names. let d: *syntax.node = file.list; @@ -7985,6 +8790,24 @@ fn checkfile(c: *checker, file: *syntax.node) void = { }; fp = fp.next; }; + if (d.initfn != 0) { + let reti: *syntax.tinfo = c.tc.tyvoid; + if (d.lhs != nil) { reti = tichase(d.lhs.type_: *syntax.tinfo); }; + if (d.list != nil || initretiserror(c, d.lhs) || reti == nil + || reti.kind != syntax.tykind.TY_VOID) { + importdiagprefix(d); + cerr("func init must have no arguments and no return values\n"); + c.errs += 1; + }; + let base: str = c.packageinitsymbol; + if (base.len == 0 && c.sepmode == 0) { + base = "__ww..pkg.v0.r0.e.init"; + }; + if (base.len != 0) { + d.linksym = strings.concat(base, ".f.", + strconv.u64tos(d.initorder, strconv.base.DEC)); + }; + }; }; d = d.next; }; @@ -8103,6 +8926,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { asserttyped(c, d, false); d = d.next; }; + initlowerpackage(c, file); // #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully // checked above (pass 2 + pass 3 walked it like every fn) but is NOT diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 5da6cc70..9035823d 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -24,14 +24,26 @@ import os; import syntax; import strconv; +let wwiwritefailed: i32 = 0; + +fn wwrite(fd: i32, p: *u8, n: u64) void = { + if (wwiwritefailed != 0) { return; }; + match (os.writeall(fd, p, n)) { + case let wrote: i64 => { + if (wrote < 0 || (wrote: u64) != n) { wwiwritefailed = 1; }; + }; + case let e: os.oserror => wwiwritefailed = 1; + }; +}; + fn wputs(fd: i32, s: str) void = { - os.write(fd, s.ptr, s.len: u64); + wwrite(fd, s.ptr, s.len: u64); }; fn wputb(fd: i32, b: u8) void = { let buf: [1]u8; buf[0] = b; - os.write(fd, buf.ptr, 1u64); + wwrite(fd, buf.ptr, 1u64); }; fn wquote(fd: i32, s: str) void = { @@ -59,7 +71,7 @@ fn wquote(fd: i32, s: str) void = { buf[1] = 120u8; buf[2] = h; buf[3] = l; - os.write(fd, buf.ptr, 4u64); + wwrite(fd, buf.ptr, 4u64); } else { wputb(fd, c); };};};};}; @@ -85,7 +97,8 @@ fn wwimodeq(a: str, b: str) bool = { fn wwiusepath(c: *checker, owner: str, source: i32, alias: str) str = { let u: *syntax.node = c.file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.sourceid == source + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.sourceid == source && syntax.streq(u.str, alias)) { let same: bool = false; if (owner.len == 0) { @@ -301,7 +314,7 @@ fn wwirune(fd: i32, cp: u64) void = { buf[1] = 120u8; buf[2] = h; buf[3] = l; - os.write(fd, buf.ptr, 4u64); + wwrite(fd, buf.ptr, 4u64); } else { wputb(fd, c); };};};};}; @@ -591,6 +604,7 @@ fn wwiprimary(n: *syntax.node) bool = { }; fn wwiisdecl(d: *syntax.node) bool = { + if (d.initfn != 0 || d.initsynthetic != 0) { return false; }; return d.kind == syntax.nkind.N_FNDECL || d.kind == syntax.nkind.N_TYPEDECL || d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_LET; }; @@ -887,7 +901,7 @@ fn wwisortfacts(fs: *wwifactset) void = { }; fn wwiowneduse(u: *syntax.node, owner: str, source: i32) bool = { - return u.kind == syntax.nkind.N_USE && u.imported != 0 + return u.kind == syntax.nkind.N_USE && u.useblank == 0 && u.imported != 0 && u.sourceid == source && wwimodeq(u.nmod, owner); }; @@ -900,7 +914,8 @@ fn wwiemitimports(fd: i32, file: *syntax.node, owner: str, source: i32, if (imported) { owned = wwiowneduse(u, owner, source); } else { - owned = u.kind == syntax.nkind.N_USE && u.imported == 0 + owned = u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.imported == 0 && u.sourceid == source; }; if (owned) { nuse += 1; }; @@ -916,7 +931,8 @@ fn wwiemitimports(fd: i32, file: *syntax.node, owner: str, source: i32, if (imported) { owned = wwiowneduse(u, owner, source); } else { - owned = u.kind == syntax.nkind.N_USE && u.imported == 0 + owned = u.kind == syntax.nkind.N_USE && u.useblank == 0 + && u.imported == 0 && u.sourceid == source; }; if (owned) { @@ -944,7 +960,7 @@ fn wwiprimarysectionhas(c: *checker, file: *syntax.node, source: i32) bool = { let u: *syntax.node = file.list; for (u != nil) { - if (u.kind == syntax.nkind.N_USE && u.imported == 0 + if (u.kind == syntax.nkind.N_USE && u.useblank == 0 && u.imported == 0 && u.sourceid == source) { return true; }; u = u.next; }; @@ -1025,7 +1041,8 @@ fn wwiemitprimarysection(c: *checker, fd: i32, file: *syntax.node, }; }; -fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { +fn wwiemitfd(c: *checker, file: *syntax.node, fd: i32) i32 = { + wwiwritefailed = 0; // §5: check_exported_type FIRST, before any byte — a producer // without it can emit a dangling `.wwi`. let bad: i32 = 0; @@ -1101,15 +1118,6 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { wwisortdecls(dkeys, dnodes, ndecl); }; - let fd: i32 = os.open(path, - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { - wputs(2, "w6c: cannot open "); - wputs(2, path); - wputs(2, "\n"); - return 1i32; - }; - // Canonical ownership, declared name, and lexical source scope are // independent export facts. Repeated owner sections preserve the file // that owns each binding while every symbol/action remains keyed by owner. @@ -1154,6 +1162,5 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { fi += 1; }; - os.close(fd); - return 0i32; + return wwiwritefailed; }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 94cfce4a..9fd190c7 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -910,6 +910,7 @@ type seppkg = struct { canon: *u8, // canonical location; never package identity artifact: *u8, // stable non-importable variant artifact key storage: *u8, // internal storage basename; never package identity + initsymbol: *u8, // canonical package/variant-owned hidden task storagehashed: bool, name: *u8, // validated declared name; directory packages only testpackage: *u8, @@ -926,6 +927,9 @@ type seppkg = struct { testsupport: bool, loaded: bool, exportchanged: bool, + sourcestaged: bool, + initstaged: bool, + archivestaged: bool, emitcontext: i32, contextstate: []u8, // zero-extended lazily for reached contexts bindings: []sepbind, @@ -962,6 +966,9 @@ type sepproduct = struct { root: i32, variantroot: i32, support: i32, + stageout: *u8, + stageiface: *u8, + stagestatus: *u8, }; fn sepgrowcap(current: i32, need: i32) i32 = { @@ -1983,6 +1990,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].canon = canon; g.pkg[g.n].artifact = nil; g.pkg[g.n].storage = nil; + g.pkg[g.n].initsymbol = nil; g.pkg[g.n].storagehashed = false; g.pkg[g.n].name = nil; g.pkg[g.n].testpackage = nil; @@ -2003,6 +2011,9 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].testsupport = false; g.pkg[g.n].loaded = false; g.pkg[g.n].exportchanged = false; + g.pkg[g.n].sourcestaged = false; + g.pkg[g.n].initstaged = false; + g.pkg[g.n].archivestaged = false; g.pkg[g.n].emitcontext = -1; let emptystate: []u8; g.pkg[g.n].contextstate = emptystate; @@ -2072,6 +2083,10 @@ fn sepgraphfree(g: *sepgraph) void = { if (g.pkg[i].name != nil) { os.free(g.pkg[i].name: *void, cstrlen(g.pkg[i].name) + 1u64); }; + if (g.pkg[i].initsymbol != nil) { + os.free(g.pkg[i].initsymbol: *void, + cstrlen(g.pkg[i].initsymbol) + 1u64); + }; if (g.pkg[i].contextstate.ptr != nil) { os.free(g.pkg[i].contextstate.ptr: *void, (g.pkg[i].contextstate.cap: u64) * (size(u8): u64)); @@ -2465,9 +2480,10 @@ fn seplegacyartifact(p: *seppkg) *u8 = { }; fn sepvalidatestoragepath(p: *seppkg, scratch: *u8) i32 = { + let tail: str = ".init.unit.ww.wwtxn.9223372036854775807.old"; let need: u64 = cstrlen(scratch) + 1u64 + cstrlen(p.storage) - + ".unit.new".len: u64 + 1u64; - if (cstrlen(p.storage) + ".unit.new".len: u64 > SEP_NAME_MAX + + tail.len: u64 + 1u64; + if (cstrlen(p.storage) + tail.len: u64 > SEP_NAME_MAX || need > os.PATH_MAX: u64) { cerr("ww: package artifact path is too long\n"); return -1; @@ -2476,10 +2492,11 @@ fn sepvalidatestoragepath(p: *seppkg, scratch: *u8) i32 = { }; fn sepassignstorage(p: *seppkg, scratch: *u8) i32 = { + let tail: str = ".init.unit.ww.wwtxn.9223372036854775807.old"; let base: *u8 = seplegacyartifact(p); let need: u64 = cstrlen(scratch) + 1u64 + cstrlen(base) - + ".unit.new".len: u64 + 1u64; - if (cstrlen(base) + ".unit.new".len: u64 <= SEP_NAME_MAX + + tail.len: u64 + 1u64; + if (cstrlen(base) + tail.len: u64 <= SEP_NAME_MAX && need <= os.PATH_MAX: u64) { p.storage = sepdupcstr(base, cstrlen(base)); p.storagehashed = false; @@ -3140,6 +3157,39 @@ fn sepdepcmp(g: *sepgraph, a: i32, b: i32) i32 = { return strings.compare(pathstr(g.pkg[a].entry), pathstr(g.pkg[b].entry)): i32; }; +fn seppackageinitsymbol(p: *seppkg) *u8 = { + let empty: bool = p.path[0u64] == 0u8; + let need: u64 = 0u64; + if (empty) { + if (!sepaddbytes(&need, "__ww..pkg.e.v".len: u64)) { return nil; }; + } else { + if (!sepaddbytes(&need, "__ww..pkg.p.".len: u64) + || !sepaddbytes(&need, cstrlen(p.path)) + || !sepaddbytes(&need, ".v".len: u64)) { return nil; }; + }; + if (!sepaddbytes(&need, 1u64) + || !sepaddbytes(&need, ".r".len: u64) + || !sepaddbytes(&need, 1u64) + || !sepaddbytes(&need, ".init".len: u64) + || !sepaddbytes(&need, 1u64)) { return nil; }; + let buf: []u8; + if (!sepmakebytes(need, &buf)) { return nil; }; + let off: u64 = 0u64; + if (empty) { + off = strinto(buf.ptr, off, "__ww..pkg.e.v"); + } else { + off = strinto(buf.ptr, off, "__ww..pkg.p."); + off = cstrinto(buf.ptr, off, p.path); + off = strinto(buf.ptr, off, ".v"); + }; + off = byteinto(buf.ptr, off, (('0': i32) + p.variant): u8); + off = strinto(buf.ptr, off, ".r"); + off = byteinto(buf.ptr, off, (('0': i32) + p.role): u8); + off = strinto(buf.ptr, off, ".init"); + cstrseal(buf.ptr, off); + return buf.ptr; +}; + fn generatedmainkind(variant: i32) str = { if (variant == SEP_VARIANT_SAME_TEST) { return "internal"; }; if (variant == SEP_VARIANT_EXTERNAL) { return "external"; }; @@ -3222,6 +3272,7 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, p.artifact = sepappendlit(variantartifact, "-main"); if (p.artifact == nil) { return -1; }; p.storage = nil; + p.initsymbol = nil; p.storagehashed = false; p.name = sepdupcstr("main\0".ptr, 4u64); if (p.name == nil) { return -1; }; @@ -3239,6 +3290,9 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, p.testsupport = false; p.loaded = true; p.exportchanged = false; + p.sourcestaged = false; + p.initstaged = false; + p.archivestaged = false; p.emitcontext = product.context; let emptystate: []u8; p.contextstate = emptystate; @@ -3909,8 +3963,169 @@ fn sepinternalreplacesproduction(g: *sepgraph, a: i32, b: i32) bool = { return g.pkg[internal].variant == SEP_VARIANT_SAME_TEST && g.pkg[production].variant == SEP_VARIANT_PRODUCTION && g.pkg[production].role != SEP_ROLE_TEST_SUPPORT - && os.samefile(pathstr(g.pkg[internal].entry), - pathstr(g.pkg[production].entry)); + && cstreq(g.pkg[internal].canon, g.pkg[production].canon); +}; + +// A production action is omitted from an internal-test product because the +// augmented variant owns those same production sources. Map initialization +// edges through that replacement exactly as the link closure does. +fn sepiniteffective(g: *sepgraph, variantroot: i32, pi: i32) i32 = { + if (variantroot >= 0 && variantroot < g.n + && sepinternalreplacesproduction(g, variantroot, pi)) { + return variantroot; + }; + return pi; +}; + +fn sepinitcmp(g: *sepgraph, a: i32, b: i32) i32 = { + let r: i32 = strings.compare(pathstr(g.pkg[a].path), + pathstr(g.pkg[b].path)): i32; + if (r != 0) { return r; }; + if (g.pkg[a].variant < g.pkg[b].variant) { return -1; }; + if (g.pkg[a].variant > g.pkg[b].variant) { return 1; }; + if (g.pkg[a].role < g.pkg[b].role) { return -1; }; + if (g.pkg[a].role > g.pkg[b].role) { return 1; }; + return 0; +}; + +// Go's linker uses a lexical ready queue over the reachable init-task DAG. +// Dependencies become ready first; canonical package identity breaks ties. +fn sepinitorder(g: *sepgraph, root: i32, variantroot: i32, + out: *[]i32, nout: *i32) i32 = { + let active: []u8; + let done: []u8; + let todo: []i32; + let order: []i32; + if (!sepmakebytes(g.n: u64, &active) + || !sepmakebytes(g.n: u64, &done) + || !sepmakeints(g.n, &todo) + || !sepmakeints(g.n, &order)) { + return -1; + }; + let zi: i32 = 0; + for (zi < g.n) { + active[zi] = 0u8; + done[zi] = 0u8; + zi += 1; + }; + let ntodo: i32 = 0; + let effectiveroot: i32 = sepiniteffective(g, variantroot, root); + active[effectiveroot] = 1u8; + todo[ntodo] = effectiveroot; + ntodo += 1; + for (ntodo > 0) { + ntodo -= 1; + let pi: i32 = todo[ntodo]; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + let dep: i32 = sepiniteffective(g, variantroot, + g.pkg[pi].deps[k]); + if (active[dep] == 0u8) { + active[dep] = 1u8; + todo[ntodo] = dep; + ntodo += 1; + }; + k += 1; + }; + }; + let nactive: i32 = 0; + let pi: i32 = 0; + for (pi < g.n) { + if (active[pi] != 0u8) { nactive += 1; }; + pi += 1; + }; + let no: i32 = 0; + for (no < nactive) { + let best: i32 = -1; + pi = 0; + for (pi < g.n) { + if (active[pi] != 0u8 && done[pi] == 0u8) { + let blocked: bool = false; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps && !blocked) { + let dep: i32 = sepiniteffective(g, variantroot, + g.pkg[pi].deps[k]); + if (dep != pi && active[dep] != 0u8 + && done[dep] == 0u8) { blocked = true; }; + k += 1; + }; + if (!blocked && (best < 0 || sepinitcmp(g, pi, best) < 0)) { + best = pi; + }; + }; + pi += 1; + }; + if (best < 0) { + cerr("ww: dependency cycle in initialization closure\n"); + return -1; + }; + done[best] = 1u8; + order[no] = best; + no += 1; + }; + *out = order; + *nout = no; + return 0; +}; + +fn sepcomposeinitdispatch(g: *sepgraph, product: *sepproduct, + unitpath: *u8, asmpath: *u8) i32 = { + let order: []i32; + let norder: i32 = 0; + if (sepinitorder(g, product.root, product.variantroot, + &order, &norder) < 0) { return -1; }; + let unit: i32 = os.open(pathstr(unitpath), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); + if (unit < 0) { + cerrpath("ww: cannot open ", unitpath, "\n"); + return -1; + }; + let bad: bool = !sepwriteall(unit, "//ww:init-root ".ptr, + "//ww:init-root ".len: u64) + || !sepwriteall(unit, g.pkg[product.root].initsymbol, + cstrlen(g.pkg[product.root].initsymbol)) + || !sepwriteall(unit, "\n".ptr, 1u64); + let i: i32 = 0; + for (i < norder && !bad) { + bad = !sepwriteall(unit, "//ww:init-call ".ptr, + "//ww:init-call ".len: u64) + || !sepwriteall(unit, g.pkg[order[i]].initsymbol, + cstrlen(g.pkg[order[i]].initsymbol)) + || !sepwriteall(unit, "\n".ptr, 1u64); + i += 1; + }; + if (os.close(unit) != 0) { bad = true; }; + if (bad) { + cerr("ww: cannot write initialization unit\n"); + return -1; + }; + let assembly: i32 = os.open(pathstr(asmpath), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); + if (assembly < 0) { + cerrpath("ww: cannot open ", asmpath, "\n"); + os.remove(pathstr(unitpath)); + return -1; + }; + let prologue: str = "TEXT __ww..dispatch,$0\n\tPUSHQ\tBP\n\tMOVQ\tSP, BP\n\tSUBQ\t$0, SP\n"; + bad = !sepwriteall(assembly, prologue.ptr, prologue.len: u64); + i = 0; + for (i < norder && !bad) { + bad = !sepwriteall(assembly, "\tCALL\t".ptr, "\tCALL\t".len: u64) + || !sepwriteall(assembly, g.pkg[order[i]].initsymbol, + cstrlen(g.pkg[order[i]].initsymbol)) + || !sepwriteall(assembly, "(SB)\n".ptr, "(SB)\n".len: u64); + i += 1; + }; + let epilogue: str = "\tMOVQ\t$0, AX\n\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"; + if (!bad) { bad = !sepwriteall(assembly, epilogue.ptr, epilogue.len: u64); }; + if (os.close(assembly) != 0) { bad = true; }; + if (bad) { + cerr("ww: cannot write initialization assembly\n"); + os.remove(pathstr(unitpath)); + os.remove(pathstr(asmpath)); + return -1; + }; + return 0; }; fn sepvalidatemoduleclosure(g: *sepgraph, order: []i32, n: i32, @@ -3998,9 +4213,28 @@ fn sepwritehex(fd: i32, value: *u8) bool = { return true; }; +fn sepwritefilehex(fd: i32, path: *u8) bool = { + let data: *u8; + let n: u64; + data, n = slurp(path); + if (data == nil) { return false; }; + let digits: str = "0123456789abcdef"; + let pair: [2]u8; + let i: u64 = 0u64; + for (i < n) { + let high: i32 = (data[i] / 16u8): i32; + let low: i32 = (data[i] % 16u8): i32; + pair[0] = digits[high]; + pair[1] = digits[low]; + if (!sepwriteall(fd, pair.ptr, 2u64)) { return false; }; + i += 1u64; + }; + return true; +}; + // Compose pi's sep-unit from only pi's byte-sorted sources. Direct exports are // separate compiler inputs; the linker retains the reachable archive closure. -fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { +fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, unitf: *u8) i32 = { if (g.pkg[pi].emitcontext < 0 || g.pkg[pi].emitcontext >= g.ncontext) { return -1; }; let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 @@ -4065,6 +4299,25 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { }; bi += 1; }; + // Pin exact sorted direct semantic exports into the source-action voucher. + // A rejected importer may retain its old committed voucher without ever + // accepting it after a dependency export change. + let di: i32 = 0; + for (di < g.pkg[pi].ndeps && bodyrc == 0) { + let dep: i32 = g.pkg[pi].deps[di]; + let ifacesuffix: str = ".wwi"; + if (g.pkg[dep].sourcestaged) { ifacesuffix = ".wwi.new"; }; + let interface: *u8 = sepfname(g, dep, scratch, ifacesuffix); + let pre: str = "//ww:direct-export "; + if (interface == nil + || !sepwriteall(u, pre.ptr, pre.len: u64) + || !sepwriteall(u, g.pkg[dep].path, + cstrlen(g.pkg[dep].path)) + || !sepwriteall(u, " ".ptr, 1u64) + || !sepwritefilehex(u, interface) + || !sepwriteall(u, "\n".ptr, 1u64)) { bodyrc = -1; }; + di += 1; + }; if (os.close(u) != 0) { cerr("ww: cannot close package unit\n"); return -1; @@ -4072,86 +4325,110 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = { return bodyrc; }; -// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic -// single-member SysV ar archive at `apath` wrapping the `.o` at -// `objpath`. No armap / long-name table: w6l reads each member's ELF -// .symtab directly and skips '/'-named members, so a package `.a` is -// just the global magic + one 60-byte member header + the `.o` bytes -// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed -// member name make the bytes a pure function of the `.o` content → -// cstage `.a` == wwstage `.a` (rule 10). -fn archiveo(objpath: *u8, apath: *u8) i32 = { - let objp: *u8; - let objn: u64; - objp, objn = slurp(objpath); - if (objp == nil) { - cerr("ww: cannot read object for archive\n"); - return -1; +// Stream one fixed-name SysV ar member. The caller owns the global magic and +// exact member order. A fixed transfer buffer avoids archive-size-dependent +// allocation in both driver stages. +fn archivemember(out: i32, objpath: *u8, member: str) bool = { + if (member.len > 16) { return false; }; + let input: i32 = os.open(pathstr(objpath), os.flag.RDONLY, 0i32); + if (input < 0) { + cerrpath("ww: cannot read ", objpath, "\n"); + return false; }; - let pad: u64 = 0u64; - if ((objn & 1u64) != 0u64) { pad = 1u64; }; - // ar(5) fixes the archive magic at 8 bytes and each serialized - // member header at 60 bytes. - let total: u64 = 0u64; - if (!sepaddbytes(&total, 8u64) || !sepaddbytes(&total, 60u64) - || !sepaddbytes(&total, objn) || !sepaddbytes(&total, pad)) { - return -1; + let sr: (i64 | os.oserror) = os.filesize(input); + let objn: i64 = -1i64; + match (sr) { + case let n: i64 => objn = n; + case let e: os.oserror => { os.close(input); return false; }; }; - let outs: []u8; - if (!sepmakebytes(total, &outs)) { return -1; }; - let out: *u8 = outs.ptr; - - // 60-byte member header at offset 8, ASCII space-filled, fields - // left-justified; the 8-byte global magic precedes it. strinto - // copies a str's bytes (the working i32-index idiom) — a direct - // `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm. - let h: u64 = 8u64; + if (objn < 0i64) { os.close(input); return false; }; + let objnu: u64 = objn: u64; + let header: [60]u8; let j: u64 = 0u64; - for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill - strinto(out, 0u64, "!\n"); // global magic - strinto(out, h, "pkg.o/"); // name (GNU '/' terminator) - out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism) - out[h + 28u64] = 48u8; // uid "0" - out[h + 34u64] = 48u8; // gid "0" - strinto(out, h + 40u64, "100644"); // mode (fixed octal) - // size: decimal byte-count of the .o, left-justified at [48..58) - if (objn == 0u64) { - out[h + 48u64] = 48u8; - } else { - let ndig: u64 = 0u64; - let t: u64 = objn; + for (j < 60u64) { header[j] = 32u8; j += 1u64; }; + strinto(&header[0], 0u64, member); + header[16] = 48u8; + header[28] = 48u8; + header[34] = 48u8; + strinto(&header[0], 40u64, "100644"); + let ndig: u64 = 1u64; + if (objnu != 0u64) { + ndig = 0u64; + let t: u64 = objnu; for (t > 0u64) { ndig += 1u64; t = t / 10u64; }; + }; + if (ndig > 10u64) { os.close(input); return false; }; + if (objnu == 0u64) { + header[48] = 48u8; + } else { let d: u64 = ndig; - t = objn; + let t: u64 = objnu; for (t > 0u64) { d -= 1u64; - out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8; + header[48u64 + d] = ((t % 10u64): u8) + 48u8; t = t / 10u64; }; }; - out[h + 58u64] = 96u8; // member-header magic 0x60 - out[h + 59u64] = 10u8; // 0x0a - - // the .o bytes, then a '\n' pad iff the size is odd (2-byte align). - let k: u64 = 0u64; - for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; }; - if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; }; + header[58] = 96u8; + header[59] = 10u8; + let good: bool = sepwriteall(out, &header[0], 60u64); + let buf: [8192]u8; + let remaining: u64 = objnu; + for (good && remaining > 0u64) { + let want: u64 = remaining; + if (want > 8192u64) { want = 8192u64; }; + let got: i64 = os.read(input, &buf[0], want); + if (got <= 0i64 || (got: u64) > want + || !sepwriteall(out, &buf[0], got: u64)) { + good = false; + } else { remaining -= got: u64; }; + }; + if (os.close(input) != 0) { good = false; }; + if (good && (objnu & 1u64) != 0u64) { + let pad: [1]u8 = [10u8]; + good = sepwriteall(out, &pad[0], 1u64); + }; + return good; +}; +// Deterministic package archive: `pkg.o/` and, for an executable or +// generated-test root only, the root-owned `init.o/` dispatcher member. +fn archiveo(objpath: *u8, initpath: *u8, apath: *u8) i32 = { let fd: i32 = os.open(pathstr(apath), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); if (fd < 0) { - cerr("ww: cannot open archive\n"); + cerrpath("ww: cannot open ", apath, "\n"); return -1; }; - let bad: bool = !sepwriteall(fd, out, total); + let magic: str = "!\n"; + let good: bool = sepwriteall(fd, magic.ptr, magic.len: u64); + if (good) { good = archivemember(fd, objpath, "pkg.o/"); }; + if (good && initpath != nil) { + good = archivemember(fd, initpath, "init.o/"); + }; + let bad: bool = !good; if (os.close(fd) != 0) { bad = true; }; if (bad) { - cerr("ww: cannot write archive\n"); + cerrpath("ww: cannot write archive ", apath, "\n"); return -1; }; return 0; }; +fn runassembler(tool: *u8, output: *u8, input: *u8) i32 = { + let argv: []str = ["w6a", "-o", pathstr(output), pathstr(input)]; + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio(pathstr(tool), argv, env, &result); + if (result.termination == exec.termination.EXIT && result.code == 0) { + return 0; + }; + if (result.termination == exec.termination.ERROR && result.code == 127) { + cerr("ww: execve failed\n"); + }; + return -1; +}; + // buildonesep — discover deps, reverse-topo, // the dependency-first producer loop (one `w6c -c -I` per package, // each package `.o` wrapped in its own deterministic per-package `.a`), then a @@ -4165,12 +4442,12 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { // unit byte-equals the committed unit, no direct dependency emitted a changed // export, AND the driver/tool copies recorded in the dir byte-equal the live // executables — every decision is -// reproducible by hand with cmp(1) against plain files. Artifacts commit -// via temp + rename with the unit renamed last, so a killed build can -// never leave a committed unit vouching for uncommitted artifacts. The -// caller serializes invocations per workdir and `make clean` reclaims -// the state. Cstage twin: cmd/ww/main.c file_equal/copy_file_atomic/ -// workdir_stamp_text group. +// reproducible by hand with cmp(1) against plain files. Artifacts, units, tool +// records, stamp, products, and statuses stage together and publish through one +// rollback-capable request transaction, so a killed or rejected build cannot +// expose a mixed generation. The caller serializes invocations per workdir and +// `make clean` reclaims the state. Cstage twin: cmd/ww/main.c +// file_equal/workdir_stamp_text/transaction group. // `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt // emits no text), so committed presence is their freshness test; the @@ -4179,7 +4456,7 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { fn fileisreg(path: *u8) bool = { let fi: os.filestat; let ok: bool = false; - match (os.stat(&fi, pathstr(path))) { + match (os.lstat(&fi, pathstr(path))) { case void => { let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT if (t == os.mode.REG: u32) { ok = true; }; @@ -4192,7 +4469,7 @@ fn fileisreg(path: *u8) bool = { fn filesizenonzero(path: *u8) bool = { let fi: os.filestat; let ok: bool = false; - match (os.stat(&fi, pathstr(path))) { + match (os.lstat(&fi, pathstr(path))) { case void => { let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT if (t == os.mode.REG: u32) { @@ -4204,6 +4481,21 @@ fn filesizenonzero(path: *u8) bool = { return ok; }; +// 1 means occupied by any terminal directory entry (including a dangling +// symlink), 0 means ENOENT, and -1 is another lookup failure. Staging and +// rollback paths fail closed on every nonzero result. +fn pathexistsnofollow(path: *u8) i32 = { + let fi: os.filestat; + match (os.lstat(&fi, pathstr(path))) { + case void => return 1; + case let e: os.oserror => { + if ((e: i64) == -2i64) { return 0; }; + return -1; + }; + }; + return -1; +}; + fn sepvalidateunitowner(g: *sepgraph, pi: i32, scratch: *u8) i32 = { let unit: *u8 = sepfname(g, pi, scratch, ".unit.ww"); if (unit == nil) { return -1; }; @@ -4275,19 +4567,13 @@ fn fileequal(a: *u8, b: *u8) bool = { if (fa < 0) { return false; }; let fb: i32 = os.open(pathstr(b), os.flag.RDONLY, 0i32); if (fb < 0) { os.close(fa); return false; }; - let bufa: []u8; - if (!sepmakebytes(65536u64, &bufa)) { - os.close(fa); os.close(fb); return false; - }; - let bufb: []u8; - if (!sepmakebytes(65536u64, &bufb)) { - os.close(fa); os.close(fb); return false; - }; + let bufa: [8192]u8; + let bufb: [8192]u8; let eq: bool = true; let done: bool = false; for (!done) { - let na: i64 = os.read(fa, bufa.ptr, 65536u64); - let nb: i64 = os.read(fb, bufb.ptr, 65536u64); + let na: i64 = os.read(fa, &bufa[0], 8192u64); + let nb: i64 = os.read(fb, &bufb[0], 8192u64); if (na < 0 || na != nb) { eq = false; done = true; } else { if (na == 0) { done = true; } else { @@ -4305,45 +4591,372 @@ fn fileequal(a: *u8, b: *u8) bool = { return eq; }; -// Replace dst with src's bytes via temp + rename, so a torn write can -// never masquerade as a committed tool copy. -fn copyfileatomic(src: *u8, dst: *u8) i32 = { - let tmpp: *u8 = sepappendlit(dst, ".new"); - if (tmpp == nil) { return -1; }; +fn copyfilestage(src: *u8, dst: *u8) i32 = { let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32); if (in < 0) { return -1; }; - let out: i32 = os.open(pathstr(tmpp), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + let out: i32 = os.open(pathstr(dst), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); if (out < 0) { os.close(in); return -1; }; - let buf: []u8; - if (!sepmakebytes(65536u64, &buf)) { - os.close(in); os.close(out); return -1; - }; + let buf: [65536]u8; let bad: bool = false; - let done: bool = false; - for (!done) { - let n: i64 = os.read(in, buf.ptr, 65536u64); - if (n < 0) { bad = true; done = true; } - else { if (n == 0) { done = true; } - else { - match (os.writeall(out, buf.ptr, n: u64)) { - case let w: i64 => { - if (w != n) { bad = true; done = true; }; - }; - case let e: os.oserror => { bad = true; done = true; }; - }; - }; }; + for (!bad) { + let n: i64 = os.read(in, &buf[0], 65536u64); + if (n < 0) { bad = true; break; }; + if (n == 0) { break; }; + match (os.writeall(out, &buf[0], n: u64)) { + case let wrote: i64 => { if (wrote != n) { bad = true; }; }; + case let e: os.oserror => bad = true; + }; }; if (os.close(in) != 0) { bad = true; }; if (os.close(out) != 0) { bad = true; }; - if (bad) { return -1; }; - return os.rename(pathstr(tmpp), pathstr(dst)); + if (bad) { os.remove(pathstr(dst)); return -1; }; + return 0; +}; + +fn sepproductstagepath(dst: *u8) *u8 = { + let path: *u8 = sepappendlit(dst, ".new"); + if (path != nil && cstrlen(path) + 1u64 > os.PATH_MAX: u64) { + cerr("ww: product staging path is too long\n"); + return nil; + }; + return path; +}; + +fn sepprepareproductstage(current: *u8, dst: *u8) *u8 = { + let stage: *u8 = current; + if (stage == nil) { stage = sepproductstagepath(dst); }; + if (stage == nil) { return nil; }; + if (pathexistsnofollow(stage) != 0) { + cerrpath("ww: product staging path already exists: ", + stage, "\n"); + return nil; + }; + return stage; +}; + +fn sepwritetextstage(path: *u8, body: str) i32 = { + let fd: i32 = os.open(pathstr(path), + os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); + if (fd < 0) { return -1; }; + let bad: bool = false; + match (os.writeall(fd, body.ptr, body.len: u64)) { + case let n: i64 => { if (n != body.len: i64) { bad = true; }; }; + case let e: os.oserror => { bad = true; }; + }; + if (os.close(fd) != 0) { bad = true; }; + if (bad) { os.remove(pathstr(path)); return -1; }; + return 0; +}; + +fn sepstageproductstatus(product: *sepproduct) i32 = { + if (product.status == nil) { return 0; }; + product.stagestatus = sepprepareproductstage(product.stagestatus, + product.status); + if (product.stagestatus == nil) { return -1; }; + return sepwritetextstage(product.stagestatus, "ok\n"); +}; + +// Fail closed on every coordinator-owned staging name before scratch or tool +// acquisition. lstat keeps dangling symlinks occupied rather than following +// them through a later truncate. +fn sepvalidaterequeststaging(g: *sepgraph, scratch: *u8, warm: bool, + products: *sepproduct, nproducts: i32, rootpackage: bool, + publishpackage: i32, emitasm: i32, istest: i32) i32 = { + if (warm) { + let suffix: []str = [".unit.new", ".wwi.new", ".s.new", ".o.new", + ".a.new", ".init.unit.new", ".init.s.new", ".init.o.new"]; + let pi: i32 = 0; + for (pi < g.n) { + if (!g.pkg[pi].failed && g.pkg[pi].loaded) { + let si: i32 = 0; + for (si < suffix.len) { + let path: *u8 = sepfname(g, pi, scratch, suffix[si]); + if (path == nil) { return -1; }; + if (pathexistsnofollow(path) != 0) { + cerrpath("ww: package staging path already exists: ", + path, "\n"); + return -1; + }; + si += 1; + }; + }; + pi += 1; + }; + let toolsuffix: []str = [".wwtool.ww.new", ".wwtool.w6c.new", + ".wwtool.w6a.new", ".wwtool.stamp.new"]; + let ti: i32 = 0; + for (ti < toolsuffix.len) { + let path: *u8 = sepjoinpathlit(scratch, toolsuffix[ti]); + if (path == nil) { return -1; }; + if (pathexistsnofollow(path) != 0) { + cerrpath("ww: tool staging path already exists: ", path, + "\n"); + return -1; + }; + ti += 1; + }; + }; + let i: i32 = 0; + for (i < nproducts) { + if (products[i].status != nil) { + products[i].stagestatus = sepprepareproductstage( + products[i].stagestatus, products[i].status); + if (products[i].stagestatus == nil) { return -1; }; + }; + if (emitasm == 0) { + let ownsoutput: bool = false; + if (rootpackage) { ownsoutput = publishpackage != 0; } + else { ownsoutput = istest != 0 + || seprootiscommand(&g.pkg[products[i].root]); }; + if (ownsoutput) { + products[i].stageout = sepprepareproductstage( + products[i].stageout, products[i].out); + if (products[i].stageout == nil) { return -1; }; + if (rootpackage) { + let iface: *u8 = sepappendlit(products[i].out, ".wwi"); + if (iface == nil) { return -1; }; + products[i].stageiface = sepprepareproductstage( + products[i].stageiface, iface); + if (products[i].stageiface == nil) { return -1; }; + }; + }; + }; + i += 1; + }; + return 0; +}; + +type septxnentry = struct { + stage: *u8, + dst: *u8, + backup: *u8, + hadold: bool, + installed: bool, +}; + +fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = { + let value: []septxnentry = alloc([], cap: u64)?; + return value; +}; + +fn sepreservetxnentries(entries: *[]septxnentry, used: i32, + need: i32) bool = { + if (need <= entries.len) { return true; }; + let cap: i32 = sepgrowcap(entries.len, need); + if (cap < 0) { return false; }; + let allocation: ([]septxnentry | nomem) = sepalloctxnentries(cap); + let next: []septxnentry; + match (allocation) { + case let value: []septxnentry => next = value; + case nomem => { sepfailnomem(); return false; }; + }; + next.len = cap; + let i: i32 = 0; + for (i < used) { next[i] = (*entries)[i]; i += 1; }; + if (entries.ptr != nil) { + os.free(entries.ptr: *void, + (entries.cap: u64) * (size(septxnentry): u64)); + }; + *entries = next; + return true; +}; + +fn septxnbackup(dst: *u8) *u8 = { + let dn: u64 = cstrlen(dst); + let pid: i32 = os.getpid(); + let v: i32 = pid; + if (v < 0) { v = -v; }; + let digits: i32 = 1; + let q: i32 = v; + for (q >= 10) { digits += 1; q = q / 10; }; + let suffix: str = ".wwtxn."; + let tail: str = ".old"; + let need: u64 = dn; + if (!sepaddbytes(&need, suffix.len: u64) + || !sepaddbytes(&need, digits: u64) + || !sepaddbytes(&need, tail.len: u64) + || !sepaddbytes(&need, 1u64)) { return nil; }; + let allocation: ([]u8 | nomem) = sepallocbytes(need: i32); + let buf: []u8; + match (allocation) { + case let value: []u8 => buf = value; + case nomem => { sepfailnomem(); return nil; }; + }; + let off: u64 = 0u64; + let i: u64 = 0u64; + for (i < dn) { buf[off] = dst[i]; off += 1u64; i += 1u64; }; + i = 0u64; + for (i < suffix.len: u64) { + buf[off] = suffix.ptr[i]; off += 1u64; i += 1u64; + }; + let rev: [16]u8; + let n: i32 = 0; + if (v == 0) { rev[0] = '0'; n = 1; } + else { for (v > 0) { + rev[n] = ((v % 10) + 48): u8; n += 1; v = v / 10; + }; }; + let ri: i32 = n - 1; + for (ri >= 0) { buf[off] = rev[ri]; off += 1u64; ri -= 1; }; + i = 0u64; + for (i < tail.len: u64) { + buf[off] = tail.ptr[i]; off += 1u64; i += 1u64; + }; + buf[off] = 0u8; + return buf.ptr; +}; + +fn septxnadd(entries: *[]septxnentry, n: *i32, + stage: *u8, dst: *u8) bool = { + if (cstreq(stage, dst)) { + cerrpath("ww: transaction path collision: ", dst, "\n"); + return false; + }; + let i: i32 = 0; + for (i < *n) { + if (cstreq((*entries)[i].dst, dst) + || cstreq((*entries)[i].stage, stage) + || cstreq((*entries)[i].dst, stage) + || cstreq((*entries)[i].stage, dst)) { + cerrpath("ww: transaction path collision: ", dst, "\n"); + return false; + }; + i += 1; + }; + if (*n == SEP_COUNT_MAX + || !sepreservetxnentries(entries, *n, *n + 1)) { return false; }; + let stagecopy: *u8 = sepdupcstr(stage, cstrlen(stage)); + let dstcopy: *u8 = sepdupcstr(dst, cstrlen(dst)); + let backup: *u8 = septxnbackup(dst); + if (stagecopy == nil || dstcopy == nil || backup == nil) { + if (stagecopy != nil) { + os.free(stagecopy: *void, cstrlen(stagecopy) + 1u64); + }; + if (dstcopy != nil) { + os.free(dstcopy: *void, cstrlen(dstcopy) + 1u64); + }; + if (backup != nil) { + os.free(backup: *void, cstrlen(backup) + 1u64); + }; + return false; + }; + if (cstrlen(backup) + 1u64 > os.PATH_MAX: u64) { + cerr("ww: transaction path is too long\n"); + os.free(stagecopy: *void, cstrlen(stagecopy) + 1u64); + os.free(dstcopy: *void, cstrlen(dstcopy) + 1u64); + os.free(backup: *void, cstrlen(backup) + 1u64); + return false; + }; + (*entries)[*n].stage = stagecopy; + (*entries)[*n].dst = dstcopy; + (*entries)[*n].backup = backup; + (*entries)[*n].hadold = false; + (*entries)[*n].installed = false; + *n += 1; + return true; +}; + +fn septxnaddpkgsuffix(entries: *[]septxnentry, n: *i32, + g: *sepgraph, pi: i32, scratch: *u8, + stagesuffix: str, dstsuffix: str) bool = { + let stage: *u8 = sepfname(g, pi, scratch, stagesuffix); + let dst: *u8 = sepfname(g, pi, scratch, dstsuffix); + if (stage == nil || dst == nil) { return false; }; + return septxnadd(entries, n, stage, dst); +}; + +fn septxndiscard(entries: []septxnentry, n: i32) void = { + let i: i32 = 0; + for (i < n) { os.remove(pathstr(entries[i].stage)); i += 1; }; +}; + +fn septxncommit(entries: []septxnentry, n: i32) bool = { + let i: i32 = 0; + let valid: bool = true; + for (i < n) { + if (!fileisreg(entries[i].stage)) { + cerrpath("ww: transaction stage is not a regular file: ", + entries[i].stage, "\n"); + valid = false; + break; + }; + i += 1; + }; + if (valid) { + i = 0; + for (i < n) { + if (pathexistsnofollow(entries[i].backup) != 0) { + cerrpath("ww: transaction backup already exists: ", + entries[i].backup, "\n"); + break; + }; + i += 1; + }; + }; + if (i == n) { + i = 0; + for (i < n) { + let rr: i32 = os.rename(pathstr(entries[i].dst), + pathstr(entries[i].backup)); + if (rr == 0) { entries[i].hadold = true; } + else { if (rr != -2) { + cerrpath("ww: cannot preserve transaction destination ", + entries[i].dst, "\n"); + break; + }; }; + i += 1; + }; + }; + if (i == n) { + i = 0; + for (i < n) { + if (os.rename(pathstr(entries[i].stage), + pathstr(entries[i].dst)) != 0) { + cerrpath("ww: cannot install transaction destination ", + entries[i].dst, "\n"); + break; + }; + entries[i].installed = true; + i += 1; + }; + if (i == n) { + i = 0; + for (i < n) { + if (entries[i].hadold + && os.remove(pathstr(entries[i].backup)) != 0) { + cerrpath("ww: cannot remove transaction backup ", + entries[i].backup, "\n"); + }; + i += 1; + }; + return true; + }; + }; + i = n - 1; + for (i >= 0) { + if (entries[i].installed) { + let rr: i32 = os.remove(pathstr(entries[i].dst)); + if (rr != 0 && rr != -2) { + cerrpath("ww: cannot roll back ", entries[i].dst, "\n"); + }; + }; + if (entries[i].hadold) { + if (os.rename(pathstr(entries[i].backup), + pathstr(entries[i].dst)) != 0) { + cerrpath("ww: cannot restore ", entries[i].dst, "\n"); + }; + }; + os.remove(pathstr(entries[i].stage)); + i -= 1; + }; + return false; }; // Package publication writes OUT, OUT.new, OUT.wwi, and OUT.wwi.new. Check // the longest spelling before any producer tool can run. fn validatepackageoutputpath(out: *u8) i32 = { - let suffix: u64 = ".wwi.new".len: u64 + 1u64; + let suffix: u64 = ".wwi.wwtxn.9223372036854775807.old".len: u64 + + 1u64; let limit: u64 = os.PATH_MAX: u64; if (suffix > limit || cstrlen(out) > limit - suffix) { cerr("ww: package output path is too long\n"); @@ -4353,7 +4966,9 @@ fn validatepackageoutputpath(out: *u8) i32 = { }; fn validatecommandoutputpath(out: *u8) i32 = { - if (out == nil || cstrlen(out) + 1u64 > os.PATH_MAX: u64) { + let suffix: u64 = ".wwtxn.9223372036854775807.old".len: u64 + 1u64; + if (out == nil || suffix > os.PATH_MAX: u64 + || cstrlen(out) > os.PATH_MAX: u64 - suffix) { cerr("ww: command output path is too long\n"); return -1; }; @@ -4366,14 +4981,14 @@ fn validatecommandoutputpath(out: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 14 mode test asm 1\n"; + return "ww workdir fmt 16 mode test asm 1\n"; }; - return "ww workdir fmt 14 mode test asm 0\n"; + return "ww workdir fmt 16 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 15 mode build asm 1\n"; + return "ww workdir fmt 17 mode build asm 1\n"; }; - return "ww workdir fmt 15 mode build asm 0\n"; + return "ww workdir fmt 17 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = { @@ -4394,84 +5009,9 @@ fn stampmatches(path: *u8, want: str) bool = { return eq; }; -fn writestampatomic(path: *u8, want: str) i32 = { - let tmpp: *u8 = sepappendlit(path, ".new"); - if (tmpp == nil) { return -1; }; - let fd: i32 = os.open(pathstr(tmpp), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { return -1; }; - let bad: bool = false; - match (os.writeall(fd, want.ptr, want.len: u64)) { - case let w: i64 => { if (w != want.len: i64) { bad = true; }; }; - case let e: os.oserror => { bad = true; }; - }; - if (os.close(fd) != 0) { bad = true; }; - if (bad) { return -1; }; - return os.rename(pathstr(tmpp), pathstr(path)); -}; - -// A coordinator-private completion marker distinguishes a newly linked -// product from a caller-owned binary left by an earlier invocation. -fn recordproductstatus(path: *u8) i32 = { - if (path == nil) { return 0; }; - let tmpp: *u8 = sepappendlit(path, ".new"); - if (tmpp == nil) { return -1; }; - let fd: i32 = os.open(pathstr(tmpp), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); - if (fd < 0) { return -1; }; - let body: str = "ok\n"; - let bad: bool = false; - match (os.writeall(fd, body.ptr, body.len: u64)) { - case let n: i64 => { if (n != body.len: i64) { bad = true; }; }; - case let e: os.oserror => { bad = true; }; - }; - if (os.close(fd) != 0) { bad = true; }; - if (bad) { return -1; }; - return os.rename(pathstr(tmpp), pathstr(path)); -}; - -// Remove every committed unit voucher before a stale-builder pass. Remaining -// artifacts cannot be reused without their matching unit, so a partial pass -// may safely record its new tool identity: successful actions have current -// units and failed/no-longer-requested actions have none. -fn invalidateworkdirunits(scratch: *u8) i32 = { - let fd: i32 = os.open(pathstr(scratch), os.flag.RDONLY, 0i32); - if (fd < 0) { return -1; }; - let buf: []u8; - if (!sepmakebytes(8192u64, &buf)) { os.close(fd); return -1; }; - let rc: i32 = 0; - let r: i64 = os.getdents64(fd, buf.ptr, 8192u64); - for (r > 0i64 && rc == 0) { - let off: u64 = 0u64; - for (off < r: u64) { - let reclen: u64 = (buf[off + 16u64]): u64 - + ((buf[off + 17u64]): u64) * 256u64; - if (reclen == 0u64) { rc = -1; break; }; - let name: *u8 = buf.ptr + off + 19u64; - let ns: str = pathstr(name); - if (strings.hassuffix(ns, ".unit.ww")) { - if (cstrlen(scratch) + 1u64 + cstrlen(name) + 1u64 - > os.PATH_MAX: u64) { - rc = -1; - break; - }; - let path: *u8 = joinpath(scratch, name); - let rr: i32 = os.remove(pathstr(path)); - if (rr != 0 && rr != -2) { rc = -1; break; }; - }; - off += reclen; - }; - if (rc == 0) { r = os.getdents64(fd, buf.ptr, 8192u64); }; - }; - if (r < 0i64) { rc = -1; }; - if (os.close(fd) != 0) { rc = -1; }; - if (rc != 0) { cerr("ww: cannot invalidate stale package units\n"); }; - return rc; -}; - fn sepdiscardactionstaging(warm: bool, unit: *u8, wwi: *u8, assembly: *u8, object: *u8, archive: *u8) i32 = { - if (!warm) { return 0; }; + let ignoredwarm: bool = warm; let paths: []*u8 = [unit, wwi, assembly, object, archive]; let i: i32 = 0; for (i < paths.len) { @@ -4485,6 +5025,79 @@ fn sepdiscardactionstaging(warm: bool, unit: *u8, wwi: *u8, return 0; }; +fn sepdiscardinitstaging(warm: bool, unit: *u8, assembly: *u8, + object: *u8) i32 = { + let ignoredwarm: bool = warm; + let paths: []*u8 = [unit, assembly, object]; + let i: i32 = 0; + for (i < paths.len) { + if (paths[i] != nil && paths[i][0u64] != 0u8) { + let rr: i32 = os.remove(pathstr(paths[i])); + if (rr != 0 && rr != -2) { + cerr("ww: cannot remove staged initialization artifacts\n"); + return -1; + }; + }; + i += 1; + }; + return 0; +}; + +fn sepdiscardrequeststaging(g: *sepgraph, scratch: *u8, warm: bool, + products: *sepproduct, nproducts: i32) i32 = { + let warmsuffix: []str = [".unit.new", ".wwi.new", ".s.new", ".o.new", + ".a.new", ".init.unit.new", ".init.s.new", ".init.o.new"]; + let coldsuffix: []str = [".unit.ww", ".wwi", ".s", ".o", ".a", + ".init.unit.ww", ".init.s", ".init.o"]; + let suffix: []str = coldsuffix; + if (warm) { suffix = warmsuffix; }; + let rc: i32 = 0; + let pi: i32 = 0; + for (pi < g.n) { + let si: i32 = 0; + for (si < suffix.len) { + let path: *u8 = sepfname(g, pi, scratch, suffix[si]); + if (path == nil) { rc = -1; } + else { + let rr: i32 = os.remove(pathstr(path)); + if (rr != 0 && rr != -2) { rc = -1; }; + }; + si += 1; + }; + pi += 1; + }; + let producti: i32 = 0; + for (producti < nproducts) { + let paths: []*u8 = [products[producti].stageout, + products[producti].stageiface, products[producti].stagestatus]; + let si: i32 = 0; + for (si < paths.len) { + if (paths[si] != nil) { + let rr: i32 = os.remove(pathstr(paths[si])); + if (rr != 0 && rr != -2) { rc = -1; }; + }; + si += 1; + }; + producti += 1; + }; + if (warm) { + let toolsuffix: []str = [".wwtool.ww.new", ".wwtool.w6c.new", + ".wwtool.w6a.new", ".wwtool.stamp.new"]; + let ti: i32 = 0; + for (ti < toolsuffix.len) { + let path: *u8 = sepjoinpathlit(scratch, toolsuffix[ti]); + if (path == nil) { rc = -1; } + else { + let rr: i32 = os.remove(pathstr(path)); + if (rr != 0 && rr != -2) { rc = -1; }; + }; + ti += 1; + }; + }; + if (rc != 0) { cerr("ww: cannot discard rejected request staging\n"); }; + return rc; +}; + type sepcreateddirs = struct { path: [4096]u8, offset: [2048]u16, @@ -4572,6 +5185,361 @@ fn cerrpath(head: str, path: *u8, tail: str) void = { cerr(tail); }; +fn septxnrelease(entries: *[]septxnentry, n: i32) void = { + let i: i32 = 0; + for (i < n) { + if ((*entries)[i].stage != nil) { + os.free((*entries)[i].stage: *void, + cstrlen((*entries)[i].stage) + 1u64); + }; + if ((*entries)[i].dst != nil) { + os.free((*entries)[i].dst: *void, + cstrlen((*entries)[i].dst) + 1u64); + }; + if ((*entries)[i].backup != nil) { + os.free((*entries)[i].backup: *void, + cstrlen((*entries)[i].backup) + 1u64); + }; + i += 1; + }; + if (entries.ptr != nil) { + os.free(entries.ptr: *void, + (entries.cap: u64) * (size(septxnentry): u64)); + }; + entries.ptr = nil; + entries.len = 0; + entries.cap = 0; +}; + +fn sepfinishfail(entries: *[]septxnentry, n: i32) i32 = { + septxndiscard(*entries, n); + septxnrelease(entries, n); + return 1; +}; + +fn sepfreeproductstaging(products: *sepproduct, nproducts: i32) void = { + let i: i32 = 0; + for (i < nproducts) { + let paths: []*u8 = [products[i].stageout, products[i].stageiface, + products[i].stagestatus]; + let k: i32 = 0; + for (k < paths.len) { + if (paths[k] != nil) { + os.free(paths[k]: *void, cstrlen(paths[k]) + 1u64); + }; + k += 1; + }; + products[i].stageout = nil; + products[i].stageiface = nil; + products[i].stagestatus = nil; + i += 1; + }; +}; + +fn seprejectrequest(g: *sepgraph, scratch: *u8, warm: bool, + products: *sepproduct, nproducts: i32, + createdwork: *sepcreateddirs, createdoutput: *sepcreateddirs, + scratchout: **u8) i32 = { + sepdiscardrequeststaging(g, scratch, warm, products, nproducts); + sepfreeproductstaging(products, nproducts); + if (!warm) { + let rr: i32 = os.rmdir(pathstr(scratch)); + if (rr != 0 && rr != -2) { + cerrpath("ww: cannot remove rejected scratch ", scratch, "\n"); + } else { if (scratchout != nil) { *scratchout = nil; }; }; + } else { + seprollbackdirs(createdwork); + }; + seprollbackdirs(createdoutput); + return 1; +}; + +// Finish all products before opening one request-wide rollback group. Package +// actions remain in `.new`; caller-visible outputs and statuses are adjacent +// `.new` files so final rename never crosses filesystems. +fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8, + g: *sepgraph, scratch: *u8, warm: bool, rootpackage: bool, + publishpackage: i32, istest: i32, emitasm: i32, + products: *sepproduct, nproducts: i32, + order: []i32, norder: i32, rtpaths: []*u8, nrt: i32, lf: *lflags, + toolw: *u8, toolc: *u8, toola: *u8, stampf: *u8, + stampwant: str, stampok: bool) i32 = { + let entries: []septxnentry; + entries.ptr = nil; + entries.len = 0; + entries.cap = 0; + let ntxn: i32 = 0; + let producti: i32 = 0; + + if (emitasm != 0) { + for (producti < nproducts) { + if (sepstageproductstatus(&products[producti]) != 0) { + cerr("ww: cannot stage package-build product\n"); + return sepfinishfail(&entries, ntxn); + }; + producti += 1; + }; + } else { if (rootpackage) { + let root: i32 = products[0].root; + if (publishpackage != 0) { + let asuffix: str = ".a"; + let isuffix: str = ".wwi"; + if (warm && g.pkg[root].archivestaged) { asuffix = ".a.new"; }; + if (warm && g.pkg[root].sourcestaged) { isuffix = ".wwi.new"; }; + let archive: *u8 = sepfname(g, root, scratch, asuffix); + let iface: *u8 = sepfname(g, root, scratch, isuffix); + let outiface: *u8 = sepappendlit(products[0].out, ".wwi"); + if (archive == nil || iface == nil || outiface == nil) { + cerrpath("ww: cannot stage package artifact ", + products[0].out, "\n"); + return sepfinishfail(&entries, ntxn); + }; + products[0].stageout = sepprepareproductstage( + products[0].stageout, products[0].out); + products[0].stageiface = sepprepareproductstage( + products[0].stageiface, outiface); + if (products[0].stageout == nil + || products[0].stageiface == nil + || copyfilestage(archive, products[0].stageout) != 0 + || copyfilestage(iface, products[0].stageiface) != 0) { + cerrpath("ww: cannot stage package artifact ", + products[0].out, "\n"); + return sepfinishfail(&entries, ntxn); + }; + }; + if (sepstageproductstatus(&products[0]) != 0) { + cerr("ww: cannot stage package-build product\n"); + return sepfinishfail(&entries, ntxn); + }; + } else { + let nldirs: i32 = 0; + let nllibs: i32 = 0; + let ldirs: **u8 = nil; + let llibs: **u8 = nil; + if (lf != nil) { + nldirs = lf.nlibdirs; + nllibs = lf.nlibs; + ldirs = lf.libdirs; + llibs = lf.libs; + }; + producti = 0; + for (producti < nproducts) { + let root: i32 = products[producti].root; + let variantroot: i32 = products[producti].variantroot; + if (istest == 0 && !seprootiscommand(&g.pkg[root])) { + if (sepstageproductstatus(&products[producti]) != 0) { + cerr("ww: cannot stage package-build product\n"); + return sepfinishfail(&entries, ntxn); + }; + producti += 1; + continue; + }; + products[producti].stageout = sepprepareproductstage( + products[producti].stageout, products[producti].out); + if (products[producti].stageout == nil) { + return sepfinishfail(&entries, ntxn); + }; + let ci: i32 = 0; + for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; + let linkorder: []i32; + let linkstack: []i32; + if (!sepmakeints(g.n, &linkorder) + || !sepmakeints(g.n, &linkstack)) { + return sepfinishfail(&entries, ntxn); + }; + let nlink: i32 = 0; + if (septopovisit(g, root, linkorder, &nlink, + linkstack, 0) < 0) { + return sepfinishfail(&entries, ntxn); + }; + let total: i32 = 4; + if (nlink > SEP_COUNT_MAX - total) { + sepfailsize(); return sepfinishfail(&entries, ntxn); + }; + total += nlink; + if (nrt > SEP_COUNT_MAX - total) { + sepfailsize(); return sepfinishfail(&entries, ntxn); + }; + total += nrt; + if (nldirs > (SEP_COUNT_MAX - total) / 2) { + sepfailsize(); return sepfinishfail(&entries, ntxn); + }; + total += nldirs * 2; + if (nllibs > (SEP_COUNT_MAX - total) / 2) { + sepfailsize(); return sepfinishfail(&entries, ntxn); + }; + total += nllibs * 2; + let largvallocation: ([]*u8 | nomem) = sepallocptrs(total); + let largv: []*u8; + match (largvallocation) { + case let value: []*u8 => largv = value; + case nomem => { + sepfailnomem(); return sepfinishfail(&entries, ntxn); + }; + }; + largv.len = total; + largv[0] = "w6l\0".ptr; + largv[1] = "-o\0".ptr; + largv[2] = products[producti].stageout; + let pos: i32 = 3; + let li: i32 = nlink - 1; + for (li >= 0) { + let pi: i32 = linkorder[li]; + if (variantroot >= 0 + && sepinternalreplacesproduction(g, variantroot, pi)) { + li -= 1; + continue; + }; + let suffix: str = ".a"; + if (warm && g.pkg[pi].archivestaged) { suffix = ".a.new"; }; + largv[pos] = sepfname(g, pi, scratch, suffix); + if (largv[pos] == nil) { + return sepfinishfail(&entries, ntxn); + }; + pos += 1; + li -= 1; + }; + let ri: i32 = 0; + for (ri < nrt) { largv[pos] = rtpaths[ri]; pos += 1; ri += 1; }; + let k: i32 = 0; + for (k < nldirs) { + largv[pos] = "-L\0".ptr; pos += 1; + largv[pos] = ldirs[k]; pos += 1; k += 1; + }; + k = 0; + for (k < nllibs) { + largv[pos] = "-l\0".ptr; pos += 1; + largv[pos] = llibs[k]; pos += 1; k += 1; + }; + largv[pos] = nil; + let linkallocation: ([]str | nomem) = sepallocstrs(pos); + let linkargs: []str; + match (linkallocation) { + case let value: []str => linkargs = value; + case nomem => { + sepfailnomem(); return sepfinishfail(&entries, ntxn); + }; + }; + let ai: i32 = 0; + for (ai < pos) { append(linkargs, pathstr(largv[ai])); ai += 1; }; + let linkenv: []str = os.getenvs(); + let linkresult: exec.result; + exec.runstdio(pathstr(l6), linkargs, linkenv, &linkresult); + if (linkresult.termination != exec.termination.EXIT + || linkresult.code != 0) { + if (linkresult.termination == exec.termination.ERROR + && linkresult.code == 127) { cerr("ww: execve failed\n"); }; + cerr("ww: w6l failed\n"); + g.pkg[root].failed = true; + return sepfinishfail(&entries, ntxn); + }; + if (sepstageproductstatus(&products[producti]) != 0) { + cerr("ww: cannot stage package-test product\n"); + return sepfinishfail(&entries, ntxn); + }; + producti += 1; + }; + }; }; + + if (warm) { + let oi: i32 = 0; + for (oi < norder) { + let pi: i32 = order[oi]; + if (g.pkg[pi].sourcestaged) { + if (!septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".wwi.new", ".wwi") + || !septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".s.new", ".s") + || (emitasm == 0 + && !septxnaddpkgsuffix(&entries, &ntxn, g, pi, + scratch, ".o.new", ".o"))) { + return sepfinishfail(&entries, ntxn); + }; + }; + if (g.pkg[pi].initstaged) { + if (!septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".init.s.new", ".init.s") + || (emitasm == 0 + && !septxnaddpkgsuffix(&entries, &ntxn, g, pi, + scratch, ".init.o.new", ".init.o"))) { + return sepfinishfail(&entries, ntxn); + }; + }; + if (g.pkg[pi].archivestaged + && !septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".a.new", ".a")) { + return sepfinishfail(&entries, ntxn); + }; + if (g.pkg[pi].sourcestaged + && !septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".unit.new", ".unit.ww")) { + return sepfinishfail(&entries, ntxn); + }; + if (g.pkg[pi].initstaged + && !septxnaddpkgsuffix(&entries, &ntxn, g, pi, scratch, + ".init.unit.new", ".init.unit.ww")) { + return sepfinishfail(&entries, ntxn); + }; + oi += 1; + }; + let toolsrc: []*u8 = [selfpath, c6, a6]; + let tooldst: []*u8 = [toolw, toolc, toola]; + let ntools: i32 = 3; + if (emitasm != 0) { ntools = 2; }; + let ti: i32 = 0; + for (ti < ntools) { + if (!fileequal(tooldst[ti], toolsrc[ti])) { + let stage: *u8 = sepappendlit(tooldst[ti], ".new"); + if (stage == nil || copyfilestage(toolsrc[ti], stage) != 0 + || !septxnadd(&entries, &ntxn, stage, tooldst[ti])) { + cerr("ww: cannot stage workdir tool identity\n"); + return sepfinishfail(&entries, ntxn); + }; + }; + ti += 1; + }; + if (!stampok) { + let stage: *u8 = sepappendlit(stampf, ".new"); + if (stage == nil || sepwritetextstage(stage, stampwant) != 0 + || !septxnadd(&entries, &ntxn, stage, stampf)) { + cerr("ww: cannot stage workdir stamp\n"); + return sepfinishfail(&entries, ntxn); + }; + }; + }; + producti = 0; + for (producti < nproducts) { + if (products[producti].stageout != nil + && !septxnadd(&entries, &ntxn, products[producti].stageout, + products[producti].out)) { + return sepfinishfail(&entries, ntxn); + }; + if (products[producti].stageiface != nil) { + let outiface: *u8 = sepappendlit(products[producti].out, ".wwi"); + if (outiface == nil + || !septxnadd(&entries, &ntxn, + products[producti].stageiface, outiface)) { + return sepfinishfail(&entries, ntxn); + }; + }; + producti += 1; + }; + producti = 0; + for (producti < nproducts) { + if (products[producti].stagestatus != nil + && !septxnadd(&entries, &ntxn, products[producti].stagestatus, + products[producti].status)) { + return sepfinishfail(&entries, ntxn); + }; + producti += 1; + }; + if (!septxncommit(entries, ntxn)) { + return sepfinishfail(&entries, ntxn); + }; + septxnrelease(&entries, ntxn); + return 0; +}; + fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32, requirecommand: i32, istest: i32, @@ -4773,6 +5741,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let producti: i32 = 0; for (producti < nproducts) { products[producti].support = -1; + products[producti].stageout = nil; + products[producti].stageiface = nil; + products[producti].stagestatus = nil; producti += 1; }; producti = 0; @@ -4965,6 +5936,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti += 1; }; }; + let initpi: i32 = 0; + for (initpi < g.n) { + g.pkg[initpi].initsymbol = seppackageinitsymbol(&g.pkg[initpi]); + if (g.pkg[initpi].initsymbol == nil) { return 1; }; + initpi += 1; + }; producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; @@ -4972,12 +5949,21 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, && validatecommandoutputpath(products[producti].out) < 0) { return 1; }; + if (products[producti].status != nil + && validatecommandoutputpath(products[producti].status) < 0) { + return 1; + }; producti += 1; }; let rootpackage: bool = istest == 0 && nproducts == 1 && !g.pkg[products[0].root].failed && !seprootiscommand(&g.pkg[products[0].root]); if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; + if (sepvalidaterequeststaging(g, scratch, warm, products, nproducts, + rootpackage, publishpackage, emitasm, istest) < 0) { + sepfreeproductstaging(products, nproducts); + return 1; + }; if (warm && workdirexists && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; let ci: i32 = 0; @@ -4988,7 +5974,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let norder: i32 = 0; // Diagnose cycles per product before constructing the shared union. A - // variant-local cycle must not suppress an independent sibling root. + // variant-local cycle does not erase another root's attribution, although + // any failure still rejects the request-wide publication transaction. producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; @@ -5006,6 +5993,26 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti += 1; }; + // Go rejects cycles introduced by internal-test substitution before any + // compiler/link action (I -> X -> P becomes I -> X -> I). + producti = 0; + for (producti < nproducts) { + let root: i32 = products[producti].root; + if (!g.pkg[root].failed) { + let initcheck: []i32; + let ninitcheck: i32 = 0; + if (sepinitorder(g, root, products[producti].variantroot, + &initcheck, &ninitcheck) < 0) { + g.pkg[root].failed = true; + }; + if (initcheck.ptr != nil) { + os.free(initcheck.ptr: *void, + (initcheck.cap: u64) * (size(i32): u64)); + }; + }; + producti += 1; + }; + if (sepfatalallocation) { return 1; }; if (istest == 0 && requirecommand != 0) { let root: i32 = products[0].root; if (!g.pkg[root].failed && !seprootiscommand(&g.pkg[root])) { @@ -5033,8 +6040,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti += 1; }; // Propagate already-known package-load failures before scratch or status - // acquisition. Good sibling roots remain viable, while an entirely failed - // cold request leaves no empty persistent or caller-visible tree. + // acquisition. Good sibling roots may remain viable for deterministic + // staging/diagnosis, but any failure rejects publication; an entirely failed + // cold request leaves no persistent or caller-visible tree. let preoi: i32 = 0; for (preoi < norder) { let pi: i32 = order[preoi]; @@ -5087,20 +6095,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, // The wrapper owns only the directory this invocation acquired. if (scratchout != nil) { *scratchout = scratch; }; }; - let statusi: i32 = 0; - for (statusi < nproducts) { - if (products[statusi].status != nil) { - let rr: i32 = os.remove(pathstr(products[statusi].status)); - if (rr != 0 && rr != -2) { return 1; }; - }; - statusi += 1; - }; - if (warm && staleall && invalidateworkdirunits(scratch) != 0) { - return 1; - }; let anyfailed: bool = false; - let commitopen: bool = false; - let commitintegrityfailed: bool = false; producti = 0; for (producti < nproducts) { if (g.pkg[products[producti].root].failed) { anyfailed = true; }; @@ -5133,7 +6128,54 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let anew: *u8 = sepfname(g, pi, scratch, ".a.new"); if (unitf == nil || wwi == nil || asmf == nil || objf == nil || apath == nil || unitnew == nil || wwinew == nil - || asmnew == nil || objnew == nil || anew == nil) { return 1; }; + || asmnew == nil || objnew == nil || anew == nil) { + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); + }; + let initunitf: *u8 = nil; + let initasmf: *u8 = nil; + let initobj: *u8 = nil; + let initunitnew: *u8 = nil; + let initasmnew: *u8 = nil; + let initobjnew: *u8 = nil; + let productindex: i32 = -1; + if (g.pkg[pi].linkentry) { + let owneri: i32 = 0; + for (owneri < nproducts && productindex < 0) { + if (products[owneri].root == pi) { productindex = owneri; }; + owneri += 1; + }; + if (productindex < 0) { + cerr("ww: executable action has no owning product\n"); + g.pkg[pi].failed = true; + anyfailed = true; + oi += 1; + continue; + }; + initunitf = sepfname(g, pi, scratch, ".init.unit.ww"); + initasmf = sepfname(g, pi, scratch, ".init.s"); + initobj = sepfname(g, pi, scratch, ".init.o"); + initunitnew = sepfname(g, pi, scratch, ".init.unit.new"); + initasmnew = sepfname(g, pi, scratch, ".init.s.new"); + initobjnew = sepfname(g, pi, scratch, ".init.o.new"); + if (initunitf == nil || initasmf == nil || initobj == nil + || initunitnew == nil || initasmnew == nil + || initobjnew == nil) { + return seprejectrequest(g, scratch, warm, products, + nproducts, &createdwork, &createdoutput, scratchout); + }; + }; + // Classic scratch has no committed generation. Alias cleanup paths to + // the in-place outputs so rejected producers leave no partial action. + if (!warm) { + unitnew = unitf; wwinew = wwi; asmnew = asmf; + objnew = objf; anew = apath; + if (productindex >= 0) { + initunitnew = initunitf; + initasmnew = initasmf; + initobjnew = initobj; + }; + }; // Warm mode compiles from staged `.new` paths and commits by // rename; classic mode keeps its exact in-place paths. let cu: *u8 = unitf; @@ -5141,32 +6183,40 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let cs: *u8 = asmf; let co: *u8 = objf; let ca: *u8 = apath; + let ciu: *u8 = initunitf; + let cis: *u8 = initasmf; + let cio: *u8 = initobj; if (warm) { cu = unitnew; cw = wwinew; cs = asmnew; co = objnew; ca = anew; + ciu = initunitnew; cis = initasmnew; cio = initobjnew; }; if (sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, - objnew, anew) < 0) { - if (warm) { - let unitrr: i32 = os.remove(pathstr(unitf)); - if (unitrr != 0 && unitrr != -2) { - commitintegrityfailed = true; - }; - }; + objnew, anew) < 0 + || sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew) < 0) { g.pkg[pi].failed = true; anyfailed = true; oi += 1; continue; }; - if (sepcomposeunit(g, pi, cu) < 0) { + if (sepcomposeunit(g, pi, scratch, cu) < 0) { sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - if (warm) { - let unitrr: i32 = os.remove(pathstr(unitf)); - if (unitrr != 0 && unitrr != -2) { - commitintegrityfailed = true; - }; - }; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + g.pkg[pi].failed = true; + anyfailed = true; + oi += 1; + continue; + }; + if (productindex >= 0 + && sepcomposeinitdispatch(g, &products[productindex], + ciu, cis) < 0) { + sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, + objnew, anew); + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); g.pkg[pi].failed = true; anyfailed = true; oi += 1; @@ -5180,48 +6230,94 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; changedk += 1; }; - let fresh: bool = false; + let sourcereusable: bool = false; if (warm) { - if (!staleall && !depschanged && !commitintegrityfailed) { - fresh = fileequal(unitnew, unitf); - if (fresh) { fresh = fileisreg(asmf); }; - if (fresh) { fresh = fileisreg(wwi); }; - if (fresh) { + if (!staleall && !depschanged) { + sourcereusable = fileequal(unitnew, unitf); + if (sourcereusable) { sourcereusable = fileisreg(asmf); }; + if (sourcereusable) { sourcereusable = fileisreg(wwi); }; + if (sourcereusable) { if (emitasm == 0) { - fresh = filesizenonzero(objf); + sourcereusable = filesizenonzero(objf); }; }; - if (fresh && emitasm == 0) { - fresh = filesizenonzero(apath); + if (sourcereusable && emitasm == 0 + && productindex < 0) { + sourcereusable = filesizenonzero(apath); }; }; }; + let initreusable: bool = productindex < 0; + if (productindex >= 0 && warm && !staleall) { + initreusable = fileisreg(initunitf) + && fileequal(initunitnew, initunitf); + if (initreusable) { initreusable = fileisreg(initasmf); }; + if (initreusable && emitasm == 0) { + initreusable = filesizenonzero(initobj); + }; + }; if (sepfatalallocation) { sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; - if (fresh) { - if (os.remove(pathstr(unitnew)) != 0) { - cerrpath("ww: cannot remove ", unitnew, "\n"); + let archivereusable: bool = emitasm != 0 || filesizenonzero(apath); + if (sourcereusable && initreusable && archivereusable) { + if (sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, + objnew, anew) < 0 + || sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew) < 0) { g.pkg[pi].failed = true; anyfailed = true; }; oi += 1; continue; }; - // Once an action is not reusable, its old unit must not vouch for - // artifacts after a later producer or commit failure. This also makes a - // failed importer retry after a dependency committed a changed export. - if (warm) { - let unitrr: i32 = os.remove(pathstr(unitf)); - if (unitrr != 0 && unitrr != -2) { - cerrpath("ww: cannot invalidate package unit ", unitf, "\n"); - sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, - objnew, anew); + // Closure-only changes rebuild the root-owned dispatcher/archive without + // invoking the compiler for an otherwise reusable root source action. + if (sourcereusable && productindex >= 0) { + let ignoredunit: i32 = os.remove(pathstr(unitnew)); + if (emitasm == 0 + && (runassembler(a6, cio, cis) != 0 + || archiveo(objf, cio, ca) != 0)) { + if (g.pkg[pi].path[0u64] != 0u8) { + cerrpath("ww: initialization archive failed for ", + g.pkg[pi].path, "\n"); + } else { + cerr("ww: initialization archive failed for (root)\n"); + }; + g.pkg[pi].failed = true; + anyfailed = true; + sepdiscardactionstaging(warm, unitnew, wwinew, + asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, + initasmnew, initobjnew); + oi += 1; + continue; + }; + if (warm) { + g.pkg[pi].initstaged = true; + if (emitasm == 0) { g.pkg[pi].archivestaged = true; }; + }; + oi += 1; + continue; + }; + // Staged producers cannot disturb the previous generation. Its source + // and dispatcher vouchers remain committed until commit actually opens; + // direct-export bytes in the source voucher prevent stale later reuse. + if (productindex >= 0 && initreusable && warm) { + if (sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew) < 0) { + cerr("ww: cannot remove staged initialization assembly\n"); + sepdiscardactionstaging(warm, unitnew, wwinew, + asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, + initasmnew, initobjnew); g.pkg[pi].failed = true; anyfailed = true; - commitintegrityfailed = true; oi += 1; continue; }; @@ -5243,32 +6339,35 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, sepfailsize(); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, + initasmnew, initobjnew); + return seprejectrequest(g, scratch, warm, products, + nproducts, &createdwork, &createdoutput, + scratchout); }; nmaps += 1; }; mapk += 1; }; - let alen: i32 = 8; - if (gent) { alen += 4; if (g.pkg[pi].generatedmain) { alen += 2; }; } - else { - if (testpkg) { alen += 1; }; - if (commandpkg) { alen += 1; }; - if (entry) { alen += 1; }; - if (supportpkg) { alen += 2; }; - }; + let alen: i32 = 18; if (g.pkg[pi].ndeps > (SEP_COUNT_MAX - alen) / 3) { sepfailsize(); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; alen += g.pkg[pi].ndeps * 3; if (nmaps > (SEP_COUNT_MAX - alen) / 3) { sepfailsize(); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; alen += nmaps * 3; let allocation: ([]str | nomem) = sepallocstrs(alen); @@ -5279,7 +6378,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, sepfailnomem(); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + return seprejectrequest(g, scratch, warm, products, + nproducts, &createdwork, &createdoutput, + scratchout); }; }; append(argv, "w6c"); @@ -5301,17 +6404,29 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, append(argv, testsupportmodule); }; }; + append(argv, "--package-init-symbol"); + append(argv, pathstr(g.pkg[pi].initsymbol)); + if (productindex >= 0) { + append(argv, "--init-dispatch-symbol"); + append(argv, "__ww..dispatch"); + }; append(argv, "-c"); let importk: i32 = 0; for (importk < g.pkg[pi].ndeps) { let dj: i32 = g.pkg[pi].deps[importk]; append(argv, "--import"); append(argv, pathstr(g.pkg[dj].path)); - let depinterface: *u8 = sepfname(g, dj, scratch, ".wwi"); + let depsuffix: str = ".wwi"; + if (g.pkg[dj].sourcestaged) { depsuffix = ".wwi.new"; }; + let depinterface: *u8 = sepfname(g, dj, scratch, depsuffix); if (depinterface == nil) { sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, + initasmnew, initobjnew); + return seprejectrequest(g, scratch, warm, products, + nproducts, &createdwork, &createdoutput, + scratchout); }; append(argv, pathstr(depinterface)); importk += 1; @@ -5350,6 +6465,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, anyfailed = true; sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); oi += 1; continue; }; @@ -5360,33 +6477,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (sepfatalallocation) { sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); - return 1; + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; - if (emitasm == 0) { - let argallocation: ([]str | nomem) = sepallocstrs(4); - let argv: []str; - match (argallocation) { - case let value: []str => argv = value; - case nomem => { - sepfailnomem(); - sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, - objnew, anew); - return 1; - }; - }; - append(argv, "w6a"); - append(argv, "-o"); - append(argv, pathstr(co)); - append(argv, pathstr(cs)); - let env: []str = os.getenvs(); - let result: exec.result; - exec.runstdio(pathstr(a6), argv, env, &result); - if (result.termination != exec.termination.EXIT - || result.code != 0) { - if (result.termination == exec.termination.ERROR - && result.code == 127) { - cerr("ww: execve failed\n"); - }; + if (emitasm == 0 && runassembler(a6, co, cs) != 0) { if (g.pkg[pi].path[0u64] != 0u8) { cerrpath("ww: w6a failed for ", g.pkg[pi].path, "\n"); @@ -5397,314 +6493,74 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, anyfailed = true; sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); oi += 1; continue; - }; }; - // Every package action, including executable/generated roots, produces - // the existing deterministic single-member archive. + if (emitasm == 0 && productindex >= 0 && !initreusable + && runassembler(a6, cio, cis) != 0) { + if (g.pkg[pi].path[0u64] != 0u8) { + cerrpath("ww: w6a failed for initialization of ", + g.pkg[pi].path, "\n"); + } else { + cerr("ww: w6a failed for initialization of (root)\n"); + }; + g.pkg[pi].failed = true; + anyfailed = true; + sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, + objnew, anew); + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); + oi += 1; + continue; + }; + // Root archives add the dispatcher as the fixed second member. if (emitasm == 0) { - if (archiveo(co, ca) != 0) { - cerr("ww: archive failed\n"); + let archiveinit: *u8 = nil; + if (productindex >= 0) { + archiveinit = cio; + if (initreusable) { archiveinit = initobj; }; + }; + if (archiveo(co, archiveinit, ca) != 0) { + if (g.pkg[pi].path[0u64] != 0u8) { + cerrpath("ww: archive failed for ", g.pkg[pi].path, + "\n"); + } else { + cerr("ww: archive failed for (root)\n"); + }; g.pkg[pi].failed = true; anyfailed = true; sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, initasmnew, + initobjnew); oi += 1; continue; }; }; - // Commit order: artifacts before the unit that vouches for them, unit - // strictly last. Remove the workdir identity before the first artifact - // rename; failure is a pre-commit rejection with old artifacts intact. if (warm) { - if (!commitopen) { - let stamprr: i32 = os.remove(pathstr(stampf)); - if (stamprr != 0 && stamprr != -2) { - cerr("ww: cannot invalidate package workdir\n"); - g.pkg[pi].failed = true; - anyfailed = true; - sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, - objnew, anew); - oi += 1; - continue; - }; - commitopen = true; - }; - let bad: bool = false; - if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { - bad = true; - }; - if (!bad) { - if (os.rename(pathstr(asmnew), pathstr(asmf)) != 0) { - bad = true; - }; - }; - if (!bad) { - if (emitasm == 0) { - if (os.rename(pathstr(objnew), pathstr(objf)) != 0) { - bad = true; - }; - }; - }; - if (!bad) { - if (emitasm == 0) { - if (os.rename(pathstr(anew), pathstr(apath)) != 0) { - bad = true; - }; - }; - }; - if (!bad) { - if (os.rename(pathstr(unitnew), pathstr(unitf)) != 0) { - bad = true; - }; - }; - if (bad) { - if (g.pkg[pi].path[0u64] != 0u8) { - cerrpath("ww: cannot commit ", - g.pkg[pi].path, "\n"); - } else { - cerr("ww: cannot commit (root)\n"); - }; - g.pkg[pi].failed = true; - anyfailed = true; - // A failed rename sequence may already have replaced the - // interface or another artifact. The stamp is already absent; - // invalidate all vouchers and force later actions through tools. - commitintegrityfailed = true; - invalidateworkdirunits(scratch); - sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, - objnew, anew); - oi += 1; - continue; + g.pkg[pi].sourcestaged = true; + if (emitasm == 0) { g.pkg[pi].archivestaged = true; }; + if (productindex >= 0 && !initreusable) { + g.pkg[pi].initstaged = true; }; }; oi += 1; }; - // A stale pass removed every old unit voucher before compiling. Current - // successful units remain safe when a sibling compiler rejects. A partial - // artifact commit invalidates all vouchers and suppresses the workdir - // identity so the next pass starts stale. - if (warm && !commitintegrityfailed) { - let sametool: bool = fileequal(toolw, selfpath); - if (sepfatalallocation) { return 1; }; - if (!sametool) { - if (copyfileatomic(selfpath, toolw) != 0) { - cerrpath("ww: cannot record ", toolw, "\n"); - return 1; - }; - }; - sametool = fileequal(toolc, c6); - if (sepfatalallocation) { return 1; }; - if (!sametool) { - if (copyfileatomic(c6, toolc) != 0) { - cerrpath("ww: cannot record ", toolc, "\n"); - return 1; - }; - }; - if (emitasm == 0) { - sametool = fileequal(toola, a6); - if (sepfatalallocation) { return 1; }; - if (!sametool) { - if (copyfileatomic(a6, toola) != 0) { - cerrpath("ww: cannot record ", toola, "\n"); - return 1; - }; - }; - }; - if (!stampok || commitopen) { - if (writestampatomic(stampf, stampwant) != 0) { - cerrpath("ww: cannot record ", stampf, "\n"); - return 1; - }; - }; + if (anyfailed) { + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; - if (emitasm != 0) { - let producti: i32 = 0; - for (producti < nproducts) { - let root: i32 = products[producti].root; - if (g.pkg[root].failed) { - anyfailed = true; - } else if (recordproductstatus(products[producti].status) != 0) { - cerr("ww: cannot record package-build product\n"); - g.pkg[root].failed = true; - anyfailed = true; - }; - producti += 1; - }; - if (anyfailed) { return 1; }; - return 0; + let finishresult: i32 = sepfinishrequest(selfdir, l6, c6, a6, + g, scratch, warm, rootpackage, publishpackage, istest, emitasm, + products, nproducts, order, norder, rtpaths, nrt, lf, + toolw, toolc, toola, stampf, stampwant, stampok); + if (finishresult != 0) { + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); }; - if (rootpackage) { - let root: i32 = products[0].root; - if (g.pkg[root].failed) { return 1; }; - if (publishpackage != 0) { - let archive: *u8 = sepfname(g, root, scratch, ".a"); - let iface: *u8 = sepfname(g, root, scratch, ".wwi"); - let outiface: *u8 = sepappendlit(out, ".wwi"); - if (archive == nil || iface == nil || outiface == nil) { - return 1; - }; - if (copyfileatomic(archive, out) != 0 - || copyfileatomic(iface, outiface) != 0) { - cerrpath("ww: cannot write package artifact ", out, "\n"); - return 1; - }; - }; - if (recordproductstatus(products[0].status) != 0) { - cerr("ww: cannot record package-build product\n"); - return 1; - }; - return 0; - }; - - // Each product gets its own reverse-topological link closure. Shared - // production actions do not turn variant-local archives into link inputs. - let nldirs: i32 = 0; - let nllibs: i32 = 0; - let ldirs: **u8 = nil; - let llibs: **u8 = nil; - if (lf != nil) { - nldirs = lf.nlibdirs; - nllibs = lf.nlibs; - ldirs = lf.libdirs; - llibs = lf.libs; - }; - producti = 0; - for (producti < nproducts) { - let root: i32 = products[producti].root; - let variantroot: i32 = products[producti].variantroot; - if (g.pkg[root].failed) { - anyfailed = true; - producti += 1; - continue; - }; - if (istest == 0 && !seprootiscommand(&g.pkg[root])) { - if (recordproductstatus(products[producti].status) != 0) { - cerr("ww: cannot record package-build product\n"); - g.pkg[root].failed = true; - anyfailed = true; - }; - producti += 1; - continue; - }; - ci = 0; - for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; - let linkorder: []i32; - let linkstack: []i32; - if (!sepmakeints(g.n, &linkorder) - || !sepmakeints(g.n, &linkstack)) { return 1; }; - let nlink: i32 = 0; - if (septopovisit(g, root, linkorder, &nlink, - linkstack, 0) < 0) { return 1; }; - // argv: 3 fixed + closure + runtime inputs + flag/value pairs + nil. - let total: i32 = 4; - if (nlink > SEP_COUNT_MAX - total) { - sepfailsize(); return 1; - }; - total += nlink; - if (nrt > SEP_COUNT_MAX - total) { - sepfailsize(); return 1; - }; - total += nrt; - if (nldirs > (SEP_COUNT_MAX - total) / 2) { - sepfailsize(); return 1; - }; - total += nldirs * 2; - if (nllibs > (SEP_COUNT_MAX - total) / 2) { - sepfailsize(); return 1; - }; - total += nllibs * 2; - let largvallocation: ([]*u8 | nomem) = sepallocptrs(total); - let largv: []*u8; - match (largvallocation) { - case let value: []*u8 => largv = value; - case nomem => { sepfailnomem(); return 1; }; - }; - largv.len = total; - largv[0] = "w6l\0".ptr; - largv[1] = "-o\0".ptr; - largv[2] = products[producti].out; - let pos: i32 = 3; - let li: i32 = nlink - 1; - for (li >= 0) { - let pi: i32 = linkorder[li]; - if (variantroot >= 0 - && g.pkg[variantroot].variant == SEP_VARIANT_SAME_TEST - && pi != variantroot - && g.pkg[pi].variant == SEP_VARIANT_PRODUCTION - && g.pkg[pi].role != SEP_ROLE_TEST_SUPPORT - && g.pkg[pi].importbase != nil - && g.pkg[variantroot].importbase != nil - && cstreq(g.pkg[pi].importbase, - g.pkg[variantroot].importbase) - && os.samefile(pathstr(g.pkg[pi].entry), - pathstr(g.pkg[variantroot].entry))) { - li -= 1; - continue; - }; - largv[pos] = sepfname(g, pi, scratch, ".a"); - if (largv[pos] == nil) { return 1; }; - pos += 1; - li -= 1; - }; - let ri: i32 = 0; - for (ri < nrt) { - largv[pos] = rtpaths[ri]; - pos += 1; - ri += 1; - }; - let k: i32 = 0; - for (k < nldirs) { - largv[pos] = "-L\0".ptr; - pos += 1; - largv[pos] = ldirs[k]; - pos += 1; - k += 1; - }; - k = 0; - for (k < nllibs) { - largv[pos] = "-l\0".ptr; - pos += 1; - largv[pos] = llibs[k]; - pos += 1; - k += 1; - }; - largv[pos] = nil; - let linkallocation: ([]str | nomem) = sepallocstrs(pos); - let linkargs: []str; - match (linkallocation) { - case let value: []str => linkargs = value; - case nomem => { sepfailnomem(); return 1; }; - }; - let ai: i32 = 0; - for (ai < pos) { - append(linkargs, pathstr(largv[ai])); - ai += 1; - }; - let linkenv: []str = os.getenvs(); - let linkresult: exec.result; - exec.runstdio(pathstr(l6), linkargs, linkenv, &linkresult); - if (linkresult.termination != exec.termination.EXIT - || linkresult.code != 0) { - if (linkresult.termination == exec.termination.ERROR - && linkresult.code == 127) { - cerr("ww: execve failed\n"); - }; - cerr("ww: w6l failed\n"); - g.pkg[root].failed = true; - anyfailed = true; - producti += 1; - continue; - }; - if (recordproductstatus(products[producti].status) != 0) { - cerr("ww: cannot record package-test product\n"); - g.pkg[root].failed = true; - anyfailed = true; - }; - producti += 1; - }; - if (anyfailed) { return 1; }; + sepfreeproductstaging(products, nproducts); return 0; }; diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 3e8c791d..29d70d52 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -148,7 +148,8 @@ export fn main(argc: i32, argv: **u8) i32 = { // silently). Mirrors w6c main.ww:162 / cmd/w6c/main.c. if (l.errs > 0 || ps.errs > 0) { return 1; }; let empty: str; - if (wcc.compilefile(f, 0, 0, empty, empty, 0, empty, 0) != 0) { return 1; }; + if (wcc.compilefile(f, 0, 0, empty, empty, 0, -1, 0, 0, + empty, empty) != 0) { return 1; }; };};};}; if (l.errs > 0) { return 1; };