Skip to content

SymbolKey: Cross-compilation symbol identity for all symbol types #280

Description

@amis92

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

  1. Find the containing module by ContainingModuleId
  2. For roster symbols → walk roster force/selection tree by ID
  3. 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

  • Add SymbolKey struct to WarHub.ArmouryModel.Extensions
  • Add SymbolKeyResolution and SymbolKeyResolutionKind
  • Implement SymbolKey.Create(ISymbol) — builds key from symbol hierarchy
  • Implement SymbolKey.Resolve(Compilation) — resolves in any compilation
  • Add lazy SymbolIndex to WhamCompilation for O(1) resolution
  • Refactor AddRootEntryFromSymbol to use SymbolKey
  • Refactor AddSelectionFromLinkOp and AddRootEntryFromSymbol force lookups
  • Add tests for round-trip (Create → new compilation → Resolve) for all symbol types
  • Add tests for edge cases: Missing, Ambiguous, nested entries

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

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions