RFD: Per-Kind SymbolData
Status: Implemented
Depends on:
- Type signatures — per-kind symbol wrappers (
FnSymbol,StructSymbol, …) - Module sym tree —
Symbol,SymbolData,SymExt,ModSymbol - Tuple struct constructors —
TupleStructCtorvariant
Goal
Make Symbol::data() return a per-kind SymbolData that tells callers "this is a function", "this is a struct", etc., regardless of whether the symbol is workspace-local or external. Today callers must first check Ast vs Ext, then (for Ast) branch on ItemAst — two levels of dispatch to answer one question. External symbols carry no kind information at all; callers that need it must query TcxDb separately.
Current state
#![allow(unused)] fn main() { pub enum SymbolData<'db> { Ast(ItemAst<'db>), TupleStructCtor(LocalStructSym<'db>), Ext(SymExt), Intrinsic(Intrinsic), } }
SymExt is { crate_num: CrateNum, def_index: DefIndex } — an opaque handle with no kind. The type-signatures RFD added per-kind wrappers (FnSymbol, StructSymbol, …) with Ast/Ext inner enums, but Symbol::data() still returns the old three-variant enum.
Callers typically do:
#![allow(unused)] fn main() { match sym { SymbolData::Ast(ItemAst::Function(f)) => { /* fn-specific */ } SymbolData::Ast(ItemAst::Struct(s)) => { /* struct-specific */ } // ... SymbolData::Ext(ext) => { /* no kind info, must query TcxDb or give up */ } } }
Design
SymbolData becomes per-kind
#![allow(unused)] fn main() { pub enum SymbolData<'db> { Fn(FnSymbol<'db>), Struct(StructSymbol<'db>), TupleStructCtor(StructSymbol<'db>), Enum(EnumSymbol<'db>), Trait(TraitSymbol<'db>), Impl(ImplSymbol<'db>), Mod(ModSymbol<'db>), TypeAlias(TypeAliasSymbol<'db>), Const(ConstSymbol<'db>), Static(StaticSymbol<'db>), MacroDef(LocalMacroDefSym<'db>), Use(UseGroupAst<'db>), MacroInvocation(MacroInvocationAst<'db>), Error(AbsoluteSpan), Unknown(SymExt), } }
Intrinsic primitives (bool, i32, str, etc.) are modeled as SymbolData::Struct(StructSymbol::intrinsic(...)) — they are effectively magic structs.
Symbol remains a Copy newtype with a private data field. Symbol::data() returns the enum by value.
SymExt gains a kind
#![allow(unused)] fn main() { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct SymExt { pub crate_num: CrateNum, pub def_index: DefIndex, pub kind: SymExtKind, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum SymExtKind { Fn, Struct, TupleStructCtor, Enum, Trait, Impl, Mod, TypeAlias, Const, Static, MacroDef, Use, Other, } }
The kind is populated at the point of creation — TcxDb::module_children already knows the namespace and def kind of each child. The RawChild struct gains a kind: SymExtKind field, and the caller that interns RawChild into Symbol uses it to construct the correct SymbolData variant.
Symbol construction
Symbol::ast(item: ItemAst) dispatches on ItemAst to create the matching variant:
#![allow(unused)] fn main() { impl<'db> Symbol<'db> { pub fn ast(item: ItemAst<'db>) -> Self { let data = match item { ItemAst::Function(f) => SymbolData::Fn(FnSymbol::ast(f)), ItemAst::Struct(s) => SymbolData::Struct(StructSymbol::ast(s)), ItemAst::Enum(e) => SymbolData::Enum(EnumSymbol::ast(e)), ItemAst::Impl(i) => SymbolData::Impl(ImplSymbol::ast(i)), ItemAst::Error(span) => SymbolData::Error(span), // ... }; Self { data } } } }
Symbol::ext(ext: SymExt) dispatches on ext.kind:
#![allow(unused)] fn main() { impl<'db> Symbol<'db> { pub fn ext(ext: SymExt) -> Self { let data = match ext.kind { SymExtKind::Fn => SymbolData::Fn(FnSymbol::ext(ext)), SymExtKind::Struct => SymbolData::Struct(StructSymbol::ext(ext)), SymExtKind::TupleStructCtor => SymbolData::TupleStructCtor(StructSymbol::ext(ext)), SymExtKind::Impl => SymbolData::Impl(ImplSymbol::ext(ext)), // ... SymExtKind::Other => SymbolData::Unknown(ext), }; Self { data } } } }
Symbol::tuple_struct_ctor(s: StructAst) → SymbolData::TupleStructCtor(StructSymbol::ast(s)).
ModSymbol integration
ModSymbol is already a per-kind wrapper with Ast(ModAst) / Ext(ModExt). As part of this refactor, ModExt is removed and ModSymbol switches to storing SymExt (with kind: SymExtKind::Mod) like all other per-kind wrappers. This eliminates the historical asymmetry — ModExt was structurally identical to SymExt and predated the per-kind wrapper design.
The SymbolData::Mod variant wraps ModSymbol directly. The From<ModSymbol> for Symbol impl constructs SymbolData::Mod(m).
TcxDb changes
RawChild gains a kind field:
#![allow(unused)] fn main() { pub struct RawChild { pub name: String, pub crate_num: CrateNum, pub def_index: DefIndex, pub namespace: Namespace, pub kind: SymExtKind, } }
The ProxyTcxDb implementation populates kind from rustc's DefKind. The mapping is straightforward:
rustc DefKind | SymExtKind |
|---|---|
Fn, AssocFn | Fn |
Struct | Struct |
Ctor(CtorOf::Struct, _) | TupleStructCtor |
Enum | Enum |
Trait, TraitAlias | Trait |
Impl { .. } | Impl |
Mod | Mod |
TyAlias, AssocTy | TypeAlias |
Const, AssocConst | Const |
Static { .. } | Static |
Macro(..) | MacroDef |
Use | Use |
| everything else | Other |
AllocStashData for Symbol
Symbol derives AllocStashData today and implements StashDirect. After the refactor, SymbolData is larger (it now includes the per-kind wrapper which itself is an enum). This is fine — Symbol is still Copy (all arms are thin handles or small structs), and StashDirect remains valid since equality is structural.
The current Symbol is already ~24 bytes (driven by ItemAst::Error(AbsoluteSpan)). The proposed layout should be similar — per-kind wrappers are thin (discriminant + salsa ID or SymExt). Validate with std::mem::size_of::<Symbol>() before and after. Still small enough for value semantics.
Callers that just want ast-vs-ext
Some callers (display, debug) only need "is this local or external?" without caring about kind. Add convenience methods:
#![allow(unused)] fn main() { impl<'db> Symbol<'db> { pub fn is_local(&self) -> bool { ... } pub fn is_external(&self) -> bool { !self.is_local() } pub fn as_ext(&self) -> Option<SymExt> { ... } } }
These iterate the known variants. Alternatively each per-kind wrapper already has as_ast() / as_ext() — callers can match on SymbolData and then call those.
Migration
The migration is mechanical but wide — every match sym in the codebase changes shape.
Before
#![allow(unused)] fn main() { match sym { SymbolData::Ast(ItemAst::Function(f)) => { ... } SymbolData::Ast(ItemAst::Struct(s)) => { ... } SymbolData::Ast(_) => { ... } SymbolData::TupleStructCtor(s) => { ... } SymbolData::Ext(ext) => { ... } } }
After
#![allow(unused)] fn main() { match sym { SymbolData::Fn(fn_sym) => { ... } SymbolData::Struct(struct_sym) => { ... } SymbolData::TupleStructCtor(struct_sym) => { ... } // ... SymbolData::Error(span) => { ... } SymbolData::Unknown(ext) => { ... } } }
Callers that need the FnAst use fn_sym.as_ast(). Callers that need SymExt use fn_sym.as_ext().
Callers that group many kinds
Some callers (e.g. item_name in display) handle all named items. These get a wildcard with a helper:
#![allow(unused)] fn main() { impl<'db> Symbol<'db> { pub fn name(&self, db: &'db dyn Db) -> Option<Name<'db>> { ... } } }
Or they match the relevant variants and use _ for the rest.
Implementation plan
Step 1: Add SymExtKind and extend SymExt ✅
Add the SymExtKind enum. Add a kind field to SymExt (default Other for back-compat). Add kind to RawChild. Update ProxyTcxDb to populate it from rustc's DefKind. Remove ModExt and switch ModSymbol to store SymExt (with kind: SymExtKind::Mod).
Implemented. The sym_ext_kind_for_def_kind mapping lives in src/tcx_impl.rs (alongside the existing namespaces_for_def_kind). ModExt is fully removed; ModSymbol::external(cn, di) now constructs SymExt::new(cn, di, SymExtKind::Mod) internally. The From<ModExt> for SymExt impl is removed since ModSymbol::Ext now holds SymExt directly. No deviations from plan.
Step 2: Add ImplSymbol and StashDirect impls ✅
The type-signatures RFD added FnSymbol through StaticSymbol. Add ImplSymbol (same Ast/Ext shape). MacroDef, Use, and MacroInvocation don't need wrappers — store the AST directly. All per-kind wrappers need StashDirect impls (trivial — they contain only salsa IDs and SymExt scalars).
Implemented. Added ImplSymbol via define_kind_symbol! macro. Added StashDirect impl to the macro itself so all per-kind wrappers (FnSymbol, StructSymbol, EnumSymbol, TraitSymbol, TypeAliasSymbol, ConstSymbol, StaticSymbol, ImplSymbol) get it automatically. Also added StashDirect for SymExtKind. No deviations from plan.
Step 3: Rewrite SymbolData and Symbol construction ✅
Replace the three-variant SymbolData with the per-kind enum. Update Symbol::ast(), Symbol::ext(), Symbol::tuple_struct_ctor(), From<ModSymbol>, From<ItemAst>, etc. Update AllocStashData derive (may need manual impl if the enum is too complex for the macro).
Implemented. SymbolData now has 16 variants (per-kind wrappers + MacroDef/Use/MacroInvocation AST-only + Intrinsic + Error + Unknown). Symbol::ast() dispatches on ItemAst to create per-kind wrappers. Symbol::ext() dispatches on SymExtKind. Added Symbol::as_ext() convenience method used by derive.rs and display code. AllocStashData derive still works fine. Per-kind wrappers gained salsa::Update derive (needed because SymbolData derives it). No deviations from plan.
Step 4: Migrate callers ✅
Mechanically update every match sym across:
resolve.rs—symbol_to_moduleand friendsdisplay.rs—fmt_resbody_resolve.rs— if it dispatches onSymbolDataderive.rs/derive/builtins.rsmemmap/— any SymbolData matches- Test files:
common/mod.rs,memmap_tests.rs,expand_tests.rs
Add Symbol::name() and Symbol::is_local() convenience methods to reduce per-site boilerplate.
Implemented. Added Symbol::name(db), Symbol::is_local(), and Symbol::is_external(). Caller migration was done as part of Step 3 (the two steps were effectively combined since all callers needed updating when SymbolData changed shape). body_resolve.rs did not dispatch on SymbolData — no changes needed there. derive.rs now uses sym.as_ext() instead of matching SymbolData::Ext.
Step 5: Remove the old Symbol::ast(ItemAst) if desired — kept ✅
After all callers use per-kind constructors, the Symbol::ast(item) constructor that takes a full ItemAst could be removed in favor of direct SymbolData::Fn(FnSymbol::ast(f)). Or keep it as a convenience — it's a one-line dispatcher.
Decision: kept. Symbol::ast(item) is still used by callers that receive an ItemAst (e.g., walk_entries in resolve). It dispatches internally to per-kind wrappers, so it's correct and convenient.
Scope and non-goals
In scope:
SymExtKindonSymExtandRawChild- Per-kind
SymbolDataenum - Migrating all
sym.data()match sites - Convenience methods (
name(),is_local())
Out of scope:
- Removing
ModSymbolas a separate type (it has its own parent/file methods; onlyModExtis removed) - Per-kind ext data (
FnExt,StructExtwith signature info) — that's a separate concern tied to external signature loading - Changing how
TupleStructCtorworks — it remains aStructSymbolwrapped in a dedicated variant
Resolved decisions
- Intrinsics (
bool,i32,str, etc.) are modeled asSymbolData::Struct(StructSymbol::intrinsic(...))— magic structs. - Impl blocks get a first-class
SymbolData::Impl(ImplSymbol<'db>)variant. - Local parse errors (
ItemAst::Error(span)) map toSymbolData::Error(AbsoluteSpan). - Unmodeled external kinds (
ExternCrate,GlobalAsm,OpaqueTy,Closure, etc.) map toSymbolData::Unknown(SymExt), preserving the handle for debugging. - External tuple struct ctors arrive as rustc
DefKind::Ctor(CtorOf::Struct, _)and map toSymExtKind::TupleStructCtor→SymbolData::TupleStructCtor(StructSymbol::ext(...)). ModExtis removed —ModSymbolstoresSymExtlike all other per-kind wrappers.- Enum variant symbols are out of scope — tracked in a separate RFD.
Resolved decisions (continued)
- Macro/use wrappers: No per-kind wrappers for
MacroDef,Use, orMacroInvocation. These are only ever local, soSymbolDatastores the AST directly (e.g.,MacroDef(LocalMacroDefSym<'db>)) without a*Symbolwrapper.