After the frontend consolidated into one syntax package, the wcc backend
imports syntax and calls its exported fns — whose signatures reference
types that were unexported. Producing syntax's .wwi interface re-triggered
check_exported_type ("exported declaration references unexported type"):
the residual of BUG-A at the one surviving syntax->wcc boundary. Export
the 16 types that appear in syntax's wcc-facing public surface (directly
in an exported signature, or via a recursively-referenced exported struct
field): nkind, node, lex, tok, tkind, parser, scope, sym, skind, tinfo,
tykind, tfield, tparam, ttupleelem, tctx, tinfocacheent. The set is
minimal (unexporting any one re-breaks the producer) and complete; pos
stays internal. Pure source change — exporting a type emits no code, so
the bootstrap binaries are byte-identical (verified against a clean base
build); only syntax's .wwi gains the type decls.
Post-frontend-reorg residual (#74). syntax now sep-produces clean both
stages. The separate concern of wcc's currently-unqualified refs to
syntax symbols (#75) is a distinct follow-up. Gate 989_syntaxexport_run.
334 lines
11 KiB
Plaintext
334 lines
11 KiB
Plaintext
// 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;
|
|
};
|