build: omit named test source operands
This commit is contained in:
@@ -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
|
static u64
|
||||||
parseint(const char *s, u64 n, int base, int *ok)
|
parseint(const char *s, u64 n, int base, int *ok)
|
||||||
{
|
{
|
||||||
|
|||||||
145
cmd/wcc/parse.c
145
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
|
static Tok
|
||||||
peek(Parser *p)
|
peek(Parser *p)
|
||||||
{
|
{
|
||||||
@@ -1359,6 +1370,140 @@ parseuse(Parser *p)
|
|||||||
return n;
|
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 *
|
Node *
|
||||||
parseimports(Parser *p)
|
parseimports(Parser *p)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ struct Lex {
|
|||||||
|
|
||||||
void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len);
|
void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len);
|
||||||
Tok lexnext(Lex*);
|
Tok lexnext(Lex*);
|
||||||
|
int lexheaderimport(Lex*, Tok*);
|
||||||
const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */
|
const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */
|
||||||
void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */
|
void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */
|
||||||
Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */
|
Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */
|
||||||
@@ -408,6 +409,8 @@ struct Parser {
|
|||||||
|
|
||||||
void parserinit(Parser*, Arena*, Lex*);
|
void parserinit(Parser*, Arena*, Lex*);
|
||||||
Node *parsefile(Parser*);
|
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,
|
/* Imports-only N_FILE: module/pos identify the first package clause,
|
||||||
* list holds N_USE declarations, and body holds package-clause markers. */
|
* list holds N_USE declarations, and body holds package-clause markers. */
|
||||||
Node *parseimports(Parser*);
|
Node *parseimports(Parser*);
|
||||||
|
|||||||
125
cmd/ww/main.c
125
cmd/ww/main.c
@@ -693,6 +693,111 @@ source_package_name(const char *path, char **out)
|
|||||||
return 0;
|
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
|
static int
|
||||||
source_list_add(char ***list, int *n, int *cap, const char *path)
|
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;
|
struct stat requested;
|
||||||
int literal = stat(src, &requested) == 0;
|
int literal = stat(src, &requested) == 0;
|
||||||
|
int requested_nondirectory = literal && !S_ISDIR(requested.st_mode);
|
||||||
if (source_operand_ignored(src)) {
|
if (source_operand_ignored(src)) {
|
||||||
source_operand_no_sources(src);
|
source_operand_no_sources(src);
|
||||||
free(incs);
|
free(incs);
|
||||||
return 1;
|
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];
|
char resolved[PATH_MAX];
|
||||||
int is_dir = 0;
|
int is_dir = 0;
|
||||||
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
|
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
|
||||||
|
|||||||
@@ -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
|
format remains `19`, semantic storage format remains `3`, and there is no test
|
||||||
result cache.
|
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
|
## 12. Candidate architectures and hard-gate decision
|
||||||
|
|
||||||
Five candidates were developed as coherent systems, not as feature bins.
|
Five candidates were developed as coherent systems, not as feature bins.
|
||||||
|
|||||||
50
docs/spec.md
50
docs/spec.md
@@ -326,15 +326,38 @@ ImportPath = ident { "." ident } .
|
|||||||
eligible. Only the requested basename participates: a hidden parent does not
|
eligible. Only the requested basename participates: a hidden parent does not
|
||||||
hide visible `main.ww`, a hidden symlink spelling stays hidden for any
|
hide visible `main.ww`, a hidden symlink spelling stays hidden for any
|
||||||
existing non-directory target, and a visible symlink spelling stays eligible.
|
existing non-directory target, and a visible symlink spelling stays eligible.
|
||||||
The excluded operand creates no package, declaration, import binding/edge,
|
The prefix-excluded operand creates no package, declaration, import
|
||||||
action, artifact, initialization, test,
|
binding/edge, action, artifact, initialization, test, publication, or
|
||||||
publication, or persistent state. `ww build` reports the existing
|
persistent state. `ww build` reports the existing `directory contains no WW
|
||||||
`directory contains no WW package sources` condition; an explicit running
|
package sources` condition; an explicit running raw `ww test` also emits its
|
||||||
raw `ww test` also emits its command-owned `FAIL`, while `-c` and `-S` do not.
|
command-owned `FAIL`, while `-c` and `-S` do not.
|
||||||
This rule does not add multiple named-source package support, does not change
|
- A visible single raw operand passed to `ww build` whose requested final
|
||||||
visible `*_test.ww` handling, and does not apply to logical operands,
|
basename ends exactly `_test.ww` is test-only after its package clause and
|
||||||
directory/recursive requests, imports, or `ww run`. The diagnostic parent is
|
contiguous initial import section have been read. Prefix exclusion remains
|
||||||
presentation metadata and never canonical identity.
|
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
|
- `import acme.codec;` loads the canonical package `acme.codec`. If that
|
||||||
package declares `package wire;`, the importing file sees its exported names
|
package declares `package wire;`, the importing file sees its exported names
|
||||||
as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias
|
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,
|
Output paths and directory metadata never become package, import, graph, action,
|
||||||
symbol, artifact, `.wwi`, or persistence identity.
|
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
|
Every caller-visible build installation checks its destination after all
|
||||||
applicable compile, assemble, archive, and link producers finish. Ordinary
|
applicable compile, assemble, archive, and link producers finish. Ordinary
|
||||||
`stat` follows symlinks. An existing directory rejects as
|
`stat` follows symlinks. An existing directory rejects as
|
||||||
|
|||||||
@@ -239,6 +239,32 @@ platform classification, symlink spelling, output rollback, residue, and
|
|||||||
diagnostic/artifact parity. Directory package coordination and test process,
|
diagnostic/artifact parity. Directory package coordination and test process,
|
||||||
filter, timeout, signal, and descendant topology are unchanged.
|
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
|
List mode uses that same product process and initialization boundary but starts
|
||||||
no per-test child. The shared language harness emits only selected qualified
|
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
|
test names, one per line in descriptor order. A valid filter selecting no tests
|
||||||
|
|||||||
@@ -410,6 +410,75 @@ fn skipws(l: *lex) bool = {
|
|||||||
return false;
|
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 = {
|
fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = {
|
||||||
let v: u64 = 0u64;
|
let v: u64 = 0u64;
|
||||||
let b: u64 = base: u64;
|
let b: u64 = base: u64;
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ export type parser = struct {
|
|||||||
commandpackage: bool,
|
commandpackage: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn refill(p: *parser) void = {
|
fn installtok(p: *parser, t: *tok) void = {
|
||||||
let t: tok;
|
|
||||||
lexnext(p.l, &t);
|
|
||||||
p.curkind = t.kind;
|
p.curkind = t.kind;
|
||||||
p.curfile = t.file;
|
p.curfile = t.file;
|
||||||
p.curline = t.line;
|
p.curline = t.line;
|
||||||
@@ -70,6 +68,19 @@ fn refill(p: *parser) void = {
|
|||||||
p.curtsuffix = t.tsuffix;
|
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 = {
|
export fn parserinit(p: *parser, l: *lex) void = {
|
||||||
p.l = l;
|
p.l = l;
|
||||||
p.errs = 0;
|
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 = {
|
export fn parseimports(p: *parser) *node = {
|
||||||
let f = newnode(nkind.N_FILE, p.curfile, 1, 1);
|
let f = newnode(nkind.N_FILE, p.curfile, 1, 1);
|
||||||
let head: *node = nil;
|
let head: *node = nil;
|
||||||
|
|||||||
@@ -991,6 +991,102 @@ fn slurp(pathcs: *u8) (*u8, u64) = {
|
|||||||
return buf.ptr, nu;
|
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 = {
|
fn makestem(stem: *u8, src: *u8) void = {
|
||||||
let n: u64 = cstrlen(src);
|
let n: u64 = cstrlen(src);
|
||||||
let stop: u64 = n;
|
let stop: u64 = n;
|
||||||
@@ -8943,15 +9039,35 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
|||||||
nil, nil, false, true);
|
nil, nil, false, true);
|
||||||
};
|
};
|
||||||
let requestedliteral: bool = false;
|
let requestedliteral: bool = false;
|
||||||
|
let requestednondirectory: bool = false;
|
||||||
let requestedstat: os.filestat;
|
let requestedstat: os.filestat;
|
||||||
match (os.stat(&requestedstat, pathstr(src))) {
|
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;
|
case let e: os.oserror => void;
|
||||||
};
|
};
|
||||||
if (sourceoperandignored(src)) {
|
if (sourceoperandignored(src)) {
|
||||||
sourceoperandnosources(src);
|
sourceoperandnosources(src);
|
||||||
return 1;
|
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 isdir: i32 = 0;
|
||||||
let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir);
|
let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir);
|
||||||
if (resolved == nil) {
|
if (resolved == nil) {
|
||||||
|
|||||||
@@ -705,6 +705,136 @@ fn directoryisempty(path: str) bool = {
|
|||||||
return true;
|
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 = {
|
fn discardedlinkcleaned(marker: str) void = {
|
||||||
let output: str = readfile(marker);
|
let output: str = readfile(marker);
|
||||||
assert(output.len != 0);
|
assert(output.len != 0);
|
||||||
@@ -1335,6 +1465,493 @@ fn cwdwritedata(dir: str, label: str) void = {
|
|||||||
clean(root);
|
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
|
// A source file has one contiguous import section immediately after its
|
||||||
// package clause. The parser may continue after a late import for recovery,
|
// 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.
|
// but the imports-only loader and the full compiler parser both reject it.
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ package wwdump_test;
|
|||||||
// wwdump_ww's exit code is deliberately NOT gated: the diagnostics
|
// wwdump_ww's exit code is deliberately NOT gated: the diagnostics
|
||||||
// land on stderr regardless, and the armed bail flips the exit code —
|
// land on stderr regardless, and the armed bail flips the exit code —
|
||||||
// the line count is the one signal stable across the fold sequence.
|
// the line count is the one signal stable across the fold sequence.
|
||||||
// #90 sep-feed: `ww build -S` resolves import-bearing fixtures to an
|
// #90 sep-feed: `ww build -S` resolves production fixtures and `ww test
|
||||||
// owner unit and direct exports; this raw wwdump-only gate explicitly
|
// -S` resolves test fixtures to an owner unit and direct exports; this raw
|
||||||
// composes those artifacts because wwdump has no package-input CLI. The
|
// wwdump-only gate explicitly composes those artifacts because wwdump has no
|
||||||
// import-free test/wcc/901_*.ww companions stay raw-fed.
|
// 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
|
// wwdumpgate (#52, F15 c4) — the -c and -r arms gate on parse-stage
|
||||||
// errors instead of silently emitting over a broken AST: parse-errored
|
// errors instead of silently emitting over a broken AST: parse-errored
|
||||||
@@ -87,18 +88,20 @@ fn countdiag(stderr: str) i32 = {
|
|||||||
if (seps[i]) {
|
if (seps[i]) {
|
||||||
let stem: str = strings.concat(td, "/unit");
|
let stem: str = strings.concat(td, "/unit");
|
||||||
let bo: testenv.commandout;
|
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];
|
stem, src];
|
||||||
testenv.runcommand(td, td, "resolve", bav, tmo(), &bo);
|
testenv.runcommand(td, td, "resolve", bav, tmo(), &bo);
|
||||||
if (bo.termination != exec.termination.EXIT || bo.code != 0) {
|
if (bo.termination != exec.termination.EXIT || bo.code != 0) {
|
||||||
fail(classes[i], strings.concat(rels[i],
|
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 work: str = strings.concat(stem, ".sepwork/");
|
||||||
let owner: str = strings.concat(work, "__root.unit.ww");
|
let owner: str = strings.concat(work, "__root.unit.ww");
|
||||||
if (!testenv.exists(owner)) {
|
if (!testenv.exists(owner)) {
|
||||||
fail(classes[i], strings.concat(rels[i],
|
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 = [];
|
let deps: []str = [];
|
||||||
if (i == 0) { append(deps, "math.checked"); append(deps, "types"); };
|
if (i == 0) { append(deps, "math.checked"); append(deps, "types"); };
|
||||||
|
|||||||
@@ -100,6 +100,40 @@ imports_to_str(const char *src, int *errs, const char **pkg)
|
|||||||
return buf;
|
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
|
static int
|
||||||
must_import_shape(const char *src, int imports_only, const char *binding,
|
must_import_shape(const char *src, int imports_only, const char *binding,
|
||||||
const char *path, const char *alias)
|
const char *path, const char *alias)
|
||||||
@@ -300,6 +334,119 @@ main(void)
|
|||||||
free((void *)pkg);
|
free((void *)pkg);
|
||||||
free(got);
|
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;
|
int errs;
|
||||||
const char *pkg;
|
const char *pkg;
|
||||||
@@ -333,6 +480,6 @@ main(void)
|
|||||||
fprintf(stderr, "%d parse tests failed\n", fail);
|
fprintf(stderr, "%d parse tests failed\n", fail);
|
||||||
return 1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user