Files
ww/test/wcc/738_module_decl.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;
}