compiler: implement blank package name semantics

This commit is contained in:
2026-08-23 15:33:59 +09:00
parent e67b8e1bc4
commit 0b24b11d65
14 changed files with 2862 additions and 112 deletions

View File

@@ -63,6 +63,167 @@ check_module_stamps(const char *src, const char *want_first, const char *want_la
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)
{
@@ -114,6 +275,69 @@ main(void)
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;
}