// lib/ww/syntax/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 syntax; // Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. export 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, }; export type sym = struct { name: str, skind: skind, type_: *tinfo, decl: *node, exported: i32, is_const: i32, // const-bound (assignment rejected) use_alias: i32, // #30: this value/type decl ALSO names an imported // module (the fnmatch.fnmatch / random.random shape). // Set when installtop promotes a same-leaf SK_USE in // place; the N_DOT guards treat such a sym as a module // for `name.member`. Mirror cstage Sym.use_alias // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). 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; export 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, same-module preferred. // // 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 (a same-name `import X;` // SK_USE shadowing a struct X declared in another module), the resolver // needs the type entry — the struct's mod may differ from the leaf so // 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. // 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 // reverted attempt compared str.len as u64 — str.len is i32). export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { 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.skind == skind.SK_TYPE) { if (mod.len > 0) { 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; }; // 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. // // #30 (design reversal): this serves the DISTINCT-mod two-sym case only — // a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE // (mod=""), where scopelookupprefer lands on the value and the dot re- // resolves to the SK_USE here. The SAME-mod collision (a primary-package // decl whose leaf also names a bundled module — `type sym` vs `import sym`, // `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to // two coexisting syms: that ripples into every bare-ref resolver (a missed // site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove // away). Instead installtop now PROMOTES the SK_USE in place to the value // kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring // cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the // N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE // correctly-kinded sym → all resolvers correct by construction. Cite: // task #30; project memory module_type_name_collision (cstage 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, use_alias=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; }; // scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a // duplicate (same name, same mod-key), or nil if the key is free. Lets a // caller that got a nil from scopedefineinmodule learn WHAT it collided // with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The // match logic mirrors scopedefineinmodule's reject branch exactly. export fn scopesamekeysym(s: *scope, name: str, mod: str) *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 b; }; } else { if (mod.len > 0) { if (streq(b.mod, mod)) { return b; }; }; }; }; b = b.hashnext; }; return nil; };