ww source: reject raw NUL characters

This commit is contained in:
2026-08-21 23:03:14 +09:00
parent 9381f8fb8e
commit 4b9968e4e6
10 changed files with 1304 additions and 320 deletions

View File

@@ -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;
@@ -67,6 +119,7 @@ lget(Lex *l)
} }
return c; return c;
} }
}
static Pos static Pos
lpos(Lex *l) lpos(Lex *l)
@@ -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;
} }

View File

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

View File

@@ -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 113165](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/source.go#L113-L165),
detects ASCII zero at lines 121129, reports `invalid NUL character`, and
continues decoding. `TestScanErrors` pins the positioned diagnostic at
[`cmd/compile/internal/syntax/scanner_test.go`, lines 587600](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/syntax/scanner_test.go#L587-L600),
and compiler testdata [`test/nul1.go`, lines 752](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 63108](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/scanner/scanner.go#L63-L108)
and its tests at lines 790819. 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.

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -124,6 +170,29 @@ fn lget(l: *lex) i32 = {
}; };
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 = {
out.file = l.file; out.file = l.file;
@@ -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 = {

View File

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

View File

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

View File

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