RFD: Stash-allocated MEM-map entries
Status: Completed (historical; storage mechanism superseded)
Depends on:
- Hash-consed stash —
Stash,Slice<T>,Stashed<T>,AllocStashData
Current disposition. This RFD records an intermediate representation which was completed and later replaced. Its fixpoint originally mutated hash-consed stash entries in place. That mechanism is superseded by Stash for type-level interning and crates.sage_stash.storage.immutable_entries: allocated stash entries are now immutable. Current module expansion returns an ordinary ordered symbol sequence and lets Salsa perform fixed-point recovery; see Module and Macro Expansion.
Background
The MEM-map (ExpandedModule) stores a module's resolved/expanded member entries. Currently it uses heap-allocated Vecs throughout:
ExpandedModule.entries: Vec<MemmapEntry<'db>>MemmapEntry::Redirect { target: Vec<Name<'db>> }MemmapEntry::Glob { path: Vec<Name<'db>> }MacroUse { path: Vec<Name<'db>>, expansions: Vec<Expansion<'db>> }Expansion { entries: Vec<MemmapEntry<'db>> }
This is a recursive tree of heap allocations. Comparing two memmaps (needed by salsa for incremental reuse) requires deep traversal.
Goal
Replace all Vecs with stash-allocated Slices, making MemmapEntry a Copy type stored in a single Stash. The ExpandedModule becomes:
#![allow(unused)] fn main() { #[salsa::tracked(debug)] pub struct ExpandedModule<'db> { #[returns(ref)] pub entries: Stashed<Slice<MemmapEntry<'db>>>, } }
Benefits:
MemmapEntryisCopy— no cloning during resolution walks.Stashedequality is O(1) fingerprint comparison — salsa can cheaply detect unchanged memmaps.- All data is in one contiguous allocation — better cache locality.
- Hash-consing deduplicates identical subtrees (e.g., common paths).
Design
Data types
#![allow(unused)] fn main() { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, AllocStashData)] pub enum MemmapEntry<'db> { Item(ItemAst<'db>), TupleStructCtor(LocalStructSym<'db>), MacroDef(LocalMacroDefSym<'db>), Redirect { name: Name<'db>, target: Slice<Name<'db>> }, Glob { path: Slice<Name<'db>> }, MacroUse(MacroUse<'db>), } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, AllocStashData)] pub struct MacroUse<'db> { pub path: Slice<Name<'db>>, pub input: MacroInput<'db>, pub expansions: Slice<Expansion<'db>>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, AllocStashData)] pub struct Expansion<'db> { pub callee: MacroCallee<'db>, pub entries: Slice<MemmapEntry<'db>>, } pub type Memmap<'db> = Stashed<Slice<MemmapEntry<'db>>>; }
Historical stash extensions
-
Mutable indexing (removed) — the intermediate implementation allowed the fixpoint loop to replace a
MacroUse.expansionsslice handle in place. The append-only stash contract now forbids this API. -
Stash::append_one(or similar) — allocates a new slice consisting of an existing slice's contents plus one appended element. Used to growMacroUse.expansionswhen a new callee is discovered. -
Empty slices are free —
alloc_slice(&[])is hash-consed, so all empty slices of the same type share one entry. This is already the case.
Historical fixpoint loop strategy
The expansion loop operates on a Stash + root Slice<MemmapEntry>:
-
Seeding produces an initial
Stashwith all entries (paths already asSlice<Name>).MacroUseentries start withexpansionspointing to an empty slice. -
Each pass walked and mutated the entries, resolved macro paths, and expanded callees. When a new expansion was discovered:
- Allocate the expansion's child entries into the stash → get a
Slice<MemmapEntry> - Build an
Expansion { callee, entries }and allocate a new expansions slice = old contents + new element viaappend_one - Mutate the
MacroUsein place to pointexpansionsat the new slice
- Allocate the expansion's child entries into the stash → get a
-
Convergence — when no pass discovers new callees, wrap the stash as
Stashed::new(stash, root).
The common case (exactly one expansion per macro use) means one append_one call per macro — growing from empty to a 1-element slice. Multiple expansions (ambiguity) are rare and just do another append_one.
Consumers
Code that reads from the memmap (resolve_member_impl, walk_entries, validate, resolve_path) currently takes &[MemmapEntry]. After this change, it will receive a &Stash plus a Slice<MemmapEntry> and index into the stash. The recursive walk pattern changes from:
#![allow(unused)] fn main() { for entry in entries { ... } }
to:
#![allow(unused)] fn main() { for entry in &stash[slice] { ... } }
Functions need access to the stash to dereference nested Slice fields (e.g., Redirect.target, MacroUse.expansions). The stash reference is threaded through or obtained from the Stashed wrapper.
Historical implementation plan
- Add mutable indexing and
append_onetosage-stash(the mutable indexing portion was later removed by STASH-A5). - Update
MemmapEntry,MacroUse,Expansionto stash-allocatedCopytypes. - Update
ExpandedModuleto holdMemmap<'db>(=Stashed<Slice<MemmapEntry>>). - Update seeding (
seed.rs) to allocate into aStash. - Update the fixpoint loop (
expand.rs) to operate on&mut Stash+ root slice. - Update consumers (
resolve/mod.rs,validate.rs,resolve_path.rs) to thread a stash reference. - Update tests.
Open questions
- Naming:
append_onevspush_slicevsextend_slice? - Should
Stash::append_onebe generic over "append N elements" or just one?