compiler: load imports and enforce package exports

This commit is contained in:
2026-08-11 22:25:59 +09:00
parent 7bc96b4e03
commit db96422f74
14 changed files with 894 additions and 118 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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; };

View File

@@ -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);

View File

@@ -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;
};
};

View File

@@ -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 : "<null>");
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;

View File

@@ -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;