344 lines
11 KiB
C
344 lines
11 KiB
C
/*
|
|
* Pins three invariants from the module-system rewrite:
|
|
* 1. The parser ACCEPTS `package foo;` as the first non-comment item.
|
|
* 2. The parser ACCEPTS multiple `package X;` decls (concatenated
|
|
* multi-file streams from the driver) and stamps subsequent
|
|
* decls with whichever package is "current".
|
|
* 3. Bare comments + a `package foo;` is still legal — the keyword
|
|
* may follow leading comments.
|
|
*
|
|
* The relaxed-or-strict missing-package check was softened to a
|
|
* silent default (curmod=NULL) so legacy fragment-driven tests
|
|
* still parse. That softening is documented at parsefile() in
|
|
* cmd/wcc/parse.c.
|
|
*/
|
|
#include "ww.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
static int
|
|
parses_clean(const char *src)
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
lexinit(&l, a, "<test>", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *n = parsefile(&p);
|
|
int ok = (n != NULL && p.errs == 0 && l.errs == 0);
|
|
freearena(a);
|
|
return ok;
|
|
}
|
|
|
|
static const char *first_decl_module(Node *file) {
|
|
if (file == NULL || file->list == NULL) return NULL;
|
|
return file->list->module;
|
|
}
|
|
|
|
static int
|
|
check_module_stamps(const char *src, const char *want_first, const char *want_last)
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
lexinit(&l, a, "<test>", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *n = parsefile(&p);
|
|
int ok = (n != NULL && p.errs == 0 && l.errs == 0);
|
|
if (ok) {
|
|
const char *first = first_decl_module(n);
|
|
Node *last = n->list;
|
|
while (last && last->next) last = last->next;
|
|
const char *lastmod = last ? last->module : NULL;
|
|
int match_first = (first == NULL && want_first == NULL)
|
|
|| (first != NULL && want_first != NULL
|
|
&& strcmp(first, want_first) == 0);
|
|
int match_last = (lastmod == NULL && want_last == NULL)
|
|
|| (lastmod != NULL && want_last != NULL
|
|
&& strcmp(lastmod, want_last) == 0);
|
|
ok = match_first && match_last;
|
|
}
|
|
freearena(a);
|
|
return ok;
|
|
}
|
|
|
|
enum packagemode {
|
|
PACKAGE_FULL,
|
|
PACKAGE_IMPORTS,
|
|
PACKAGE_HEADER,
|
|
};
|
|
|
|
static const char *
|
|
packagemodename(enum packagemode mode)
|
|
{
|
|
switch (mode) {
|
|
case PACKAGE_FULL: return "full";
|
|
case PACKAGE_IMPORTS: return "imports";
|
|
case PACKAGE_HEADER: return "header";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
static Node *
|
|
parsepackage(Parser *p, enum packagemode mode)
|
|
{
|
|
switch (mode) {
|
|
case PACKAGE_FULL: return parsefile(p);
|
|
case PACKAGE_IMPORTS: return parseimports(p);
|
|
case PACKAGE_HEADER: return parsepackageheader(p);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* The three package-clause parsers intentionally expose different AST
|
|
* boundaries. Header/imports outer nodes stay on the package keyword; the
|
|
* full parser's outer file node retains its historical 1:1 root position.
|
|
* Imports-only parsing keeps its package marker on the keyword, while full
|
|
* parsing moves only a blank package marker to the underscore so the checker
|
|
* can diagnose the name token. An ordinary package marker never moves.
|
|
*/
|
|
static int
|
|
check_package_shape(enum packagemode mode, const char *name,
|
|
int markerline, int markercol)
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
char src[160];
|
|
snprintf(src, sizeof src,
|
|
"// leading comment\npackage %s;\nimport alpha;\n"
|
|
"fn x() void = {};\n", name);
|
|
lexinit(&l, a, "shape.ww", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *file = parsepackage(&p, mode);
|
|
Node *marker = file ? file->body : NULL;
|
|
int wantmarker = mode != PACKAGE_HEADER;
|
|
int outerline = mode == PACKAGE_FULL ? 1 : 2;
|
|
int ok = file != NULL && file->kind == N_FILE
|
|
&& p.errs == 0 && l.errs == 0
|
|
&& file->pkgname != NULL && strcmp(file->pkgname, name) == 0
|
|
&& file->pos.line == outerline && file->pos.col == 1
|
|
&& ((mode == PACKAGE_FULL)
|
|
|| (file->module != NULL && strcmp(file->module, name) == 0))
|
|
&& ((!wantmarker && marker == NULL)
|
|
|| (wantmarker && marker != NULL && marker->next == NULL
|
|
&& marker->kind == N_FILE
|
|
&& marker->pkgname != NULL
|
|
&& strcmp(marker->pkgname, name) == 0
|
|
&& marker->pos.line == markerline
|
|
&& marker->pos.col == markercol));
|
|
if (!ok) {
|
|
fprintf(stderr,
|
|
"package shape mismatch: mode=%s name=%s "
|
|
"errs=%d/%d outer=%d:%d marker=%d:%d\n",
|
|
packagemodename(mode), name, p.errs, l.errs,
|
|
file ? file->pos.line : 0, file ? file->pos.col : 0,
|
|
marker ? marker->pos.line : 0, marker ? marker->pos.col : 0);
|
|
}
|
|
freearena(a);
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
rejects_malformed_package(enum packagemode mode)
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
const char *src = "package ;\nfn x() void = {};\n";
|
|
char *diag = NULL;
|
|
size_t diaglen = 0;
|
|
FILE *prev = errout;
|
|
FILE *capture = open_memstream(&diag, &diaglen);
|
|
if (capture != NULL)
|
|
errout = capture;
|
|
lexinit(&l, a, "malformed.ww", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *file = parsepackage(&p, mode);
|
|
int ok = file != NULL && p.errs + l.errs > 0;
|
|
if (capture != NULL) {
|
|
fclose(capture);
|
|
errout = prev;
|
|
}
|
|
if (!ok)
|
|
fprintf(stderr, "malformed package accepted in %s mode\n",
|
|
packagemodename(mode));
|
|
free(diag);
|
|
freearena(a);
|
|
return ok;
|
|
}
|
|
|
|
/*
|
|
* N_USE keeps the first import-spec token (alias when present) separately
|
|
* from the first dotted-path token. That distinction is checker-visible for
|
|
* import errors, so pin it in each parser boundary rather than inferring the
|
|
* path position later from the binding spelling.
|
|
*/
|
|
static int
|
|
check_import_path_position(enum packagemode mode, const char *spec,
|
|
int speccol, int pathcol, const char *alias, const char *binding,
|
|
int blank)
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
char src[192];
|
|
snprintf(src, sizeof src,
|
|
"package foo;\nimport %s;\nfn x() void = {};\n", spec);
|
|
lexinit(&l, a, "usepath.ww", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *file = parsepackage(&p, mode);
|
|
Node *u = file ? file->list : NULL;
|
|
while (u != NULL && u->kind != N_USE)
|
|
u = u->next;
|
|
int ok = file != NULL && p.errs == 0 && l.errs == 0
|
|
&& u != NULL
|
|
&& u->pos.file != NULL && strcmp(u->pos.file, "usepath.ww") == 0
|
|
&& u->pos.line == 2 && u->pos.col == 1
|
|
&& u->usefile != NULL && strcmp(u->usefile, "usepath.ww") == 0
|
|
&& u->useline == 2 && u->usecol == speccol
|
|
&& u->usepathfile != NULL
|
|
&& strcmp(u->usepathfile, "usepath.ww") == 0
|
|
&& u->usepathline == 2 && u->usepathcol == pathcol
|
|
&& u->usesource != NULL
|
|
&& strcmp(u->usesource, "alpha.beta") == 0
|
|
&& u->usepath != NULL && strcmp(u->usepath, "alpha.beta") == 0
|
|
&& ((alias == NULL && u->usealias == NULL)
|
|
|| (alias != NULL && u->usealias != NULL
|
|
&& strcmp(u->usealias, alias) == 0))
|
|
&& ((binding == NULL && u->str == NULL)
|
|
|| (binding != NULL && u->str != NULL
|
|
&& strcmp(u->str, binding) == 0))
|
|
&& u->useblank == blank;
|
|
if (!ok) {
|
|
fprintf(stderr,
|
|
"import path position mismatch: mode=%s spec=%s "
|
|
"errs=%d/%d specpos=%d:%d pathpos=%d:%d\n",
|
|
packagemodename(mode), spec, p.errs, l.errs,
|
|
u ? u->useline : 0, u ? u->usecol : 0,
|
|
u ? u->usepathline : 0, u ? u->usepathcol : 0);
|
|
}
|
|
freearena(a);
|
|
return ok;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
int pass = 0, fail = 0;
|
|
|
|
if (parses_clean("package foo;\nfn x() void = {};\n")) pass++;
|
|
else { fprintf(stderr, "738[1] basic package accept FAILED\n"); fail++; }
|
|
|
|
if (parses_clean("// header\n// more\npackage foo;\nfn x() void = {};\n")) pass++;
|
|
else { fprintf(stderr, "738[2] package after comments FAILED\n"); fail++; }
|
|
|
|
if (check_module_stamps(
|
|
"package foo;\nfn x() void = {};\nfn y() void = {};\n",
|
|
"foo", "foo")) pass++;
|
|
else { fprintf(stderr, "738[3] decl module stamping FAILED\n"); fail++; }
|
|
|
|
/* Mirrors driver-emitted multi-file concatenated streams. */
|
|
if (check_module_stamps(
|
|
"package foo;\nfn a() void = {};\n"
|
|
"package bar;\nfn b() void = {};\n",
|
|
"foo", "bar")) pass++;
|
|
else { fprintf(stderr, "738[4] mid-stream package switch FAILED\n"); fail++; }
|
|
|
|
if (parses_clean(
|
|
"package foo;\nimport encoding.utf8;\nfn x() void = {};\n")) pass++;
|
|
else { fprintf(stderr, "738[5] dotted import accept FAILED\n"); fail++; }
|
|
|
|
/* Dotted import stores only the leaf identifier on N_USE.str
|
|
* (post-task-#22 — the driver translates the full dotted path to
|
|
* a directory walk; the checker only needs the package bareword
|
|
* for n_use → decl disambiguation). */
|
|
{
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
const char *src = "package foo;\nimport encoding.utf8;\n";
|
|
lexinit(&l, a, "<test>", src, strlen(src));
|
|
parserinit(&p, a, &l);
|
|
Node *n = parsefile(&p);
|
|
int ok = 0;
|
|
if (n != NULL && p.errs == 0 && l.errs == 0 && n->list != NULL) {
|
|
Node *u = n->list;
|
|
ok = (u->kind == N_USE
|
|
&& u->str != NULL
|
|
&& strcmp(u->str, "utf8") == 0);
|
|
}
|
|
freearena(a);
|
|
if (ok) pass++;
|
|
else { fprintf(stderr, "738[6] dotted import leaf-store FAILED\n"); fail++; }
|
|
}
|
|
|
|
/* `package _;` is syntax in every loader/compiler parser mode. */
|
|
if (check_package_shape(PACKAGE_HEADER, "_", 0, 0)) pass++;
|
|
else { fprintf(stderr, "738[7] blank package header shape FAILED\n"); fail++; }
|
|
|
|
if (check_package_shape(PACKAGE_IMPORTS, "_", 2, 1)) pass++;
|
|
else { fprintf(stderr, "738[8] blank imports-only shape FAILED\n"); fail++; }
|
|
|
|
if (check_package_shape(PACKAGE_FULL, "_", 2, 9)) pass++;
|
|
else { fprintf(stderr, "738[9] blank full-parser shape FAILED\n"); fail++; }
|
|
|
|
/* Nonblank package-marker positions remain on the package keyword. */
|
|
if (check_package_shape(PACKAGE_HEADER, "foo", 0, 0)) pass++;
|
|
else { fprintf(stderr, "738[10] named package header shape FAILED\n"); fail++; }
|
|
|
|
if (check_package_shape(PACKAGE_IMPORTS, "foo", 2, 1)) pass++;
|
|
else { fprintf(stderr, "738[11] named imports-only shape FAILED\n"); fail++; }
|
|
|
|
if (check_package_shape(PACKAGE_FULL, "foo", 2, 1)) pass++;
|
|
else { fprintf(stderr, "738[12] named full-parser shape FAILED\n"); fail++; }
|
|
|
|
/* Extending the name slot to `_` must not accept a missing name. */
|
|
if (rejects_malformed_package(PACKAGE_HEADER)) pass++;
|
|
else { fprintf(stderr, "738[13] malformed package header FAILED\n"); fail++; }
|
|
|
|
if (rejects_malformed_package(PACKAGE_IMPORTS)) pass++;
|
|
else { fprintf(stderr, "738[14] malformed imports-only package FAILED\n"); fail++; }
|
|
|
|
if (rejects_malformed_package(PACKAGE_FULL)) pass++;
|
|
else { fprintf(stderr, "738[15] malformed full-parser package FAILED\n"); fail++; }
|
|
|
|
/* Default imports use the first dotted-path identifier for both slots. */
|
|
for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) {
|
|
if (check_import_path_position(mode, "alpha.beta", 8, 8,
|
|
NULL, "beta", 0)) pass++;
|
|
else {
|
|
fprintf(stderr, "738 default import path position FAILED (%s)\n",
|
|
packagemodename(mode));
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
/* Explicit aliases keep the binding/spec token at col 8, path at 14. */
|
|
for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) {
|
|
if (check_import_path_position(mode, "local alpha.beta", 8, 14,
|
|
"local", "local", 0)) pass++;
|
|
else {
|
|
fprintf(stderr, "738 explicit import path position FAILED (%s)\n",
|
|
packagemodename(mode));
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
/* A blank alias likewise keeps `_` at col 8 and the path at col 10. */
|
|
for (enum packagemode mode = PACKAGE_FULL; mode <= PACKAGE_HEADER; mode++) {
|
|
if (check_import_path_position(mode, "_ alpha.beta", 8, 10,
|
|
NULL, NULL, 1)) pass++;
|
|
else {
|
|
fprintf(stderr, "738 blank import path position FAILED (%s)\n",
|
|
packagemodename(mode));
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
printf("738_module_decl: %d pass, %d fail\n", pass, fail);
|
|
return fail == 0 ? 0 : 1;
|
|
}
|