Files
ww/lib/ww/sym.ww
Hojun-Cho 884dbb402b wcc: dot-lhs prefers SK_USE module over same-leaf type/fn name
The wwstage checker resolved a module-qualified call/access mod.x by the
same-module preference in scopelookupprefer, so when the importing package's
name collides with a type/fn of the same leaf (package fnmatch with fn fnmatch;
package random with type random), the dot-lhs mod resolved to the same-leaf
SK_TYPE/SK_FN instead of the coexisting SK_USE import — the N_DOT module-qual
arm never fired and the call went nil-stamped (the D class of the asserttyped
gap audit: fnmatch 2, random 16). cstage resolves this via Sym.use_alias; this
ports the equivalent to wwstage.

Add scopelookupuselocal (a single-scope SK_USE lookup, twin of scopelookuptype)
and prefer SK_USE for a dot-lhs in exprtype's N_CALL and N_DOT arms, keyed on
the scope where scopelookupprefer landed so a local binding sharing a module's
leaf keeps value semantics. Scope-layer only — no type-identity touch (cstage
use_alias never reaches type_eq).

Drives the 901 gap-corpus D count to 0 (random_test now byte-id cs==ww).
Compiler binary unchanged (no such collision in its own source); 990-997 hold.
The separate fnmatch bare-enum-member cgen cs!=ww is unrelated (filed).
2026-05-28 05:17:28 +09:00

279 lines
9.0 KiB
Plaintext

