Goal
Introduce a unified SymbolKey mechanism — a stable, serializable identifier for any ISymbol that can be resolved in a different compilation built from compatible data. Analogous to Roslyn's internal SymbolKey.
Parent epic: #279
Problem
When a roster edit creates a new WhamCompilation, all old ISymbol instances become stale. Editor code that references symbols from a previous compilation must manually re-find them, duplicating binding logic.
Current pain points:
AddRootEntryFromSymbol (RosterOperations.cs:195) has an explicit hack:
// hack for referencing symbols from previous compilations, until SymbolReference is done
var entryLocal = state.Compilation.GlobalNamespace.Catalogues
.FirstOrDefault(x => x.Id == Entry.ContainingModule!.Id)
?.RootContainerEntries
.Where(x => x.IsContainerKind(ContainerKind.Selection))
.OfType<ISelectionEntryContainerSymbol>()
.FirstOrDefault(x => x.Id == Entry.Id)
AddSelectionFromLinkOp (line 179): force lookup by string ID because ForceNode object reference is invalidated
AddRootEntryFromSymbol (line 222): same force-by-ID pattern
Design
Core Types
/// Lightweight, serializable identifier for an ISymbol that can be resolved across compilations.
public readonly record struct SymbolKey
{
public SymbolKind Kind { get; init; }
public string? SymbolId { get; init; }
public string? ContainingModuleId { get; init; }
public string? ContainingEntryId { get; init; }
public static SymbolKey Create(ISymbol symbol);
public SymbolKeyResolution Resolve(Compilation compilation);
}
public readonly struct SymbolKeyResolution
{
public ISymbol? Symbol { get; }
public ImmutableArray<ISymbol> CandidateSymbols { get; }
public SymbolKeyResolutionKind Kind { get; } // Resolved / Missing / Ambiguous
}
public enum SymbolKeyResolutionKind { Resolved, Missing, Ambiguous }
How It Works for Different Symbol Types
Catalogue entries (stable data):
Kind = ContainerEntry, SymbolId = "abc-1234", ContainingModuleId = "cat-space-marines", ContainingEntryId = null
Nested catalogue entries (child of another entry):
Kind = ContainerEntry, SymbolId = "weapon-5678", ContainingModuleId = "cat-space-marines", ContainingEntryId = "abc-1234"
Roster forces (generated unique ID per instance):
Kind = Force, SymbolId = "1234-5678-abcd", ContainingModuleId = "roster-id", ContainingEntryId = null
Roster selections (generated unique ID per instance):
Kind = Selection, SymbolId = "5678-abcd-1234", ContainingModuleId = "roster-id", ContainingEntryId = "force-id"
Resolution Strategy
- Find the containing module by
ContainingModuleId
- For roster symbols → walk roster force/selection tree by ID
- For catalogue symbols → search catalogue scope (can use existing Binder/lookup infrastructure)
Per-Compilation Index (Performance)
/// Lazy per-compilation index for O(1) SymbolKey resolution.
internal class SymbolIndex
{
private readonly Dictionary<(string?, string?), List<ISymbol>> _index;
internal static SymbolIndex GetOrCreate(WhamCompilation compilation); // CAS-protected lazy field
}
Visibility
Following Roslyn's pattern, SymbolKey should be internal initially, used by EditorServices operations. Operations store SymbolKey internally and resolve on Apply(). Can be made public later if needed.
Tasks
Edge Cases to Handle
- Deleted entry after data change →
Missing
- Duplicate IDs in same scope →
Ambiguous with candidate list
- Nested entries sharing ID with root entries → disambiguated by
ContainingEntryId
- Link symbol vs. referenced target → same
SymbolId resolves to same symbol; caller distinguishes via IEntrySymbol.IsReference
Goal
Introduce a unified
SymbolKeymechanism — a stable, serializable identifier for anyISymbolthat can be resolved in a different compilation built from compatible data. Analogous to Roslyn's internalSymbolKey.Parent epic: #279
Problem
When a roster edit creates a new
WhamCompilation, all oldISymbolinstances become stale. Editor code that references symbols from a previous compilation must manually re-find them, duplicating binding logic.Current pain points:
AddRootEntryFromSymbol(RosterOperations.cs:195) has an explicit hack:AddSelectionFromLinkOp(line 179): force lookup by string ID becauseForceNodeobject reference is invalidatedAddRootEntryFromSymbol(line 222): same force-by-ID patternDesign
Core Types
How It Works for Different Symbol Types
Catalogue entries (stable data):
Nested catalogue entries (child of another entry):
Roster forces (generated unique ID per instance):
Roster selections (generated unique ID per instance):
Resolution Strategy
ContainingModuleIdPer-Compilation Index (Performance)
Visibility
Following Roslyn's pattern,
SymbolKeyshould be internal initially, used byEditorServicesoperations. Operations storeSymbolKeyinternally and resolve onApply(). Can be made public later if needed.Tasks
SymbolKeystruct toWarHub.ArmouryModel.ExtensionsSymbolKeyResolutionandSymbolKeyResolutionKindSymbolKey.Create(ISymbol)— builds key from symbol hierarchySymbolKey.Resolve(Compilation)— resolves in any compilationSymbolIndextoWhamCompilationfor O(1) resolutionAddRootEntryFromSymbolto useSymbolKeyAddSelectionFromLinkOpandAddRootEntryFromSymbolforce lookupsEdge Cases to Handle
MissingAmbiguouswith candidate listContainingEntryIdSymbolIdresolves to same symbol; caller distinguishes viaIEntrySymbol.IsReference