fix: reject bare import bindings

This commit is contained in:
2026-08-22 15:17:10 +09:00
parent 91fce80b37
commit 89f519d5cb
6 changed files with 1490 additions and 38 deletions

View File

@@ -237,6 +237,7 @@ fn srcimports(file: *syntax.node, modtag: str, source: i32, name: str) bool = {
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE && u.useblank == 0
&& !invalidinitimport(u)
&& u.sourceid == source) {
// Skip self-imports: lib/fmt/fmt_test.ww carries
// `use fmt;` while its module tag is also "fmt".
@@ -267,8 +268,10 @@ 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;
// A pure SK_USE belongs to one source file. Defer it to the
// source-owned path below instead of leaking a sibling qualifier.
if (found.decl != nil && found.skind != syntax.skind.SK_USE) { return found; };
if (found.decl == nil) { builtin = found; };
};
// Flat scope installation may coalesce equal qualifiers from distinct
// files. A bare mention only locates the marker; the DOT resolution owns
@@ -289,6 +292,45 @@ fn lookupvisible(c: *checker, name: str) *syntax.sym = {
return builtin;
};
// Return the source-local package-name object for a bare identifier without
// marking the import used. A closer lexical value wins; a coexisting SK_USE or
// promoted use_alias retains package-qualifier identity in the flat scope.
fn lookupbareimportbinding(c: *checker, name: str) *syntax.sym = {
if (name.len == 0) { return nil; };
let found: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, name);
if (found == nil) { return nil; };
if (found.skind != syntax.skind.SK_USE && found.use_alias == 0i32) {
let use: *syntax.sym = syntax.scopelookupuselocal(found.scope, name);
if (use != nil) { found = use; };
};
if (found.skind == syntax.skind.SK_USE || found.use_alias != 0i32) {
if (srcimports(c.file, c.curmod, c.cursource, name)) { return found; };
};
return nil;
};
fn bareimportvalueerr(c: *checker, n: *syntax.node) void = {
if (n.type_ == c.tc.tyerr: *void) { return; };
cerr(n.file); cerr(":");
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(n.col, strconv.base.DEC));
cerr(": error: use of package "); cerr(n.str);
cerr(" not in selector\n");
c.errs += 1;
n.type_ = c.tc.tyerr: *void;
};
fn bareimporttypeerr(c: *checker, n: *syntax.node) void = {
if (n.type_ == c.tc.tyerr: *void) { return; };
cerr(n.file); cerr(":");
cerr(strconv.i32tos(n.line, strconv.base.DEC)); cerr(":");
cerr(strconv.i32tos(n.col, strconv.base.DEC));
cerr(": error: "); cerr(n.str);
cerr(" (package name) is not a type\n");
c.errs += 1;
n.type_ = c.tc.tyerr: *void;
};
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
@@ -672,6 +714,11 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (c.sepmode != 0 && n.type_ != nil) { c.nresolved += 1; return; };
let nm: str = n.str;
if (nm.len > 0) {
if (lookupbareimportbinding(c, nm) != nil) {
bareimporttypeerr(c, n);
c.nresolved += 1;
return;
};
let s: *syntax.sym = lookupvisibletype(c, nm);
let builtin: bool = builtintypename(nm);
// `pkg.Type` — strip the last dot prefix and look up
@@ -1011,13 +1058,35 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.lhs != nil) {
if (n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
// A package receiver is a lookup target, not a value expression.
c.nresolved += 1;
} else { resolvewalk(c, n.lhs); };
};
// A.6.0: branch returns early; stamp here so the post-walk
// dispatch below sees N_DOT covered.
let _t: *syntax.node = exprtype(c, n, nil);
return;
};
if (k == syntax.nkind.N_STRUCTLIT) {
// The literal head is a type designator. A package binding there gets
// the type diagnostic, never the ordinary bare-value diagnostic.
if (n.lhs != nil) {
if (n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
bareimporttypeerr(c, n.lhs);
c.nresolved += 1;
} else { resolvewalk(c, n.lhs); };
};
let sf: *syntax.node = n.list;
for (sf != nil) { resolvewalk(c, sf); sf = sf.next; };
let _st: *syntax.node = exprtype(c, n, nil);
return;
};
if (k == syntax.nkind.N_FIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
@@ -2098,8 +2167,18 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
if (k == syntax.nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; };
if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &a, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (!evaldefconst(c, n.rhs, &b, depth + 1)) {
if (n.rhs != nil && n.rhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (foldbinop(n.op, a, b, out)) { return true; };
if ((n.op == syntax.tkind.TK_SLASH || n.op == syntax.tkind.TK_PERCENT) && b == 0u64) {
deffolderr(c, n, "def value: division by zero");
@@ -2112,7 +2191,12 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
// foldintliteral already covers unary-over-leaf; this arm
// catches unary over a resolved ref, e.g. `-A`.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &v, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
if (n.op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (n.op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (n.op == syntax.tkind.TK_PLUS) { *out = v; return true; };
@@ -2124,7 +2208,12 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
// exprtype's N_CAST arm during resolvewalk). Strip the cast
// keeping the value; a narrowing cast that loses it fails loud.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
if (!evaldefconst(c, n.lhs, &v, depth + 1)) {
if (n.lhs != nil && n.lhs.type_ == c.tc.tyerr: *void) {
n.type_ = c.tc.tyerr: *void;
};
return false;
};
let t: *syntax.tinfo = (n.type_): *syntax.tinfo;
if (!defcastfits(t, v)) {
deffolderr(c, n, "def value: narrowing cast loses value");
@@ -2134,6 +2223,10 @@ fn evaldefconst(c: *checker, n: *syntax.node, out: *u64, depth: i32) bool = {
return true;
};
if (k == syntax.nkind.N_IDENT) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimportvalueerr(c, n);
return false;
};
let s: *syntax.sym = lookupvisible(c, n.str);
if (s == nil) { return false; };
if (s.skind != syntax.skind.SK_DEF) { return false; };
@@ -2419,6 +2512,130 @@ fn validatestructfields(c: *checker, n: *syntax.node) void = {
// resolvewalk's eager type-decl dispatch, sibling to validatestructfields
// (rule-10 symmetric with cstage's once-per-resolve_type), not from the
// per-query size/align arms.
fn bareimporttypenode(k: syntax.nkind) bool = {
return k == syntax.nkind.N_TPTR || k == syntax.nkind.N_TSLICE
|| k == syntax.nkind.N_TARRAY
|| k == syntax.nkind.N_TFN || k == syntax.nkind.N_TSTRUCT
|| k == syntax.nkind.N_TFIELD || k == syntax.nkind.N_TCHAN
|| k == syntax.nkind.N_TTUPLE || k == syntax.nkind.N_TTAGGED
|| k == syntax.nkind.N_TBANG || k == syntax.nkind.N_TENUM
|| k == syntax.nkind.N_TENUMMEMBER || k == syntax.nkind.N_TPARAM;
};
fn rejectbareimporttypes(c: *checker, n: *syntax.node) bool = {
if (n == nil) { return false; };
if (n.kind == syntax.nkind.N_IDENT || n.kind == syntax.nkind.N_TNAME) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimporttypeerr(c, n);
return true;
};
// A qualified type is a legal selector use even when its surrounding
// constant-expression shape is independently invalid.
if (n.kind == syntax.nkind.N_TNAME && strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
if (lookupbareimportbinding(c, head) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, head);
};
};
return false;
};
let bad: bool = false;
if (n.kind == syntax.nkind.N_TARRAY) {
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
if (rejectbareimportvalues(c, n.rhs)) { bad = true; };
return bad;
};
if (n.kind == syntax.nkind.N_TENUM) {
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
let m: *syntax.node = n.list;
for (m != nil) {
if (rejectbareimportvalues(c, m.lhs)) { bad = true; };
m = m.next;
};
return bad;
};
if (n.kind == syntax.nkind.N_TENUMMEMBER) {
return rejectbareimportvalues(c, n.lhs);
};
if (rejectbareimporttypes(c, n.lhs)) { bad = true; };
if (rejectbareimporttypes(c, n.rhs)) { bad = true; };
if (rejectbareimporttypes(c, n.cond)) { bad = true; };
if (rejectbareimporttypes(c, n.body)) { bad = true; };
if (rejectbareimporttypes(c, n.els)) { bad = true; };
let p: *syntax.node = n.list;
for (p != nil) {
if (rejectbareimporttypes(c, p)) { bad = true; };
p = p.next;
};
return bad;
};
fn rejectbareimportvalues(c: *checker, n: *syntax.node) bool = {
if (n == nil) { return false; };
if (bareimporttypenode(n.kind)) { return rejectbareimporttypes(c, n); };
if (n.kind == syntax.nkind.N_IDENT || n.kind == syntax.nkind.N_TNAME) {
if (lookupbareimportbinding(c, n.str) != nil) {
bareimportvalueerr(c, n);
return true;
};
if (n.kind == syntax.nkind.N_TNAME && strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
if (lookupbareimportbinding(c, head) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, head);
};
};
return false;
};
if (n.kind == syntax.nkind.N_DOT) {
if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, n.lhs.str) != nil) {
let marked: str = usepathfor(c.file, c.curmod, c.cursource, n.lhs.str);
return false;
};
return rejectbareimportvalues(c, n.lhs);
};
if (n.kind == syntax.nkind.N_CAST || n.kind == syntax.nkind.N_TYPEASSERT
|| n.kind == syntax.nkind.N_TYPETEST) {
let bad: bool = rejectbareimportvalues(c, n.lhs);
if (rejectbareimporttypes(c, n.rhs)) { bad = true; };
return bad;
};
if (n.kind == syntax.nkind.N_STRUCTLIT) {
let bad: bool = rejectbareimporttypes(c, n.lhs);
let f: *syntax.node = n.list;
for (f != nil) {
if (rejectbareimportvalues(c, f.lhs)) { bad = true; };
f = f.next;
};
return bad;
};
if (n.kind == syntax.nkind.N_CALL) {
let bad: bool = rejectbareimportvalues(c, n.lhs);
let typearg: bool = n.lhs != nil && n.lhs.kind == syntax.nkind.N_IDENT
&& (syntax.streq(n.lhs.str, "size") || syntax.streq(n.lhs.str, "align"));
let a: *syntax.node = n.list;
for (a != nil) {
if (typearg) {
if (rejectbareimporttypes(c, a)) { bad = true; };
} else { if (rejectbareimportvalues(c, a)) { bad = true; }; };
a = a.next;
};
return bad;
};
let bad: bool = false;
if (rejectbareimportvalues(c, n.lhs)) { bad = true; };
if (rejectbareimportvalues(c, n.rhs)) { bad = true; };
if (rejectbareimportvalues(c, n.cond)) { bad = true; };
if (rejectbareimportvalues(c, n.body)) { bad = true; };
if (rejectbareimportvalues(c, n.els)) { bad = true; };
let p: *syntax.node = n.list;
for (p != nil) {
if (rejectbareimportvalues(c, p)) { bad = true; };
p = p.next;
};
return bad;
};
fn validateenummembers(c: *checker, n: *syntax.node) void = {
// storage type: cstage resolves n->lhs then gates type_isint
// (check.c:1004-1009); default storage is i32, always integer.
@@ -2464,7 +2681,8 @@ fn validateenummembers(c: *checker, n: *syntax.node) void = {
// `until = m` enforces harec's forward-only sibling-ref discipline.
if (m.lhs != nil) {
let v: u64 = 0u64;
if (!enumvalfold(n, m, m.lhs, &v)) {
let barebad: bool = rejectbareimportvalues(c, m.lhs);
if (!barebad && !enumvalfold(n, m, m.lhs, &v)) {
cerr(m.file);
cerr(": error: enum value must be a constant integer expression\n");
c.errs += 1;
@@ -2475,14 +2693,20 @@ fn validateenummembers(c: *checker, n: *syntax.node) void = {
};
// stampnilexpr — stamp nil-typed nodes in a constant expr subtree to
// `ti`. lhs/rhs cover the enum constexpr grammar enumvalfold accepts
// (literals, unary, binary, sibling backref); non-nil nodes keep the
// `ti`. The specialised fold accepts lhs/rhs, but invalid outer shapes still
// pass through the checker and must not leak nil-typed descendants into the
// final invariant walk. Non-nil nodes, including package-name errors, keep the
// type exprtype already derived.
fn stampnilexpr(n: *syntax.node, ti: *syntax.tinfo) void = {
if (n == nil) { return; };
if (n.type_ == nil) { n.type_ = ti: *void; };
stampnilexpr(n.lhs, ti);
stampnilexpr(n.rhs, ti);
stampnilexpr(n.cond, ti);
stampnilexpr(n.body, ti);
stampnilexpr(n.els, ti);
let p: *syntax.node = n.list;
for (p != nil) { stampnilexpr(p, ti); p = p.next; };
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
@@ -2624,6 +2848,13 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
switch (k) {
case syntax.nkind.N_TNAME:
let nm: str = n.str;
// Synthetic mktname nodes have no file. Only parsed source syntax can
// denote a bare package binding; internal primitive/type stamps must not.
if (n.file.len != 0 && lookupbareimportbinding(c, nm) != nil) {
bareimporttypeerr(c, n);
syntax.tinfocachebind(c.tc, n, c.tc.tyerr);
return c.tc.tyerr;
};
if (syntax.streq(nm, "void")) { r = c.tc.tyvoid; };
if (syntax.streq(nm, "bool")) { r = c.tc.tybool; };
if (syntax.streq(nm, "rune")) { r = c.tc.tyrune; };
@@ -2760,13 +2991,16 @@ fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
elen = n.rhs.uval;
} else {
let v: u64 = 0u64;
if (evaldefconst(c, n.rhs, &v, 0)) {
let barebad: bool = rejectbareimportvalues(c, n.rhs);
if (!barebad && evaldefconst(c, n.rhs, &v, 0)) {
elen = v;
} else {
cerr(n.file); cerr(": ");
cerr("error: array length must be an integer literal\n");
c.errs += 1;
};
} else { if (!barebad) {
if (n.rhs.type_ != c.tc.tyerr: *void) {
cerr(n.file); cerr(": ");
cerr("error: array length must be an integer literal\n");
c.errs += 1;
};
}; };
};
};
let sub: *syntax.tinfo = tinfofornode(c, n.lhs);
@@ -3352,6 +3586,13 @@ fn binoptype(c: *checker, e: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let ltn: *syntax.node = exprtype(c, e.lhs, nil);
let rtn: *syntax.node = exprtype(c, e.rhs, nil);
// Both operands were checked. Preserve their causal error on the parent and
// do not manufacture an operator-type recovery diagnostic or nil stamp.
if ((e.lhs != nil && e.lhs.type_ == c.tc.tyerr: *void)
|| (e.rhs != nil && e.rhs.type_ == c.tc.tyerr: *void)) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// #120 (B): a binop/compare with one f32 operand lowers an untyped-
// float peer to f32 — harec unifies both operands to the operand type
// (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so
@@ -3485,6 +3726,10 @@ fn binoptype(c: *checker, e: *syntax.node) *syntax.node = {
fn unoptype(c: *checker, e: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let opt: *syntax.node = exprtype(c, e.lhs, nil);
if (e.lhs != nil && e.lhs.type_ == c.tc.tyerr: *void) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// #38/F2 (review item 5): unop operand-kind gates the wwstage checker
// elided. Mirror cstage cunop (cmd/wcc/check.c:1224-1238): unary +/-
// want a numeric operand, ~ wants an integer, ! wants a bool. A nil
@@ -3820,6 +4065,10 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
// resolvewalk visits the dot receiver before its parent. Preserve the
// first causal undefined error just as cstage's cached cexpr does.
if (e.type_ == c.tc.tyerr: *void) { return nil; };
if (lookupbareimportbinding(c, e.str) != nil) {
bareimportvalueerr(c, e);
return nil;
};
// #55: bare-leaf value-ident must prefer curmod. Flat-scope
// scopelookup bucket-walks and can bind a same-leaf symbol from
// the wrong module under a foreign curmod, dragging its decl's
@@ -3848,6 +4097,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
return nil;
};
if (s.decl == nil) { return nil; };
// Propagate an errored inferred initializer through its binding. Without
// this, a later `bad.field` is left nil-typed and asserttyped obscures the
// causal checker diagnostic emitted for the initializer.
if (s.decl.kind == syntax.nkind.N_LET && s.decl.lhs == nil
&& s.decl.rhs != nil && s.decl.rhs.type_ == c.tc.tyerr: *void) {
e.type_ = c.tc.tyerr: *void;
return nil;
};
e.refdecl = s.decl;
// #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
@@ -3906,6 +4163,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
if (k == syntax.nkind.N_BIN) {
let tn: *syntax.node = binoptype(c, e);
if (e.type_ == c.tc.tyerr: *void) { return nil; };
// #59.9: checkisas pre-stamps an enum OR-fold (`(m.A|m.B) as
// u32`) TY_ENUM and folds the member N_DOTs to int literals;
// this post-order revisit re-derives from those now-untyped
@@ -3927,6 +4185,7 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
};
if (k == syntax.nkind.N_UN) {
let tn: *syntax.node = unoptype(c, e);
if (e.type_ == c.tc.tyerr: *void) { return nil; };
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
@@ -3945,6 +4204,14 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
if (e.type_ == c.tc.tyerr: *void) { return nil; };
let callee: *syntax.node = e.lhs;
if (callee == nil) { return nil; };
// Package qualifiers with builtin spelling remain package objects. Reject
// before alloc/size/len/etc. can rewrite away the callee identifier.
if (callee.kind == syntax.nkind.N_IDENT
&& lookupbareimportbinding(c, callee.str) != nil) {
bareimportvalueerr(c, callee);
e.type_ = c.tc.tyerr: *void;
return nil;
};
// A module-qualified leaf may already have been rejected while the
// N_DOT callee was checked on an earlier resolve walk. cstage caches
// that failure on the call; mirror its once-only diagnostic here rather
@@ -4534,6 +4801,9 @@ fn exprtype(c: *checker, e: *syntax.node, hint: *syntax.node) *syntax.node = {
e.type_ = c.tc.tyerr: *void;
return nil;
};
// Raw unresolved external selectors retain selector semantics;
// never reinterpret the package receiver as a bare value below.
if (fs == nil && ms.skind == syntax.skind.SK_USE) { return nil; };
};
// Fold case 2 inner: bare `EnumT.MEMBER` where EnumT
// is an SK_TYPE in the flat scope. Mirror cstage
@@ -4683,6 +4953,11 @@ 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) {
if (lookupbareimportbinding(c, e.lhs.str) != nil) {
bareimporttypeerr(c, e.lhs);
e.type_ = c.tc.tyerr: *void;
return nil;
};
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;
@@ -7578,8 +7853,10 @@ fn markimportusesnode(c: *checker, n: *syntax.node, owner: str,
source: i32) void = {
if (n == nil) { return; };
if (n.kind == syntax.nkind.N_TNAME) {
let (head, leaf) = strings.rcut(n.str, ".");
if (head.len > 0) {
// strings.rcut returns the whole input as its head on a miss. Require
// a real qualifier separator so a bare package TNAME never counts as use.
if (strings.contains(n.str, ".")) {
let (head, leaf) = strings.rcut(n.str, ".");
findusepath(c.file, owner, source, head, true);
};
} else { if (n.kind == syntax.nkind.N_DOT && n.lhs != nil