ww source: reject raw NUL characters
This commit is contained in:
296
cmd/wcc/lex.c
296
cmd/wcc/lex.c
@@ -34,22 +34,74 @@ lexinit(Lex *l, Arena *a, const char *file, const char *src, u64 len)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
lskipnul(Lex *l)
|
||||||
|
{
|
||||||
|
while (l->pos < l->srclen && l->src[l->pos] == '\0') {
|
||||||
|
Pos p = { l->file, l->line, l->col };
|
||||||
|
l->pos++;
|
||||||
|
l->col++;
|
||||||
|
errorf(p, "invalid NUL character");
|
||||||
|
l->errs++;
|
||||||
|
l->nulcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the raw offset of a logical byte lookahead. Raw NUL bytes do not
|
||||||
|
* occupy a slot in the token stream: Go's source.nextch diagnoses them and
|
||||||
|
* immediately resumes decoding at the following character. */
|
||||||
|
static u64
|
||||||
|
lrawoff(Lex *l, u64 ahead)
|
||||||
|
{
|
||||||
|
u64 p = l->pos;
|
||||||
|
for (;;) {
|
||||||
|
while (p < l->srclen && l->src[p] == '\0')
|
||||||
|
p++;
|
||||||
|
if (ahead == 0 || p >= l->srclen)
|
||||||
|
return p;
|
||||||
|
p++;
|
||||||
|
ahead--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
lpeek(Lex *l, u64 ahead)
|
lpeek(Lex *l, u64 ahead)
|
||||||
{
|
{
|
||||||
u64 p = l->pos + ahead;
|
/* Drain NUL at the current decoder position even when it is followed by
|
||||||
|
* EOF; otherwise a trailing NUL could disappear without a diagnostic. */
|
||||||
|
for (;;) {
|
||||||
|
if (l->pos >= l->srclen)
|
||||||
|
return -1;
|
||||||
|
int c = (unsigned char)l->src[l->pos];
|
||||||
|
if (c == 0) {
|
||||||
|
lskipnul(l);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ahead == 0) {
|
||||||
|
if (bomat(l->src, l->srclen, l->pos))
|
||||||
|
return 0xfeff;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
u64 p = lrawoff(l, ahead);
|
||||||
if (p >= l->srclen)
|
if (p >= l->srclen)
|
||||||
return -1;
|
return -1;
|
||||||
if (bomat(l->src, l->srclen, p))
|
if (bomat(l->src, l->srclen, p))
|
||||||
return 0xfeff;
|
return 0xfeff;
|
||||||
return (unsigned char)l->src[p];
|
return (unsigned char)l->src[p];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
lget(Lex *l)
|
lget(Lex *l)
|
||||||
{
|
{
|
||||||
|
for (;;) {
|
||||||
if (l->pos >= l->srclen)
|
if (l->pos >= l->srclen)
|
||||||
return -1;
|
return -1;
|
||||||
|
int c = (unsigned char)l->src[l->pos];
|
||||||
|
if (c == 0) {
|
||||||
|
lskipnul(l);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (bomat(l->src, l->srclen, l->pos)) {
|
if (bomat(l->src, l->srclen, l->pos)) {
|
||||||
Pos p = { l->file, l->line, l->col };
|
Pos p = { l->file, l->line, l->col };
|
||||||
l->pos += 3;
|
l->pos += 3;
|
||||||
@@ -58,7 +110,7 @@ lget(Lex *l)
|
|||||||
l->errs++;
|
l->errs++;
|
||||||
return 0xfeff;
|
return 0xfeff;
|
||||||
}
|
}
|
||||||
int c = (unsigned char)l->src[l->pos++];
|
l->pos++;
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
l->line++;
|
l->line++;
|
||||||
l->col = 1;
|
l->col = 1;
|
||||||
@@ -66,6 +118,7 @@ lget(Lex *l)
|
|||||||
l->col++;
|
l->col++;
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Pos
|
static Pos
|
||||||
@@ -75,6 +128,22 @@ lpos(Lex *l)
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Copy one raw source span into token text while omitting diagnosed NUL
|
||||||
|
* bytes. This keeps keyword, numeric, suffix, and directive recovery on the
|
||||||
|
* same logical character stream as lpeek/lget. */
|
||||||
|
static char *
|
||||||
|
lexspan(Lex *l, u64 begin, u64 end, u64 *len)
|
||||||
|
{
|
||||||
|
char *s = amalloc(l->a, end - begin + 1);
|
||||||
|
u64 j = 0;
|
||||||
|
for (u64 i = begin; i < end; i++)
|
||||||
|
if (l->src[i] != '\0')
|
||||||
|
s[j++] = l->src[i];
|
||||||
|
s[j] = '\0';
|
||||||
|
*len = j;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
isidstart(int c)
|
isidstart(int c)
|
||||||
{
|
{
|
||||||
@@ -94,6 +163,70 @@ ishex(int c)
|
|||||||
(c >= 'A' && c <= 'F');
|
(c >= 'A' && c <= 'F');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Consume one // body, then classify the driver's internal module directive
|
||||||
|
* from its logical (NUL-filtered) text. Keeping a single moving lexer cursor
|
||||||
|
* makes even long generated module paths linear rather than repeatedly
|
||||||
|
* rescanning from the start of the comment. The trailing newline remains for
|
||||||
|
* the ordinary whitespace loop. */
|
||||||
|
static void
|
||||||
|
linecomment(Lex *l)
|
||||||
|
{
|
||||||
|
u64 begin = l->pos;
|
||||||
|
u64 nulbegin = l->nulcount;
|
||||||
|
int c;
|
||||||
|
while ((c = lpeek(l, 0)) >= 0 && c != '\n')
|
||||||
|
lget(l);
|
||||||
|
u64 end = l->pos;
|
||||||
|
u64 n;
|
||||||
|
const char *body;
|
||||||
|
if (l->nulcount == nulbegin) {
|
||||||
|
n = end - begin;
|
||||||
|
body = l->src + begin;
|
||||||
|
} else {
|
||||||
|
body = lexspan(l, begin, end, &n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char pre[] = "ww:module";
|
||||||
|
const u64 plen = sizeof pre - 1;
|
||||||
|
if (n < plen || memcmp(body, pre, plen) != 0 || n == plen)
|
||||||
|
return;
|
||||||
|
u64 i = plen;
|
||||||
|
if (body[i] == '-') {
|
||||||
|
static const char rest[] = "-reset";
|
||||||
|
const u64 rlen = sizeof rest - 1;
|
||||||
|
if (n - i < rlen || memcmp(body + i, rest, rlen) != 0)
|
||||||
|
return;
|
||||||
|
i += rlen;
|
||||||
|
if (i == n) {
|
||||||
|
l->modreset = 1;
|
||||||
|
l->modpath = NULL; /* #9: reset supersedes a pending path */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body[i] != ' ' && body[i] != '\t')
|
||||||
|
return;
|
||||||
|
while (i < n && (body[i] == ' ' || body[i] == '\t'))
|
||||||
|
i++;
|
||||||
|
u64 s = i;
|
||||||
|
while (i < n && body[i] != '\r' && body[i] != ' '
|
||||||
|
&& body[i] != '\t')
|
||||||
|
i++;
|
||||||
|
l->modreset = 1;
|
||||||
|
l->modpath = NULL; /* #9: see above */
|
||||||
|
if (i > s)
|
||||||
|
l->modresetpath = astrndup(l->a, body + s, i - s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body[i] != ' ' && body[i] != '\t')
|
||||||
|
return;
|
||||||
|
while (i < n && (body[i] == ' ' || body[i] == '\t'))
|
||||||
|
i++;
|
||||||
|
u64 s = i;
|
||||||
|
while (i < n && body[i] != '\r' && body[i] != ' ' && body[i] != '\t')
|
||||||
|
i++;
|
||||||
|
if (i > s)
|
||||||
|
l->modpath = astrndup(l->a, body + s, i - s);
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
skipws(Lex *l)
|
skipws(Lex *l)
|
||||||
{
|
{
|
||||||
@@ -107,78 +240,9 @@ skipws(Lex *l)
|
|||||||
}
|
}
|
||||||
if (c == '/' && lpeek(l, 1) == '/') {
|
if (c == '/' && lpeek(l, 1) == '/') {
|
||||||
lget(l); lget(l);
|
lget(l); lget(l);
|
||||||
/* #16 option-B: the driver emits `//ww:module-reset`
|
/* #16 option-B: classify the driver's whole-line internal
|
||||||
* before a package-less file's bytes; recognize the
|
* module boundary after consuming it once. */
|
||||||
* whole-line directive (without consuming differently)
|
linecomment(l);
|
||||||
* and flag it so lexnext emits TK_MODRESET. The body is
|
|
||||||
* then skipped like any comment. Mirrors the removed
|
|
||||||
* `// MODULE:` lexer directive. */
|
|
||||||
{
|
|
||||||
static const char pre[] = "ww:module";
|
|
||||||
size_t i = 0;
|
|
||||||
while (pre[i] && lpeek(l, i) == pre[i])
|
|
||||||
i++;
|
|
||||||
if (pre[i] == '\0') {
|
|
||||||
int nx = lpeek(l, i);
|
|
||||||
if (nx == '-') {
|
|
||||||
static const char rest[] = "-reset";
|
|
||||||
size_t j = 0;
|
|
||||||
while (rest[j] && lpeek(l, i + j) == rest[j])
|
|
||||||
j++;
|
|
||||||
if (rest[j] == '\0') {
|
|
||||||
int af = lpeek(l, i + j);
|
|
||||||
if (af == '\n' || af < 0) {
|
|
||||||
l->modreset = 1;
|
|
||||||
l->modpath = NULL; /* #9: a reset supersedes a path opened
|
|
||||||
* earlier in this skipws run (empty/
|
|
||||||
* export-less inlined module body) */
|
|
||||||
} else if (af == ' '
|
|
||||||
|| af == '\t') {
|
|
||||||
/* `//ww:module-reset <path>`
|
|
||||||
* — sep primary body tagged by
|
|
||||||
* its full dotted import path so
|
|
||||||
* definer == importer (#57). */
|
|
||||||
size_t k = i + j;
|
|
||||||
while (lpeek(l, k) == ' '
|
|
||||||
|| lpeek(l, k) == '\t')
|
|
||||||
k++;
|
|
||||||
size_t s = k;
|
|
||||||
int dch;
|
|
||||||
while ((dch = lpeek(l, k)) >= 0
|
|
||||||
&& dch != '\n'
|
|
||||||
&& dch != '\r'
|
|
||||||
&& dch != ' '
|
|
||||||
&& dch != '\t')
|
|
||||||
k++;
|
|
||||||
l->modreset = 1;
|
|
||||||
l->modpath = NULL; /* #9: see above — clear pending path */
|
|
||||||
if (k > s)
|
|
||||||
l->modresetpath =
|
|
||||||
astrndup(l->a,
|
|
||||||
l->src + l->pos + s,
|
|
||||||
k - s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (nx == ' ' || nx == '\t') {
|
|
||||||
/* `//ww:module <path>` — M1 import boundary. */
|
|
||||||
size_t k = i;
|
|
||||||
while (lpeek(l, k) == ' '
|
|
||||||
|| lpeek(l, k) == '\t')
|
|
||||||
k++;
|
|
||||||
size_t s = k;
|
|
||||||
int ch;
|
|
||||||
while ((ch = lpeek(l, k)) >= 0
|
|
||||||
&& ch != '\n' && ch != '\r'
|
|
||||||
&& ch != ' ' && ch != '\t')
|
|
||||||
k++;
|
|
||||||
if (k > s)
|
|
||||||
l->modpath = astrndup(l->a,
|
|
||||||
l->src + l->pos + s, k - s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while ((c = lpeek(l, 0)) >= 0 && c != '\n')
|
|
||||||
lget(l);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (c == '/' && lpeek(l, 1) == '*') {
|
if (c == '/' && lpeek(l, 1) == '*') {
|
||||||
@@ -318,11 +382,38 @@ escape(Lex *l, int *out)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
lextypesuffix(Lex *l, u64 *len)
|
||||||
|
{
|
||||||
|
char got[4];
|
||||||
|
u64 n = 0;
|
||||||
|
while (n < sizeof got && isidcont(lpeek(l, n))) {
|
||||||
|
got[n] = (char)lpeek(l, n);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (n == 0 || n > 3 || isidcont(lpeek(l, n)))
|
||||||
|
return NULL;
|
||||||
|
static const char *const names[] = {
|
||||||
|
"i8", "i16", "i32", "i64",
|
||||||
|
"u8", "u16", "u32", "u64",
|
||||||
|
"f32", "f64", NULL
|
||||||
|
};
|
||||||
|
for (int i = 0; names[i]; i++) {
|
||||||
|
u64 nl = strlen(names[i]);
|
||||||
|
if (nl == n && memcmp(names[i], got, n) == 0) {
|
||||||
|
*len = n;
|
||||||
|
return names[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
static Tok
|
static Tok
|
||||||
lexnum(Lex *l, Pos start)
|
lexnum(Lex *l, Pos start)
|
||||||
{
|
{
|
||||||
Tok t = (Tok){ TK_INT, start, NULL, 0, {0}, TK_NONE };
|
Tok t = (Tok){ TK_INT, start, NULL, 0, {0}, TK_NONE };
|
||||||
u64 begin = l->pos;
|
u64 begin = l->pos;
|
||||||
|
u64 nulbegin = l->nulcount;
|
||||||
int base = 10;
|
int base = 10;
|
||||||
int isfloat = 0;
|
int isfloat = 0;
|
||||||
int c = lpeek(l, 0);
|
int c = lpeek(l, 0);
|
||||||
@@ -361,8 +452,14 @@ lexnum(Lex *l, Pos start)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 n = l->pos - begin;
|
u64 rawend = l->pos;
|
||||||
|
u64 n;
|
||||||
|
if (l->nulcount == nulbegin) {
|
||||||
|
n = rawend - begin;
|
||||||
t.text = astrndup(l->a, l->src + begin, n);
|
t.text = astrndup(l->a, l->src + begin, n);
|
||||||
|
} else {
|
||||||
|
t.text = lexspan(l, begin, rawend, &n);
|
||||||
|
}
|
||||||
t.tlen = n;
|
t.tlen = n;
|
||||||
|
|
||||||
if (isfloat) {
|
if (isfloat) {
|
||||||
@@ -371,8 +468,8 @@ lexnum(Lex *l, Pos start)
|
|||||||
char *clean = amalloc(l->a, n + 1);
|
char *clean = amalloc(l->a, n + 1);
|
||||||
u64 j = 0;
|
u64 j = 0;
|
||||||
for (u64 i = 0; i < n; i++)
|
for (u64 i = 0; i < n; i++)
|
||||||
if (l->src[begin + i] != '_')
|
if (t.text[i] != '_')
|
||||||
clean[j++] = l->src[begin + i];
|
clean[j++] = t.text[i];
|
||||||
clean[j] = '\0';
|
clean[j] = '\0';
|
||||||
errno = 0;
|
errno = 0;
|
||||||
t.v.fval = strtod(clean, NULL);
|
t.v.fval = strtod(clean, NULL);
|
||||||
@@ -381,7 +478,7 @@ lexnum(Lex *l, Pos start)
|
|||||||
l->errs++;
|
l->errs++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const char *digs = l->src + begin;
|
const char *digs = t.text;
|
||||||
u64 dn = n;
|
u64 dn = n;
|
||||||
if (base != 10) {
|
if (base != 10) {
|
||||||
digs += 2;
|
digs += 2;
|
||||||
@@ -398,28 +495,12 @@ lexnum(Lex *l, Pos start)
|
|||||||
|
|
||||||
/* A typed suffix must be glued (no whitespace) to the digits. */
|
/* A typed suffix must be glued (no whitespace) to the digits. */
|
||||||
if (isidstart(lpeek(l, 0))) {
|
if (isidstart(lpeek(l, 0))) {
|
||||||
u64 sb = l->pos;
|
u64 sl;
|
||||||
i32 sc = l->col;
|
const char *match = lextypesuffix(l, &sl);
|
||||||
while (isidcont(lpeek(l, 0))) lget(l);
|
|
||||||
u64 sl = l->pos - sb;
|
|
||||||
const char *names[] = {
|
|
||||||
"i8", "i16", "i32", "i64",
|
|
||||||
"u8", "u16", "u32", "u64",
|
|
||||||
"f32", "f64", NULL
|
|
||||||
};
|
|
||||||
const char *match = NULL;
|
|
||||||
for (int i = 0; names[i]; i++) {
|
|
||||||
u64 nl = strlen(names[i]);
|
|
||||||
if (nl == sl && memcmp(names[i], l->src + sb, nl) == 0) {
|
|
||||||
match = names[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
if (match) {
|
||||||
t.tsuffix = astrndup(l->a, l->src + sb, sl);
|
for (u64 i = 0; i < sl; i++)
|
||||||
} else {
|
lget(l);
|
||||||
l->pos = sb;
|
t.tsuffix = astrndup(l->a, match, sl);
|
||||||
l->col = sc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
@@ -429,18 +510,27 @@ static Tok
|
|||||||
lexident(Lex *l, Pos start)
|
lexident(Lex *l, Pos start)
|
||||||
{
|
{
|
||||||
u64 begin = l->pos;
|
u64 begin = l->pos;
|
||||||
|
u64 nulbegin = l->nulcount;
|
||||||
while (isidcont(lpeek(l, 0)))
|
while (isidcont(lpeek(l, 0)))
|
||||||
lget(l);
|
lget(l);
|
||||||
u64 n = l->pos - begin;
|
u64 n;
|
||||||
const char *p = l->src + begin;
|
const char *p;
|
||||||
|
int filtered = l->nulcount != nulbegin;
|
||||||
|
if (!filtered) {
|
||||||
|
n = l->pos - begin;
|
||||||
|
p = l->src + begin;
|
||||||
|
} else {
|
||||||
|
p = lexspan(l, begin, l->pos, &n);
|
||||||
|
}
|
||||||
|
char *text = filtered ? (char *)p : astrndup(l->a, p, n);
|
||||||
/* bare '_' is the discard marker. `_x`, `_1` are normal idents. */
|
/* bare '_' is the discard marker. `_x`, `_1` are normal idents. */
|
||||||
if (n == 1 && p[0] == '_') {
|
if (n == 1 && p[0] == '_') {
|
||||||
Tok t = (Tok){ TK_UNDER, start, astrndup(l->a, p, n), n, {0}, TK_NONE };
|
Tok t = (Tok){ TK_UNDER, start, text, n, {0}, TK_NONE };
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
Tkind k = kwlookup(p, n);
|
Tkind k = kwlookup(p, n);
|
||||||
Tok t = (Tok){ k != TK_NONE ? k : TK_IDENT, start,
|
Tok t = (Tok){ k != TK_NONE ? k : TK_IDENT, start,
|
||||||
astrndup(l->a, p, n), n, {0}, TK_NONE };
|
text, n, {0}, TK_NONE };
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ struct Lex {
|
|||||||
i32 col;
|
i32 col;
|
||||||
Arena *a; /* token-text arena */
|
Arena *a; /* token-text arena */
|
||||||
int errs;
|
int errs;
|
||||||
|
u64 nulcount; /* raw NUL bytes diagnosed by the source decoder */
|
||||||
int modreset; /* a `//ww:module-reset` directive was seen in
|
int modreset; /* a `//ww:module-reset` directive was seen in
|
||||||
* the last skipped run; lexnext emits TK_MODRESET
|
* the last skipped run; lexnext emits TK_MODRESET
|
||||||
* before the next real token. */
|
* before the next real token. */
|
||||||
|
|||||||
@@ -8545,6 +8545,119 @@ and diagnostic text is not a persisted-byte contract. Build workdir format
|
|||||||
remains `18`, test workdir format remains `19`, and semantic storage format
|
remains `18`, test workdir format remains `19`, and semantic storage format
|
||||||
remains `3`.
|
remains `3`.
|
||||||
|
|
||||||
|
### 11.43 Implemented selected-source U+0000 rejection
|
||||||
|
|
||||||
|
Every selected physical `.ww` source rejects a raw byte `00` (U+0000) at its
|
||||||
|
physical line and byte column with exactly `invalid NUL character`. The rule is
|
||||||
|
source-wide: comments, interpreted-string text, rune text, and between-token
|
||||||
|
positions cannot turn a raw NUL into payload. An escape spelling such as
|
||||||
|
`\x00` remains a legal literal value because it is not byte `00` in
|
||||||
|
the source file. This section is only the raw-U+0000 rule: malformed UTF-8
|
||||||
|
and the independently implemented per-source BOM boundary in §11.42 are not
|
||||||
|
changed or broadened here.
|
||||||
|
|
||||||
|
#### Pinned evidence, applicability, and measured prior behavior
|
||||||
|
|
||||||
|
The sole authority is official Go 1.26.5 at
|
||||||
|
`c19862e5f8415b4f24b189d065ed739517c548ba`. Its compiler owner,
|
||||||
|
`(*source).nextch` in
|
||||||
|
[`cmd/compile/internal/syntax/source.go`, lines 113–165](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/source.go#L113-L165),
|
||||||
|
detects ASCII zero at lines 121–129, reports `invalid NUL character`, and
|
||||||
|
continues decoding. `TestScanErrors` pins the positioned diagnostic at
|
||||||
|
[`cmd/compile/internal/syntax/scanner_test.go`, lines 587–600](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner_test.go#L587-L600),
|
||||||
|
and compiler testdata [`test/nul1.go`, lines 7–52](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/nul1.go#L7-L52)
|
||||||
|
requires NUL errors in strings, raw strings, line/block comments, and ordinary
|
||||||
|
source. Go's independent public scanner has the same rule in
|
||||||
|
[`go/scanner/scanner.go`, lines 63–108](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/scanner/scanner.go#L63-L108)
|
||||||
|
and its tests at lines 790–819. These are **behavior directly implemented or
|
||||||
|
asserted by pinned Go**.
|
||||||
|
|
||||||
|
Before this change, directly measured Cstage and WWstage package builds both
|
||||||
|
accepted a NUL in a line comment, wrote it into the committed synthetic unit,
|
||||||
|
published byte-identical archive/interface products, and allowed a dotted local
|
||||||
|
importer to link and run. The retained audit commands used `out/bin/ww` and
|
||||||
|
`out/bin/ww_ww` with `WW_SRCLIB=/home/kimchi/src/ww/lib` over
|
||||||
|
`/tmp/ww-pkgaudit.qtjcpO/src/nulcomment`; both exited zero with empty streams.
|
||||||
|
The same acceptance applied to raw NUL in strings and runes. Those observations
|
||||||
|
are **directly measured WW behavior**. Applying pinned Go's physical-source
|
||||||
|
rule independently to WW's selected files is **behavior derived from the
|
||||||
|
pinned implementation** and is applicable without importing Go's module,
|
||||||
|
manifest, registry, lock, cache, database, CAS, network, generalized-import,
|
||||||
|
or source-level build-expression model.
|
||||||
|
|
||||||
|
#### Ownership, timing, and four axes
|
||||||
|
|
||||||
|
The direct-frontend semantic owners are the logical source-decoder helpers in
|
||||||
|
`cmd/wcc/lex.c` and their exact twins in `lib/ww/syntax/lex.ww`: each raw zero
|
||||||
|
is consumed, reports the same positioned error, and is omitted before token
|
||||||
|
recovery. Filtered token spans preserve that decoder rule through identifiers,
|
||||||
|
numbers and suffixes, directives, escapes, operators, comments, and EOF. The
|
||||||
|
line-comment decoder consumes each body once before classifying an internal
|
||||||
|
module directive, so NUL filtering does not make generated path handling
|
||||||
|
superlinear. The
|
||||||
|
shared `internal/wwpackage/package.ww` coordinator owns public package/test
|
||||||
|
diagnostic precedence: its length-aware preflight reports every raw NUL in an
|
||||||
|
invalid physical source before its manual package-clause classifier. It is not
|
||||||
|
another identity policy. Driver source slurping and synthetic-unit composition
|
||||||
|
preserve byte lengths and are not semantic owners.
|
||||||
|
|
||||||
|
- **Go-like build:** after fixed-target filename selection, an invalid selected
|
||||||
|
root or dependency rejects during loading, before graph completion and before
|
||||||
|
compiler, assembler, archiver, linker, install, publication, or execution.
|
||||||
|
Source rejection in a root precedes resolution of that root's missing
|
||||||
|
imports. A wrong-target file is excluded before this rule and remains unread
|
||||||
|
by its semantic owners.
|
||||||
|
- **Go-like test:** selected production, same-package, external-package, and
|
||||||
|
test-only source each receive the rule before variant construction. Failure
|
||||||
|
emits the established attributable `FAIL\n` without a generated main, test
|
||||||
|
process, accounting, `ok` result, retained binary, or public test product.
|
||||||
|
- **Go-like package:** each selected physical file owns its diagnostic and
|
||||||
|
position. Declared package name, source role, package conflict handling,
|
||||||
|
command/test family, physical directory, and exact canonical dotted identity
|
||||||
|
are unchanged.
|
||||||
|
- **Go-like import:** invalid bytes create no import edge or graph node. Valid
|
||||||
|
import spelling, aliases, local/vendor/internal resolution, visibility,
|
||||||
|
cycle handling, and initialization order remain unchanged.
|
||||||
|
|
||||||
|
Thus raw NUL is never an input to manifest-free package identity, graph/action
|
||||||
|
keys, symbols, `.wwi`, archive naming, publication names, or persistence keys.
|
||||||
|
It neither changes local dotted-import boundaries nor introduces a manifest.
|
||||||
|
|
||||||
|
#### Failure, publication, persistence, and parity
|
||||||
|
|
||||||
|
Cold invalid requests create no unit, assembly, object, archive, executable,
|
||||||
|
capture, `.new`, `.install`, `.wwtxn.*`, or public output. A warm edit that
|
||||||
|
introduces NUL stops before a producer or install action, preserving the prior
|
||||||
|
committed unit/interface/assembly/object/archive generation, tool vouchers,
|
||||||
|
stamp, and public output byte for byte. Removing the NUL restores the ordinary
|
||||||
|
selected-source fingerprint; exact restoration may reuse the earlier generation.
|
||||||
|
Existing producer/runtime failure and transaction rollback remain their own
|
||||||
|
owners because this branch creates no new rollback mechanism.
|
||||||
|
|
||||||
|
Lexer and coordinator state are request/source-local. Concurrent valid and
|
||||||
|
invalid requests keep independent workdirs, captures, diagnostics, and
|
||||||
|
products; an invalid request cannot contaminate a valid sibling. The rule adds
|
||||||
|
no process, wait, or cancellation boundary, so signal, timeout, interruption,
|
||||||
|
process-group cleanup, and ordinary scratch cleanup retain their established
|
||||||
|
owners. Validation itself leaves no durable residue. Cstage and WWstage are
|
||||||
|
semantic twins: diagnostics match exactly, and valid unit/compiler/product
|
||||||
|
bytes retain their existing byte-identity contract.
|
||||||
|
|
||||||
|
`raw_nul_is_rejected_in_every_selected_source` in
|
||||||
|
`test/package/package_test.ww`, with focused C and WW lexer coverage, proves
|
||||||
|
literal and comment contexts plus adjacency recovery across escapes,
|
||||||
|
identifiers, numbers and typed suffixes, operators, comment delimiters, and
|
||||||
|
EOF; it also proves selected/imported builds, wrong-target exclusion, every
|
||||||
|
directory-test source role, precedence, cold cleanup, warm rollback and reuse,
|
||||||
|
concurrent isolation, valid escaped-NUL behavior, runtime/publication behavior,
|
||||||
|
and no-residue/parity observations.
|
||||||
|
|
||||||
|
No format bump. This changes invalid-source acceptance and diagnostics only;
|
||||||
|
valid source composition and valid `.wwi`, assembly, object, archive,
|
||||||
|
executable, and retained-test-product bytes are unchanged. Build workdir format
|
||||||
|
remains `18`, test workdir format remains `19`, and semantic storage format
|
||||||
|
remains `3`.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
10
docs/spec.md
10
docs/spec.md
@@ -60,6 +60,16 @@ every other source position, including inside string and rune literals and
|
|||||||
comments. Apart from that marker rule, the lexer operates on bytes and
|
comments. Apart from that marker rule, the lexer operates on bytes and
|
||||||
non-ASCII bytes are legal only inside string and rune literals and comments.
|
non-ASCII bytes are legal only inside string and rune literals and comments.
|
||||||
|
|
||||||
|
Each raw byte `00` (U+0000) is invalid at every physical source position,
|
||||||
|
including in comments and string or rune literal text. It produces one
|
||||||
|
positioned `invalid NUL character` error at that byte's source position and
|
||||||
|
is omitted from the lexer's logical character stream before token recovery.
|
||||||
|
It therefore cannot split an identifier, number, operator, escape, or comment
|
||||||
|
boundary into different tokens. This is a source-representation rule, before
|
||||||
|
package/import interpretation; it does not make U+0000 a package, import, or
|
||||||
|
artifact identity component. An escape spelling such as `"\\x00"` is not a
|
||||||
|
raw source byte and remains a legal literal value.
|
||||||
|
|
||||||
### 2.2 Comments
|
### 2.2 Comments
|
||||||
|
|
||||||
Line comments only, introduced by `//` and running to end of line. There
|
Line comments only, introduced by `//` and running to end of line. There
|
||||||
|
|||||||
@@ -429,6 +429,26 @@ before either operation. The marker is source representation only and never
|
|||||||
package/import/action/artifact/publication/persistence identity; no test result
|
package/import/action/artifact/publication/persistence identity; no test result
|
||||||
is cached.
|
is cached.
|
||||||
|
|
||||||
|
After filename eligibility and before package-clause classification, each raw
|
||||||
|
source byte `00` in a selected production, same-package test,
|
||||||
|
external-package test, or test-only source is one positioned
|
||||||
|
`invalid NUL character` error. Comments and literal text do not hide it. The
|
||||||
|
shared coordinator owns that early package/test rejection and reports every
|
||||||
|
raw NUL in the invalid physical source. On complete direct
|
||||||
|
frontend inputs, the C and WW source decoders diagnose each raw NUL and omit it
|
||||||
|
from their logical character streams before token recovery. Thus a NUL cannot
|
||||||
|
split an escape, identifier, number or suffix, operator, comment delimiter, or
|
||||||
|
EOF boundary into a stage-dependent second error. Test loading maintains a
|
||||||
|
stage-equal source-diagnostic stream before import discovery, graph actions,
|
||||||
|
producers, test execution, accounting, result output, or retained publication.
|
||||||
|
An invalid selected test request emits only the existing attributable
|
||||||
|
`FAIL\n`; it does not construct a variant, generated main, test child, or new
|
||||||
|
persistent generation. Wrong-target exclusion remains first, so an excluded
|
||||||
|
file with a raw NUL has no diagnostic or persistence effect. A `\\x00` escape
|
||||||
|
remains a valid literal value. This rule is limited to raw U+0000; malformed
|
||||||
|
UTF-8 and the independent per-source BOM rule retain their existing, separate
|
||||||
|
contracts.
|
||||||
|
|
||||||
After that eligibility boundary and the coordinator's required package-clause
|
After that eligibility boundary and the coordinator's required package-clause
|
||||||
classification and production `@test` validation parses, the delegated loader
|
classification and production `@test` validation parses, the delegated loader
|
||||||
performs selected-basename Go 1.26.5 simple-fold preflight before its graph
|
performs selected-basename Go 1.26.5 simple-fold preflight before its graph
|
||||||
|
|||||||
@@ -624,6 +624,16 @@ fn pkgfailpath(path: str, reason: str) void = {
|
|||||||
pkgputln(os.STDERR_FILENO, reason);
|
pkgputln(os.STDERR_FILENO, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn pkgfailsource(path: str, line: i32, col: i32, reason: str) void = {
|
||||||
|
pkgput(os.STDERR_FILENO, path);
|
||||||
|
pkgput(os.STDERR_FILENO, ":");
|
||||||
|
pkgput(os.STDERR_FILENO, strconv.i32tos(line, strconv.base.DEC));
|
||||||
|
pkgput(os.STDERR_FILENO, ":");
|
||||||
|
pkgput(os.STDERR_FILENO, strconv.i32tos(col, strconv.base.DEC));
|
||||||
|
pkgput(os.STDERR_FILENO, ": error: ");
|
||||||
|
pkgputln(os.STDERR_FILENO, reason);
|
||||||
|
};
|
||||||
|
|
||||||
fn pkgusage() void = {
|
fn pkgusage() void = {
|
||||||
pkgput(os.STDERR_FILENO,
|
pkgput(os.STDERR_FILENO,
|
||||||
"usage: wwtest package [-c] [-S] [-list] [-j N] [-I DIR] [-L DIR] [-l LIB] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/... ...] [-- GLOB ...]\n");
|
"usage: wwtest package [-c] [-S] [-list] [-j N] [-I DIR] [-L DIR] [-l LIB] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/... ...] [-- GLOB ...]\n");
|
||||||
@@ -689,6 +699,27 @@ fn pkgident(c: u8) bool = {
|
|||||||
return c >= '0' && c <= '9';
|
return c >= '0' && c <= '9';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn pkgnulerrors(path: str, src: str) bool = {
|
||||||
|
let i: i32 = 0;
|
||||||
|
let ln: i32 = 1;
|
||||||
|
let cl: i32 = 1;
|
||||||
|
let found: bool = false;
|
||||||
|
for (i < src.len) {
|
||||||
|
if (src[i] == 0u8) {
|
||||||
|
pkgfailsource(path, ln, cl, "invalid NUL character");
|
||||||
|
found = true;
|
||||||
|
};
|
||||||
|
if (src[i] == '\n') {
|
||||||
|
ln += 1;
|
||||||
|
cl = 1;
|
||||||
|
} else {
|
||||||
|
cl += 1;
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return found;
|
||||||
|
};
|
||||||
|
|
||||||
fn pkgskipspace(src: str, start: i32) i32 = {
|
fn pkgskipspace(src: str, start: i32) i32 = {
|
||||||
let i: i32 = start;
|
let i: i32 = start;
|
||||||
for (i < src.len) {
|
for (i < src.len) {
|
||||||
@@ -2260,7 +2291,14 @@ export fn packagecommand(args: []str) int = {
|
|||||||
};
|
};
|
||||||
let body: str;
|
let body: str;
|
||||||
let pn: str;
|
let pn: str;
|
||||||
if (!pkgread(ds.paths[i], &body) || !pkgclause(body, &pn)) {
|
if (!pkgread(ds.paths[i], &body)) {
|
||||||
|
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
||||||
|
return pkgteststatusfail(explicitstatus, compileonly);
|
||||||
|
};
|
||||||
|
if (pkgnulerrors(ds.paths[i], body)) {
|
||||||
|
return pkgteststatusfail(explicitstatus, compileonly);
|
||||||
|
};
|
||||||
|
if (!pkgclause(body, &pn)) {
|
||||||
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
||||||
return pkgteststatusfail(explicitstatus, compileonly);
|
return pkgteststatusfail(explicitstatus, compileonly);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type lex = struct {
|
|||||||
line: i32,
|
line: i32,
|
||||||
col: i32,
|
col: i32,
|
||||||
errs: i32,
|
errs: i32,
|
||||||
|
nulcount: u64,
|
||||||
// a `//ww:module-reset` directive was seen in the last skipped run;
|
// a `//ww:module-reset` directive was seen in the last skipped run;
|
||||||
// lexnext emits TK_MODRESET before the next real token (#16 opt-B).
|
// lexnext emits TK_MODRESET before the next real token (#16 opt-B).
|
||||||
modreset: i32,
|
modreset: i32,
|
||||||
@@ -81,6 +82,7 @@ export fn lexinit(l: *lex, file: str, src: *u8, len: u64) void = {
|
|||||||
l.line = 1;
|
l.line = 1;
|
||||||
l.col = 1;
|
l.col = 1;
|
||||||
l.errs = 0;
|
l.errs = 0;
|
||||||
|
l.nulcount = 0u64;
|
||||||
l.modreset = 0;
|
l.modreset = 0;
|
||||||
l.modpathset = 0;
|
l.modpathset = 0;
|
||||||
l.modresetpathset = 0;
|
l.modresetpathset = 0;
|
||||||
@@ -95,15 +97,60 @@ fn srcb(l: *lex, off: u64) i32 = {
|
|||||||
return b: i32;
|
return b: i32;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn lskipnul(l: *lex) void = {
|
||||||
|
for (l.lpos < l.srclen) {
|
||||||
|
if (srcb(l, l.lpos) != 0) { break; };
|
||||||
|
let np: pos;
|
||||||
|
np.file = l.file;
|
||||||
|
np.line = l.line;
|
||||||
|
np.col = l.col;
|
||||||
|
l.lpos += 1u64;
|
||||||
|
l.col += 1;
|
||||||
|
errat(l, &np, "invalid NUL character");
|
||||||
|
l.nulcount += 1u64;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the raw offset of a logical byte lookahead. Raw NUL bytes do not
|
||||||
|
// occupy a slot in the token stream: Go's source.nextch diagnoses them and
|
||||||
|
// immediately resumes decoding at the following character.
|
||||||
|
fn lrawoff(l: *lex, ahead0: u64) u64 = {
|
||||||
|
let p: u64 = l.lpos;
|
||||||
|
let ahead: u64 = ahead0;
|
||||||
|
for (true) {
|
||||||
|
for (p < l.srclen) {
|
||||||
|
if (srcb(l, p) != 0) { break; };
|
||||||
|
p += 1u64;
|
||||||
|
};
|
||||||
|
if (ahead == 0u64 || p >= l.srclen) { return p; };
|
||||||
|
p += 1u64;
|
||||||
|
ahead -= 1u64;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
fn lpeek(l: *lex, ahead: u64) i32 = {
|
fn lpeek(l: *lex, ahead: u64) i32 = {
|
||||||
let p: u64 = l.lpos + ahead;
|
// Drain NUL at the current decoder position even when it is followed by
|
||||||
|
// EOF; otherwise a trailing NUL could disappear without a diagnostic.
|
||||||
|
for (true) {
|
||||||
|
if (l.lpos >= l.srclen) { return -1; };
|
||||||
|
let c: i32 = srcb(l, l.lpos);
|
||||||
|
if (c == 0) { lskipnul(l); continue; };
|
||||||
|
if (ahead == 0u64) {
|
||||||
|
if (bomat(l.src, l.srclen, l.lpos)) { return 0xFEFF; };
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
let p: u64 = lrawoff(l, ahead);
|
||||||
if (p >= l.srclen) { return -1; };
|
if (p >= l.srclen) { return -1; };
|
||||||
if (bomat(l.src, l.srclen, p)) { return 0xFEFF; };
|
if (bomat(l.src, l.srclen, p)) { return 0xFEFF; };
|
||||||
return srcb(l, p);
|
return srcb(l, p);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fn lget(l: *lex) i32 = {
|
fn lget(l: *lex) i32 = {
|
||||||
|
for (true) {
|
||||||
if (l.lpos >= l.srclen) { return -1; };
|
if (l.lpos >= l.srclen) { return -1; };
|
||||||
|
let c: i32 = srcb(l, l.lpos);
|
||||||
|
if (c == 0) { lskipnul(l); continue; };
|
||||||
if (bomat(l.src, l.srclen, l.lpos)) {
|
if (bomat(l.src, l.srclen, l.lpos)) {
|
||||||
let bp: pos;
|
let bp: pos;
|
||||||
bp.file = l.file;
|
bp.file = l.file;
|
||||||
@@ -114,7 +161,6 @@ fn lget(l: *lex) i32 = {
|
|||||||
errat(l, &bp, "invalid BOM in the middle of the file");
|
errat(l, &bp, "invalid BOM in the middle of the file");
|
||||||
return 0xFEFF;
|
return 0xFEFF;
|
||||||
};
|
};
|
||||||
let c: i32 = srcb(l, l.lpos);
|
|
||||||
l.lpos += 1u64;
|
l.lpos += 1u64;
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
l.line += 1;
|
l.line += 1;
|
||||||
@@ -123,6 +169,29 @@ fn lget(l: *lex) i32 = {
|
|||||||
l.col += 1;
|
l.col += 1;
|
||||||
};
|
};
|
||||||
return c;
|
return c;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy one raw source span into token text while omitting diagnosed NUL
|
||||||
|
// bytes. This keeps keyword, numeric, suffix, and directive recovery on the
|
||||||
|
// same logical character stream as lpeek/lget.
|
||||||
|
fn lexspan(l: *lex, begin: u64, end: u64) str = {
|
||||||
|
let buf: []u8 = alloc([], end - begin + 1u64)!;
|
||||||
|
let i: u64 = begin;
|
||||||
|
let j: u64 = 0u64;
|
||||||
|
for (i < end) {
|
||||||
|
let b: i32 = srcb(l, i);
|
||||||
|
if (b != 0) {
|
||||||
|
buf[j] = b: u8;
|
||||||
|
j += 1u64;
|
||||||
|
};
|
||||||
|
i += 1u64;
|
||||||
|
};
|
||||||
|
buf[j] = 0u8;
|
||||||
|
let out: str;
|
||||||
|
out.ptr = buf.ptr;
|
||||||
|
out.len = j: i32;
|
||||||
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn curpos(l: *lex, out: *pos) void = {
|
fn curpos(l: *lex, out: *pos) void = {
|
||||||
@@ -146,6 +215,81 @@ fn errat(l: *lex, p: *pos, msg: str) void = {
|
|||||||
l.errs += 1;
|
l.errs += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Consume one // body, then classify the driver's internal module directive
|
||||||
|
// from its logical (NUL-filtered) text. A single moving lexer cursor keeps long
|
||||||
|
// generated module paths linear. The trailing newline remains for skipws.
|
||||||
|
fn linecomment(l: *lex) void = {
|
||||||
|
let begin: u64 = l.lpos;
|
||||||
|
let nulbegin: u64 = l.nulcount;
|
||||||
|
for (true) {
|
||||||
|
let c: i32 = lpeek(l, 0u64);
|
||||||
|
if (c < 0 || c == '\n') { break; };
|
||||||
|
lget(l);
|
||||||
|
};
|
||||||
|
let end: u64 = l.lpos;
|
||||||
|
let body: str;
|
||||||
|
if (l.nulcount == nulbegin) {
|
||||||
|
body.ptr = l.src + begin;
|
||||||
|
body.len = (end - begin): i32;
|
||||||
|
} else {
|
||||||
|
body = lexspan(l, begin, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
let pre: str = "ww:module";
|
||||||
|
if (!strings.hasprefix(body, pre) || body.len == pre.len) { return; };
|
||||||
|
let i: i32 = pre.len;
|
||||||
|
if (body[i] == '-') {
|
||||||
|
let rest: str = "-reset";
|
||||||
|
if (body.len - i < rest.len) { return; };
|
||||||
|
let j: i32 = 0;
|
||||||
|
for (j < rest.len) {
|
||||||
|
if (body[i + j] != rest[j]) { return; };
|
||||||
|
j += 1;
|
||||||
|
};
|
||||||
|
i += rest.len;
|
||||||
|
if (i == body.len) {
|
||||||
|
l.modreset = 1;
|
||||||
|
l.modpathset = 0; // #9: reset supersedes a pending path
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (body[i] != ' ' && body[i] != '\t') { return; };
|
||||||
|
for (i < body.len && (body[i] == ' ' || body[i] == '\t')) {
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
let s: i32 = i;
|
||||||
|
for (i < body.len && body[i] != '\r' && body[i] != ' '
|
||||||
|
&& body[i] != '\t') {
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
l.modreset = 1;
|
||||||
|
l.modpathset = 0; // #9: see above
|
||||||
|
if (i > s) {
|
||||||
|
let view: str;
|
||||||
|
view.ptr = body.ptr + (s: u64);
|
||||||
|
view.len = i - s;
|
||||||
|
l.modresetpath = strings.dup(view);
|
||||||
|
l.modresetpathset = 1;
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (body[i] != ' ' && body[i] != '\t') { return; };
|
||||||
|
for (i < body.len && (body[i] == ' ' || body[i] == '\t')) {
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
let s: i32 = i;
|
||||||
|
for (i < body.len && body[i] != '\r' && body[i] != ' '
|
||||||
|
&& body[i] != '\t') {
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
if (i > s) {
|
||||||
|
let view: str;
|
||||||
|
view.ptr = body.ptr + (s: u64);
|
||||||
|
view.len = i - s;
|
||||||
|
l.modpath = strings.dup(view);
|
||||||
|
l.modpathset = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
fn skipws(l: *lex) bool = {
|
fn skipws(l: *lex) bool = {
|
||||||
for (true) {
|
for (true) {
|
||||||
let c: i32 = lpeek(l, 0u64);
|
let c: i32 = lpeek(l, 0u64);
|
||||||
@@ -158,111 +302,9 @@ fn skipws(l: *lex) bool = {
|
|||||||
let c2: i32 = lpeek(l, 1u64);
|
let c2: i32 = lpeek(l, 1u64);
|
||||||
if (c2 == '/') {
|
if (c2 == '/') {
|
||||||
lget(l); lget(l);
|
lget(l); lget(l);
|
||||||
// #16 opt-B: recognize the driver's curmod-reset
|
// #16 opt-B: classify the driver's whole-line internal
|
||||||
// boundary directive `//ww:module-reset` (whole
|
// module boundary after consuming it once.
|
||||||
// line) and flag it; lexnext emits TK_MODRESET.
|
linecomment(l);
|
||||||
// The body is then skipped like any comment.
|
|
||||||
// Mirrors cstage lex.c skipws. Compare via lpeek
|
|
||||||
// (no consume) so the skip loop below is unchanged.
|
|
||||||
let pre: str = "ww:module";
|
|
||||||
let di: i32 = 0;
|
|
||||||
let matched: bool = true;
|
|
||||||
for (di < pre.len) {
|
|
||||||
if (lpeek(l, di: u64) != pre[di]: i32) {
|
|
||||||
matched = false; break;
|
|
||||||
};
|
|
||||||
di += 1;
|
|
||||||
};
|
|
||||||
if (matched) {
|
|
||||||
let nx: i32 = lpeek(l, pre.len: u64);
|
|
||||||
if (nx == '-') {
|
|
||||||
let rest: str = "-reset";
|
|
||||||
let rj: i32 = 0;
|
|
||||||
let rm: bool = true;
|
|
||||||
for (rj < rest.len) {
|
|
||||||
if (lpeek(l, (pre.len + rj): u64)
|
|
||||||
!= rest[rj]: i32) {
|
|
||||||
rm = false; break;
|
|
||||||
};
|
|
||||||
rj += 1;
|
|
||||||
};
|
|
||||||
if (rm) {
|
|
||||||
let af: i32 = lpeek(l,
|
|
||||||
(pre.len + rest.len): u64);
|
|
||||||
if (af == '\n') { l.modreset = 1; l.modpathset = 0; } // #9: reset supersedes pending path (empty module body)
|
|
||||||
else { if (af < 0) { l.modreset = 1; l.modpathset = 0; } // #9: see above
|
|
||||||
else { if (af == ' ' || af == '\t') {
|
|
||||||
// `//ww:module-reset <path>` — sep
|
|
||||||
// primary body tagged by its full
|
|
||||||
// dotted import path (#57).
|
|
||||||
let k: u64 =
|
|
||||||
(pre.len + rest.len): u64;
|
|
||||||
for (true) {
|
|
||||||
let sc: i32 = lpeek(l, k);
|
|
||||||
if (sc == ' ' || sc == '\t') {
|
|
||||||
k += 1u64; continue;
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let s0: u64 = k;
|
|
||||||
for (true) {
|
|
||||||
let pc: i32 = lpeek(l, k);
|
|
||||||
if (pc < 0) { break; };
|
|
||||||
if (pc == '\n' || pc == '\r'
|
|
||||||
|| pc == ' '
|
|
||||||
|| pc == '\t') {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
k += 1u64;
|
|
||||||
};
|
|
||||||
l.modreset = 1;
|
|
||||||
l.modpathset = 0; // #9: see above — clear pending path
|
|
||||||
if (k > s0) {
|
|
||||||
let view: str;
|
|
||||||
view.ptr =
|
|
||||||
l.src + l.lpos + s0;
|
|
||||||
view.len = (k - s0): i32;
|
|
||||||
l.modresetpath =
|
|
||||||
strings.dup(view);
|
|
||||||
l.modresetpathset = 1;
|
|
||||||
};
|
|
||||||
}; }; };
|
|
||||||
};
|
|
||||||
} else { if (nx == ' ' || nx == '\t') {
|
|
||||||
// `//ww:module <path>` — M1 import boundary.
|
|
||||||
let k: u64 = pre.len: u64;
|
|
||||||
for (true) {
|
|
||||||
let sc: i32 = lpeek(l, k);
|
|
||||||
if (sc == ' ' || sc == '\t') {
|
|
||||||
k += 1u64; continue;
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let s0: u64 = k;
|
|
||||||
for (true) {
|
|
||||||
let pc: i32 = lpeek(l, k);
|
|
||||||
if (pc < 0) { break; };
|
|
||||||
if (pc == '\n' || pc == '\r'
|
|
||||||
|| pc == ' ' || pc == '\t') {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
k += 1u64;
|
|
||||||
};
|
|
||||||
if (k > s0) {
|
|
||||||
let view: str;
|
|
||||||
view.ptr = l.src + l.lpos + s0;
|
|
||||||
view.len = (k - s0): i32;
|
|
||||||
l.modpath = strings.dup(view);
|
|
||||||
l.modpathset = 1;
|
|
||||||
};
|
|
||||||
}; };
|
|
||||||
};
|
|
||||||
for (true) {
|
|
||||||
let cx: i32 = lpeek(l, 0u64);
|
|
||||||
if (cx < 0) { return false; };
|
|
||||||
if (cx == '\n') { break; };
|
|
||||||
lget(l);
|
|
||||||
};
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if (c2 == '*') {
|
if (c2 == '*') {
|
||||||
@@ -395,6 +437,38 @@ fn escape(l: *lex, out: *i32) bool = {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn lextypesuffix(l: *lex, out: *str) bool = {
|
||||||
|
let got: [4]u8;
|
||||||
|
let n: u64 = 0u64;
|
||||||
|
for (n < 4u64) {
|
||||||
|
let c: i32 = lpeek(l, n);
|
||||||
|
if (c < 0 || !isidpart(c: rune)) { break; };
|
||||||
|
got[n] = c: u8;
|
||||||
|
n += 1u64;
|
||||||
|
};
|
||||||
|
if (n == 0u64 || n > 3u64) { return false; };
|
||||||
|
let tail: i32 = lpeek(l, n);
|
||||||
|
if (tail >= 0) {
|
||||||
|
if (isidpart(tail: rune)) { return false; };
|
||||||
|
};
|
||||||
|
let names: []str = ["i8", "i16", "i32", "i64",
|
||||||
|
"u8", "u16", "u32", "u64", "f32", "f64"];
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < names.len) {
|
||||||
|
if (names[i].len: u64 == n) {
|
||||||
|
let same: bool = true;
|
||||||
|
let j: i32 = 0;
|
||||||
|
for (j < names[i].len) {
|
||||||
|
if (names[i][j] != got[j]) { same = false; break; };
|
||||||
|
j += 1;
|
||||||
|
};
|
||||||
|
if (same) { *out = names[i]; return true; };
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
fn scandecimalrun(l: *lex) void = {
|
fn scandecimalrun(l: *lex) void = {
|
||||||
for (true) {
|
for (true) {
|
||||||
let c: i32 = lpeek(l, 0u64);
|
let c: i32 = lpeek(l, 0u64);
|
||||||
@@ -459,6 +533,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
out.line = start.line;
|
out.line = start.line;
|
||||||
out.col = start.col;
|
out.col = start.col;
|
||||||
let begin: u64 = l.lpos;
|
let begin: u64 = l.lpos;
|
||||||
|
let nulbegin: u64 = l.nulcount;
|
||||||
let base: i32 = 10;
|
let base: i32 = 10;
|
||||||
let isfloat: bool = false;
|
let isfloat: bool = false;
|
||||||
|
|
||||||
@@ -507,11 +582,16 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let n: u64 = l.lpos - begin;
|
let rawend: u64 = l.lpos;
|
||||||
|
if (l.nulcount == nulbegin) {
|
||||||
let view: str;
|
let view: str;
|
||||||
view.ptr = l.src + begin;
|
view.ptr = l.src + begin;
|
||||||
view.len = n: i32;
|
view.len = (rawend - begin): i32;
|
||||||
out.text = strings.dup(view);
|
out.text = strings.dup(view);
|
||||||
|
} else {
|
||||||
|
out.text = lexspan(l, begin, rawend);
|
||||||
|
};
|
||||||
|
let n: u64 = out.text.len: u64;
|
||||||
|
|
||||||
if (isfloat) {
|
if (isfloat) {
|
||||||
out.kind = tkind.TK_FLOAT;
|
out.kind = tkind.TK_FLOAT;
|
||||||
@@ -522,7 +602,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
let i: u64 = 0u64;
|
let i: u64 = 0u64;
|
||||||
let j: u64 = 0u64;
|
let j: u64 = 0u64;
|
||||||
for (i < n) {
|
for (i < n) {
|
||||||
let b: u8 = l.src[begin + i];
|
let b: u8 = out.text[i];
|
||||||
if (b != '_') {
|
if (b != '_') {
|
||||||
clean[j] = b;
|
clean[j] = b;
|
||||||
j += 1u64;
|
j += 1u64;
|
||||||
@@ -561,7 +641,7 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
let pu: *u64 = (&fv): *u64;
|
let pu: *u64 = (&fv): *u64;
|
||||||
out.uval = *pu;
|
out.uval = *pu;
|
||||||
} else {
|
} else {
|
||||||
let digs: *u8 = l.src + begin;
|
let digs: *u8 = out.text.ptr;
|
||||||
let dn: u64 = n;
|
let dn: u64 = n;
|
||||||
if (base != 10) {
|
if (base != 10) {
|
||||||
digs = digs + 2u64;
|
digs = digs + 2u64;
|
||||||
@@ -578,49 +658,11 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
let pc: i32 = lpeek(l, 0u64);
|
let pc: i32 = lpeek(l, 0u64);
|
||||||
if (pc >= 0) {
|
if (pc >= 0) {
|
||||||
if (isidstart(pc: rune)) {
|
if (isidstart(pc: rune)) {
|
||||||
let sb: u64 = l.lpos;
|
let suffix: str;
|
||||||
let sc: i32 = l.col;
|
if (lextypesuffix(l, &suffix)) {
|
||||||
for (true) {
|
let i: i32 = 0;
|
||||||
let cc: i32 = lpeek(l, 0u64);
|
for (i < suffix.len) { lget(l); i += 1; };
|
||||||
if (cc < 0) { break; };
|
out.tsuffix = strings.dup(suffix);
|
||||||
if (!isidpart(cc: rune)) { break; };
|
|
||||||
lget(l);
|
|
||||||
};
|
|
||||||
let sl: u64 = l.lpos - sb;
|
|
||||||
let p: *u8 = l.src + sb;
|
|
||||||
let isok: bool = false;
|
|
||||||
if (sl == 2u64) {
|
|
||||||
if (p[0] == 'i') {
|
|
||||||
if (p[1] == '8') { isok = true; }; // i8
|
|
||||||
};
|
|
||||||
if (p[0] == 'u') {
|
|
||||||
if (p[1] == '8') { isok = true; }; // u8
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (sl == 3u64) {
|
|
||||||
if (p[0] == 'i') {
|
|
||||||
if (p[1] == '1') { if (p[2] == '6') { isok = true; }; }; // i16
|
|
||||||
if (p[1] == '3') { if (p[2] == '2') { isok = true; }; }; // i32
|
|
||||||
if (p[1] == '6') { if (p[2] == '4') { isok = true; }; }; // i64
|
|
||||||
};
|
|
||||||
if (p[0] == 'u') {
|
|
||||||
if (p[1] == '1') { if (p[2] == '6') { isok = true; }; };
|
|
||||||
if (p[1] == '3') { if (p[2] == '2') { isok = true; }; };
|
|
||||||
if (p[1] == '6') { if (p[2] == '4') { isok = true; }; };
|
|
||||||
};
|
|
||||||
if (p[0] == 'f') {
|
|
||||||
if (p[1] == '3') { if (p[2] == '2') { isok = true; }; }; // f32
|
|
||||||
if (p[1] == '6') { if (p[2] == '4') { isok = true; }; }; // f64
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (isok) {
|
|
||||||
let view: str;
|
|
||||||
view.ptr = p;
|
|
||||||
view.len = sl: i32;
|
|
||||||
out.tsuffix = strings.dup(view);
|
|
||||||
} else {
|
|
||||||
l.lpos = sb;
|
|
||||||
l.col = sc;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -628,14 +670,24 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
|
|
||||||
fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
||||||
let begin: u64 = l.lpos;
|
let begin: u64 = l.lpos;
|
||||||
|
let nulbegin: u64 = l.nulcount;
|
||||||
for (true) {
|
for (true) {
|
||||||
let c: i32 = lpeek(l, 0u64);
|
let c: i32 = lpeek(l, 0u64);
|
||||||
if (c < 0) { break; };
|
if (c < 0) { break; };
|
||||||
if (!isidpart(c: rune)) { break; };
|
if (!isidpart(c: rune)) { break; };
|
||||||
lget(l);
|
lget(l);
|
||||||
};
|
};
|
||||||
let n: u64 = l.lpos - begin;
|
let text: str;
|
||||||
let p: *u8 = l.src + begin;
|
if (l.nulcount == nulbegin) {
|
||||||
|
let view: str;
|
||||||
|
view.ptr = l.src + begin;
|
||||||
|
view.len = (l.lpos - begin): i32;
|
||||||
|
text = strings.dup(view);
|
||||||
|
} else {
|
||||||
|
text = lexspan(l, begin, l.lpos);
|
||||||
|
};
|
||||||
|
let n: u64 = text.len: u64;
|
||||||
|
let p: *u8 = text.ptr;
|
||||||
out.file = start.file;
|
out.file = start.file;
|
||||||
out.line = start.line;
|
out.line = start.line;
|
||||||
out.col = start.col;
|
out.col = start.col;
|
||||||
@@ -643,10 +695,7 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
if (n == 1u64) {
|
if (n == 1u64) {
|
||||||
if (p[0] == '_') {
|
if (p[0] == '_') {
|
||||||
out.kind = tkind.TK_UNDER;
|
out.kind = tkind.TK_UNDER;
|
||||||
let view: str;
|
out.text = text;
|
||||||
view.ptr = p;
|
|
||||||
view.len = n: i32;
|
|
||||||
out.text = strings.dup(view);
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -656,10 +705,7 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
|
|||||||
} else {
|
} else {
|
||||||
out.kind = tkind.TK_IDENT;
|
out.kind = tkind.TK_IDENT;
|
||||||
};
|
};
|
||||||
let view: str;
|
out.text = text;
|
||||||
view.ptr = p;
|
|
||||||
view.len = n: i32;
|
|
||||||
out.text = strings.dup(view);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn lexstr(l: *lex, start: *pos, out: *tok) void = {
|
fn lexstr(l: *lex, start: *pos, out: *tok) void = {
|
||||||
|
|||||||
@@ -339,6 +339,30 @@ fn checkfloat(src: str, want: u64) void = {
|
|||||||
assert(!(t.text != "bar"));
|
assert(!(t.text != "bar"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@test fn long_module_directive_is_linear() void = {
|
||||||
|
let pre: str = "//ww:module ";
|
||||||
|
let tail: str = "\nfn";
|
||||||
|
let pathlen: i32 = 16384;
|
||||||
|
let total: i32 = pre.len + pathlen + tail.len;
|
||||||
|
let src: []u8 = alloc([], total: u64)!;
|
||||||
|
src.len = total;
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < pre.len) { src[i] = pre[i]; i += 1; };
|
||||||
|
for (i < pre.len + pathlen) { src[i] = 'a': u8; i += 1; };
|
||||||
|
let j: i32 = 0;
|
||||||
|
for (j < tail.len) { src[i] = tail[j]; i += 1; j += 1; };
|
||||||
|
let l: syntax.lex;
|
||||||
|
syntax.lexinit(&l, "t", src.ptr, src.len: u64);
|
||||||
|
let t: syntax.tok;
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_MODPATH);
|
||||||
|
assert(t.text.len == pathlen);
|
||||||
|
assert(t.text[0] == 'a' && t.text[pathlen - 1] == 'a');
|
||||||
|
syntax.lexnext(&l, &t);
|
||||||
|
assert(t.kind == syntax.tkind.TK_FN);
|
||||||
|
assert(l.errs == 0);
|
||||||
|
};
|
||||||
|
|
||||||
fn bomsource(dst: *u8, before: str, after: str) u64 = {
|
fn bomsource(dst: *u8, before: str, after: str) u64 = {
|
||||||
let n: u64 = 0u64;
|
let n: u64 = 0u64;
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
|
|||||||
@@ -130,6 +130,58 @@ fn rewritemidbomfile(path: str, before: str, after: str) void = {
|
|||||||
putbomfile(path, before, after, true);
|
putbomfile(path, before, after, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn putnulfile(path: str, before: str, after: str, rewrite: bool) void = {
|
||||||
|
let flags: os.flag = os.flag.WRONLY;
|
||||||
|
if (rewrite) { flags |= os.flag.TRUNC; }
|
||||||
|
else { flags |= os.flag.CREATE | os.flag.EXCL; };
|
||||||
|
let fd: i32 = os.open(path, flags, 384i32);
|
||||||
|
assert(fd >= 0);
|
||||||
|
match (os.writeall(fd, before.ptr, before.len: u64)) {
|
||||||
|
case let n: i64 => assert(n == before.len: i64);
|
||||||
|
case let e: os.oserror => abort("write before NUL failed");
|
||||||
|
};
|
||||||
|
let nul: [1]u8;
|
||||||
|
nul[0] = 0u8;
|
||||||
|
match (os.writeall(fd, nul.ptr, 1u64)) {
|
||||||
|
case let n: i64 => assert(n == 1i64);
|
||||||
|
case let e: os.oserror => abort("write NUL failed");
|
||||||
|
};
|
||||||
|
match (os.writeall(fd, after.ptr, after.len: u64)) {
|
||||||
|
case let n: i64 => assert(n == after.len: i64);
|
||||||
|
case let e: os.oserror => abort("write after NUL failed");
|
||||||
|
};
|
||||||
|
assert(os.close(fd) == 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writenulfile(path: str, before: str, after: str) void = {
|
||||||
|
putnulfile(path, before, after, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn rewritenulfile(path: str, before: str, after: str) void = {
|
||||||
|
putnulfile(path, before, after, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn writedoublenulfile(path: str, before: str, after: str) void = {
|
||||||
|
let fd: i32 = os.open(path,
|
||||||
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
||||||
|
assert(fd >= 0);
|
||||||
|
match (os.writeall(fd, before.ptr, before.len: u64)) {
|
||||||
|
case let n: i64 => assert(n == before.len: i64);
|
||||||
|
case let e: os.oserror => abort("write before NUL pair failed");
|
||||||
|
};
|
||||||
|
let pair: [2]u8;
|
||||||
|
pair[0] = 0u8; pair[1] = 0u8;
|
||||||
|
match (os.writeall(fd, pair.ptr, 2u64)) {
|
||||||
|
case let n: i64 => assert(n == 2i64);
|
||||||
|
case let e: os.oserror => abort("write NUL pair failed");
|
||||||
|
};
|
||||||
|
match (os.writeall(fd, after.ptr, after.len: u64)) {
|
||||||
|
case let n: i64 => assert(n == after.len: i64);
|
||||||
|
case let e: os.oserror => abort("write after NUL pair failed");
|
||||||
|
};
|
||||||
|
assert(os.close(fd) == 0);
|
||||||
|
};
|
||||||
|
|
||||||
fn writeexecutable(path: str, content: str) void = {
|
fn writeexecutable(path: str, content: str) void = {
|
||||||
let fd: i32 = os.open(path,
|
let fd: i32 = os.open(path,
|
||||||
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32);
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32);
|
||||||
@@ -16968,3 +17020,479 @@ fn runtimepath(relative: str) str = {
|
|||||||
&& !directoryhasnew(root));
|
&& !directoryhasnew(root));
|
||||||
clean(root);
|
clean(root);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Go 1.26.5's physical-source reader rejects U+0000 before token context can
|
||||||
|
// turn it into comment or literal payload. Selected roots, dependencies, and
|
||||||
|
// every test-source role share that rule; target-excluded files do not.
|
||||||
|
@test fn raw_nul_is_rejected_in_every_selected_source() void = {
|
||||||
|
let root: str = fresh();
|
||||||
|
let source: str = strings.concat(root, "/source");
|
||||||
|
mkdirall(source);
|
||||||
|
|
||||||
|
// Direct frontend ownership and exact C/WW recovery parity.
|
||||||
|
let leading: str = strings.concat(root, "/leading.ww");
|
||||||
|
let comment: str = strings.concat(root, "/comment.ww");
|
||||||
|
let stringlit: str = strings.concat(root, "/string.ww");
|
||||||
|
let runelit: str = strings.concat(root, "/rune.ww");
|
||||||
|
let between: str = strings.concat(root, "/between.ww");
|
||||||
|
let escapechar: str = strings.concat(root, "/escape-char.ww");
|
||||||
|
let escapedigit: str = strings.concat(root, "/escape-digit.ww");
|
||||||
|
let identifier: str = strings.concat(root, "/identifier.ww");
|
||||||
|
let lineend: str = strings.concat(root, "/line-end.ww");
|
||||||
|
let blockend: str = strings.concat(root, "/block-end.ww");
|
||||||
|
let operator: str = strings.concat(root, "/operator.ww");
|
||||||
|
let number: str = strings.concat(root, "/number.ww");
|
||||||
|
let suffix: str = strings.concat(root, "/suffix.ww");
|
||||||
|
let trailing: str = strings.concat(root, "/trailing.ww");
|
||||||
|
writenulfile(leading, "",
|
||||||
|
"package main;\nfn main() void = {};\n");
|
||||||
|
writenulfile(comment, "package main;\n// NUL: ",
|
||||||
|
" here\nfn main() void = {};\n");
|
||||||
|
writenulfile(stringlit,
|
||||||
|
"package main;\nfn main() void = { let value: str = \"NUL: ",
|
||||||
|
"\"; };\n");
|
||||||
|
writenulfile(runelit,
|
||||||
|
"package main;\nfn main() void = { let value: rune = '",
|
||||||
|
"A'; };\n");
|
||||||
|
writenulfile(between, "package main;\n",
|
||||||
|
"\nfn main() void = {};\n");
|
||||||
|
writenulfile(escapechar,
|
||||||
|
"package main;\nfn main() void = { let value: str = \"\\",
|
||||||
|
"n\"; };\n");
|
||||||
|
writenulfile(escapedigit,
|
||||||
|
"package main;\nfn main() void = { let value: str = \"\\x0",
|
||||||
|
"0\"; };\n");
|
||||||
|
writenulfile(identifier, "pack",
|
||||||
|
"age main;\nfn main() void = {};\n");
|
||||||
|
writenulfile(lineend, "package main;\n// comment",
|
||||||
|
"\nfn main() void = {};\n");
|
||||||
|
writenulfile(blockend, "package main;\n/* end *",
|
||||||
|
"/\nfn main() void = {};\n");
|
||||||
|
writenulfile(operator,
|
||||||
|
"package main;\nfn main() void = { let value: bool = true =",
|
||||||
|
"= true; };\n");
|
||||||
|
writenulfile(number,
|
||||||
|
"package main;\nfn main() void = { let value: i32 = 1",
|
||||||
|
"_0; };\n");
|
||||||
|
writenulfile(suffix,
|
||||||
|
"package main;\nfn main() void = { let value: i32 = 1i",
|
||||||
|
"32; };\n");
|
||||||
|
writenulfile(trailing,
|
||||||
|
"package main;\nfn main() void = {};\n", "");
|
||||||
|
let direct: []str = [leading, comment, stringlit, runelit, between,
|
||||||
|
escapechar, escapedigit, identifier, lineend, blockend, operator,
|
||||||
|
number, suffix, trailing];
|
||||||
|
let positions: []str = [":1:1: error: invalid NUL character\n",
|
||||||
|
":2:9: error: invalid NUL character\n",
|
||||||
|
":2:43: error: invalid NUL character\n",
|
||||||
|
":2:39: error: invalid NUL character\n",
|
||||||
|
":2:1: error: invalid NUL character\n",
|
||||||
|
":2:39: error: invalid NUL character\n",
|
||||||
|
":2:41: error: invalid NUL character\n",
|
||||||
|
":1:5: error: invalid NUL character\n",
|
||||||
|
":2:11: error: invalid NUL character\n",
|
||||||
|
":2:9: error: invalid NUL character\n",
|
||||||
|
":2:44: error: invalid NUL character\n",
|
||||||
|
":2:38: error: invalid NUL character\n",
|
||||||
|
":2:39: error: invalid NUL character\n",
|
||||||
|
":3:1: error: invalid NUL character\n"];
|
||||||
|
let compilers: []str = ["w6c", "w6c_ww"];
|
||||||
|
let stages: []str = ["ww", "ww_ww"];
|
||||||
|
let tags: []str = ["c", "ww"];
|
||||||
|
let di: i32 = 0;
|
||||||
|
for (di < direct.len) {
|
||||||
|
let reference: str = "";
|
||||||
|
let si: i32 = 0;
|
||||||
|
for (si < compilers.len) {
|
||||||
|
let asmout: str = strings.concat(root, "/direct-", tags[si], "-",
|
||||||
|
boundarypkgname(di), ".s");
|
||||||
|
let av: []str = [driver(compilers[si]), "-c", "--command-package",
|
||||||
|
"-o", asmout, direct[di]];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("nul-direct-", tags[si], "-",
|
||||||
|
boundarypkgname(di)), av,
|
||||||
|
(30i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0 && has(out.stderr, positions[di]));
|
||||||
|
assert(occurrences(out.stderr, "invalid NUL character") == 1);
|
||||||
|
assert(!os.exists(asmout));
|
||||||
|
if (si == 0) { reference = strings.dup(out.stderr); }
|
||||||
|
else { assert(same(reference, out.stderr)); };
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
di += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decoder recovery is also exact for repeated bytes and the internal
|
||||||
|
// module-boundary directive used by package-unit composition.
|
||||||
|
let repeated: str = strings.concat(root, "/repeated.ww");
|
||||||
|
let directive: str = strings.concat(root, "/directive.ww");
|
||||||
|
writedoublenulfile(repeated, "pack",
|
||||||
|
"age main;\nfn main() void = {};\n");
|
||||||
|
writenulfile(directive, "//ww:module-reset ma",
|
||||||
|
"in\npackage main;\nfn main() void = {};\n");
|
||||||
|
let recoverypaths: []str = [repeated, directive];
|
||||||
|
let recoverycounts: []i32 = [2, 1];
|
||||||
|
let recoverypositions: []str = [
|
||||||
|
":1:5: error: invalid NUL character\n",
|
||||||
|
":1:21: error: invalid NUL character\n"];
|
||||||
|
let ri: i32 = 0;
|
||||||
|
for (ri < recoverypaths.len) {
|
||||||
|
let reference: str = "";
|
||||||
|
let rsi: i32 = 0;
|
||||||
|
for (rsi < compilers.len) {
|
||||||
|
let asmout: str = strings.concat(root, "/recovery-", tags[rsi],
|
||||||
|
"-", boundarypkgname(ri), ".s");
|
||||||
|
let av: []str = [driver(compilers[rsi]), "-c",
|
||||||
|
"--command-package", "-o", asmout, recoverypaths[ri]];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("nul-recovery-", tags[rsi], "-",
|
||||||
|
boundarypkgname(ri)), av,
|
||||||
|
(30i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr, recoverypositions[ri])
|
||||||
|
&& occurrences(out.stderr,
|
||||||
|
"invalid NUL character") == recoverycounts[ri]
|
||||||
|
&& !os.exists(asmout));
|
||||||
|
if (ri == 0) {
|
||||||
|
assert(has(out.stderr,
|
||||||
|
":1:6: error: invalid NUL character\n"));
|
||||||
|
};
|
||||||
|
if (rsi == 0) { reference = strings.dup(out.stderr); }
|
||||||
|
else { assert(same(reference, out.stderr)); };
|
||||||
|
rsi += 1;
|
||||||
|
};
|
||||||
|
ri += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// An escape denotes a literal value; it is not a raw source NUL.
|
||||||
|
let escaped: str = strings.concat(root, "/escaped.ww");
|
||||||
|
writefile(escaped, strings.concat(
|
||||||
|
"package main;\n",
|
||||||
|
"fn main() i32 = { let value: str = \"\\x00\"; return value.len - 1; };\n"));
|
||||||
|
let escapedasm: []str = [strings.concat(root, "/escaped-c.s"),
|
||||||
|
strings.concat(root, "/escaped-ww.s")];
|
||||||
|
let si: i32 = 0;
|
||||||
|
for (si < compilers.len) {
|
||||||
|
let av: []str = [driver(compilers[si]), "-c", "--command-package",
|
||||||
|
"-o", escapedasm[si], escaped];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("nul-escaped-", tags[si]), av,
|
||||||
|
(30i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0);
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
assert(same(readfile(escapedasm[0]), readfile(escapedasm[1])));
|
||||||
|
|
||||||
|
let dep: str = strings.concat(source, "/dep");
|
||||||
|
let app: str = strings.concat(source, "/app");
|
||||||
|
let bad: str = strings.concat(source, "/bad");
|
||||||
|
let good: str = strings.concat(source, "/good");
|
||||||
|
mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(good);
|
||||||
|
writenulfile(strings.concat(dep, "/dep.ww"),
|
||||||
|
"package dep;\n// NUL: ",
|
||||||
|
" dependency\nexport fn value() i32 = { return 42; };\n");
|
||||||
|
writefile(strings.concat(app, "/main.ww"), strings.concat(
|
||||||
|
"package main;\nimport dep;\n",
|
||||||
|
"fn main() i32 = { return dep.value() - 42; };\n"));
|
||||||
|
writefile(strings.concat(bad, "/a.ww"),
|
||||||
|
"package main;\nfn helper() i32 = { return 1; };\n");
|
||||||
|
writenulfile(strings.concat(bad, "/main.ww"),
|
||||||
|
"package main;\n// NUL: ", strings.concat(
|
||||||
|
" root\nimport missing;\n",
|
||||||
|
"fn main() i32 = { return 0; };\n"));
|
||||||
|
writefile(strings.concat(good, "/main.ww"),
|
||||||
|
"package main;\nfn main() i32 = { return 0; };\n");
|
||||||
|
writenulfile(strings.concat(good, "/ignored_windows.ww"),
|
||||||
|
"not package NUL: ", " and not WW\n");
|
||||||
|
|
||||||
|
// Invalid root and dependency sources stop before graph actions and public
|
||||||
|
// output. The wrong-target source remains outside the semantic input set.
|
||||||
|
let goodbins: []str = [strings.concat(root, "/good-c"),
|
||||||
|
strings.concat(root, "/good-ww")];
|
||||||
|
let invaliddiags: []str = ["", ""];
|
||||||
|
let rootdiags: []str = ["", ""];
|
||||||
|
si = 0;
|
||||||
|
for (si < stages.len) {
|
||||||
|
let depwork: str = strings.concat(root, "/dep-work-", tags[si]);
|
||||||
|
let depout: str = strings.concat(root, "/dep-output-", tags[si]);
|
||||||
|
mkdirall(depwork);
|
||||||
|
let depav: []str = [driver(stages[si]), "build", "-w", depwork,
|
||||||
|
"-I", source, "-o", depout, "app"];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("nul-dependency-", tags[si]), depav,
|
||||||
|
(60i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr, "/dep/dep.ww:2:9: error: invalid NUL character\n"));
|
||||||
|
assert(occurrences(out.stderr, "invalid NUL character") == 1
|
||||||
|
&& !has(out.stderr, "w6c failed"));
|
||||||
|
assert(!os.exists(depout) && directoryisempty(depwork));
|
||||||
|
invaliddiags[si] = strings.dup(out.stderr);
|
||||||
|
|
||||||
|
let badwork: str = strings.concat(root, "/bad-work-", tags[si]);
|
||||||
|
let badout: str = strings.concat(root, "/bad-output-", tags[si]);
|
||||||
|
mkdirall(badwork);
|
||||||
|
let badav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||||
|
"-I", source, "-o", badout, "bad"];
|
||||||
|
runcommand(root, strings.concat("nul-root-", tags[si]), badav,
|
||||||
|
(60i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr, "/bad/main.ww:2:9: error: invalid NUL character\n"));
|
||||||
|
assert(!has(out.stderr, "cannot find import")
|
||||||
|
&& occurrences(out.stderr, "invalid NUL character") == 1);
|
||||||
|
assert(!os.exists(badout) && directoryisempty(badwork));
|
||||||
|
rootdiags[si] = strings.dup(out.stderr);
|
||||||
|
|
||||||
|
let goodwork: str = strings.concat(root, "/good-work-", tags[si]);
|
||||||
|
mkdirall(goodwork);
|
||||||
|
let goodav: []str = [driver(stages[si]), "build", "-w", goodwork,
|
||||||
|
"-I", source, "-o", goodbins[si], "good"];
|
||||||
|
runcommand(root, strings.concat("nul-wrong-target-", tags[si]), goodav,
|
||||||
|
(120i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||||
|
&& !directoryhasnew(goodwork)
|
||||||
|
&& !directoryhasfragment(goodwork, ".wwtxn."));
|
||||||
|
let runav: []str = [goodbins[si]];
|
||||||
|
runcommand(root, strings.concat("nul-good-run-", tags[si]), runav,
|
||||||
|
time.second, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0);
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
assert(same(invaliddiags[0], invaliddiags[1])
|
||||||
|
&& same(rootdiags[0], rootdiags[1])
|
||||||
|
&& same(readfile(goodbins[0]), readfile(goodbins[1])));
|
||||||
|
|
||||||
|
// Production, same-package, external-package, and test-only sources all
|
||||||
|
// reject before product construction, execution, accounting, and retention.
|
||||||
|
let prodcase: str = strings.concat(source, "/prodcase");
|
||||||
|
let samecase: str = strings.concat(source, "/samecase");
|
||||||
|
let externalcase: str = strings.concat(source, "/externalcase");
|
||||||
|
let onlycase: str = strings.concat(source, "/onlycase");
|
||||||
|
let repeatcase: str = strings.concat(source, "/repeatcase");
|
||||||
|
mkdirall(prodcase); mkdirall(samecase); mkdirall(externalcase);
|
||||||
|
mkdirall(onlycase); mkdirall(repeatcase);
|
||||||
|
writefile(strings.concat(prodcase, "/more.ww"),
|
||||||
|
"package prodcase;\nfn more() i32 = { return 2; };\n");
|
||||||
|
writenulfile(strings.concat(prodcase, "/prod.ww"),
|
||||||
|
"package prodcase;\n// NUL: ",
|
||||||
|
" production\nexport fn value() i32 = { return 1; };\n");
|
||||||
|
writefile(strings.concat(prodcase, "/same_test.ww"),
|
||||||
|
"package prodcase;\n@test fn must_not_run() void = { abort(); };\n");
|
||||||
|
writefile(strings.concat(samecase, "/prod.ww"),
|
||||||
|
"package samecase;\nfn value() i32 = { return 1; };\n");
|
||||||
|
writenulfile(strings.concat(samecase, "/same_test.ww"),
|
||||||
|
"package samecase;\n// NUL: ",
|
||||||
|
" same\n@test fn must_not_run() void = { abort(); };\n");
|
||||||
|
writefile(strings.concat(externalcase, "/prod.ww"),
|
||||||
|
"package externalcase;\nexport fn value() i32 = { return 1; };\n");
|
||||||
|
writenulfile(strings.concat(externalcase, "/external_test.ww"),
|
||||||
|
"package externalcase_test;\n// NUL: ", strings.concat(
|
||||||
|
" external\nimport externalcase;\n",
|
||||||
|
"@test fn must_not_run() void = { abort(); };\n"));
|
||||||
|
writenulfile(strings.concat(onlycase, "/only_test.ww"), "",
|
||||||
|
"package onlycase;\n@test fn must_not_run() void = { abort(); };\n");
|
||||||
|
writedoublenulfile(strings.concat(repeatcase, "/prod.ww"),
|
||||||
|
"package repeatcase;\n// pair ",
|
||||||
|
" here\nfn value() i32 = { return 1; };\n");
|
||||||
|
let testids: []str = ["prodcase", "samecase", "externalcase", "onlycase",
|
||||||
|
"repeatcase"];
|
||||||
|
let testpositions: []str = ["/prodcase/prod.ww:2:9: error: invalid NUL character\n",
|
||||||
|
"/samecase/same_test.ww:2:9: error: invalid NUL character\n",
|
||||||
|
"/externalcase/external_test.ww:2:9: error: invalid NUL character\n",
|
||||||
|
"/onlycase/only_test.ww:1:1: error: invalid NUL character\n",
|
||||||
|
"/repeatcase/prod.ww:2:9: error: invalid NUL character\n"];
|
||||||
|
let testcounts: []i32 = [1, 1, 1, 1, 2];
|
||||||
|
let testdiags: []str = ["", "", "", "", ""];
|
||||||
|
si = 0;
|
||||||
|
for (si < stages.len) {
|
||||||
|
let ti: i32 = 0;
|
||||||
|
for (ti < testids.len) {
|
||||||
|
let testwork: str = strings.concat(root, "/test-work-", tags[si], "-",
|
||||||
|
boundarypkgname(ti));
|
||||||
|
let testout: str = strings.concat(root, "/test-output-", tags[si], "-",
|
||||||
|
boundarypkgname(ti));
|
||||||
|
mkdirall(testwork);
|
||||||
|
let av: []str = [driver(stages[si]), "test", "-w", testwork,
|
||||||
|
"-I", source, "-o", testout, testids[ti]];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, strings.concat("nul-test-", tags[si], "-",
|
||||||
|
boundarypkgname(ti)), av,
|
||||||
|
(60i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(same(out.stdout, "FAIL\n")
|
||||||
|
&& has(out.stderr, testpositions[ti]));
|
||||||
|
assert(occurrences(out.stderr,
|
||||||
|
"invalid NUL character") == testcounts[ti]
|
||||||
|
&& !has(out.stdout, "must_not_run")
|
||||||
|
&& !has(out.stdout, "discovered") && !has(out.stdout, "ok "));
|
||||||
|
if (ti == 4) {
|
||||||
|
assert(has(out.stderr,
|
||||||
|
"/repeatcase/prod.ww:2:10: error: invalid NUL character\n"));
|
||||||
|
};
|
||||||
|
assert(!os.exists(testout) && directoryisempty(testwork));
|
||||||
|
if (si == 0) { testdiags[ti] = strings.dup(out.stderr); }
|
||||||
|
else { assert(same(testdiags[ti], out.stderr)); };
|
||||||
|
ti += 1;
|
||||||
|
};
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A warm invalid edit commits nothing and preserves prior public and action
|
||||||
|
// bytes. Exact restoration reuses the committed generation without a tool.
|
||||||
|
let warm: str = strings.concat(source, "/warm");
|
||||||
|
mkdirall(warm);
|
||||||
|
let warmpath: str = strings.concat(warm, "/main.ww");
|
||||||
|
let warmvalid: str =
|
||||||
|
"package main;\nfn main() i32 = { return 0; };\n";
|
||||||
|
writefile(warmpath, warmvalid);
|
||||||
|
let wrapper: str = strings.concat(root, "/nul-compiler-wrapper.sh");
|
||||||
|
writeexecutable(wrapper, strings.concat(
|
||||||
|
"#!/bin/sh\n",
|
||||||
|
"printf 'compile\\n' >> \"$WW_NUL_TRACE\"\n",
|
||||||
|
"exec \"$WW_NUL_REAL\" \"$@\"\n"));
|
||||||
|
let commandsuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
|
||||||
|
".init.unit.ww", ".init.s", ".init.o"];
|
||||||
|
si = 0;
|
||||||
|
for (si < stages.len) {
|
||||||
|
let warmwork: str = strings.concat(root, "/warm-work-", tags[si]);
|
||||||
|
let warmout: str = strings.concat(root, "/warm-output-", tags[si]);
|
||||||
|
let trace: str = strings.concat(root, "/warm-trace-", tags[si]);
|
||||||
|
mkdirall(warmwork); writefile(trace, "");
|
||||||
|
let env: []str = os.getenvs();
|
||||||
|
append(env, strings.concat("WW_W6C=", wrapper));
|
||||||
|
append(env, strings.concat("WW_NUL_TRACE=", trace));
|
||||||
|
append(env, strings.concat("WW_NUL_REAL=", driver(compilers[si])));
|
||||||
|
let av: []str = [driver(stages[si]), "build", "-w", warmwork,
|
||||||
|
"-I", source, "-o", warmout, "warm"];
|
||||||
|
let out: commandout;
|
||||||
|
runcommandenv(root, strings.concat("nul-warm-cold-", tags[si]), av,
|
||||||
|
env, (120i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||||
|
&& readfile(trace).len != 0);
|
||||||
|
let refs: []str = alloc([], commandsuffixes.len: u64)!;
|
||||||
|
let fi: i32 = 0;
|
||||||
|
for (fi < commandsuffixes.len) {
|
||||||
|
append(refs, strings.dup(readfile(strings.concat(warmwork,
|
||||||
|
"/warm", commandsuffixes[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
let toolpaths: []str = ["/.wwtool.ww", "/.wwtool.w6c",
|
||||||
|
"/.wwtool.w6a", "/.wwtool.stamp"];
|
||||||
|
let toolrefs: []str = alloc([], toolpaths.len: u64)!;
|
||||||
|
fi = 0;
|
||||||
|
for (fi < toolpaths.len) {
|
||||||
|
append(toolrefs, strings.dup(readfile(strings.concat(warmwork,
|
||||||
|
toolpaths[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
let binref: str = strings.dup(readfile(warmout));
|
||||||
|
rewritefile(trace, "");
|
||||||
|
rewritenulfile(warmpath, "package main;\n// NUL: ",
|
||||||
|
" invalid\nfn main() i32 = { return 0; };\n");
|
||||||
|
runcommandenv(root, strings.concat("nul-warm-invalid-", tags[si]), av,
|
||||||
|
env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 1);
|
||||||
|
assert(out.stdout.len == 0
|
||||||
|
&& has(out.stderr, ":2:9: error: invalid NUL character\n"));
|
||||||
|
assert(readfile(trace).len == 0 && same(binref, readfile(warmout)));
|
||||||
|
fi = 0;
|
||||||
|
for (fi < commandsuffixes.len) {
|
||||||
|
assert(same(refs[fi], readfile(strings.concat(warmwork,
|
||||||
|
"/warm", commandsuffixes[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
fi = 0;
|
||||||
|
for (fi < toolpaths.len) {
|
||||||
|
assert(same(toolrefs[fi], readfile(strings.concat(warmwork,
|
||||||
|
toolpaths[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
assert(!directoryhasnew(warmwork)
|
||||||
|
&& !directoryhasfragment(warmwork, ".wwtxn."));
|
||||||
|
rewritefile(warmpath, warmvalid);
|
||||||
|
runcommandenv(root, strings.concat("nul-warm-restored-", tags[si]), av,
|
||||||
|
env, (120i64 * (time.second: i64)): time.duration, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||||
|
&& readfile(trace).len == 0 && same(binref, readfile(warmout)));
|
||||||
|
fi = 0;
|
||||||
|
for (fi < commandsuffixes.len) {
|
||||||
|
assert(same(refs[fi], readfile(strings.concat(warmwork,
|
||||||
|
"/warm", commandsuffixes[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
fi = 0;
|
||||||
|
for (fi < toolpaths.len) {
|
||||||
|
assert(same(toolrefs[fi], readfile(strings.concat(warmwork,
|
||||||
|
toolpaths[fi]))));
|
||||||
|
fi += 1;
|
||||||
|
};
|
||||||
|
si += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Source validation is request-local under overlapping valid and invalid
|
||||||
|
// builds, including independent work, outputs, diagnostics, and cleanup.
|
||||||
|
let pcwork: str = strings.concat(root, "/parallel-c-work");
|
||||||
|
let pwwork: str = strings.concat(root, "/parallel-ww-work");
|
||||||
|
let pcout: str = strings.concat(root, "/parallel-c-output");
|
||||||
|
let pwout: str = strings.concat(root, "/parallel-ww-output");
|
||||||
|
mkdirall(pcwork); mkdirall(pwwork);
|
||||||
|
let pcav: []str = [driver("ww"), "build", "-w", pcwork,
|
||||||
|
"-I", source, "-o", pcout, "good"];
|
||||||
|
let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork,
|
||||||
|
"-I", source, "-o", pwout, "bad"];
|
||||||
|
let pc: exec.command;
|
||||||
|
pc.path = pcav[0]; pc.argv = pcav; pc.env = os.getenvs(); 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),
|
||||||
|
(120i64 * (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 = os.getenvs(); 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),
|
||||||
|
(120i64 * (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);
|
||||||
|
assert(pwp.result.errno == 0 && pwp.result.cleanuperrno == 0
|
||||||
|
&& pwp.result.termination == exec.termination.EXIT
|
||||||
|
&& pwp.result.code == 1);
|
||||||
|
assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0);
|
||||||
|
assert(readfile(pw.stdoutpath).len == 0
|
||||||
|
&& has(readfile(pw.stderrpath), "invalid NUL character"));
|
||||||
|
assert(same(readfile(pcout), readfile(goodbins[0])) && !os.exists(pwout));
|
||||||
|
assert(directoryisempty(pwwork) && !directoryhasnew(pcwork)
|
||||||
|
&& !directoryhasfragment(pcwork, ".wwtxn."));
|
||||||
|
let runav: []str = [pcout];
|
||||||
|
let out: commandout;
|
||||||
|
runcommand(root, "nul-parallel-run", runav, time.second, &out);
|
||||||
|
expectexit(&out, 0);
|
||||||
|
assert(out.stdout.len == 0 && out.stderr.len == 0);
|
||||||
|
assert(!directoryhasfragment(root, ".wwtxn.")
|
||||||
|
&& !directoryhasfragment(root, ".install")
|
||||||
|
&& !directoryhasnew(root));
|
||||||
|
clean(root);
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ toklit(Arena *a, Tok t)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
runrow(const char *src, const char *expect)
|
runrown(const char *src, size_t len, const char *expect, int expecterrs)
|
||||||
{
|
{
|
||||||
Arena *a = newarena();
|
Arena *a = newarena();
|
||||||
Lex l;
|
Lex l;
|
||||||
lexinit(&l, a, "<test>", src, strlen(src));
|
lexinit(&l, a, "<test>", src, len);
|
||||||
|
|
||||||
char *got = amalloc(a, 1);
|
char *got = amalloc(a, 1);
|
||||||
got[0] = '\0';
|
got[0] = '\0';
|
||||||
@@ -48,15 +48,23 @@ runrow(const char *src, const char *expect)
|
|||||||
got[n] = '\0';
|
got[n] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
int ok = strcmp(got, expect) == 0;
|
int ok = strcmp(got, expect) == 0
|
||||||
|
&& (expecterrs < 0 || l.errs == expecterrs);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
fprintf(stderr, "lex mismatch:\n src: %s\n"
|
fprintf(stderr, "lex mismatch:\n want: %s (%d errors)\n"
|
||||||
" want: %s\n got: %s\n", src, expect, got);
|
" got: %s (%d errors)\n", expect, expecterrs, got,
|
||||||
|
l.errs);
|
||||||
}
|
}
|
||||||
freearena(a);
|
freearena(a);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runrow(const char *src, const char *expect)
|
||||||
|
{
|
||||||
|
return runrown(src, strlen(src), expect, -1);
|
||||||
|
}
|
||||||
|
|
||||||
struct row { const char *src, *expect; };
|
struct row { const char *src, *expect; };
|
||||||
|
|
||||||
static const struct row rows[] = {
|
static const struct row rows[] = {
|
||||||
@@ -133,11 +141,11 @@ static const struct row rows[] = {
|
|||||||
* not the token stream. The verbatim Hare messages live at lexunicode
|
* not the token stream. The verbatim Hare messages live at lexunicode
|
||||||
* in cmd/wcc/lex.c. */
|
* in cmd/wcc/lex.c. */
|
||||||
static int
|
static int
|
||||||
runerr(const char *src)
|
runerrn(const char *src, size_t len)
|
||||||
{
|
{
|
||||||
Arena *a = newarena();
|
Arena *a = newarena();
|
||||||
Lex l;
|
Lex l;
|
||||||
lexinit(&l, a, "<test>", src, strlen(src));
|
lexinit(&l, a, "<test>", src, len);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
Tok t = lexnext(&l);
|
Tok t = lexnext(&l);
|
||||||
if (t.kind == TK_EOF)
|
if (t.kind == TK_EOF)
|
||||||
@@ -151,6 +159,12 @@ runerr(const char *src)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runerr(const char *src)
|
||||||
|
{
|
||||||
|
return runerrn(src, strlen(src));
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
runrewind(void)
|
runrewind(void)
|
||||||
{
|
{
|
||||||
@@ -205,6 +219,93 @@ static const char *const errrows[] = {
|
|||||||
"'\\uDFFF'", /* top of the UTF-16 surrogate range */
|
"'\\uDFFF'", /* top of the UTF-16 surrogate range */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const struct {
|
||||||
|
const char *src;
|
||||||
|
size_t len;
|
||||||
|
const char *expect;
|
||||||
|
int errs;
|
||||||
|
} nulrows[] = {
|
||||||
|
{ "\0package main;", sizeof "\0package main;" - 1,
|
||||||
|
"package IDENT(main) ;", 1 },
|
||||||
|
{ "pack\0age main;", sizeof "pack\0age main;" - 1,
|
||||||
|
"package IDENT(main) ;", 1 },
|
||||||
|
{ "1\0_0", sizeof "1\0_0" - 1, "INT(10)", 1 },
|
||||||
|
{ "1f\0oo", sizeof "1f\0oo" - 1, "INT(1) IDENT(foo)", 1 },
|
||||||
|
{ "=\0=", sizeof "=\0=" - 1, "==", 1 },
|
||||||
|
{ "/\0/ comment\nfn", sizeof "/\0/ comment\nfn" - 1, "fn", 1 },
|
||||||
|
{ "/* end *\0/ fn", sizeof "/* end *\0/ fn" - 1, "fn", 1 },
|
||||||
|
{ "\"a\0b\"", sizeof "\"a\0b\"" - 1, "STR(ab)", 1 },
|
||||||
|
{ "'\0A'", sizeof "'\0A'" - 1, "RUNE(65)", 1 },
|
||||||
|
{ "fn\0", sizeof "fn\0" - 1, "fn", 1 },
|
||||||
|
{ "f\0\0n", sizeof "f\0\0n" - 1, "fn", 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
runescapenul(void)
|
||||||
|
{
|
||||||
|
static const char line[] = "\"\\\0n\"";
|
||||||
|
static const char hex[] = "\"\\x0" "\0" "0\"";
|
||||||
|
Arena *a = newarena();
|
||||||
|
Lex l;
|
||||||
|
lexinit(&l, a, "<test>", line, sizeof line - 1);
|
||||||
|
Tok t = lexnext(&l);
|
||||||
|
int ok = t.kind == TK_STR && t.tlen == 1 && t.text[0] == '\n'
|
||||||
|
&& l.errs == 1 && lexnext(&l).kind == TK_EOF;
|
||||||
|
lexinit(&l, a, "<test>", hex, sizeof hex - 1);
|
||||||
|
t = lexnext(&l);
|
||||||
|
ok = ok && t.kind == TK_STR && t.tlen == 1 && t.text[0] == '\0'
|
||||||
|
&& l.errs == 1 && lexnext(&l).kind == TK_EOF;
|
||||||
|
if (!ok)
|
||||||
|
fprintf(stderr, "NUL escape recovery failed\n");
|
||||||
|
freearena(a);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runsuffixnul(void)
|
||||||
|
{
|
||||||
|
static const char src[] = "1i" "\0" "8";
|
||||||
|
Arena *a = newarena();
|
||||||
|
Lex l;
|
||||||
|
lexinit(&l, a, "<test>", src, sizeof src - 1);
|
||||||
|
Tok t = lexnext(&l);
|
||||||
|
int ok = t.kind == TK_INT && t.v.uval == 1 && t.tsuffix != NULL
|
||||||
|
&& strcmp(t.tsuffix, "i8") == 0 && l.errs == 1
|
||||||
|
&& lexnext(&l).kind == TK_EOF;
|
||||||
|
if (!ok)
|
||||||
|
fprintf(stderr, "NUL typed-suffix recovery failed\n");
|
||||||
|
freearena(a);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runlongdirective(void)
|
||||||
|
{
|
||||||
|
static const char pre[] = "//ww:module ";
|
||||||
|
static const char tail[] = "\nfn";
|
||||||
|
const size_t pathlen = 16384;
|
||||||
|
const size_t len = sizeof pre - 1 + pathlen + sizeof tail - 1;
|
||||||
|
char *src = malloc(len);
|
||||||
|
if (!src)
|
||||||
|
return 0;
|
||||||
|
memcpy(src, pre, sizeof pre - 1);
|
||||||
|
memset(src + sizeof pre - 1, 'a', pathlen);
|
||||||
|
memcpy(src + sizeof pre - 1 + pathlen, tail, sizeof tail - 1);
|
||||||
|
Arena *a = newarena();
|
||||||
|
Lex l;
|
||||||
|
lexinit(&l, a, "<test>", src, len);
|
||||||
|
Tok m = lexnext(&l);
|
||||||
|
Tok f = lexnext(&l);
|
||||||
|
int ok = m.kind == TK_MODPATH && m.tlen == pathlen
|
||||||
|
&& m.text[0] == 'a' && m.text[pathlen - 1] == 'a'
|
||||||
|
&& f.kind == TK_FN && l.errs == 0;
|
||||||
|
if (!ok)
|
||||||
|
fprintf(stderr, "long module directive failed\n");
|
||||||
|
freearena(a);
|
||||||
|
free(src);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
main(void)
|
main(void)
|
||||||
{
|
{
|
||||||
@@ -225,6 +326,19 @@ main(void)
|
|||||||
fail++;
|
fail++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (size_t i = 0; i < sizeof nulrows / sizeof nulrows[0]; i++) {
|
||||||
|
if (!runrown(nulrows[i].src, nulrows[i].len,
|
||||||
|
nulrows[i].expect, nulrows[i].errs)) {
|
||||||
|
fprintf(stderr, "NUL row %zu failed\n", i);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!runescapenul())
|
||||||
|
fail++;
|
||||||
|
if (!runsuffixnul())
|
||||||
|
fail++;
|
||||||
|
if (!runlongdirective())
|
||||||
|
fail++;
|
||||||
if (fail) {
|
if (fail) {
|
||||||
fprintf(stderr, "%d/%zu lex tests failed\n", fail,
|
fprintf(stderr, "%d/%zu lex tests failed\n", fail,
|
||||||
sizeof rows / sizeof rows[0]);
|
sizeof rows / sizeof rows[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user