diff --git a/cmd/wcc/lex.c b/cmd/wcc/lex.c index ddb6fa13..d50f76c8 100644 --- a/cmd/wcc/lex.c +++ b/cmd/wcc/lex.c @@ -358,6 +358,78 @@ skipws(Lex *l) } } +/* Return the next initial import token without ever lexing the first ordinary + * declaration token. Go's named-file reader consumes header trivia while + * looking for another import, so NUL/comment diagnostics in that trivia remain + * visible; a malformed UTF-8 byte or non-leading BOM that itself begins the + * ordinary body is outside the header and must not be diagnosed here. */ +int +lexheaderimport(Lex *l, Tok *out) +{ + for (;;) { + if (l->pos >= l->srclen) + return 0; + unsigned char c = (unsigned char)l->src[l->pos]; + if (c == 0) { + /* The loader necessarily reaches this byte while deciding whether + * another import follows. Preserve the source decoder diagnostic. */ + lskipnul(l); + return 0; + } + if ((c >= 0x80 && !utf8bytevalid(l->src, l->srclen, l->pos)) + || bomat(l->src, l->srclen, l->pos)) + return 0; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + lget(l); + continue; + } + if (c == '/' && l->pos + 1 < l->srclen + && l->src[l->pos + 1] == '/') { + lget(l); lget(l); + linecomment(l); + if (l->errs != 0) + return 0; + continue; + } + if (c == '/' && l->pos + 1 < l->srclen + && l->src[l->pos + 1] == '*') { + lget(l); lget(l); + int prev = -1; + for (;;) { + int x = lget(l); + if (x < 0) { + Pos p = lpos(l); + errorf(p, "unterminated /* comment"); + l->errs++; + return 0; + } + if (prev == '*' && x == '/') + break; + prev = x; + } + if (l->errs != 0) + return 0; + continue; + } + break; + } + + /* A compiler-owned source-boundary directive is an ordinary declaration + * boundary here, just as it was when lexnext eagerly exposed the token. */ + if (l->modreset || l->modpath != NULL) + return 0; + static const char kw[] = "import"; + const u64 n = sizeof kw - 1; + if (l->srclen - l->pos < n + || memcmp(l->src + l->pos, kw, n) != 0) + return 0; + if (l->srclen - l->pos > n + && isidcont((unsigned char)l->src[l->pos + n])) + return 0; + *out = lexnext(l); + return out->kind == TK_USE; +} + static u64 parseint(const char *s, u64 n, int base, int *ok) { diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index f39a5336..37029d62 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -35,6 +35,17 @@ advance(Parser *p) } } +static int +advanceheaderimport(Parser *p) +{ + Tok next; + if (!lexheaderimport(p->l, &next)) + return 0; + p->cur = next; + p->hasla = 0; + return 1; +} + static Tok peek(Parser *p) { @@ -1359,6 +1370,140 @@ parseuse(Parser *p) return n; } +/* Parse one initial import without pulling the declaration that follows it + * into the header pass. This intentionally has stricter, single-error + * recovery than parseuse: named test-source build loading needs only decide + * whether the package/import header itself is valid. */ +static Node * +parseheaderuse(Parser *p) +{ + Pos pp = p->cur.pos; + advance(p); + Node *n = newnode(p->a, N_USE, pp); + n->usefile = p->cur.pos.file; + n->useline = p->cur.pos.line; + n->usecol = p->cur.pos.col; + const char *alias = NULL; + const char *first; + if (p->cur.kind == TK_UNDER) { + n->useblank = 1; + advance(p); + } else if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + return n; + } + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + return n; + } + first = p->cur.text; + advance(p); + if (!n->useblank && p->cur.kind == TK_IDENT) { + alias = first; + first = p->cur.text; + advance(p); + } + const char *leaf = first; + const char *path = leaf; + while (p->cur.kind == TK_DOT) { + advance(p); + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + return n; + } + leaf = p->cur.text; + path = aprintf(p->a, "%s.%s", path, leaf); + advance(p); + } + if (!n->useblank) { + n->str = alias ? alias : leaf; + n->strlen = strlen(n->str); + } + n->usesource = path; + n->usepath = path; + n->usealias = alias; + if (p->cur.kind != TK_SEMI) { + errorf(p->cur.pos, "expected ';' after import"); + p->errs++; + return n; + } + return n; +} + +/* Go's named-file loader parses a valid source only through its initial + * package/import section. Keep the broader parseimports recovery pass for + * graph-bearing sources, but give actionless test-source omission a boundary + * that cannot diagnose late imports or an invalid ordinary declaration body. */ +Node * +parsepackageheader(Parser *p) +{ + Pos fp = { p->l->file, 1, 1 }; + Node *file = newnode(p->a, N_FILE, fp); + Node *head = NULL, *tail = NULL; + + while (p->cur.kind == TK_MODPATH || p->cur.kind == TK_MODRESET) { + if (p->cur.kind == TK_MODPATH) { + p->pathmod = p->cur.text; + p->curmod = p->cur.text; + p->resetmod = NULL; + } else { + p->pathmod = NULL; + p->curmod = p->cur.text; + p->resetmod = p->cur.text; + } + p->sourceid++; + advance(p); + } + if (p->cur.kind != TK_MODULE) { + errorf(p->cur.pos, "invalid or missing package clause"); + p->errs++; + return file; + } + Pos pp = p->cur.pos; + advance(p); + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "invalid or missing package clause"); + p->errs++; + return file; + } + const char *name = p->cur.text; + advance(p); + if (p->cur.kind != TK_SEMI) { + errorf(p->cur.pos, "expected ';' after package name"); + p->errs++; + return file; + } + p->curpkg = name; + if (p->pathmod == NULL && p->resetmod == NULL) + p->curmod = name; + file->module = name; + file->pkgname = name; + file->sourceid = p->sourceid; + file->pos = pp; + + while (advanceheaderimport(p)) { + Node *d = parseheaderuse(p); + d->module = p->curmod; + d->pkgname = p->curpkg; + d->sourceid = p->sourceid; + if (head == NULL) + head = d; + else + tail->next = d; + tail = d; + if (p->errs != 0) + break; + } + file->list = head; + return file; +} + Node * parseimports(Parser *p) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index aede6e54..db319c4d 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -218,6 +218,7 @@ struct Lex { void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len); Tok lexnext(Lex*); +int lexheaderimport(Lex*, Tok*); const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */ void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */ Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */ @@ -408,6 +409,8 @@ struct Parser { void parserinit(Parser*, Arena*, Lex*); Node *parsefile(Parser*); +/* Package/import header only: stop before the first ordinary declaration. */ +Node *parsepackageheader(Parser*); /* Imports-only N_FILE: module/pos identify the first package clause, * list holds N_USE declarations, and body holds package-clause markers. */ Node *parseimports(Parser*); diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 6c46e01f..a04b70b4 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -693,6 +693,111 @@ source_package_name(const char *path, char **out) return 0; } +/* Named-file loading is allowed for every non-directory Stat result. Unlike + * sep_slurp, this reader must not require seekability: a finite FIFO is still + * a named source stream, just as it is for GoFilesPackage. */ +static int +source_header_slurp(const char *path, char **out, u64 *len) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + size_t cap = 4096, n = 0; + char *buf = malloc(cap); + if (buf == NULL) { + (void)sep_fail_nomem(); + (void)close(fd); + return -1; + } + for (;;) { + if (n == cap) { + if (cap >= INT_MAX) { + (void)sep_fail_size(); + free(buf); + (void)close(fd); + return -1; + } + size_t nextcap = cap > (size_t)INT_MAX / 2 + ? (size_t)INT_MAX : cap * 2; + char *next = realloc(buf, nextcap); + if (next == NULL) { + (void)sep_fail_nomem(); + free(buf); + (void)close(fd); + return -1; + } + buf = next; + cap = nextcap; + } + ssize_t got = read(fd, buf + n, cap - n); + if (got < 0) { + if (errno == EINTR) continue; + free(buf); + (void)close(fd); + return -1; + } + if (got == 0) break; + n += (size_t)got; + } + if (close(fd) != 0) { + free(buf); + return -1; + } + if (n == cap) { + /* The loop normally grows before the read that can fill cap, but + * retain an explicit terminator guard for future reader changes. */ + if (cap >= INT_MAX) { + (void)sep_fail_size(); + free(buf); + return -1; + } + char *next = realloc(buf, cap + 1); + if (next == NULL) { + (void)sep_fail_nomem(); + free(buf); + return -1; + } + buf = next; + } + buf[n] = '\0'; + *out = buf; + *len = (u64)n; + return 0; +} + +/* Validate only the package/import header before a named test source is + * omitted from an ordinary build. Imports are syntax, not graph edges here. */ +static int +source_build_header(const char *path) +{ + char *buf; + u64 len; + if (source_header_slurp(path, &buf, &len) < 0) { + fprintf(stderr, "ww: cannot read %s\n", path); + return -1; + } + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, path, buf, len); + parserinit(&p, a, &l); + Node *header = parsepackageheader(&p); + if (l.errs || p.errs) { + freearena(a); + free(buf); + return -1; + } + if (header->module == NULL) { + Pos pp = { path, 1, 1 }; + errorf(pp, "invalid or missing package clause"); + freearena(a); + free(buf); + return -1; + } + freearena(a); + free(buf); + return 0; +} + static int source_list_add(char ***list, int *n, int *cap, const char *path) { @@ -7474,11 +7579,31 @@ do_build(int argc, char **argv) } struct stat requested; int literal = stat(src, &requested) == 0; + int requested_nondirectory = literal && !S_ISDIR(requested.st_mode); if (source_operand_ignored(src)) { source_operand_no_sources(src); free(incs); return 1; } + const char *requested_base = strrchr(src, '/'); + requested_base = requested_base ? requested_base + 1 : src; + size_t requested_baselen = strlen(requested_base); + if (requested_nondirectory && requested_baselen >= 8 + && strcmp(requested_base + requested_baselen - 8, + "_test.ww") == 0) { + if (source_build_header(src) < 0) { + free(incs); + return 1; + } + free(incs); + if (!outflag[0] || strcmp(outflag, "/dev/null") == 0) + return 0; + if (build_output_dir(outflag)) + fputs("ww: no main packages to build\n", stderr); + else + fputs("ww: no packages to build\n", stderr); + return 1; + } char resolved[PATH_MAX]; int is_dir = 0; if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) { diff --git a/docs/build-system.md b/docs/build-system.md index fd1156fb..4f0501eb 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -10080,6 +10080,170 @@ No serialized format changes. Build workdir format remains `18`, test workdir format remains `19`, semantic storage format remains `3`, and there is no test result cache. +### 11.51 Implemented explicit named test-source build omission + +A single existing raw `ww build` operand whose requested final basename ends +exactly `_test.ww` is now a test-only named source. WW syntax-observes only the +package clause and its initial contiguous import section, preserves any read/header +diagnostic, then omits the valid test-only root before logical resolution or +action construction. This is deliberately distinct from the leading-dot and +underscore rule in 11.50: `_test.ww` reaches that earlier exclusion, whereas a +visible `x_test.ww` reaches this test-only omission. + +#### Pinned authority, tests, and applicability + +- **behavior directly implemented or asserted by pinned Go** — official Go + 1.26.5 commit `c19862e5f8415b4f24b189d065ed739517c548ba` recognizes an + existing non-directory named `.go` operand in `PackagesAndErrors` + ([`cmd/go/internal/load/pkg.go`, lines 2903–2918](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2903-L2918)), + builds its synthetic command-line package with `UseAllFiles` + ([lines 3244–3315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L3244-L3315)), + and classifies `_test.go` separately from `GoFiles` + ([`go/build/build.go`, lines 930–1036](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L930-L1036)). + `UseAllFiles` bypasses ordinary target and build-expression rejection, but + not that test-file classification + ([lines 1438–1509](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L1438-L1509)). +- **behavior directly implemented or asserted by pinned Go** — on a + successfully scanned header, `readGoInfo` reads the initial package/import + header and one stop byte, then removes that byte before parsing; on header + syntax recovery it deliberately drains the remaining source + ([`go/build/read.go`, lines 265–315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L265-L315)). + Every raw NUL reached by its reader is a read error + ([lines 71–89](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L71-L89)); + `parser.ImportsOnly` stops before ordinary declarations + ([`go/parser/parser.go`, lines 2887–2923](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/parser/parser.go#L2887-L2923)). + Thus a package or initial-import header error precedes omission, while a + later declaration/body error does not become an ordinary-build error. +- **behavior directly implemented or asserted by pinned Go** — ordinary + initial loading does not recursively resolve test imports + ([`cmd/go/internal/load/pkg.go`, lines 350–358](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L350-L358)); + `go/build` records their metadata at `go/build/build.go:1037–1040`. + Build checks loader errors first and then omits a test-only root before + output/action construction + ([`cmd/go/internal/work/build.go`, lines 459–559 and 731–745](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L559)). +- **behavior directly implemented or asserted by pinned Go** — related + official anchors are `go/build/build_test.go:382–421` (`TestMatchFile`), + `go/build/build_test.go:812–831` (`TestDirectives` and `XTestDirectives`), + `go/build/read_test.go:17–73,120–159,165–249` (header boundary, NUL, and + syntax recovery), + `cmd/go/testdata/script/test_relative_cmdline.txt:1–49`, and + `cmd/go/testdata/script/build_test_only.txt:1–18`. The pinned official + repository has no script that directly invokes `go build NAME_test.go`; the + exact named-build result below is derived from its pinned loader and action + ordering, not presented as an unanchored direct script assertion. +- **behavior derived from the pinned implementation** — one valid named test + source produces no build action. No effective `-o` and exact `/dev/null` + succeed silently; a non-directory output reports `ww: no packages to + build\n`; an existing or trailing-slash output directory reports + `ww: no main packages to build\n`. Header loading occurs before those + empty-selection branches. +- **behavior derived from the pinned implementation** — the pinned body + boundary is byte-sensitive: an ordinary non-`i` stop byte, a first malformed + UTF-8 byte, or a later BOM byte is excluded, while a reached NUL and an + unterminated comment remain load errors; a following byte `i` is attempted as + another import. WW applies the corresponding rule at its lexical `import` + boundary: it never lexes the first ordinary body token, but preserves NUL and + comment diagnostics encountered while skipping header trivia. + +The rule honestly applies to WW's local, manifest-free literal `.ww` model: +`*_test.ww` already means test source for directory selection and `ww test`. +It introduces no module, manifest, registry, lock, network resolver, cache, +generalized import grammar, source build expression, or test-result cache. + +#### Source and identity ownership + +- **behavior directly implemented or asserted by pinned Go** — named-source + package construction presents `Stat`-derived file information through its + synthetic directory. The operand spelling remains the file name presented + to selection, even though `Stat` follows a symlink. +- **behavior derived from the pinned implementation** — the twin true owners + are `cmd/ww/main.c::do_build` and `selfhost/cmd/ww/main.ww::dobuild`, after + the existing hidden-prefix check and before `resolve_module` / `resolvemodule`. + A requested visible non-directory spelling ending `_test.ww` is classified + from that spelling; a symlink to a directory remains a directory request. + The finite-stream header reader is not a regular-file restriction: a supplied + finite FIFO is read as a source, while an unreadable entry reports its header + read failure. Physical parent directories and symlink targets remain loader + metadata, never canonical package or import identity. +- **behavior derived from the pinned implementation** — platform-looking + visible names such as `x_windows_test.ww` remain test-only named sources. + Multiple named sources, logical operands without `.ww`, directories, + recursive requests, `ww run`, and all visible non-test raw operands retain + their prior routes. In particular, this does not implement Go's multiple + named-source package collection. +- **behavior derived from the pinned implementation** — the header observation + is discarded after classification. It creates no canonical package, + command-line package representative, dotted import identity, qualifier, + symbol namespace, `.wwi`, action, archive, publication, or persistence key. + Initial import syntax is checked solely for load-error precedence; omitted + test-source imports are not resolved and create no graph edge or action. + +#### Build, test, package, and import effects + +- **behavior directly measured WW behavior** — before this change, both stages + compiled, linked, published, persisted, and ran a valid visible + `only_test.ww`; `ww build -w WORK -o OUT only_test.ww` exited 0 with empty + streams, a byte-identical mode-0755 executable, populated work state, and + runtime status 19. A missing test-only import likewise reached ordinary + import resolution. +- **behavior derived from the pinned implementation** — post-contract build + behavior is an empty production selection after a valid header: no compiler, + assembler, archiver, linker, generated main, test harness, test child, or + program process starts. `-S` follows the same no-action rule. No runtime + result can occur. +- **behavior derived from the pinned implementation** — package behavior is + confined to the transient header check; no production/test variant or + package action remains. Import behavior is likewise confined to syntax; + missing test-only dotted imports, late imports, body syntax/type errors, and + runtime faults cannot enter the production graph. +- **behavior derived from the pinned implementation** — test behavior is an + explicit non-effect. Raw `ww test`, `ww test -c`, and `ww test -S` continue + to select named test files. Directory and recursive `ww build` already + exclude selected test sources and remain unchanged. + +#### Diagnostics, artifacts, and lifecycle + +- **behavior derived from the pinned implementation** — CLI-shape and + multiple-operand delegation retain their prior precedence; 11.50 prefix + exclusion precedes this header reader. Header read/package/initial-import + diagnostics precede output policy. A first ordinary malformed UTF-8/BOM body + byte is excluded, while a reached NUL or unterminated header-trivia comment + is a header diagnostic. Once the header is valid, no unresolved import, + graph, producer, linker, or runtime diagnostic may surface. Both + stages use the same header boundary and must produce byte-identical status, + stdout, and stderr. +- **behavior derived from the pinned implementation** — cold no-action success + creates no default/explicit output, output directory, `.wwi`, unit, assembly, + object, archive, init product, `.sepwork`, workdir, stamp, transaction, + capture, result, or private build temporary. Output-policy diagnostics also + create none of those products. +- **behavior derived from the pinned implementation** — a warm request starts + no transaction, mutation, invalidation, reuse check, timestamp refresh, or + producer. Existing output, sidecar, workdir and artifacts remain + byte-identical on success and failure; no `.new`, `.install`, `.wwtxn.*`, + backup, or recovery residue remains. Restoring a visible non-test source + reuses prior valid warm state by the unchanged ordinary route. +- **behavior derived from the pinned implementation** — no action means no + publication, rollback work, producer/runtime failure path, shared lock, or + child process. Concurrent Cstage/WWstage no-action and header-diagnostic + requests are isolated. Interruption during header reading leaves no owned + persistent state; after a valid header there is no child or publication window + to clean up. Every reader closes its descriptor and releases request-local + storage before return. +- **behavior derived from the pinned implementation** — focused proof covers + valid, malformed-header, missing-test-import, wrong-platform, symlink and + finite-stream operands; default/file/directory/null/assembly output modes; + cold/warm preservation; trace-proven tool and runtime absence; rollback, + interruption, concurrency, cleanup, complete per-stage work preservation, and + Cstage/WWstage semantic-artifact parity. The complete per-stage snapshot + includes `.wwtool.ww`; cross-stage comparison excludes only that intentionally + different producer binary snapshot. Unaffected visible production controls + retain byte-identical outputs. + +No serialized format changes accompany this omission. Build workdir format +remains `18`, test workdir format remains `19`, semantic storage format remains +`3`, and the slice adds no cache or persistent record. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 7cb7b7ec..7ffc3777 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -326,15 +326,38 @@ ImportPath = ident { "." ident } . eligible. Only the requested basename participates: a hidden parent does not hide visible `main.ww`, a hidden symlink spelling stays hidden for any existing non-directory target, and a visible symlink spelling stays eligible. - The excluded operand creates no package, declaration, import binding/edge, - action, artifact, initialization, test, - publication, or persistent state. `ww build` reports the existing - `directory contains no WW package sources` condition; an explicit running - raw `ww test` also emits its command-owned `FAIL`, while `-c` and `-S` do not. - This rule does not add multiple named-source package support, does not change - visible `*_test.ww` handling, and does not apply to logical operands, - directory/recursive requests, imports, or `ww run`. The diagnostic parent is - presentation metadata and never canonical identity. + The prefix-excluded operand creates no package, declaration, import + binding/edge, action, artifact, initialization, test, publication, or + persistent state. `ww build` reports the existing `directory contains no WW + package sources` condition; an explicit running raw `ww test` also emits its + command-owned `FAIL`, while `-c` and `-S` do not. +- A visible single raw operand passed to `ww build` whose requested final + basename ends exactly `_test.ww` is test-only after its package clause and + contiguous initial import section have been read. Prefix exclusion remains + first, so a bare `_test.ww` is never opened. Header read, package-clause, and + initial-import syntax errors retain their ordinary precedence; after a valid + header the sole root is omitted. Its dotted imports are not resolved, and a + late import, body parse/type error, or runtime behavior is not observed. The + first ordinary body token is not lexed, so an immediately following malformed + UTF-8 byte or non-leading BOM is outside the header. A raw NUL reached while + locating that token and an unterminated comment in header trivia remain load + errors. Go's byte reader probes a following `i` as a possible `import`; the + corresponding WW boundary is the exact lexical `import` token, so an ordinary + WW identifier merely beginning with `i` is body syntax. This named-source + rule retains all-files platform behavior: a + visible `x_windows_test.ww` is still test-only. It classifies the requested + basename of a visible symlink, not its target name; a symlink whose target is + a directory remains a directory request. +- Omission creates no canonical package, command-line package node, import + binding or edge, graph/action, symbol, initializer, executable, archive, + interface, publication, transaction, default `.sepwork`, or work-state + mutation. It does not alter `ww test`, `ww test -c`, `ww test -S`, logical + operands, directory/recursive selection, multiple named-source support, + imports, or `ww run`. Physical directory and symlink-target data remain + observation metadata, never package, import, graph, action, artifact, + symbol, `.wwi`, publication, or persistence identity. Build workdir format + remains 18, test workdir format remains 19, and semantic storage format + remains 3. - `import acme.codec;` loads the canonical package `acme.codec`. If that package declares `package wire;`, the importing file sees its exported names as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias @@ -467,6 +490,15 @@ ImportPath = ident { "." ident } . Output paths and directory metadata never become package, import, graph, action, symbol, artifact, `.wwi`, or persistence identity. + A valid omitted single raw `*_test.ww` build has an empty selection. With no + effective `-o`, including an assembly-only request, and with exact + `-o /dev/null`, it succeeds silently. A non-directory effective output fails + with `ww: no packages to build`; an output-directory effective output fails + with `ww: no main packages to build`. These empty-selection outcomes occur + only after the raw test source's header has loaded successfully, create no + output path or parent, and preserve every pre-existing output and work-state + byte unchanged. + Every caller-visible build installation checks its destination after all applicable compile, assemble, archive, and link producers finish. Ordinary `stat` follows symlinks. An existing directory rejects as diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 17b59a11..35c51e84 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -239,6 +239,32 @@ platform classification, symlink spelling, output rollback, residue, and diagnostic/artifact parity. Directory package coordination and test process, filter, timeout, signal, and descendant topology are unchanged. +The corresponding focused dual-stage named-test-source build observer proves +the distinct `ww build` rule for one visible raw operand ending exactly +`_test.ww`. It requires each stage to read the package clause and contiguous +initial imports before omitting the sole root: unreadable/header-syntax inputs +retain their diagnostics, while a valid header prevents import resolution, +graph/action creation, producers, runtime, publication, persistence, and all +ordinary body-derived diagnostics. It separately proves that a first malformed +UTF-8 or non-leading BOM body byte is outside the header, while a reached NUL +and an unterminated header-trivia comment retain loader diagnostics. It also +proves WW's grammar-level adaptation of Go's following-`i` byte probe: only the +exact lexical `import` token continues the header, while an identifier such as +`imported` begins the ordinary body. It covers +normal, `-S`, and `/dev/null` successful +empty selections; the exact `no packages to build` and `no main packages to +build` output branches; requested-basename prefix, symlink, symlink-directory, +and wrong-platform spelling controls; cold absence and warm preservation of +outputs/work state; tool/process absence, interruption, concurrency, and +residue cleanup. Cstage and WWstage agree on status, stdout, stderr, +diagnostics, semantic artifact/work snapshots, and unaffected-control artifact +bytes. Complete per-stage warm snapshots additionally cover the intentionally +stage-specific `.wwtool.ww` producer-provenance file. +Raw `ww test`, `-c`, and `-S` test routes retain explicit test-file selection; +directory/recursive selection, package/import identities, graph/action +identities, persistence formats (build 18, test 19, semantic 3), and test +process topology are explicit non-effects. + List mode uses that same product process and initialization boundary but starts no per-test child. The shared language harness emits only selected qualified test names, one per line in descriptor order. A valid filter selecting no tests diff --git a/lib/ww/syntax/lex.ww b/lib/ww/syntax/lex.ww index 5d645df2..0e4d8849 100644 --- a/lib/ww/syntax/lex.ww +++ b/lib/ww/syntax/lex.ww @@ -410,6 +410,75 @@ fn skipws(l: *lex) bool = { return false; }; +// Return the next initial import without lexing the first ordinary body token. +// Header trivia is still decoded, so reached NUL and comment diagnostics keep +// loader precedence; a malformed UTF-8 byte or middle BOM that begins the body +// is deliberately outside this pass. +export fn lexheaderimport(l: *lex, out: *tok) bool = { + for (true) { + if (l.lpos >= l.srclen) { return false; }; + let c: i32 = srcb(l, l.lpos); + if (c == 0) { + lskipnul(l); + return false; + }; + if ((c >= 128 && !utf8bytevalid(l.src, l.srclen, l.lpos)) + || bomat(l.src, l.srclen, l.lpos)) { + return false; + }; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + lget(l); + continue; + }; + if (c == '/' && l.lpos + 1u64 < l.srclen + && srcb(l, l.lpos + 1u64) == '/') { + lget(l); lget(l); + linecomment(l); + if (l.errs != 0) { return false; }; + continue; + }; + if (c == '/' && l.lpos + 1u64 < l.srclen + && srcb(l, l.lpos + 1u64) == '*') { + lget(l); lget(l); + let prev: i32 = -1; + for (true) { + let x: i32 = lget(l); + if (x < 0) { + let cp: pos; + curpos(l, &cp); + errat(l, &cp, "unterminated /* comment"); + return false; + }; + if (prev == '*' && x == '/') { break; }; + prev = x; + }; + if (l.errs != 0) { return false; }; + continue; + }; + break; + }; + if (l.modreset != 0 || l.modpathset != 0) { return false; }; + let kw: str = tokname(tkind.TK_USE); + if (l.srclen - l.lpos < kw.len: u64) { return false; }; + let i: i32 = 0; + for (i < kw.len) { + if (srcb(l, l.lpos + (i: u64)) != kw[i]: i32) { + return false; + }; + i += 1; + }; + if (l.srclen - l.lpos > kw.len: u64) { + let next: i32 = srcb(l, l.lpos + (kw.len: u64)); + if ((next >= 'a' && next <= 'z') + || (next >= 'A' && next <= 'Z') + || (next >= '0' && next <= '9') || next == '_') { + return false; + }; + }; + lexnext(l, out); + return out.kind == tkind.TK_USE; +}; + fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = { let v: u64 = 0u64; let b: u64 = base: u64; diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index cde23a2a..6764aa73 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -57,9 +57,7 @@ export type parser = struct { commandpackage: bool, }; -fn refill(p: *parser) void = { - let t: tok; - lexnext(p.l, &t); +fn installtok(p: *parser, t: *tok) void = { p.curkind = t.kind; p.curfile = t.file; p.curline = t.line; @@ -70,6 +68,19 @@ fn refill(p: *parser) void = { p.curtsuffix = t.tsuffix; }; +fn refill(p: *parser) void = { + let t: tok; + lexnext(p.l, &t); + installtok(p, &t); +}; + +fn advanceheaderimport(p: *parser) bool = { + let t: tok; + if (!lexheaderimport(p.l, &t)) { return false; }; + installtok(p, &t); + return true; +}; + export fn parserinit(p: *parser, l: *lex) void = { p.l = l; p.errs = 0; @@ -490,6 +501,121 @@ fn skipimportattrs(p: *parser) void = { }; }; +// Parse one initial import for the named-source header pass. Unlike the full +// recovery scanner, this stops on the first malformed header token and never +// consumes an ordinary declaration body. +fn parseheaderuse(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + let n: *node = newnode(nkind.N_USE, pf, pl, pc); + n.usefile = p.curfile; + n.useline = p.curline; + n.usecol = p.curcol; + let alias: str; + let first: str; + if (p.curkind == tkind.TK_UNDER) { + n.useblank = 1; + advance(p); + }; + if (p.curkind != tkind.TK_IDENT) { + errmsg(p, strings.concat("expected identifier, got ", + tokname(p.curkind))); + return n; + }; + first = p.curtext; + advance(p); + if (n.useblank == 0 && p.curkind == tkind.TK_IDENT) { + alias = first; + first = p.curtext; + advance(p); + }; + let leaf: str = first; + let path: str = leaf; + for (p.curkind == tkind.TK_DOT) { + advance(p); + if (p.curkind != tkind.TK_IDENT) { + errmsg(p, strings.concat("expected identifier, got ", + tokname(p.curkind))); + return n; + }; + leaf = p.curtext; + path = strings.concat(path, ".", leaf); + advance(p); + }; + if (n.useblank != 0) { n.str = ""; } + else { if (alias.len > 0) { n.str = alias; } else { n.str = leaf; }; }; + n.usesource = path; + n.usepath = path; + n.usealias = alias; + if (p.curkind != tkind.TK_SEMI) { + errmsg(p, "expected ';' after import"); + return n; + }; + return n; +}; + +// A valid named source is loader-visible only through its initial package and +// contiguous import section. Keep parseimports for graph-bearing source +// recovery; this boundary deliberately ignores every ordinary declaration. +export fn parsepackageheader(p: *parser) *node = { + let f: *node = newnode(nkind.N_FILE, p.curfile, 1, 1); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_MODPATH + || p.curkind == tkind.TK_MODRESET) { + if (p.curkind == tkind.TK_MODPATH) { + p.pathmod = p.curtext; + p.curmod = p.curtext; + p.resetmod = ""; + } else { + p.pathmod = ""; + p.curmod = p.curtext; + p.resetmod = p.curtext; + }; + p.sourceid += 1; + advance(p); + }; + if (p.curkind != tkind.TK_MODULE) { + errmsg(p, "invalid or missing package clause"); + return f; + }; + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + if (p.curkind != tkind.TK_IDENT) { + errmsg(p, "invalid or missing package clause"); + return f; + }; + let name: str = p.curtext; + advance(p); + if (p.curkind != tkind.TK_SEMI) { + errmsg(p, "expected ';' after package name"); + return f; + }; + p.curpkg = name; + if (p.pathmod.len == 0 && p.resetmod.len == 0) { p.curmod = name; }; + f.nmod = name; + f.pkgname = name; + f.sourceid = p.sourceid; + f.file = pf; + f.line = pl; + f.col = pc; + for (advanceheaderimport(p)) { + let d: *node = parseheaderuse(p); + d.nmod = p.curmod; + d.pkgname = p.curpkg; + d.sourceid = p.sourceid; + if (head == nil) { head = d; } else { tail.next = d; }; + tail = d; + if (p.errs != 0) { break; }; + }; + f.list = head; + return f; +}; + export fn parseimports(p: *parser) *node = { let f = newnode(nkind.N_FILE, p.curfile, 1, 1); let head: *node = nil; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 08e31d2b..fe644b91 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -991,6 +991,102 @@ fn slurp(pathcs: *u8) (*u8, u64) = { return buf.ptr, nu; }; +// Named source recognition accepts every existing non-directory Stat result. +// Read to EOF without seeking so a finite FIFO follows the same package-header +// loading path as a regular file. +fn sourceheaderslurp(pathcs: *u8) (*u8, u64, u64) = { + let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32); + if (fd < 0) { return nil, 0u64, 0u64; }; + let cap: i32 = 4096; + let allocation: ([]u8 | nomem) = sepallocbytes(cap); + let buf: []u8; + match (allocation) { + case let value: []u8 => buf = value; + case nomem => { + sepfailnomem(); + os.close(fd); + return nil, 0u64, 0u64; + }; + }; + let used: u64 = 0u64; + for (true) { + if (used == cap: u64) { + if (cap == SEP_COUNT_MAX) { + sepfailsize(); + os.close(fd); + os.free(buf.ptr: *void, cap: u64); + return nil, 0u64, 0u64; + }; + let nextcap: i32 = cap; + if (cap > SEP_COUNT_MAX / 2) { nextcap = SEP_COUNT_MAX; } + else { nextcap = cap * 2; }; + let nextallocation: ([]u8 | nomem) = sepallocbytes(nextcap); + let next: []u8; + match (nextallocation) { + case let value: []u8 => next = value; + case nomem => { + sepfailnomem(); + os.close(fd); + os.free(buf.ptr: *void, cap: u64); + return nil, 0u64, 0u64; + }; + }; + bytecpy(next.ptr, buf.ptr, used); + os.free(buf.ptr: *void, cap: u64); + buf = next; + cap = nextcap; + }; + let got: i64 = os.read(fd, buf.ptr + used, + (cap: u64) - used); + if (got == -(os.EINTR: i32): i64) { continue; }; + if (got < 0i64) { + os.close(fd); + os.free(buf.ptr: *void, cap: u64); + return nil, 0u64, 0u64; + }; + if (got == 0i64) { break; }; + used += got: u64; + }; + if (os.close(fd) != 0) { + os.free(buf.ptr: *void, cap: u64); + return nil, 0u64, 0u64; + }; + buf[used] = 0u8; + return buf.ptr, used, cap: u64; +}; + +// Validate only the package/import header. The returned syntax is deliberately +// discarded: an ordinary build creates no package or import graph for this +// test-only named source. +fn sourcebuildheader(path: *u8) bool = { + let view: str = pathstr(path); + let bufp: *u8; + let blen: u64; + let bcap: u64; + bufp, blen, bcap = sourceheaderslurp(path); + if (bufp == nil) { + cerrpath("ww: cannot read ", path, "\n"); + return false; + }; + let l: syntax.lex; + syntax.lexinit(&l, view, bufp, blen); + let ps: syntax.parser; + syntax.parserinit(&ps, &l); + let header: *syntax.node = syntax.parsepackageheader(&ps); + if (l.errs > 0 || ps.errs > 0) { + os.free(bufp: *void, bcap); + return false; + }; + if (header.nmod.len == 0) { + cerrpos(view, 1, 1); + cerr(": error: invalid or missing package clause\n"); + os.free(bufp: *void, bcap); + return false; + }; + os.free(bufp: *void, bcap); + return true; +}; + fn makestem(stem: *u8, src: *u8) void = { let n: u64 = cstrlen(src); let stop: u64 = n; @@ -8943,15 +9039,35 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { nil, nil, false, true); }; let requestedliteral: bool = false; + let requestednondirectory: bool = false; let requestedstat: os.filestat; match (os.stat(&requestedstat, pathstr(src))) { - case void => requestedliteral = true; + case void => { + requestedliteral = true; + let typ: u32 = (requestedstat.mode: u32) & 61440u32; + requestednondirectory = typ != os.mode.DIR: u32; + }; case let e: os.oserror => void; }; if (sourceoperandignored(src)) { sourceoperandnosources(src); return 1; }; + let requestedlen: u64 = cstrlen(src); + let requestedbase: u64 = basenameoff(src, requestedlen); + if (requestednondirectory && requestedlen - requestedbase >= 8u64 + && cstrendswithlit(src, "_test.ww")) { + if (!sourcebuildheader(src)) { return 1; }; + if (outflag == nil || cstreqlit(outflag, "/dev/null")) { + return 0; + }; + if (buildoutputdir(outflag)) { + cerr("ww: no main packages to build\n"); + } else { + cerr("ww: no packages to build\n"); + }; + return 1; + }; let isdir: i32 = 0; let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); if (resolved == nil) { diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 0cee0c38..1c5e5c54 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -705,6 +705,136 @@ fn directoryisempty(path: str) bool = { return true; }; +fn snapshotu64(out: *[]u8, value: u64) void = { + let v: u64 = value; + let i: i32 = 0; + for (i < 8) { + append(*out, (v & 255u64): u8); + v >>= 8u64; + i += 1; + }; +}; + +fn snapshotbytes(out: *[]u8, value: str) void = { + snapshotu64(out, value.len: u64); + let i: i32 = 0; + for (i < value.len) { append(*out, value[i]); i += 1; }; +}; + +fn treepaths(root: str, relative: str, paths: *[]str) void = { + let current: str = root; + if (relative.len != 0) { current = strings.concat(root, "/", relative); }; + let fd: i32 = os.open(current, os.flag.RDONLY, 0i32); + assert(fd >= 0); + let buf: []u8 = alloc([], 16384u64)!; + buf.len = 16384; + let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64); + for (n > 0i64) { + let off: i32 = 0; + for (off < n: i32) { + let reclen: i32 = (buf[off + 16]: i32) + + ((buf[off + 17]: i32) * 256); + assert(reclen >= 20 && off + reclen <= n: i32); + let len: i32 = 0; + for (buf[off + 19 + len] != 0u8) { len += 1; }; + let dot: bool = len == 1 && buf[off + 19] == '.'; + let dotdot: bool = len == 2 && buf[off + 19] == '.' + && buf[off + 20] == '.'; + if (!dot && !dotdot) { + let name: str; + name.ptr = buf.ptr + (off + 19): u64; + name.len = len; + let child: str = strings.dup(name); + if (relative.len != 0) { + child = strings.concat(relative, "/", child); + }; + append(*paths, child); + let full: str = strings.concat(root, "/", child); + let fi: os.filestat; + match (os.lstat(&fi, full)) { + case void => void; + case let e: os.oserror => abort("tree lstat failed"); + }; + let kind: u32 = (fi.mode: u32) & 61440u32; + if (kind == os.mode.DIR: u32) { + treepaths(root, child, paths); + }; + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, buf.len: u64); + }; + assert(n == 0i64); + assert(os.close(fd) == 0); +}; + +// Canonical complete-tree state: relative names, types, modes, and regular or +// symlink bytes. Timestamps and inode numbers are observation metadata, not +// semantic build state, and are deliberately excluded. +fn treesnapshotfiltered(root: str, skipdriver: bool) str = { + let paths: []str = alloc([], 16u64)!; + append(paths, ""); + treepaths(root, "", &paths); + let i: i32 = 1; + for (i < paths.len) { + let j: i32 = i; + for (j > 0 && strings.compare(paths[j - 1], paths[j]) > 0) { + let swap: str = paths[j - 1]; + paths[j - 1] = paths[j]; + paths[j] = swap; + j -= 1; + }; + i += 1; + }; + let out: []u8 = alloc([], 4096u64)!; + i = 0; + for (i < paths.len) { + if (skipdriver && same(paths[i], ".wwtool.ww")) { + i += 1; + continue; + }; + let full: str = root; + if (paths[i].len != 0) { + full = strings.concat(root, "/", paths[i]); + }; + let fi: os.filestat; + match (os.lstat(&fi, full)) { + case void => void; + case let e: os.oserror => abort("snapshot lstat failed"); + }; + snapshotbytes(&out, paths[i]); + snapshotu64(&out, fi.mode: u32: u64); + let kind: u32 = (fi.mode: u32) & 61440u32; + if (kind == os.mode.REG: u32) { + snapshotbytes(&out, readfile(full)); + } else { if (kind == os.mode.LINK: u32) { + let target: [4096]u8; + let got: i64 = os.readlink(full, &target[0], 4096u64); + assert(got >= 0i64 && got <= 4096i64); + let value: str; + value.ptr = &target[0]; + value.len = got: i32; + snapshotbytes(&out, value); + } else { + snapshotu64(&out, 0u64); + }; }; + i += 1; + }; + return strings.frombytes(out); +}; + +fn treesnapshot(root: str) str = { + return treesnapshotfiltered(root, false); +}; + +// The two bootstrap stages intentionally snapshot different driver binaries +// in `.wwtool.ww`. Exclude only that producer-provenance file when comparing +// semantic artifacts across stages; per-stage preservation uses the complete +// snapshot above and therefore still covers it. +fn artifacttreesnapshot(root: str) str = { + return treesnapshotfiltered(root, true); +}; + fn discardedlinkcleaned(marker: str) void = { let output: str = readfile(marker); assert(output.len != 0); @@ -1335,6 +1465,493 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +// A visible raw test-source build observes only its initial package/import +// header. It must not turn that observation into a package, import, action, +// artifact, or work-state identity. +@test fn named_test_sources_are_header_loaded_then_omitted() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let dirsource: str = strings.concat(source, "/directory"); + let dirtarget: str = strings.concat(source, "/directory-target"); + mkdirall(source); mkdirall(dirsource); mkdirall(dirtarget); + + let ignored: str = strings.concat(source, "/raw_windows_test.ww"); + let platform: str = strings.concat(source, "/platform_windows_test.ww"); + let badpackage: str = strings.concat(source, "/bad_package_test.ww"); + let badimport: str = strings.concat(source, "/bad_import_test.ww"); + let bodyutf8: str = strings.concat(source, "/body_utf8_test.ww"); + let bodybom: str = strings.concat(source, "/body_bom_test.ww"); + let bodynul: str = strings.concat(source, "/body_nul_test.ww"); + let badcomment: str = strings.concat(source, "/bad_comment_test.ww"); + let linktarget: str = strings.concat(source, "/target.ww"); + let link: str = strings.concat(source, "/requested_test.ww"); + let dirlink: str = strings.concat(source, "/directory_test.ww"); + let rawmain: str = strings.concat(source, "/raw.ww"); + let runsource: str = strings.concat(source, "/run_test.ww"); + let testroute: str = strings.concat(source, "/route_test.ww"); + let warmpath: str = strings.concat(source, "/warm.ww"); + let warmtest: str = strings.concat(source, "/warm_test.ww"); + let header: str = strings.concat( + "package raw_test;\nimport absent.pkg;\n", + "this body is deliberately malformed and never reaches a producer {\n", + "import late.missing;\n"); + writefile(ignored, header); + writefile(platform, header); + writefile(linktarget, header); + writefile(badpackage, "this source has no package clause\n"); + writefile(badimport, "package malformed;\nimport ;\n"); + putinvalidutf8file(bodyutf8, "package body;\n", + "fn deliberately_unparsed(\n", false); + writemidbomfile(bodybom, "package body;\n", + "fn deliberately_unparsed(\n"); + writenulfile(bodynul, "package body;\n", "fn body() void = {};\n"); + writefile(badcomment, "package body;\n/* unterminated"); + writefile(rawmain, "package main;\nfn main() i32 = { return 4; };\n"); + writefile(runsource, + "package main;\nfn main() i32 = { return 9; };\n"); + writefile(testroute, strings.concat( + "package route;\n", + "@test fn still_selected() void = { assert(true); };\n")); + writefile(warmpath, "package main;\nfn main() i32 = { return 6; };\n"); + writefile(strings.concat(dirsource, "/main.ww"), + "package main;\nfn main() i32 = { return 3; };\n"); + writefile(strings.concat(dirtarget, "/main.ww"), + "package main;\nfn main() i32 = { return 5; };\n"); + assert(os.symlink(linktarget, link) == 0); + assert(os.symlink(dirtarget, dirlink) == 0); + + let compilerwrapper: str = strings.concat(root, "/testonly-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/testonly-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/testonly-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_TESTONLY_CTRACE\"\n", + "exec \"$WW_TESTONLY_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_TESTONLY_ATRACE\"\n", + "exec \"$WW_TESTONLY_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_TESTONLY_LTRACE\"\n", + "exec \"$WW_TESTONLY_REAL_L\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let packagebad: []str = ["", ""]; + let importbad: []str = ["", ""]; + let nulbad: []str = ["", ""]; + let commentbad: []str = ["", ""]; + let multibad: []str = ["", ""]; + let environments: [][]str = alloc([], stages.len: u64)!; + let tracecs: []str = alloc([], stages.len: u64)!; + let traceas: []str = alloc([], stages.len: u64)!; + let tracels: []str = alloc([], stages.len: u64)!; + let cwarmbin: str = ""; + let cwarmartifacts: str = ""; + let ccompiled: str = ""; + let ctestasm: str = ""; + let crawbin: str = ""; + let baseenv: []str = os.getenvs(); + let si: i32 = 0; + for (si < stages.len) { + let ctrace: str = strings.concat(root, "/testonly-c-", tags[si]); + let atrace: str = strings.concat(root, "/testonly-a-", tags[si]); + let ltrace: str = strings.concat(root, "/testonly-l-", tags[si]); + writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, ""); + append(tracecs, ctrace); append(traceas, atrace); append(tracels, ltrace); + let env: []str = alloc([], (baseenv.len + 10): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_W6A=") + && !strings.hasprefix(baseenv[ei], "WW_W6L=") + && !strings.hasprefix(baseenv[ei], "WW_TESTONLY_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_TESTONLY_CTRACE=", ctrace)); + append(env, strings.concat("WW_TESTONLY_ATRACE=", atrace)); + append(env, strings.concat("WW_TESTONLY_LTRACE=", ltrace)); + append(env, strings.concat("WW_TESTONLY_REAL_C=", driver(compilers[si]))); + append(env, strings.concat("WW_TESTONLY_REAL_A=", driver(assemblers[si]))); + append(env, strings.concat("WW_TESTONLY_REAL_L=", driver(linkers[si]))); + append(environments, env); + + let out: commandout; + let unused: str = strings.concat(root, "/unused-", tags[si]); + let defaultav: []str = [driver(stages[si]), "build", "-w", unused, + ignored]; + runcommandenv(root, strings.concat("testonly-default-", tags[si]), + defaultav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let defaultoutput: str = strings.concat(source, "/raw_windows_test"); + let defaultsepwork: str = strings.concat(defaultoutput, ".sepwork"); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(unused) && !os.exists(defaultoutput) + && !os.exists(defaultsepwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let asmwork: str = strings.concat(root, "/asm-unused-", tags[si]); + let asmav: []str = [driver(stages[si]), "build", "-S", "-w", asmwork, + ignored]; + runcommandenv(root, strings.concat("testonly-assembly-", tags[si]), + asmav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(asmwork) + && !os.exists(defaultoutput) && !os.exists(defaultsepwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let nullwork: str = strings.concat(root, "/null-unused-", tags[si]); + let nullav: []str = [driver(stages[si]), "build", "-w", nullwork, + "-o", "/dev/null", ignored]; + runcommandenv(root, strings.concat("testonly-null-", tags[si]), + nullav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(nullwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let filework: str = strings.concat(root, "/file-unused-", tags[si]); + let fileout: str = strings.concat(root, "/file-output-", tags[si]); + let fileav: []str = [driver(stages[si]), "build", "-w", filework, + "-o", fileout, ignored]; + runcommandenv(root, strings.concat("testonly-file-", tags[si]), + fileav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n") + && !os.exists(filework) && !os.exists(fileout) + && !os.exists(strings.concat(fileout, ".new")) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + let fileasmav: []str = [driver(stages[si]), "build", "-S", "-w", + filework, "-o", fileout, ignored]; + runcommandenv(root, strings.concat("testonly-file-assembly-", tags[si]), + fileasmav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n") + && !os.exists(filework) && !os.exists(fileout)); + + let outputdir: str = strings.concat(root, "/output-dir-", tags[si]); + let marker: str = strings.concat(outputdir, "/marker"); + mkdirall(outputdir); writefile(marker, "caller-owned\n"); + let dirwork: str = strings.concat(root, "/dir-unused-", tags[si]); + let dirav: []str = [driver(stages[si]), "build", "-w", dirwork, + "-o", outputdir, ignored]; + runcommandenv(root, strings.concat("testonly-directory-", tags[si]), + dirav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: no main packages to build\n") + && same(readfile(marker), "caller-owned\n") && !os.exists(dirwork)); + let trailingav: []str = [driver(stages[si]), "build", "-w", dirwork, + "-o", strings.concat(outputdir, "/"), ignored]; + runcommandenv(root, strings.concat("testonly-directory-trailing-", tags[si]), + trailingav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: no main packages to build\n") + && same(readfile(marker), "caller-owned\n") && !os.exists(dirwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + let badwork: str = strings.concat(root, "/bad-unused-", tags[si]); + let badout: str = strings.concat(root, "/bad-output-", tags[si]); + let badpackageav: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, badpackage]; + runcommandenv(root, strings.concat("testonly-bad-package-", tags[si]), + badpackageav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && has(out.stderr, "invalid or missing package clause\n") + && !has(out.stderr, "no packages to build") && !os.exists(badwork) + && !os.exists(badout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + packagebad[si] = strings.dup(out.stderr); + let badimportav: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, badimport]; + runcommandenv(root, strings.concat("testonly-bad-import-", tags[si]), + badimportav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let importwant: str = strings.concat(badimport, + ":2:8: error: expected identifier, got ;\n"); + assert(out.stdout.len == 0 && same(out.stderr, importwant) + && !has(out.stderr, "no packages to build") && !os.exists(badwork) + && !os.exists(badout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + importbad[si] = strings.dup(out.stderr); + + // Pinned readGoInfo discards the first ordinary body stop byte, even + // when it starts malformed UTF-8 or a non-leading BOM. Reached NUL and + // unterminated header-trivia comments remain loader errors. + let utf8av: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, bodyutf8]; + runcommandenv(root, strings.concat("testonly-body-utf8-", tags[si]), + utf8av, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: no packages to build\n") + && !os.exists(badwork) && !os.exists(badout)); + let bomav: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, bodybom]; + runcommandenv(root, strings.concat("testonly-body-bom-", tags[si]), + bomav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: no packages to build\n") + && !os.exists(badwork) && !os.exists(badout)); + let nulav: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, bodynul]; + runcommandenv(root, strings.concat("testonly-body-nul-", tags[si]), + nulav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let nulwant: str = strings.concat(bodynul, + ":2:1: error: invalid NUL character\n"); + assert(out.stdout.len == 0 && same(out.stderr, nulwant) + && !os.exists(badwork) && !os.exists(badout)); + nulbad[si] = strings.dup(out.stderr); + let commentav: []str = [driver(stages[si]), "build", "-w", badwork, + "-o", badout, badcomment]; + runcommandenv(root, strings.concat("testonly-bad-comment-", tags[si]), + commentav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + let commentwant: str = strings.concat(badcomment, + ":2:16: error: unterminated /* comment\n"); + assert(out.stdout.len == 0 && same(out.stderr, commentwant) + && !os.exists(badwork) && !os.exists(badout) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + commentbad[si] = strings.dup(out.stderr); + + let spellingwork: str = strings.concat(root, "/spelling-unused-", tags[si]); + let platformav: []str = [driver(stages[si]), "build", "-w", + spellingwork, platform]; + runcommandenv(root, strings.concat("testonly-platform-", tags[si]), + platformav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(spellingwork)); + let linkav: []str = [driver(stages[si]), "build", "-w", spellingwork, + link]; + runcommandenv(root, strings.concat("testonly-requested-link-", tags[si]), + linkav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(spellingwork) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + + // A directory target remains a directory request despite a test-looking + // requested spelling; this is deliberately the one producer-positive row. + let linkdirwork: str = strings.concat(root, "/link-dir-work-", tags[si]); + let linkdirout: str = strings.concat(root, "/link-dir-output-", tags[si]); + mkdirall(linkdirwork); + let linkdirav: []str = [driver(stages[si]), "build", "-w", linkdirwork, + "-o", linkdirout, dirlink]; + runcommandenv(root, strings.concat("testonly-directory-link-", tags[si]), + linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out); + if (si == 0) { + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(linkdirout) && readfile(ctrace).len != 0 + && readfile(atrace).len != 0 && readfile(ltrace).len != 0); + } else { + // The selected non-directory classifier must not absorb this + // pre-existing WWstage resolution difference into the slice. + expectexit(&out, 1); + assert(out.stdout.len == 0 + && same(out.stderr, "ww: cannot read source\n") + && !os.exists(linkdirout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + }; + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + + // A valid retained raw source seeds state. Renaming only its requested + // basename to test-only must leave all public and private bytes untouched. + let warmwork: str = strings.concat(root, "/warm-work-", tags[si]); + let warmout: str = strings.concat(root, "/warm-output-", tags[si]); + mkdirall(warmwork); + let warmav: []str = [driver(stages[si]), "build", "-w", warmwork, + "-o", warmout, warmpath]; + runcommandenv(root, strings.concat("testonly-warm-seed-", tags[si]), + warmav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(warmout)); + let warmbin: str = strings.dup(readfile(warmout)); + let warmtree: str = strings.dup(treesnapshot(warmwork)); + if (si == 0) { + cwarmbin = strings.dup(warmbin); + cwarmartifacts = strings.dup(artifacttreesnapshot(warmwork)); + } else { + assert(same(cwarmbin, warmbin) + && same(cwarmartifacts, artifacttreesnapshot(warmwork))); + }; + assert(os.rename(warmpath, warmtest) == 0); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let warmtestav: []str = [driver(stages[si]), "build", "-w", warmwork, + "-o", warmout, warmtest]; + runcommandenv(root, strings.concat("testonly-warm-omit-", tags[si]), + warmtestav, env, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n") + && same(warmbin, readfile(warmout)) && !os.exists(strings.concat(warmout, ".new")) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && !directoryhasnew(warmwork) + && !directoryhasfragment(warmwork, ".wwtxn.") + && same(warmtree, treesnapshot(warmwork))); + if (si == 1) { + assert(same(cwarmbin, readfile(warmout)) + && same(cwarmartifacts, artifacttreesnapshot(warmwork))); + }; + assert(os.rename(warmtest, warmpath) == 0); + runcommandenv(root, strings.concat("testonly-warm-restored-", tags[si]), + warmav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && same(warmbin, readfile(warmout)) && !directoryhasnew(warmwork) + && same(warmtree, treesnapshot(warmwork))); + if (si == 1) { + assert(same(cwarmbin, readfile(warmout)) + && same(cwarmartifacts, artifacttreesnapshot(warmwork))); + }; + + // Test and run keep their existing raw-source semantics, while a visible + // non-test raw build and a multi-operand error do not enter this slice. + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let testwork: str = strings.concat(root, "/route-work-", tags[si]); + mkdirall(testwork); + let testav: []str = [driver(stages[si]), "test", "-w", testwork, + testroute]; + runcommandenv(root, strings.concat("testonly-raw-test-", tags[si]), + testav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "still_selected ... ok\n") + && readfile(ctrace).len != 0); + let compiled: str = strings.concat(root, "/route-compiled-", tags[si]); + let compileav: []str = [driver(stages[si]), "test", "-c", "-o", compiled, + testroute]; + runcommandenv(root, strings.concat("testonly-raw-test-compile-", tags[si]), + compileav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(compiled)); + let compiledbytes: str = strings.dup(readfile(compiled)); + if (si == 0) { ccompiled = strings.dup(compiledbytes); } + else { assert(same(ccompiled, compiledbytes)); }; + let testasm: str = strings.concat(root, "/route-assembly-", tags[si]); + let testasmav: []str = [driver(stages[si]), "test", "-S", "-o", testasm, + testroute]; + runcommandenv(root, strings.concat("testonly-raw-test-assembly-", tags[si]), + testasmav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let testasmtree: str = strings.concat(testasm, ".sepwork"); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && os.exists(testasmtree)); + let testasmstate: str = strings.dup(artifacttreesnapshot(testasmtree)); + if (si == 0) { ctestasm = strings.dup(testasmstate); } + else { assert(same(ctestasm, testasmstate)); }; + let runav: []str = [driver(stages[si]), "run", runsource]; + runcommandenv(root, strings.concat("testonly-run-", tags[si]), runav, + env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 9); + let rawout: str = strings.concat(root, "/raw-output-", tags[si]); + let rawav: []str = [driver(stages[si]), "build", "-o", rawout, rawmain]; + runcommandenv(root, strings.concat("testonly-raw-build-", tags[si]), + rawav, env, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(rawout)); + let rawbytes: str = strings.dup(readfile(rawout)); + if (si == 0) { crawbin = strings.dup(rawbytes); } + else { assert(same(crawbin, rawbytes)); }; + let multiout: str = strings.concat(root, "/multi-output-", tags[si]); + rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, ""); + let multiav: []str = [driver(stages[si]), "build", "-o", multiout, + rawmain, ignored]; + runcommandenv(root, strings.concat("testonly-multiple-", tags[si]), + multiav, env, (30i64 * (time.second: i64)): time.duration, &out); + let multiwant: str = strings.concat( + "wwtest package: ", rawmain, + ": cannot canonicalize package directory\n", + "wwtest package: ", ignored, + ": cannot canonicalize package directory\n"); + assert(out.termination == exec.termination.EXIT && out.code != 0 + && out.stdout.len == 0 && same(out.stderr, multiwant) + && !os.exists(multiout) && readfile(ctrace).len == 0 + && readfile(atrace).len == 0 && readfile(ltrace).len == 0); + multibad[si] = strings.dup(out.stderr); + + si += 1; + }; + assert(same(packagebad[0], packagebad[1])); + assert(same(importbad[0], importbad[1])); + assert(same(nulbad[0], nulbad[1])); + assert(same(commentbad[0], commentbad[1])); + assert(same(multibad[0], multibad[1])); + + // Two simultaneous omitted requests have no work/output collision and no + // producer invocation in either stage. + let ti: i32 = 0; + for (ti < stages.len) { + rewritefile(tracecs[ti], ""); + rewritefile(traceas[ti], ""); + rewritefile(tracels[ti], ""); + ti += 1; + }; + let pcwork: str = strings.concat(root, "/parallel-c-unused"); + let pwwork: str = strings.concat(root, "/parallel-ww-unused"); + let pcav: []str = [driver("ww"), "build", "-w", pcwork, ignored]; + let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork, ignored]; + let pc: exec.command; + pc.path = pcav[0]; pc.argv = pcav; pc.env = environments[0]; pc.dir = repo(); + pc.stdoutpath = strings.concat(root, "/parallel-c.stdout"); + pc.stderrpath = strings.concat(root, "/parallel-c.stderr"); + pc.deadline = time.add(time.now(time.clock.monotonic), + (30i64 * (time.second: i64)): time.duration); + pc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pw: exec.command; + pw.path = pwav[0]; pw.argv = pwav; pw.env = environments[1]; pw.dir = repo(); + pw.stdoutpath = strings.concat(root, "/parallel-ww.stdout"); + pw.stderrpath = strings.concat(root, "/parallel-ww.stderr"); + pw.deadline = time.add(time.now(time.clock.monotonic), + (30i64 * (time.second: i64)): time.duration); + pw.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pcp: exec.process; + let pwp: exec.process; + exec.start(&pcp, &pc); exec.start(&pwp, &pw); + let pcdone: bool = false; + let pwdone: bool = false; + for (!pcdone || !pwdone) { + if (!pcdone) { pcdone = exec.poll(&pcp); }; + if (!pwdone) { pwdone = exec.poll(&pwp); }; + if (!pcdone || !pwdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(pcp.result.errno == 0 && pcp.result.cleanuperrno == 0 + && pcp.result.termination == exec.termination.EXIT && pcp.result.code == 0 + && pwp.result.errno == 0 && pwp.result.cleanuperrno == 0 + && pwp.result.termination == exec.termination.EXIT && pwp.result.code == 0); + assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0 + && readfile(pw.stdoutpath).len == 0 && readfile(pw.stderrpath).len == 0 + && !os.exists(pcwork) && !os.exists(pwwork)); + ti = 0; + for (ti < stages.len) { + assert(readfile(tracecs[ti]).len == 0 + && readfile(traceas[ti]).len == 0 + && readfile(tracels[ti]).len == 0); + ti += 1; + }; + assert(!directoryhasnew(root) && !directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".install") + && !directoryhasfragment(root, ".capture") + && !directoryhasfragment(root, ".result") + && !directoryhasfragment(root, ".request")); + clean(root); +}; + // A source file has one contiguous import section immediately after its // package clause. The parser may continue after a late import for recovery, // but the imports-only loader and the full compiler parser both reject it. diff --git a/test/tool/wwdump_test.ww b/test/tool/wwdump_test.ww index 1ef89689..2057a74e 100644 --- a/test/tool/wwdump_test.ww +++ b/test/tool/wwdump_test.ww @@ -18,10 +18,11 @@ package wwdump_test; // wwdump_ww's exit code is deliberately NOT gated: the diagnostics // land on stderr regardless, and the armed bail flips the exit code — // the line count is the one signal stable across the fold sequence. -// #90 sep-feed: `ww build -S` resolves import-bearing fixtures to an -// owner unit and direct exports; this raw wwdump-only gate explicitly -// composes those artifacts because wwdump has no package-input CLI. The -// import-free test/wcc/901_*.ww companions stay raw-fed. +// #90 sep-feed: `ww build -S` resolves production fixtures and `ww test +// -S` resolves test fixtures to an owner unit and direct exports; this raw +// wwdump-only gate explicitly composes those artifacts because wwdump has no +// package-input CLI. The import-free test/wcc/901_*.ww companions stay +// raw-fed. // // wwdumpgate (#52, F15 c4) — the -c and -r arms gate on parse-stage // errors instead of silently emitting over a broken AST: parse-errored @@ -87,18 +88,20 @@ fn countdiag(stderr: str) i32 = { if (seps[i]) { let stem: str = strings.concat(td, "/unit"); let bo: testenv.commandout; - let bav: []str = [testenv.driver("ww"), "build", "-S", "-o", + let action: str = "build"; + if (strings.hassuffix(rels[i], "_test.ww")) { action = "test"; }; + let bav: []str = [testenv.driver("ww"), action, "-S", "-o", stem, src]; testenv.runcommand(td, td, "resolve", bav, tmo(), &bo); if (bo.termination != exec.termination.EXIT || bo.code != 0) { fail(classes[i], strings.concat(rels[i], - ": no resolved sep unit (ww build -S failed)")); + ": no resolved sep unit (ww build/test -S failed)")); }; let work: str = strings.concat(stem, ".sepwork/"); let owner: str = strings.concat(work, "__root.unit.ww"); if (!testenv.exists(owner)) { fail(classes[i], strings.concat(rels[i], - ": __root.unit.ww missing after ww build -S")); + ": __root.unit.ww missing after ww build/test -S")); }; let deps: []str = []; if (i == 0) { append(deps, "math.checked"); append(deps, "types"); }; diff --git a/test/wcc/200_parse.c b/test/wcc/200_parse.c index bfdb0dfd..9cbdad2a 100644 --- a/test/wcc/200_parse.c +++ b/test/wcc/200_parse.c @@ -100,6 +100,40 @@ imports_to_str(const char *src, int *errs, const char **pkg) return buf; } +static char * +header_to_str(const char *src, size_t len, int *errs, const char **pkg) +{ + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, "header.ww", src, len); + parserinit(&p, a, &l); + Node *n = parsepackageheader(&p); + *errs = p.errs + l.errs; + *pkg = n->module ? strdup(n->module) : NULL; + + char *buf = NULL; + size_t outlen = 0; + FILE *f = open_memstream(&buf, &outlen); + if (f != NULL) { + astprint(f, n); + fclose(f); + } + freearena(a); + return buf; +} + +static int +header_errors(const char *src) +{ + int errs; + const char *pkg; + char *got = header_to_str(src, strlen(src), &errs, &pkg); + free((void *)pkg); + free(got); + return errs; +} + static int must_import_shape(const char *src, int imports_only, const char *binding, const char *path, const char *alias) @@ -300,6 +334,119 @@ main(void) free((void *)pkg); free(got); } + { + const char *src = + "package main;\n" + "import zed;\n" + "import stable alpha.beta;\n" + "fn body() void = {};\n"; + int errs; + const char *pkg; + char *got = header_to_str(src, strlen(src), &errs, &pkg); + if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0 + || got == NULL || strstr(got, "(use \"zed\"") == NULL + || strstr(got, "(use \"stable\"") == NULL + || strstr(got, "body") != NULL) { + fputs("package-header parse mismatch\n", stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + const char *src = + "package main;\n" + "\xff"; + int errs; + const char *pkg; + char *got = header_to_str(src, strlen(src), &errs, &pkg); + if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0 + || got == NULL) { + fputs("package-header observed the first malformed UTF-8 body byte\n", + stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + const char src[] = + "package main;\n" + "import alpha;\n" + "\xef\xbb\xbf"; + int errs; + const char *pkg; + char *got = header_to_str(src, sizeof src - 1, &errs, &pkg); + if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0 + || got == NULL || strstr(got, "(use \"alpha\"") == NULL) { + fputs("package-header observed the first body BOM\n", stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + const char src[] = "package main;\n\0fn body() void = {};\n"; + int errs; + const char *pkg; + char *got = header_to_str(src, sizeof src - 1, &errs, &pkg); + if (errs == 0) { + fputs("package-header ignored a reached NUL boundary byte\n", + stderr); + fail++; + } + free((void *)pkg); + free(got); + } + if (header_errors("package main;\n/* unterminated") == 0) { + fputs("package-header accepted unterminated header trivia\n", stderr); + fail++; + } + { + const char *src = + "package main;\n" + "/* between */ import alpha; // after\n" + "import beta;\n" + "\xff"; + int errs; + const char *pkg; + char *got = header_to_str(src, strlen(src), &errs, &pkg); + if (errs != 0 || got == NULL + || strstr(got, "(use \"alpha\"") == NULL + || strstr(got, "(use \"beta\"") == NULL) { + fputs("package-header comment/import boundary mismatch\n", stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + int errs; + const char *pkg; + char *got = header_to_str( + "package main;\nimported body;\nimport late;\n", + strlen("package main;\nimported body;\nimport late;\n"), + &errs, &pkg); + if (errs != 0 || got == NULL || strstr(got, "late") != NULL) { + fputs("package-header mistook an ordinary identifier for import\n", + stderr); + fail++; + } + free((void *)pkg); + free(got); + } + if (header_errors("package ;\n") == 0) { + fputs("package-header accepted a malformed package clause\n", stderr); + fail++; + } + if (header_errors("package main;\nimport ;\n") == 0) { + fputs("package-header accepted a malformed import\n", stderr); + fail++; + } + if (header_errors("package main\nfn body() void = {};\n") == 0) { + fputs("package-header accepted a missing package semicolon\n", stderr); + fail++; + } { int errs; const char *pkg; @@ -333,6 +480,6 @@ main(void) fprintf(stderr, "%d parse tests failed\n", fail); return 1; } - printf("parse: %d/%d ok + 14 shape ok\n", n, n); + printf("parse: %d/%d ok + 14 shape + 10 header ok\n", n, n); return 0; }