/* * 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 #include #include static int parses_clean(const char *src) { Arena *a = newarena(); Lex l; Parser p; lexinit(&l, a, "", 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, "", 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, "", 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; }