build: omit named test source operands

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

View File

@@ -358,6 +358,78 @@ skipws(Lex *l)
}
}
/* Return the next initial import token without ever lexing the first ordinary
* declaration token. Go's named-file reader consumes header trivia while
* looking for another import, so NUL/comment diagnostics in that trivia remain
* visible; a malformed UTF-8 byte or non-leading BOM that itself begins the
* ordinary body is outside the header and must not be diagnosed here. */
int
lexheaderimport(Lex *l, Tok *out)
{
for (;;) {
if (l->pos >= l->srclen)
return 0;
unsigned char c = (unsigned char)l->src[l->pos];
if (c == 0) {
/* The loader necessarily reaches this byte while deciding whether
* another import follows. Preserve the source decoder diagnostic. */
lskipnul(l);
return 0;
}
if ((c >= 0x80 && !utf8bytevalid(l->src, l->srclen, l->pos))
|| bomat(l->src, l->srclen, l->pos))
return 0;
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
lget(l);
continue;
}
if (c == '/' && l->pos + 1 < l->srclen
&& l->src[l->pos + 1] == '/') {
lget(l); lget(l);
linecomment(l);
if (l->errs != 0)
return 0;
continue;
}
if (c == '/' && l->pos + 1 < l->srclen
&& l->src[l->pos + 1] == '*') {
lget(l); lget(l);
int prev = -1;
for (;;) {
int x = lget(l);
if (x < 0) {
Pos p = lpos(l);
errorf(p, "unterminated /* comment");
l->errs++;
return 0;
}
if (prev == '*' && x == '/')
break;
prev = x;
}
if (l->errs != 0)
return 0;
continue;
}
break;
}
/* A compiler-owned source-boundary directive is an ordinary declaration
* boundary here, just as it was when lexnext eagerly exposed the token. */
if (l->modreset || l->modpath != NULL)
return 0;
static const char kw[] = "import";
const u64 n = sizeof kw - 1;
if (l->srclen - l->pos < n
|| memcmp(l->src + l->pos, kw, n) != 0)
return 0;
if (l->srclen - l->pos > n
&& isidcont((unsigned char)l->src[l->pos + n]))
return 0;
*out = lexnext(l);
return out->kind == TK_USE;
}
static u64
parseint(const char *s, u64 n, int base, int *ok)
{

View File

@@ -35,6 +35,17 @@ advance(Parser *p)
}
}
static int
advanceheaderimport(Parser *p)
{
Tok next;
if (!lexheaderimport(p->l, &next))
return 0;
p->cur = next;
p->hasla = 0;
return 1;
}
static Tok
peek(Parser *p)
{
@@ -1359,6 +1370,140 @@ parseuse(Parser *p)
return n;
}
/* Parse one initial import without pulling the declaration that follows it
* into the header pass. This intentionally has stricter, single-error
* recovery than parseuse: named test-source build loading needs only decide
* whether the package/import header itself is valid. */
static Node *
parseheaderuse(Parser *p)
{
Pos pp = p->cur.pos;
advance(p);
Node *n = newnode(p->a, N_USE, pp);
n->usefile = p->cur.pos.file;
n->useline = p->cur.pos.line;
n->usecol = p->cur.pos.col;
const char *alias = NULL;
const char *first;
if (p->cur.kind == TK_UNDER) {
n->useblank = 1;
advance(p);
} else if (p->cur.kind != TK_IDENT) {
errorf(p->cur.pos, "expected identifier, got %s",
tokname(p->cur.kind));
p->errs++;
return n;
}
if (p->cur.kind != TK_IDENT) {
errorf(p->cur.pos, "expected identifier, got %s",
tokname(p->cur.kind));
p->errs++;
return n;
}
first = p->cur.text;
advance(p);
if (!n->useblank && p->cur.kind == TK_IDENT) {
alias = first;
first = p->cur.text;
advance(p);
}
const char *leaf = first;
const char *path = leaf;
while (p->cur.kind == TK_DOT) {
advance(p);
if (p->cur.kind != TK_IDENT) {
errorf(p->cur.pos, "expected identifier, got %s",
tokname(p->cur.kind));
p->errs++;
return n;
}
leaf = p->cur.text;
path = aprintf(p->a, "%s.%s", path, leaf);
advance(p);
}
if (!n->useblank) {
n->str = alias ? alias : leaf;
n->strlen = strlen(n->str);
}
n->usesource = path;
n->usepath = path;
n->usealias = alias;
if (p->cur.kind != TK_SEMI) {
errorf(p->cur.pos, "expected ';' after import");
p->errs++;
return n;
}
return n;
}
/* Go's named-file loader parses a valid source only through its initial
* package/import section. Keep the broader parseimports recovery pass for
* graph-bearing sources, but give actionless test-source omission a boundary
* that cannot diagnose late imports or an invalid ordinary declaration body. */
Node *
parsepackageheader(Parser *p)
{
Pos fp = { p->l->file, 1, 1 };
Node *file = newnode(p->a, N_FILE, fp);
Node *head = NULL, *tail = NULL;
while (p->cur.kind == TK_MODPATH || p->cur.kind == TK_MODRESET) {
if (p->cur.kind == TK_MODPATH) {
p->pathmod = p->cur.text;
p->curmod = p->cur.text;
p->resetmod = NULL;
} else {
p->pathmod = NULL;
p->curmod = p->cur.text;
p->resetmod = p->cur.text;
}
p->sourceid++;
advance(p);
}
if (p->cur.kind != TK_MODULE) {
errorf(p->cur.pos, "invalid or missing package clause");
p->errs++;
return file;
}
Pos pp = p->cur.pos;
advance(p);
if (p->cur.kind != TK_IDENT) {
errorf(p->cur.pos, "invalid or missing package clause");
p->errs++;
return file;
}
const char *name = p->cur.text;
advance(p);
if (p->cur.kind != TK_SEMI) {
errorf(p->cur.pos, "expected ';' after package name");
p->errs++;
return file;
}
p->curpkg = name;
if (p->pathmod == NULL && p->resetmod == NULL)
p->curmod = name;
file->module = name;
file->pkgname = name;
file->sourceid = p->sourceid;
file->pos = pp;
while (advanceheaderimport(p)) {
Node *d = parseheaderuse(p);
d->module = p->curmod;
d->pkgname = p->curpkg;
d->sourceid = p->sourceid;
if (head == NULL)
head = d;
else
tail->next = d;
tail = d;
if (p->errs != 0)
break;
}
file->list = head;
return file;
}
Node *
parseimports(Parser *p)
{

View File

@@ -218,6 +218,7 @@ struct Lex {
void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len);
Tok lexnext(Lex*);
int lexheaderimport(Lex*, Tok*);
const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */
void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */
Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */
@@ -408,6 +409,8 @@ struct Parser {
void parserinit(Parser*, Arena*, Lex*);
Node *parsefile(Parser*);
/* Package/import header only: stop before the first ordinary declaration. */
Node *parsepackageheader(Parser*);
/* Imports-only N_FILE: module/pos identify the first package clause,
* list holds N_USE declarations, and body holds package-clause markers. */
Node *parseimports(Parser*);

View File

@@ -693,6 +693,111 @@ source_package_name(const char *path, char **out)
return 0;
}
/* Named-file loading is allowed for every non-directory Stat result. Unlike
* sep_slurp, this reader must not require seekability: a finite FIFO is still
* a named source stream, just as it is for GoFilesPackage. */
static int
source_header_slurp(const char *path, char **out, u64 *len)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
size_t cap = 4096, n = 0;
char *buf = malloc(cap);
if (buf == NULL) {
(void)sep_fail_nomem();
(void)close(fd);
return -1;
}
for (;;) {
if (n == cap) {
if (cap >= INT_MAX) {
(void)sep_fail_size();
free(buf);
(void)close(fd);
return -1;
}
size_t nextcap = cap > (size_t)INT_MAX / 2
? (size_t)INT_MAX : cap * 2;
char *next = realloc(buf, nextcap);
if (next == NULL) {
(void)sep_fail_nomem();
free(buf);
(void)close(fd);
return -1;
}
buf = next;
cap = nextcap;
}
ssize_t got = read(fd, buf + n, cap - n);
if (got < 0) {
if (errno == EINTR) continue;
free(buf);
(void)close(fd);
return -1;
}
if (got == 0) break;
n += (size_t)got;
}
if (close(fd) != 0) {
free(buf);
return -1;
}
if (n == cap) {
/* The loop normally grows before the read that can fill cap, but
* retain an explicit terminator guard for future reader changes. */
if (cap >= INT_MAX) {
(void)sep_fail_size();
free(buf);
return -1;
}
char *next = realloc(buf, cap + 1);
if (next == NULL) {
(void)sep_fail_nomem();
free(buf);
return -1;
}
buf = next;
}
buf[n] = '\0';
*out = buf;
*len = (u64)n;
return 0;
}
/* Validate only the package/import header before a named test source is
* omitted from an ordinary build. Imports are syntax, not graph edges here. */
static int
source_build_header(const char *path)
{
char *buf;
u64 len;
if (source_header_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return -1;
}
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, path, buf, len);
parserinit(&p, a, &l);
Node *header = parsepackageheader(&p);
if (l.errs || p.errs) {
freearena(a);
free(buf);
return -1;
}
if (header->module == NULL) {
Pos pp = { path, 1, 1 };
errorf(pp, "invalid or missing package clause");
freearena(a);
free(buf);
return -1;
}
freearena(a);
free(buf);
return 0;
}
static int
source_list_add(char ***list, int *n, int *cap, const char *path)
{
@@ -7474,11 +7579,31 @@ do_build(int argc, char **argv)
}
struct stat requested;
int literal = stat(src, &requested) == 0;
int requested_nondirectory = literal && !S_ISDIR(requested.st_mode);
if (source_operand_ignored(src)) {
source_operand_no_sources(src);
free(incs);
return 1;
}
const char *requested_base = strrchr(src, '/');
requested_base = requested_base ? requested_base + 1 : src;
size_t requested_baselen = strlen(requested_base);
if (requested_nondirectory && requested_baselen >= 8
&& strcmp(requested_base + requested_baselen - 8,
"_test.ww") == 0) {
if (source_build_header(src) < 0) {
free(incs);
return 1;
}
free(incs);
if (!outflag[0] || strcmp(outflag, "/dev/null") == 0)
return 0;
if (build_output_dir(outflag))
fputs("ww: no main packages to build\n", stderr);
else
fputs("ww: no packages to build\n", stderr);
return 1;
}
char resolved[PATH_MAX];
int is_dir = 0;
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {