build: omit named test source operands

This commit is contained in:
2026-08-22 21:53:10 +09:00
parent 3922f1c34e
commit 2e5451a601
13 changed files with 1666 additions and 21 deletions

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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*);

View File

@@ -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)) {

View File

@@ -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 29032918](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 32443315](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 9301036](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 14381509](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 265315](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 7189](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 28872923](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 350358](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:10371040`.
Build checks loader errors first and then omits a test-only root before
output/action construction
([`cmd/go/internal/work/build.go`, lines 459559 and 731745](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:382421` (`TestMatchFile`),
`go/build/build_test.go:812831` (`TestDirectives` and `XTestDirectives`),
`go/build/read_test.go:1773,120159,165249` (header boundary, NUL, and
syntax recovery),
`cmd/go/testdata/script/test_relative_cmdline.txt:149`, and
`cmd/go/testdata/script/build_test_only.txt:118`. 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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"); };

View File

@@ -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;
}