diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index dc0fdd9c..5cb62645 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -77,6 +77,7 @@ main(int argc, char **argv) check_init(&c, a); c.is_test = testmode; + c.sep_mode = sepmode; check_file(&c, file); if (c.errs) return 1; diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 17f6e1d9..148ca053 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -543,8 +543,13 @@ wwi_emit(Checker *c, FILE *of, Node *file) k++; } qsort(us, (size_t)nuse, sizeof *us, usecmp); - for (int i = 0; i < nuse; i++) + const char *previous = NULL; + for (int i = 0; i < nuse; i++) { + if (previous && strcmp(previous, us[i].path) == 0) + continue; fprintf(of, "import %s;\n", us[i].path); + previous = us[i].path; + } free(us); } diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index b5f1778f..21fec9b8 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -59,6 +59,9 @@ lookup_builtin(const char *name) static const char *decl_mod(Node *file, Node *d); static const char *use_path(Node *file, const char *curmod, const char *alias); +static int src_imports(Node *file, const char *modtag, const char *name); +static Sym *lookup_visible(Checker *c, const char *name); +static Sym *lookup_visible_type(Checker *c, const char *name); static void resolve_typedecl(Checker *c, Node *d); static Type * @@ -69,7 +72,7 @@ resolve_typename(Checker *c, Node *n) if (bi) return bi; /* #225: kind-filtered so a same-named value binding (param/let/fn) * in a closer scope can't hide the type binding it shadows. */ - Sym *s = scope_lookup_type(c->cur, c->cur_mod, nm); + Sym *s = lookup_visible_type(c, nm); if (s == NULL && nm) { /* module-qualified: io.stream → strip the last dot prefix * and look up the leaf, filtering on the importing module's @@ -88,9 +91,9 @@ resolve_typename(Checker *c, Node *n) * import path (symbols are path-keyed). */ const char *mk = use_path(c->file, c->cur_mod, head); - if (mk == NULL) mk = head; - s = scope_lookup_in_module(c->cur, mk, - dot + 1); + if (mk != NULL) + s = scope_lookup_in_module(c->cur, mk, + dot + 1); } } } @@ -422,7 +425,7 @@ assignable_addrfn(Checker *c, Type *dst, Node *rhs) if (rhs->kind != N_UN || rhs->op != TK_AMP) return 0; Node *id = rhs->lhs; if (id == NULL || id->kind != N_IDENT || id->str == NULL) return 0; - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, id->str); + Sym *s = lookup_visible(c, id->str); if (s == NULL || s->kind != SK_FN) return 0; Type *fnty = s->type; if (fnty == NULL || fnty->kind != TY_FN) return 0; @@ -606,7 +609,7 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth) return 1; } case N_IDENT: { - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str); + Sym *s = lookup_visible(c, n->str); if (s == NULL || s->kind != SK_DEF || s->decl == NULL || s->decl->rhs == NULL) return 0; @@ -616,7 +619,7 @@ eval_def_const(Checker *c, Node *n, u64 *out, int depth) if (n->lhs == NULL || n->lhs->kind != N_IDENT) return 0; /* M1 #22: map the qualifier alias to its dotted import path. */ const char *mk = use_path(c->file, c->cur_mod, n->lhs->str); - if (mk == NULL) mk = n->lhs->str; + if (mk == NULL) return 0; Sym *s = scope_lookup_in_module(c->cur, mk, n->str); if (s == NULL || s->kind != SK_DEF || s->decl == NULL || s->decl->rhs == NULL) @@ -1414,7 +1417,7 @@ cexpr(Checker *c, Node *n) if (n->str && n->str[0] == '\0') return n->type = err(c, n->pos, "`_` is only valid as a binding or discard lvalue"); - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str); + Sym *s = lookup_visible(c, n->str); if (s == NULL) return n->type = err(c, n->pos, "undefined: %s", n->str); /* SK_USE has no concrete value type; the only legal use is @@ -1444,8 +1447,7 @@ cexpr(Checker *c, Node *n) * Color sitting at the head of the flat scope chain — * symmetric with wwstage's enumlookup graduation. */ if (n->lhs && n->lhs->kind == N_IDENT) { - Sym *ms = scope_lookup_prefer(c->cur, c->cur_mod, - n->lhs->str); + Sym *ms = lookup_visible(c, n->lhs->str); if (ms && (ms->kind == SK_USE || ms->use_alias)) { /* Module-qualified ref. `use_alias` covers * the self-import case where the module's @@ -1460,17 +1462,28 @@ cexpr(Checker *c, Node *n) * that path before looking up the leaf. */ const char *mk = use_path(c->file, c->cur_mod, n->lhs->str); - if (mk == NULL) mk = n->lhs->str; - Sym *fs = scope_lookup_in_module(c->cur, - mk, n->str); - if (fs) - return n->type = fs->type; - if (ms->kind == SK_USE) { - /* Pure SK_USE with missing leaf: - * external declaration. Codegen - * emits CALL/MOVQ by the leaf name - * and the linker resolves it. */ - return n->type = ty_err; + if (mk == NULL) { + if (ms->kind == SK_USE) + return n->type = err(c, n->pos, + "package '%s' is not directly imported", + n->lhs->str); + } else { + Sym *fs = scope_lookup_in_module(c->cur, + mk, n->str); + if (fs) + return n->type = fs->type; + /* A bare `w6c -T` intentionally leaves the + * compiler-generated test.run hook external; the + * ordinary driver supplies lib/test. This is the only + * missing direct member that is not a package export + * error. */ + if (c->sep_mode && ms->kind == SK_USE + && n != c->synth_test_run) + return n->type = err(c, n->pos, + "package '%s' has no exported declaration '%s'", + n->lhs->str, n->str); + if (ms->kind == SK_USE) + return n->type = ty_err; } /* SK_TYPE with use_alias=1 and no leaf * found: fall through so the enum / type- @@ -1616,8 +1629,7 @@ cexpr(Checker *c, Node *n) * def-as-constant splice-index is deferred (no * consumer). */ if (n->lhs->kind == N_IDENT) { - Sym *s = scope_lookup_prefer(c->cur, - c->cur_mod, n->lhs->str); + Sym *s = lookup_visible(c, n->lhs->str); if (s && s->kind == SK_DEF) return n->type = err(c, n->pos, "cannot index a def-constant str " @@ -1855,7 +1867,7 @@ cexpr(Checker *c, Node *n) * (e.g. lib/os/os.ww) keeps working unchanged. */ if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "abort") == 0 && - scope_lookup_prefer(c->cur, c->cur_mod, "abort") == NULL) { + lookup_visible(c, "abort") == NULL) { if (n->list) { Type *mt = cexpr(c, n->list); if (mt != ty_err && !type_assignable(ty_str, mt)) @@ -1870,7 +1882,7 @@ cexpr(Checker *c, Node *n) if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "assert") == 0 && n->list != NULL && - scope_lookup_prefer(c->cur, c->cur_mod, "assert") == NULL) { + lookup_visible(c, "assert") == NULL) { Type *ct = cexpr(c, n->list); if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool) err(c, n->pos, "assert: cond must be bool"); @@ -2019,8 +2031,7 @@ cexpr(Checker *c, Node *n) return n->type = ty_void; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) { - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, - n->lhs->str); + Sym *s = lookup_visible(c, n->lhs->str); if (s && s->is_const) err(c, n->pos, "cannot assign to const `%s`", n->lhs->str); @@ -2072,8 +2083,7 @@ cexpr(Checker *c, Node *n) * resolve_type for the synthetic-type-expr case. */ Type *t = NULL; if (n->lhs && n->lhs->kind == N_IDENT) { - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, - n->lhs->str); + Sym *s = lookup_visible(c, n->lhs->str); if (s == NULL || s->kind != SK_TYPE) t = err(c, n->pos, "unknown struct type '%s'", n->lhs->str); @@ -2865,28 +2875,34 @@ decl_mod(Node *file, Node *d) * concatenated unit may bind the same leaf alias to different paths * (sha256's `import crypto.math` and strconv's `import math` both bind * alias `math`). The import declared in the SAME module as the - * reference (`curmod`) is the authoritative one; preferring it closes - * the cross-module mis-resolution a first-match scan caused. Falls back - * to any matching alias when the referencing module has no own import - * (single-occurrence case, unchanged). Returns NULL if no such `use`. + * reference (`curmod`) is the authoritative one; requiring it closes both + * cross-module mis-resolution and accidental transitive-import visibility. + * Returns NULL if the referencing package has no such `use`. */ static const char * use_path(Node *file, const char *curmod, const char *alias) { if (file == NULL || alias == NULL) return NULL; - const char *any = NULL; + /* Legacy inline multi-package units may spell a package's own + * declarations as `pkg.member`. That is self-qualification, not an + * imported namespace; preserve it without reopening transitive lookup. */ + if (curmod != NULL) { + const char *dot = strrchr(curmod, '.'); + const char *leaf = dot ? dot + 1 : curmod; + if (strcmp(alias, leaf) == 0) return curmod; + } for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->str == NULL || strcmp(u->str, alias) != 0) continue; const char *p = u->usepath ? u->usepath : u->str; - int same = (u->module == NULL) ? (curmod == NULL) - : (curmod != NULL && strcmp(u->module, curmod) == 0); + const char *um = decl_mod(file, u); + int same = (um == NULL) ? (curmod == NULL) + : (curmod != NULL && strcmp(um, curmod) == 0); if (same) return p; - if (any == NULL) any = p; } - return any; + return NULL; } /* @@ -2967,6 +2983,67 @@ src_imports(Node *file, const char *modtag, const char *name) return 0; } +/* A flattened interface symbol is source-visible as a bare name only when + * the referencing package directly imports the symbol's defining path. The + * strict scope helpers above handle lexical locals, builtins and same-package + * declarations first; this second pass restores WW's existing direct-import + * convenience without admitting a transitive interface by accident. */ +static int +direct_module_visible(Checker *c, const char *mod) +{ + if (c == NULL || mod == NULL || mod[0] == '\0') return 0; + const char *dot = strrchr(mod, '.'); + const char *alias = dot ? dot + 1 : mod; + const char *path = use_path(c->file, c->cur_mod, alias); + return path != NULL && strcmp(path, mod) == 0; +} + +static Sym * +lookup_visible(Checker *c, const char *name) +{ + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, name); + /* A real source declaration shadows a decl-less pseudo-builtin. Keep + * the builtin as fallback while checking direct imported declarations. */ + Sym *builtin = NULL; + if (s != NULL) { + if (s->decl != NULL) return s; + builtin = s; + } + /* N_USE entries are historically coalesced by leaf in the flat scope, + * so the retained symbol may carry another source package's owner. The + * source-owned alias map is authoritative: if this package directly + * imports NAME, return the coalesced module marker only as a marker; the + * N_DOT path maps the alias to the correct full path again. */ + if (use_path(c->file, c->cur_mod, name) != NULL) { + for (Scope *p = c->cur; p; p = p->parent) + for (Sym *b = p->first; b; b = b->next) + if (strcmp(b->name, name) == 0 + && (b->kind == SK_USE || b->use_alias)) + return b; + } + for (Scope *p = c->cur; p; p = p->parent) { + for (Sym *b = p->first; b; b = b->next) + if (strcmp(b->name, name) == 0 + && direct_module_visible(c, b->mod)) + return b; + } + return builtin; +} + +static Sym * +lookup_visible_type(Checker *c, const char *name) +{ + Sym *s = scope_lookup_type(c->cur, c->cur_mod, name); + if (s != NULL) return s; + for (Scope *p = c->cur; p; p = p->parent) { + for (Sym *b = p->first; b; b = b->next) + if (b->kind == SK_TYPE && strcmp(b->name, name) == 0 + && direct_module_visible(c, b->mod)) + return b; + } + return NULL; +} + /* * check_module_shadow — refuse value bindings that shadow an * in-scope imported module bareword. "Value names and module names @@ -3402,6 +3479,7 @@ check_file(Checker *c, Node *file) dot->lhs = newnode(c->a, N_IDENT, fp); dot->lhs->str = "test"; dot->str = "run"; + c->synth_test_run = dot; call->lhs = dot; call->list = arg; Node *ret = newnode(c->a, N_RETURN, fp); diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 95a34d8c..f289cf5b 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -100,6 +100,59 @@ static Node *parsetype(Parser *p); static Node *parseblock(Parser *p); static Node *parsestmt(Parser *p); +static void +skipdecl(Parser *p) +{ + int paren = 0, bracket = 0, brace = 0; + while (p->cur.kind != TK_EOF) { + switch (p->cur.kind) { + case TK_LPAREN: paren++; break; + case TK_RPAREN: if (paren > 0) paren--; break; + case TK_LBRACK: bracket++; break; + case TK_RBRACK: if (bracket > 0) bracket--; break; + case TK_LBRACE: brace++; break; + case TK_RBRACE: if (brace > 0) brace--; break; + case TK_SEMI: + advance(p); + if (paren == 0 && bracket == 0 && brace == 0) + return; + continue; + default: + break; + } + advance(p); + } +} + +/* Consume attribute syntax without parsing its expressions or any following + * declaration. The imports-only pass needs only to recognize that the next + * declaration is an attributed import, which full parsing rejects. */ +static void +skipimportattrs(Parser *p) +{ + while (p->cur.kind == TK_AT) { + advance(p); + if (p->cur.kind == TK_IDENT) + advance(p); + else { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + } + if (!accept(p, TK_LPAREN)) continue; + int depth = 1; + while (p->cur.kind != TK_EOF && depth > 0) { + if (p->cur.kind == TK_LPAREN) depth++; + else if (p->cur.kind == TK_RPAREN) depth--; + advance(p); + } + if (depth > 0) { + errorf(p->cur.pos, "expected ')' after attribute"); + p->errs++; + } + } +} + static Node * parseparams(Parser *p) { @@ -1277,25 +1330,134 @@ parseuse(Parser *p) /* M1 #22: accumulate the full dotted import path (n->module) so the * checker can match decl identity on the path, while n->str stays the * leaf alias the user writes (`utf8.x`). */ - char pathbuf[256]; - size_t pl = 0; const char *leaf = expectident(p); - for (size_t i = 0; leaf[i] && pl + 1 < sizeof pathbuf; i++) - pathbuf[pl++] = leaf[i]; + const char *path = leaf; while (accept(p, TK_DOT)) { leaf = expectident(p); - if (pl + 1 < sizeof pathbuf) pathbuf[pl++] = '.'; - for (size_t i = 0; leaf[i] && pl + 1 < sizeof pathbuf; i++) - pathbuf[pl++] = leaf[i]; + path = aprintf(p->a, "%s.%s", path, leaf); } - pathbuf[pl] = '\0'; n->str = leaf; n->strlen = strlen(leaf); - n->usepath = astrndup(p->a, pathbuf, pl); + n->usepath = path; expect(p, TK_SEMI); return n; } +Node * +parseimports(Parser *p) +{ + Pos fp = { p->l->file, 1, 1 }; + Node *file = newnode(p->a, N_FILE, fp); + Node *head = NULL, *tail = NULL; + Node *packages = NULL, *packagetail = NULL; + int sawpackage = 0; + + while (p->cur.kind != TK_EOF) { + /* Compiler/driver bundle markers carry package identity out of + * band. They are not source declarations, so keep scanning for + * the following package clause and imports. */ + if (p->cur.kind == TK_MODPATH) { + sawpackage = 0; + p->pathmod = p->cur.text; + p->curmod = p->cur.text; + p->resetmod = NULL; + advance(p); + continue; + } + if (p->cur.kind == TK_MODRESET) { + const char *rp = p->cur.text; + sawpackage = 0; + advance(p); + p->pathmod = NULL; + p->curmod = rp; + p->resetmod = rp; + continue; + } + if (p->cur.kind == TK_MODULE) { + Pos pp = p->cur.pos; + advance(p); + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "invalid or missing package clause"); + p->errs++; + sawpackage = 1; + skipdecl(p); + continue; + } + const char *name = expectident(p); + expect(p, TK_SEMI); + p->curmod = name; + Node *package = newnode(p->a, N_FILE, pp); + package->module = name; + if (packages == NULL) + packages = package; + else + packagetail->next = package; + packagetail = package; + if (!sawpackage) { + file->module = name; + file->pos = pp; + sawpackage = 1; + } + continue; + } + if (!sawpackage && p->pathmod == NULL && p->resetmod == NULL) { + errorf(p->cur.pos, "invalid or missing package clause"); + p->errs++; + sawpackage = 1; + } + if (p->cur.kind == TK_USE) { + Node *d = parseuse(p); + d->module = p->curmod; + if (head == NULL) + head = d; + else + tail->next = d; + tail = d; + continue; + } + if (p->cur.kind == TK_AT) { + skipimportattrs(p); + if (p->cur.kind == TK_EXPORT) advance(p); + if (p->cur.kind == TK_USE) { + errorf(p->cur.pos, + "import cannot be exported or attributed"); + p->errs++; + Node *d = parseuse(p); + d->module = p->curmod; + if (head == NULL) + head = d; + else + tail->next = d; + tail = d; + continue; + } + skipdecl(p); + continue; + } + if (p->cur.kind == TK_EXPORT && peek(p).kind == TK_USE) { + advance(p); + errorf(p->cur.pos, + "import cannot be exported or attributed"); + p->errs++; + Node *d = parseuse(p); + d->module = p->curmod; + if (head == NULL) + head = d; + else + tail->next = d; + tail = d; + continue; + } + skipdecl(p); + } + file->list = head; + /* The package-clause chain lets a directory loader validate every + * selected file without inventing a second header grammar. It lives in + * body because list is the public imports chain. */ + file->body = packages; + return file; +} + static Node * parsedef(Parser *p, int exp) { diff --git a/cmd/wcc/sym.c b/cmd/wcc/sym.c index de96f9ae..25939a92 100644 --- a/cmd/wcc/sym.c +++ b/cmd/wcc/sym.c @@ -68,8 +68,10 @@ scope_lookup_in_module(Scope *s, const char *mod, const char *name) /* * Within each scope's bucket: Pass 1 prefers entries whose `sym.mod` - * matches the caller's `mod`; Pass 2 falls back to the first match - * regardless of mod (the existing scope_lookup semantics). We only + * matches the caller's `mod`; Pass 2 accepts only a source-visible bare + * entry. For a primary package that means mod==NULL. For an imported + * package it means a lexical local (a child scope) or a decl-less builtin, + * never another package's flattened interface value. We only * descend to the parent scope when the current scope has no matching * entry at all — so a local binding in a closer scope still shadows a * same-name fn from a parent scope, even when the parent entry @@ -83,14 +85,16 @@ scope_lookup_in_module(Scope *s, const char *mod, const char *name) Sym * scope_lookup_prefer(Scope *s, const char *mod, const char *name) { - if (mod == NULL) return scope_lookup(s, name); for (Scope *p = s; p; p = p->parent) { u64 h = hashstr(name) % p->nbuckets; Sym *fallback = NULL; for (Sym *b = p->buckets[h]; b; b = b->hashnext) { if (strcmp(b->name, name) != 0) continue; - if (b->mod && strcmp(b->mod, mod) == 0) return b; - if (fallback == NULL) fallback = b; + if (mod && b->mod && strcmp(b->mod, mod) == 0) + return b; + if (b->mod == NULL && (mod == NULL || p->parent != NULL + || b->decl == NULL) && fallback == NULL) + fallback = b; } if (fallback) return fallback; } @@ -115,7 +119,9 @@ scope_lookup_type(Scope *s, const char *mod, const char *name) if (b->kind != SK_TYPE) continue; if (strcmp(b->name, name) != 0) continue; if (mod && b->mod && strcmp(b->mod, mod) == 0) return b; - if (fallback == NULL) fallback = b; + if (b->mod == NULL && (mod == NULL || b->decl == NULL) + && fallback == NULL) + fallback = b; } if (fallback) return fallback; } diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index df20ffd0..adc1e8e9 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -383,6 +383,9 @@ struct Parser { void parserinit(Parser*, Arena*, Lex*); Node *parsefile(Parser*); +/* Imports-only N_FILE: module/pos identify the first package clause, + * list holds N_USE declarations, and body holds package-clause markers. */ +Node *parseimports(Parser*); Node *parseexpr_top(Parser*); /* for testing: parse one expression */ typedef enum { @@ -585,6 +588,10 @@ struct Checker { int errs; int is_test; /* #15: `w6c -T` — collect @test fns + synth * the entry; loud-reject a user main. */ + int sep_mode; /* -c package compilation: imported interfaces are + * present, so absent members are hard export errors. */ + Node *synth_test_run; /* exact compiler-generated test.run DOT; + * its unresolved external hook is intentional */ Node *alloc_octx; /* #3/B': the one empty `alloc([], n)` call node * that has let-declared slice context this walk; * any OTHER empty alloc has no element-type hint diff --git a/lib/strings/tokenize_test.ww b/lib/strings/tokenize_test.ww index 1c55a42e..052e6ffc 100644 --- a/lib/strings/tokenize_test.ww +++ b/lib/strings/tokenize_test.ww @@ -5,6 +5,7 @@ // (the package, not the file, is the unit of testing). package strings_test; +import bytes; import strings; import os; import test; diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index ca082c8b..643524fd 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -77,9 +77,26 @@ fn accepttok(p: *parser, k: tkind) bool = { return false; }; +fn putdec(v: i32) void = { + let digits: [16]u8; + let n: i32 = 0; + let x: i32 = v; + if (x <= 0) { digits[n] = '0'; n += 1; } + else { + for (x > 0) { + digits[n] = ((x % 10) + ('0': i32)): u8; + n += 1; + x = x / 10; + }; + }; + for (n > 0) { n -= 1; os.write(2, &digits[n], 1u64); }; +}; + fn errmsg(p: *parser, msg: str) void = { - let pre = "parse: "; - os.write(2, pre.ptr, pre.len: u64); + os.write(2, p.curfile.ptr, p.curfile.len: u64); + os.write(2, ":".ptr, 1u64); putdec(p.curline); + os.write(2, ":".ptr, 1u64); putdec(p.curcol); + os.write(2, ": error: ".ptr, 9u64); os.write(2, msg.ptr, msg.len: u64); os.write(2, "\n".ptr, 1u64); p.errs += 1; @@ -91,11 +108,12 @@ fn expecttok(p: *parser, k: tkind, what: str) bool = { return false; }; -// Returns the empty str on error (and advances to make progress). +// Returns false without consuming the unexpected token; the enclosing +// production owns recovery, matching the C frontend. fn expectident(p: *parser, into: *str) bool = { if (p.curkind != tkind.TK_IDENT) { - errmsg(p, "expected identifier"); - advance(p); + errmsg(p, strings.concat("expected identifier, got ", + tokname(p.curkind))); return false; }; *into = p.curtext; @@ -413,6 +431,153 @@ fn isassignop(k: tkind) bool = { return false; }; +fn skipimportdecl(p: *parser) void = { + let paren: i32 = 0; + let bracket: i32 = 0; + let brace: i32 = 0; + for (p.curkind != tkind.TK_EOF) { + if (p.curkind == tkind.TK_LPAREN) { paren += 1; } + else { if (p.curkind == tkind.TK_RPAREN) { + if (paren > 0) { paren -= 1; }; + } else { if (p.curkind == tkind.TK_LBRACK) { bracket += 1; } + else { if (p.curkind == tkind.TK_RBRACK) { + if (bracket > 0) { bracket -= 1; }; + } else { if (p.curkind == tkind.TK_LBRACE) { brace += 1; } + else { if (p.curkind == tkind.TK_RBRACE) { + if (brace > 0) { brace -= 1; }; + } else { if (p.curkind == tkind.TK_SEMI) { + advance(p); + if (paren == 0 && bracket == 0 && brace == 0) { return; }; + continue; + };};};};};};}; + advance(p); + }; +}; + +// Consume attributes without parsing their argument expressions. The +// imports-only pass only needs to recognize and reject an attributed import. +fn skipimportattrs(p: *parser) void = { + for (p.curkind == tkind.TK_AT) { + advance(p); + if (p.curkind == tkind.TK_IDENT) { advance(p); } + else { + errmsg(p, strings.concat("expected identifier, got ", + tokname(p.curkind))); + }; + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let depth: i32 = 1; + for (p.curkind != tkind.TK_EOF && depth > 0) { + if (p.curkind == tkind.TK_LPAREN) { depth += 1; } + else { if (p.curkind == tkind.TK_RPAREN) { depth -= 1; }; }; + advance(p); + }; + if (depth > 0) { errmsg(p, "expected ')' after attribute"); }; + }; + }; +}; + +export fn parseimports(p: *parser) *node = { + let f = newnode(nkind.N_FILE, p.curfile, 1, 1); + let head: *node = nil; + let tail: *node = nil; + let packages: *node = nil; + let packagetail: *node = nil; + let sawpackage: bool = false; + for (p.curkind != tkind.TK_EOF) { + // Compiler/driver bundle markers carry package identity out of + // band. They are not source declarations, so keep scanning for + // the following package clause and imports. + if (p.curkind == tkind.TK_MODPATH) { + sawpackage = false; + p.pathmod = p.curtext; + p.curmod = p.curtext; + p.resetmod = ""; + advance(p); + continue; + }; + if (p.curkind == tkind.TK_MODRESET) { + let rp: str = p.curtext; + sawpackage = false; + advance(p); + p.pathmod = ""; + p.curmod = rp; + p.resetmod = rp; + continue; + }; + if (p.curkind == tkind.TK_MODULE) { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); + if (p.curkind != tkind.TK_IDENT) { + errmsg(p, "invalid or missing package clause"); + sawpackage = true; + skipimportdecl(p); + continue; + }; + let name: str; + expectident(p, &name); + expecttok(p, tkind.TK_SEMI, "expected ';' after module name"); + p.curmod = name; + let pm = newnode(nkind.N_FILE, pf, pl, pc); + pm.nmod = name; + if (packages == nil) { packages = pm; } + else { packagetail.next = pm; }; + packagetail = pm; + if (!sawpackage) { + f.nmod = name; + f.file = pf; + f.line = pl; + f.col = pc; + sawpackage = true; + }; + continue; + }; + if (!sawpackage && p.pathmod.len == 0 && p.resetmod.len == 0) { + errmsg(p, "invalid or missing package clause"); + sawpackage = true; + }; + if (p.curkind == tkind.TK_USE) { + let d: *node = parseuse(p); + d.nmod = p.curmod; + if (head == nil) { head = d; } else { tail.next = d; }; + tail = d; + continue; + }; + if (p.curkind == tkind.TK_AT) { + skipimportattrs(p); + if (p.curkind == tkind.TK_EXPORT) { advance(p); }; + if (p.curkind == tkind.TK_USE) { + errmsg(p, "import cannot be exported or attributed"); + let d: *node = parseuse(p); + d.nmod = p.curmod; + if (head == nil) { head = d; } else { tail.next = d; }; + tail = d; + continue; + }; + skipimportdecl(p); + continue; + }; + if (p.curkind == tkind.TK_EXPORT) { + advance(p); + if (p.curkind == tkind.TK_USE) { + errmsg(p, "import cannot be exported or attributed"); + let d: *node = parseuse(p); + d.nmod = p.curmod; + if (head == nil) { head = d; } else { tail.next = d; }; + tail = d; + continue; + }; + }; + skipimportdecl(p); + }; + f.list = head; + // body carries package-clause markers; list remains imports only. + f.body = packages; + return f; +}; + // Forward references between parseunary/parseexpr/parsebin/parsepostfix // are resolved by the two-pass checker — no body-less prototypes needed. @@ -523,6 +688,9 @@ export fn parsefile(p: *parser) *node = { let d: *node = nil; if (p.curkind == tkind.TK_USE) { + if (attrs != nil || exported != 0) { + errmsg(p, "import cannot be exported or attributed"); + }; d = parseuse(p); } else { if (p.curkind == tkind.TK_DEF) { d = parsedef(p, exported); diff --git a/lib/ww/syntax/sym.ww b/lib/ww/syntax/sym.ww index dda98cee..8bc89443 100644 --- a/lib/ww/syntax/sym.ww +++ b/lib/ww/syntax/sym.ww @@ -110,11 +110,9 @@ export fn scopelookup(s: *scope, name: str) *sym = { // scopelookupinmodule(c, leaf, leaf) won't find it. // // #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` -// matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of -// mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, -// so chain-first = last-registered — when two modules export the same -// type leaf the bare walk silently picked the newest-installed one, -// install-order-dependent, while cstage is deterministic on cur_mod. +// matches `mod`; Pass-2 accepts only a primary-package or builtin type. The +// synthesized global `nomem` has an empty source location and is builtin too. +// Flattened transitive interface types are never an unqualified fallback. // Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the // kind-filtered + mod-preferring single walk); sole caller passes // c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the @@ -136,7 +134,12 @@ export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { }; }; }; - if (fallback == nil) { fallback = b; }; + let builtin: bool = b.decl == nil; + if (!builtin && b.decl.file.len == 0 && b.decl.line == 0) { + builtin = true; + }; + if (b.mod.len == 0 && (mod.len == 0 || builtin) + && fallback == nil) { fallback = b; }; }; }; b = b.hashnext; @@ -220,8 +223,10 @@ export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { }; // Within each scope's bucket: Pass 1 prefers entries whose -// `sym.mod` matches `mod`; Pass 2 falls back to the first match -// regardless of mod (same semantics as scopelookup). We only descend +// `sym.mod` matches `mod`; Pass 2 accepts only a source-visible bare entry. +// For an imported package that means a lexical local (child scope) or a +// decl-less builtin, never another package's flattened interface symbol. +// We only descend // to the parent scope when the current scope has no matching entry at // all — so a local binding in a closer scope still shadows a same-name // fn from a parent scope, even when the parent entry mod-matches. @@ -234,7 +239,6 @@ export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { // io.read that happens to hash earlier into the flat scope. Mirrors // cmd/wcc/sym.c scope_lookup_prefer. export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { - if (mod.len == 0) { return scopelookup(s, name); }; let p: *scope = s; for (p != nil) { let h: u64 = hashstr(name); @@ -243,12 +247,13 @@ export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { let fallback: *sym = nil; for (b != nil) { if (streq(b.name, name)) { - if (b.mod.len > 0) { + if (mod.len > 0 && b.mod.len > 0) { if (streq(b.mod, mod)) { return b; }; }; - if (fallback == nil) { fallback = b; }; + if (b.mod.len == 0 && (mod.len == 0 || p.parent != nil + || b.decl == nil) && fallback == nil) { fallback = b; }; }; b = b.hashnext; }; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index d55caf0f..c403a93b 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -172,6 +172,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let ck: checker; checkinit(&ck, &tc); ck.istest = testmode; + ck.sepmode = sepmode; checkfile(&ck, f); if (ck.errs > 0) { return 1; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 95be8b23..ff6ba9f8 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -5,6 +5,7 @@ package wcc; import os; import syntax; import strconv; +import strings; type checker = struct { tc: *syntax.tctx, @@ -19,6 +20,10 @@ type checker = struct { // twin (c->matcharms) istest: i32, // #15: `w6c_ww -T` — collect @test fns + // synth the entry; loud-reject a user main. + sepmode: i32, // -c package compilation: imported interfaces are + // present, so absent members are hard export errors. + synthtestrun: *syntax.node, // exact generated test.run DOT; its + // unresolved external hook is intentional verbose: i32, // when non-zero, log each unresolved name fnret: *syntax.node, // enclosing fn's return type AST (for `?`) curmod: str, // importing-module bareword for the decl @@ -164,17 +169,31 @@ fn declmod(file: *syntax.node, d: *syntax.node) str = { // to the full dotted import path it binds (`encoding.utf8`), for the // module-qualified resolution and codegen hint (M1 #22). Single-level // packages have usepath == alias so the result is unchanged. The -// current tree has one occurrence per leaf, so the map is unambiguous. -fn usepathfor(file: *syntax.node, alias: str) str = { +// Only an import owned by the referencing package is visible; a matching +// alias carried by a transitive interface is deliberately ignored. +fn usepathfor(file: *syntax.node, modtag: str, alias: str) str = { let empty: str; if (file == nil) { return empty; }; if (alias.len == 0) { return empty; }; + if (modtag.len != 0) { + let (prefix, suffix) = strings.rcut(modtag, "."); + let leaf: str = suffix; + if (leaf.len == 0) { leaf = modtag; }; + if (syntax.streq(alias, leaf)) { return modtag; }; + }; let u: *syntax.node = file.list; for (u != nil) { if (u.kind == syntax.nkind.N_USE) { if (syntax.streq(u.str, alias)) { - if (u.usepath.len != 0) { return u.usepath; }; - return u.str; + let um: str = declmod(file, u); + let same: bool = false; + if (modtag.len == 0) { + if (um.len == 0) { same = true; }; + } else { if (syntax.streq(um, modtag)) { same = true; }; }; + if (same) { + if (u.usepath.len != 0) { return u.usepath; }; + return u.str; + }; }; }; u = u.next; @@ -182,12 +201,10 @@ fn usepathfor(file: *syntax.node, alias: str) str = { return empty; }; -// modkeyfor — usepathfor with leaf-alias fallback: the module key for a -// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). +// modkeyfor — the module key for a directly imported alias. Empty means +// the referencing package did not itself declare that import. fn modkeyfor(c: *checker, alias: str) str = { - let mk: str = usepathfor(c.file, alias); - if (mk.len == 0) { return alias; }; - return mk; + return usepathfor(c.file, c.curmod, alias); }; // srcimports — does the source file that contributed decl-module @@ -225,6 +242,132 @@ fn srcimports(file: *syntax.node, modtag: str, name: str) bool = { return false; }; +// Bare imported declarations remain WW source syntax, but only across a +// direct edge. The syntax scope helpers resolve lexical locals, builtins and +// same-package declarations first; this pass admits a flattened interface +// symbol iff the referencing source package itself imported its module path. +fn directmodvisible(c: *checker, mod: str) bool = { + if (mod.len == 0) { return false; }; + let (prefix, suffix) = strings.rcut(mod, "."); + let alias: str = suffix; + if (alias.len == 0) { alias = mod; }; + let path: str = usepathfor(c.file, c.curmod, alias); + return path.len != 0 && syntax.streq(path, mod); +}; + +fn lookupvisible(c: *checker, name: str) *syntax.sym = { + let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name); + let builtin: *syntax.sym = nil; + if (found != nil) { + if (found.decl != nil) { return found; }; + builtin = found; + }; + // Flat scope installation coalesces same-leaf N_USE entries. The + // source-owned alias map, not the retained marker's mod field, decides + // whether this package can use the qualifier. + if (usepathfor(c.file, c.curmod, name).len != 0) { + let q: *syntax.scope = c.cur; + for (q != nil) { + let u: *syntax.sym = q.first; + for (u != nil) { + if (syntax.streq(u.name, name) + && (u.skind == syntax.skind.SK_USE + || u.use_alias != 0i32)) { return u; }; + u = u.snext; + }; + q = q.parent; + }; + }; + let p: *syntax.scope = c.cur; + for (p != nil) { + let b: *syntax.sym = p.first; + for (b != nil) { + if (syntax.streq(b.name, name) && directmodvisible(c, b.mod)) { + return b; + }; + b = b.snext; + }; + p = p.parent; + }; + return builtin; +}; + +fn lookupvisibletype(c: *checker, name: str) *syntax.sym = { + // C resolves intrinsic type names before consulting package symbols. + // Select the empty-module seed here so an imported interface cannot + // redefine a builtin for wwstage; size/opaque have no seed and remain + // handled directly by tinfofornode. + if (builtintypename(name)) { + let empty: str; + return syntax.scopelookuptype(c.cur, empty, name); + }; + let found: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, name); + if (found != nil) { return found; }; + let p: *syntax.scope = c.cur; + for (p != nil) { + let b: *syntax.sym = p.first; + for (b != nil) { + if (b.skind == syntax.skind.SK_TYPE + && syntax.streq(b.name, name) + && directmodvisible(c, b.mod)) { return b; }; + b = b.snext; + }; + p = p.parent; + }; + return nil; +}; + +fn builtintypename(name: str) bool = { + return syntax.streq(name, "void") + || syntax.streq(name, "bool") + || syntax.streq(name, "rune") + || syntax.streq(name, "i8") + || syntax.streq(name, "i16") + || syntax.streq(name, "i32") + || syntax.streq(name, "i64") + || syntax.streq(name, "u8") + || syntax.streq(name, "u16") + || syntax.streq(name, "u32") + || syntax.streq(name, "u64") + || syntax.streq(name, "int") + || syntax.streq(name, "uint") + || syntax.streq(name, "uintptr") + || syntax.streq(name, "size") + || syntax.streq(name, "opaque") + || syntax.streq(name, "f32") + || syntax.streq(name, "f64") + || syntax.streq(name, "str") + || syntax.streq(name, "never") + || syntax.streq(name, "nomem") + || syntax.streq(name, "untyped_int") + || syntax.streq(name, "untyped_float") + || syntax.streq(name, "untyped_str") + || syntax.streq(name, "untyped_rune") + || syntax.streq(name, "untyped_bool") + || syntax.streq(name, "untyped_nil"); +}; + +fn packageaccesserr(c: *checker, e: *syntax.node, pkg: str, member: str, + missing: bool) void = { + cerr(e.file); cerr(":"); + cerr(strconv.i32tos(e.line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(e.col, strconv.base.DEC)); + cerr(": error: package '"); cerr(pkg); + if (missing) { + cerr("' has no exported declaration '"); cerr(member); cerr("'\n"); + } else { + cerr("' is not directly imported\n"); + }; + c.errs += 1; +}; + +// A bare `w6c -T` leaves the compiler-generated test.run hook external; +// the ordinary driver supplies lib/test. No user-written missing member is +// exempt from package export checking. +fn synthesizedtestrun(c: *checker, e: *syntax.node) bool = { + return c.synthtestrun != nil && c.synthtestrun == e; +}; + // checkmoduleshadow — enforce "value names and module names are // disjoint" at nested-scope binds. Mirrors cstage check_module_shadow // (cmd/wcc/check.c). Fires for fn params / lets / forrange iters / @@ -498,7 +641,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { if (k == syntax.nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm); + let s: *syntax.sym = lookupvisible(c, nm); if (s == nil) { // Unshadowed abort/assert binds no sym BY // DESIGN (the EXPR_ASSERT family has no callee @@ -522,13 +665,14 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { if (k == syntax.nkind.N_TNAME) { let nm: str = n.str; if (nm.len > 0) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm); + let s: *syntax.sym = lookupvisibletype(c, nm); + let builtin: bool = builtintypename(nm); // `pkg.Type` — strip the last dot prefix and look up // the leaf with a mod filter so same-leaf-name types // from different imports (`bufio.stream` vs // `io.stream`) disambiguate to the right one. // Mirrors cmd/wcc/check.c resolve_typename. - if (s == nil) { + if (s == nil && !builtin) { let dot: i32 = nm.len - 1; for (dot >= 0) { if (nm[dot] == 46u8) { break; }; @@ -543,18 +687,39 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = { let leaf: str; leaf.ptr = nm.ptr + (dot + 1): u64; leaf.len = nm.len - (dot + 1); - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); + let mk: str = modkeyfor(c, head); + if (mk.len != 0) { + s = syntax.scopelookupinmodule(c.cur, mk, leaf); + }; }; }; }; - if (s == nil) { + if (s == nil && !builtin) { + if (syntax.scopelookup(c.cur, nm) != nil) { + cerr(n.file); cerr(":"); + cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(n.col, strconv.base.DEC)); + cerr(": error: unknown type '"); cerr(nm); cerr("'\n"); + c.errs += 1; + n.type_ = c.tc.tyerr: *void; + }; c.nunresolved += 1; if (c.verbose != 0) { cerr(" unresolved tname: "); cerr(nm); cerr("\n"); }; - } else { c.nresolved += 1; }; + } else { + c.nresolved += 1; + // Resolve and cache the type while c.curmod still names the + // declaration that owns this syntax node. Exported structs may + // contain a direct dependency's type; consumers need that cached + // shape for structural field access, but must not gain source + // visibility of the dependency qualifier. Cstage does the same + // owner-scoped work in resolve_typedecl. + let ti: *syntax.tinfo = tinfofornode(c, n); + if (ti != nil) { n.type_ = ti: *void; }; + }; }; }; @@ -991,6 +1156,25 @@ fn unwrapbang(n: *syntax.node) *syntax.node = { fn aliassym(c: *checker, n: *syntax.node) *syntax.sym = { if (n == nil) { return nil; }; if (n.kind != syntax.nkind.N_TNAME) { return nil; }; + // An exported signature is resolved while its owning package is the + // current module. Preserve that compiler-owned binding when the exact + // same AST node is later inspected structurally by a consumer (for + // example `os.filestat.atime.sec`, where atime is time.instant). This + // does not make a consumer-written `time.instant` visible: such a node + // has no owner-resolved named cache unless the consumer imports time. + let bound: *syntax.tinfo = n.type_: *syntax.tinfo; + if (bound != nil) { if (bound.kind == syntax.tykind.TY_NAMED) { + let owner: *syntax.scope = c.cur; + for (owner != nil) { + let bs: *syntax.sym = owner.first; + for (bs != nil) { + if (bs.skind == syntax.skind.SK_TYPE + && bs.type_ == bound) { return bs; }; + bs = bs.snext; + }; + owner = owner.parent; + }; + }; }; let nm: str = n.str; // #51: pkg.alias type refs land here as a single TNAME whose // str is the joined form (lib/ww/parse/parse.ww:258-265 in @@ -1013,7 +1197,10 @@ fn aliassym(c: *checker, n: *syntax.node) *syntax.sym = { let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, head), leaf); + let mk: str = modkeyfor(c, head); + if (mk.len != 0) { + s = syntax.scopelookupinmodule(c.cur, mk, leaf); + }; } else { // #53: same-module preference. Mirrors cstage // cmd/wcc/check.c:66 scope_lookup_prefer. Without this, @@ -1026,7 +1213,7 @@ fn aliassym(c: *checker, n: *syntax.node) *syntax.sym = { // scopelookupprefer (the #56/#4/#11a wave). The only bare-leaf // type lookups left — varianterr (:1051) + scruttype (:1098) — // stay mod-blind but have no reproducible divergence; #58. - s = syntax.scopelookupprefer(c.cur, c.curmod, nm); + s = lookupvisibletype(c, nm); // #61 A.5: bare TNAME that collides with an imported // module bareword. Two shapes hit this: // - `let l: lex;` where `lex` struct lives in @@ -1049,7 +1236,7 @@ fn aliassym(c: *checker, n: *syntax.node) *syntax.sym = { // otherwise resolve install-order-dependent here // when a value binding shadows the leaf; cstage // passes c->cur_mod to scope_lookup_type (sym.c). - let sm: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, nm); + let sm: *syntax.sym = lookupvisibletype(c, nm); if (sm != nil) { s = sm; }; }; }; @@ -1271,7 +1458,9 @@ fn scruttype(c: *checker, e: *syntax.node) *syntax.node = { if (e.kind == syntax.nkind.N_DOT) { if (e.lhs == nil) { return nil; }; if (e.lhs.kind != syntax.nkind.N_IDENT) { return nil; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.str), e.str); + let mk: str = modkeyfor(c, e.lhs.str); + if (mk.len == 0) { return nil; }; + let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, mk, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; @@ -1862,7 +2051,7 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = { return true; }; if (k == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.str); + let s: *syntax.sym = lookupvisible(c, n.str); if (s == nil) { return false; }; if (s.skind != syntax.skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; @@ -1872,7 +2061,9 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = { if (k == syntax.nkind.N_DOT) { if (n.lhs == nil) { return false; }; if (n.lhs.kind != syntax.nkind.N_IDENT) { return false; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); + let mk: str = modkeyfor(c, n.lhs.str); + if (mk.len == 0) { return false; }; + let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, mk, n.str); if (s == nil) { return false; }; if (s.skind != syntax.skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; @@ -3287,7 +3478,7 @@ fn unoptype(c: *checker, e: *syntax.node) *syntax.node = { // the foreign signature (scopedefineinmodule prepends); // cstage types a fn ident via scope_lookup_prefer with // cur_mod (cmd/wcc/check.c:1305). - let fs: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); + let fs: *syntax.sym = lookupvisible(c, e.lhs.str); if (fs != nil) { if (fs.skind == syntax.skind.SK_FN) { if (fs.decl != nil) { @@ -3313,7 +3504,11 @@ fn unoptype(c: *checker, e: *syntax.node) *syntax.node = { // cstage's actual acceptance reason. if (e.lhs.kind == syntax.nkind.N_DOT) { if (e.lhs.lhs != nil && e.lhs.lhs.kind == syntax.nkind.N_IDENT) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); + let fs: *syntax.sym = nil; + let mk: str = modkeyfor(c, e.lhs.lhs.str); + if (mk.len != 0) { + fs = syntax.scopelookupinmodule(c.cur, mk, e.lhs.str); + }; if (fs != nil) { if (fs.skind == syntax.skind.SK_FN) { if (fs.decl != nil) { @@ -3388,7 +3583,7 @@ fn indexresult(c: *checker, e: *syntax.node) *syntax.node = { // valid; only the SK_DEF scalar-str operand is // unindexable. cstage twin: check.c N_INDEX TY_STR arm. if (e.lhs != nil && e.lhs.kind == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); + let s: *syntax.sym = lookupvisible(c, e.lhs.str); if (s != nil && s.skind == syntax.skind.SK_DEF) { cerr("error: cannot index a def-constant str '"); cerr(e.lhs.str); @@ -3536,8 +3731,22 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // bare `error` then binds strconv.error not io.error). Mirrors // cstage cmd/wcc/check.c:66 scope_lookup_prefer; sibling #56 at // L2439, #53 at L688. Tracked in the cluster note at L685-687. - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.str); - if (s == nil) { return nil; }; + let s: *syntax.sym = lookupvisible(c, e.str); + if (s == nil) { + // A same-named flattened symbol that fails lookupvisible is a + // transitive implementation fact, not an unresolved external. + // Diagnose it like cstage's N_IDENT path and stamp tyerr so + // later call checking does not obscure the causal error. + if (syntax.scopelookup(c.cur, e.str) != nil) { + cerr(e.file); cerr(":"); + cerr(strconv.i32tos(e.line, strconv.base.DEC)); cerr(":"); + cerr(strconv.i32tos(e.col, strconv.base.DEC)); + cerr(": error: undefined: "); cerr(e.str); cerr("\n"); + c.errs += 1; + e.type_ = c.tc.tyerr: *void; + }; + return nil; + }; if (s.decl == nil) { return nil; }; // #34: a bare fn-name rvalue types as its FN TYPE, not its return // type. decl.lhs is the RETURN type for an N_FNDECL, so synthesize @@ -3632,6 +3841,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return e.rhs; }; if (k == syntax.nkind.N_CALL) { + if (e.type_ == c.tc.tyerr: *void) { return nil; }; let callee: *syntax.node = e.lhs; if (callee == nil) { return nil; }; // #31: synthesize the `alloc(value)` / `alloc([], n)` builtin @@ -3809,7 +4019,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // The TY_ERR stamp on the callee is cgen's routing key // (cstage spells it `n->lhs->type = ty_err`). if (syntax.streq(callee.str, "abort") - && syntax.scopelookupprefer(c.cur, c.curmod, "abort") == nil) { + && lookupvisible(c, "abort") == nil) { if (e.list != nil) { let mt: *syntax.node = exprtype(c, e.list, nil); let conf: bool = false; @@ -3828,7 +4038,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return tn; }; if (syntax.streq(callee.str, "assert") && e.list != nil - && syntax.scopelookupprefer(c.cur, c.curmod, "assert") == nil) { + && lookupvisible(c, "assert") == nil) { let ct: *syntax.node = exprtype(c, e.list, nil); if (ct != nil) { // No alias peel: cstage compares ty_bool by @@ -4010,11 +4220,11 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { if (nm.len > 0) { let s: *syntax.sym = nil; if (callee.kind == syntax.nkind.N_IDENT) { - s = syntax.scopelookupprefer(c.cur, c.curmod, nm); + s = lookupvisible(c, nm); } else { let ms: *syntax.sym = nil; if (callee.lhs != nil && callee.lhs.kind == syntax.nkind.N_IDENT) { - ms = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); + ms = lookupvisible(c, callee.lhs.str); if (ms != nil && ms.skind != syntax.skind.SK_USE) { let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, callee.lhs.str); if (mu != nil) { ms = mu; }; @@ -4036,7 +4246,17 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // global-leaf path) and harec check_autodereference // (ref/harec/src/check.c:1566-1581). if (ms != nil && (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32)) { - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); + let mk: str = modkeyfor(c, callee.lhs.str); + if (mk.len == 0) { + if (ms.skind == syntax.skind.SK_USE) { + packageaccesserr(c, callee, callee.lhs.str, nm, false); + e.type_ = c.tc.tyerr: *void; + callee.type_ = c.tc.tyerr: *void; + return nil; + }; + } else { + s = syntax.scopelookupinmodule(c.cur, mk, nm); + }; // Module-qualified callee whose leaf isn't scope-keyed // under its module: align to cstage, which stamps ty_err // here and lets cgen emit the call (cmd/wcc/check.c:1834- @@ -4050,6 +4270,10 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // keys the CALL off run's `//ww:module test` directive // either way, so the resolution change is asm-neutral. if (s == nil) { + if (c.sepmode != 0 && ms.skind == syntax.skind.SK_USE && mk.len != 0 + && !synthesizedtestrun(c, callee)) { + packageaccesserr(c, callee, callee.lhs.str, nm, true); + }; e.type_ = c.tc.tyerr: *void; callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) return nil; @@ -4106,6 +4330,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return nil; }; if (k == syntax.nkind.N_DOT) { + if (e.type_ == c.tc.tyerr: *void) { return nil; }; // A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c // :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands // in A.6.1.5b. SK_USE gates case 1; a #6a-D dot-lhs collision @@ -4120,7 +4345,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // ref/harec/src/check.c:4419-4434. let lhsn: *syntax.node = e.lhs; if (lhsn != nil) { if (lhsn.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, lhsn.str); + let ms: *syntax.sym = lookupvisible(c, lhsn.str); if (ms != nil && ms.skind != syntax.skind.SK_USE) { let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, lhsn.str); if (mu != nil) { ms = mu; }; @@ -4133,7 +4358,16 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // qualified resolution and the lenient checker policy // keeps the silent miss documented at scruttype L656. if (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, lhsn.str), e.str); + let mk: str = modkeyfor(c, lhsn.str); + if (mk.len == 0 && ms.skind == syntax.skind.SK_USE) { + packageaccesserr(c, e, lhsn.str, e.str, false); + e.type_ = c.tc.tyerr: *void; + return nil; + }; + let fs: *syntax.sym = nil; + if (mk.len != 0) { + fs = syntax.scopelookupinmodule(c.cur, mk, e.str); + }; if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is @@ -4165,6 +4399,12 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { return tn; }; }; }; + if (c.sepmode != 0 && fs == nil && ms.skind == syntax.skind.SK_USE && mk.len != 0 + && !synthesizedtestrun(c, e)) { + packageaccesserr(c, e, lhsn.str, e.str, true); + e.type_ = c.tc.tyerr: *void; + return nil; + }; }; // Fold case 2 inner: bare `EnumT.MEMBER` where EnumT // is an SK_TYPE in the flat scope. Mirror cstage @@ -4310,7 +4550,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = { // style anonymous struct lit we don't yet parse — bail. if (e.lhs == nil) { return nil; }; if (e.lhs.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); + let ms: *syntax.sym = lookupvisible(c, e.lhs.str); if (ms != nil) { if (ms.skind == syntax.skind.SK_TYPE) { if (ms.decl != nil) { let tn: *syntax.node = ms.decl.lhs; if (tn != nil) { @@ -4906,7 +5146,7 @@ fn assignableaddrfn(c: *checker, dst: *syntax.node, rhs: *syntax.node) bool = { // silently admitting a *fn into a foreign-sig slot or rejecting a // valid same-module &fn. cstage uses scope_lookup_prefer with // cur_mod (cmd/wcc/check.c:410, assignable_addrfn). - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, id.str); + let s: *syntax.sym = lookupvisible(c, id.str); if (s == nil) { return false; }; if (s.skind != syntax.skind.SK_FN) { return false; }; if (s.decl == nil) { return false; }; @@ -6043,7 +6283,7 @@ fn desugararrayslice(c: *checker, dsttn: *syntax.node, srctn: *syntax.node, val: fn calleefndecl(c: *checker, callee: *syntax.node) *syntax.node = { if (callee == nil) { return nil; }; if (callee.kind == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.str); + let s: *syntax.sym = lookupvisible(c, callee.str); if (s != nil) { if (s.skind == syntax.skind.SK_FN) { return s.decl; }; }; @@ -6052,14 +6292,18 @@ fn calleefndecl(c: *checker, callee: *syntax.node) *syntax.node = { if (callee.kind == syntax.nkind.N_DOT) { if (callee.lhs != nil) { if (callee.lhs.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); + let ms: *syntax.sym = lookupvisible(c, callee.lhs.str); if (ms != nil && ms.skind != syntax.skind.SK_USE) { let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, callee.lhs.str); if (mu != nil) { ms = mu; }; }; if (ms != nil) { if (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); + let fs: *syntax.sym = nil; + let mk: str = modkeyfor(c, callee.lhs.str); + if (mk.len != 0) { + fs = syntax.scopelookupinmodule(c.cur, mk, callee.str); + }; if (fs != nil) { if (fs.skind == syntax.skind.SK_FN) { return fs.decl; }; }; @@ -6218,7 +6462,7 @@ fn checkassign(c: *checker, n: *syntax.node) void = { // cmd/wcc/check.c:1889-1896. if (n.lhs.kind == syntax.nkind.N_IDENT) { if (n.lhs.str.len > 0) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.lhs.str); + let s: *syntax.sym = lookupvisible(c, n.lhs.str); if (s != nil) { if (s.is_const != 0i32) { cerr("error: cannot assign to const binding\n"); @@ -6763,7 +7007,7 @@ fn exprtypeoftry(c: *checker, e: *syntax.node) *syntax.node = { // either spuriously rejecting valid `?` code or skipping the F8 // reject. Mirrors exprtype's own N_IDENT arm (scopelookupprefer // at :2897); cstage types the operand via cexpr with cur_mod. - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.str); + let s: *syntax.sym = lookupvisible(c, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; @@ -6791,7 +7035,7 @@ fn exprtypeoftry(c: *checker, e: *syntax.node) *syntax.node = { // N_CALL arm (scopelookupprefer at :3336). The N_DOT-callee leaf // (callee.str above) still resolves bare-leaf, not via the module // qualifier callee.lhs.str — that module-keyed fix rides task #51. - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm); + let s: *syntax.sym = lookupvisible(c, nm); if (s == nil) { return nil; }; if (s.skind != syntax.skind.SK_FN) { return nil; }; if (s.decl == nil) { return nil; }; @@ -7028,7 +7272,7 @@ fn isassertfam(c: *checker, id: *syntax.node) bool = { if (!syntax.streq(id.str, "abort") && !syntax.streq(id.str, "assert")) { return false; }; - return syntax.scopelookupprefer(c.cur, c.curmod, id.str) == nil; + return lookupvisible(c, id.str) == nil; }; // asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the @@ -7089,7 +7333,10 @@ fn asserttyped(c: *checker, n: *syntax.node, indot: bool) void = { if (isexpr && k == syntax.nkind.N_IDENT) { if (indot) { skip = true; }; if (!skip) { - let s: *syntax.sym = syntax.scopelookup(c.cur, n.str); + // Use the same source-visibility rules as expression + // resolution. A transitive same-leaf declaration must not hide + // a decl-less pseudo-builtin from this invariant gate. + let s: *syntax.sym = lookupvisible(c, n.str); if (s != nil) { if (s.skind == syntax.skind.SK_USE) { skip = true; }; if (s.decl == nil) { skip = true; }; @@ -7146,6 +7393,8 @@ export fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.nunresolved = 0; c.errs = 0; c.istest = 0i32; // #15: caller (w6c main) sets it after init + c.sepmode = 0i32; // caller (w6c main) sets it from -c + c.synthtestrun = nil; c.verbose = 0; c.fnret = nil; let empty: str; @@ -7395,6 +7644,7 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { did.str = "test"; dot.lhs = did; dot.str = "run"; + c.synthtestrun = dot; call.lhs = dot; call.list = arg; let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 71cf73d8..da9c2334 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -668,10 +668,14 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { }; wwisortdecls(upaths, unodes, nuse); let i: i32 = 0; + let previous: str = ""; for (i < nuse) { - wputs(fd, "import "); - wputs(fd, upaths[i]); - wputs(fd, ";\n"); + if (previous.len == 0 || !syntax.streq(previous, upaths[i])) { + wputs(fd, "import "); + wputs(fd, upaths[i]); + wputs(fd, ";\n"); + previous = upaths[i]; + }; i += 1; }; }; diff --git a/test/wcc/200_parse.c b/test/wcc/200_parse.c index 8b1459e6..649b6f13 100644 --- a/test/wcc/200_parse.c +++ b/test/wcc/200_parse.c @@ -77,6 +77,29 @@ must_contain(const char *src, const char *needle) return 1; } +static char * +imports_to_str(const char *src, int *errs, const char **pkg) +{ + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, "imports.ww", src, strlen(src)); + parserinit(&p, a, &l); + Node *n = parseimports(&p); + *errs = p.errs + l.errs; + *pkg = n->module ? strdup(n->module) : NULL; + + char *buf = NULL; + size_t len = 0; + FILE *f = open_memstream(&buf, &len); + if (f != NULL) { + astprint(f, n); + fclose(f); + } + freearena(a); + return buf; +} + int main(void) { @@ -179,6 +202,68 @@ main(void) if (!must_contain("fn f() i32 = { return a + b; };", "(bin +")) fail++; if (!must_contain("@symbol(\"x\") fn f() void;", "(attr \"symbol\""))fail++; + { + const char *src = + "package main;\n" + "import zed;\n" + "fn f() void = { let s: str = \"import fake;\"; };\n" + "/* import hidden; */\n" + "import alpha;\n"; + int errs; + const char *pkg; + char *got = imports_to_str(src, &errs, &pkg); + if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0 + || got == NULL || strstr(got, "(use \"zed\"") == NULL + || strstr(got, "(use \"alpha\"") == NULL + || strstr(got, "fake") != NULL || strstr(got, "hidden") != NULL) { + fprintf(stderr, "imports-only parse mismatch:\n%s\n", + got ? got : ""); + fail++; + } + free((void *)pkg); + free(got); + } + { + int errs; + const char *pkg; + char *got = imports_to_str("package main;\nimport ;\n", &errs, + &pkg); + if (errs == 0) { + fputs("malformed import accepted\n", stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + int errs; + const char *pkg; + char *got = imports_to_str( + "package main;\n@trace import alpha;\n", &errs, &pkg); + if (errs == 0 || got == NULL + || strstr(got, "(use \"alpha\"") == NULL) { + fputs("attributed import was not rejected and retained\n", + stderr); + fail++; + } + free((void *)pkg); + free(got); + } + { + int errs; + const char *pkg; + char *got = imports_to_str( + "//ww:module example.main\n" + "package main;\nimport alpha;\n", &errs, &pkg); + if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0 + || got == NULL || strstr(got, "(use \"alpha\"") == NULL) { + fputs("imports-only parse rejected module boundary\n", stderr); + fail++; + } + free((void *)pkg); + free(got); + } + if (fail) { fprintf(stderr, "%d parse tests failed\n", fail); return 1; diff --git a/test/wcc/data/r787_xmod_variant_foreign_qualifier/case.ww b/test/wcc/data/r787_xmod_variant_foreign_qualifier/case.ww index 086e4071..f71f8e04 100644 --- a/test/wcc/data/r787_xmod_variant_foreign_qualifier/case.ww +++ b/test/wcc/data/r787_xmod_variant_foreign_qualifier/case.ww @@ -1,5 +1,8 @@ -//ww:error c "is not a variant of" ww "ambiguous without nominal layout" -// migrated from test/wcc/787_xmod_variant_match.c: a same-leaf variant from a DIFFERENT module is NOT a variant of the scrutinee — guards #13 against bare leaf-strip false-accept. +//ww:error c "is not a variant of" ww "not a variant of scrutinee" +// migrated from test/wcc/787_xmod_variant_match.c: a same-leaf variant from a +// DIFFERENT module is NOT a variant of the scrutinee — guards #13 against bare +// leaf-strip false-accept. Both checkers now resolve the direct qualifier before +// codegen and reject at the case boundary. package pkg; export type a = !void; export type b = !void;