// lib/ww/sym.ww — port of cmd/wcc/sym.c.
//
// Per-scope hashtable, chained to the parent. Lookup walks up.
// Plan 9 / Hare flavoured. Duplicate definitions in the same scope
// return nil; the caller flags the error.
package ww;
// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind.
type skind = enum i32 {
SK_NONE = 0,
SK_VAR = 1,
SK_PARAM = 2,
SK_DEF = 3,
SK_TYPE = 4,
SK_FN = 5,
SK_USE = 6,
SK_FIELD = 7,
};
type sym = struct {
name: str,
skind: skind,
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
mod: str, // importing module's bareword for symbols
// from a `use`-imported module; "" for primary
// (root) compilation unit symbols. Used by
// scopelookupinmodule to disambiguate same-leaf-
// name types coming from different imports.
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,
};
def NBUCKETS: i32 = 16;
type scope = struct {
parent: *scope,
first: *sym,
last: *sym,
buckets: **sym, // length = NBUCKETS
nbuckets: i32,
};
// FNV-1a 64 — same hash the C side uses, so bucket distribution is
// identical when both walk a scope in declaration order.
fn hashstr(s: str) u64 = {
let h: u64 = 14695981039346656037u64;
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
h = h ^ (c: u64);
h = h * 1099511628211u64;
i += 1;
};
return h;
};
export fn newscope(parent: *scope) *scope = {
let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!;
let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!;
return s;
};
export fn streq(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
export fn scopelookuplocal(s: *scope, name: str) *sym = {
if (s == nil) { return nil; };
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
let bn: str = b.name;
if (streq(bn, name)) { return b; };
b = b.hashnext;
};
return nil;
};
export fn scopelookup(s: *scope, name: str) *sym = {
for (s != nil) {
let r: *sym = scopelookuplocal(s, name);
if (r != nil) { return r; };
s = s.parent;
};
return nil;
};
// scopelookuptype — find an SK_TYPE entry by name regardless of mod.
//
// Same FNV bucket + hashnext chain + parent walk as scopelookup, with
// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME
// vs imported-module-bareword collision: when scopelookup returns the
// SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct
// declared in lib/ww/lex/tok.ww with `package lex;` while
// `import tok;` registers a same-name SK_USE), the resolver needs
// the type entry regardless of its declared package — the struct's
// mod may differ from the leaf (lex/tok pair) so
// scopelookupinmodule(c, leaf, leaf) won't find it.
//
// Mirrors the bare-vs-qualified disambiguation pattern from task #57.
export fn scopelookuptype(s: *scope, name: str) *sym = {
for (s != nil) {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.skind == skind.SK_TYPE) { return b; };
};
b = b.hashnext;
};
s = s.parent;
};
return nil;
};
// scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope.
//
// Same FNV bucket + hashnext chain as scopelookuplocal, with a
// `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of
// scopelookuptype: when a `use mod;` and a colliding top-level
// `fn mod` / `type mod` of the same leaf coexist (random.random,
// fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the
// SK_FN/SK_TYPE whose mod matches the importing unit's package, masking
// the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the
// module-qualified arm to fire, so the resolver re-resolves through this
// filter — keyed on the scope where scopelookupprefer LANDED — when it
// lands on a non-USE same-leaf entry.
//
// Single-scope (not a parent walk) so a local binding that shares a leaf
// with a top-level `use` keeps value semantics: scopelookupprefer
// resolves the local in its inner scope, whose bucket holds no SK_USE,
// so this returns nil and the dot stays field access. Only a genuine
// same-scope coexistence (top-level use + top-level type/fn) re-resolves.
//
// This is the coexistence-equivalent of cstage's Sym.use_alias bit
// (cmd/wcc/check.c: set at the SK_USE→SK_X promotion sites, consulted by
// the `kind == SK_USE || use_alias` dot guards in resolve_typename and
// cexpr's N_DOT arm). Wwstage installs the SK_USE and the same-leaf
// type/fn as SEPARATE coexisting entries (see selfhost/cmd/wcc/check.ww
// installdecl), so no flag is needed — the SK_USE is never overwritten,
// only out-preferred. Cite: project memory module_type_name_collision
// (cstage fix 2026-05-13).
export fn scopelookupuselocal(s: *scope, name: str) *sym = {
if (s == nil) { return nil; };
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.skind == skind.SK_USE) { return b; };
};
b = b.hashnext;
};
return nil;
};
// scopelookupinmodule — module-filtered chain walk.
//
// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus
// a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty
// we fall back to unfiltered scopelookup semantics, so callers that
// don't care about disambiguation get the default.
//
// Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/
// check.ww to pick the right same-leaf-name type when two imports
// each export it (`bufio.stream` vs `io.stream`).
export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = {
if (mod.len == 0) { return scopelookup(s, name); };
for (s != nil) {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.mod.len > 0) {
if (streq(b.mod, mod)) {
return b;
};
};
};
b = b.hashnext;
};
s = s.parent;
};
return nil;
};
// scopelookupprefer — bare-leaf lookup with same-module preference.
//
// Walks the same FNV bucket + hashnext chain + parent walk scopelookup
// uses. 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
// 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.
//
// When `mod` is empty we just call scopelookup — there's no module
// identity to prefer.
//
// Used at bare-leaf lookup sites inside a known current module so that
// a bare `read` inside lib/os resolves to os.read rather than the
// 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);
let bi: i32 = (h % (p.nbuckets: u64)): i32;
let b: *sym = p.buckets[bi];
let fallback: *sym = nil;
for (b != nil) {
if (streq(b.name, name)) {
if (b.mod.len > 0) {
if (streq(b.mod, mod)) {
return b;
};
};
if (fallback == nil) { fallback = b; };
};
b = b.hashnext;
};
if (fallback != nil) { return fallback; };
p = p.parent;
};
return nil;
};
export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = {
let empty: str;
return scopedefineinmodule(s, name, empty, k, t, decl);
};
// scopedefineinmodule — bucket insert with per-mod dedup.
//
// Same insertion as scopedefine, but the duplicate-rejection key is
// (name, mod) rather than name alone. This lets two imports each
// register their own `stream` SK_TYPE in the flat scope, and lets the
// primary register `stream` (mod="") alongside imported `stream`s.
//
// Within a single (name, mod) pair the first registration wins; later
// attempts return nil and the caller can flag the error.
export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.mod.len == 0) {
if (mod.len == 0) { return nil; };
} else {
if (mod.len > 0) {
if (streq(b.mod, mod)) { return nil; };
};
};
};
b = b.hashnext;
};
let sy: *sym = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!;
s.buckets[bi] = sy;
if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; };
s.last = sy;
return sy;
};