作業リポジトリ https://github.com/r-koubou/KSPCompiler/compare/fix/symboltable_reference

2026-01-27 の修正方針 by GPT-5.2

  • **前提**: こちらでは「アクティブドキュメント/関連クラス実装」を直接参照できていないため、**典型的に起きる欠陥点**に沿って、**差分最小**で入れられる修正パターンとテスト案を提示します(実コードに合わせてメソッド名/型名は微調整してください)。

1) 問題ごとの要約(症状/原因/影響/優先度)

  • **1) OverloadedSymbolTable の親スコープ検索が効かない疑い**

    • 症状: ローカルに name が無いと enableSearchParent=true でも false を返し得る
    • 原因: TryGet 等が「ローカル辞書のみ」で return しており、未ヒット時に Parent を辿る分岐が無い/早期 return
    • 影響: スコープ解決が破綻し、上位定義のコマンド/関数が見えない
    • 優先度: **高**(意味解析/名前解決の根幹)
  • **2) オーバーロードキーの等価性/ハッシュ問題**

    • 症状: Dictionary<TOverload, ...> でキー照合が不安定(同値のはずが別物扱い、または逆)
    • 原因: EqualsGetHashCode の不整合、内部要素が参照一致のまま、可変オブジェクトをキーにしている
    • 影響: 既存オーバーロード検出失敗、重複登録、探索漏れ、将来の仕様追加で破綻
    • 優先度: **高**(データ構造の前提違反)
  • **3) 引数重複チェックが参照一致で同名を弾けない**

    • 症状: Contains が参照一致で同名別インスタンスを許してしまう
    • 原因: ArgumentSymbol 等が Equals 未定義、または Contains の比較子が参照一致
    • 影響: 同名引数が混入し、後続フェーズで曖昧性/誤解決
    • 優先度: **中〜高**
  • **4) AggregateSymbolTable.Merge の副作用(TableIndex 上書き)**

    • 症状: sourceSymbol インスタンスを target に Add すると、Add が TableIndex を書き換え、source 側の見かけも変わる
    • 原因: 参照共有(同一インスタンス)+ Add 内でミュータブルなプロパティを変更
    • 影響: デバッグ困難、同一シンボルが複数テーブルに属する想定が崩れる、キャッシュ・差分出力が不安定
    • 優先度: **中**(ただし将来拡張で重大化)
  • **5) YAML 出力の差分安定性**

    • 症状: 同名オーバーロードの出力順が揺れて Git 差分がノイジー
    • 原因: OrderBy(Name) のみで、同名集合内の順が未定義(列挙順依存)
    • 影響: レビューしづらい、CI 差分検知が不安定
    • 優先度: **中**

2) 修正案(最小修正)— 問題ごとにコード例

1) OverloadedSymbolTable の Parent 検索を SymbolTable<T> と同じにする

**方針**: 「ローカルで見つからなければ Parent に委譲」を全対象メソッドへ統一。enableSearchParent を尊重し、Parent が null なら終了。

// 例: `OverloadedSymbolTable<TSymbol, TOverload>` を想定(実クラス名に合わせて適用)
public bool TryGet(string name, out TSymbol symbol, bool enableSearchParent = true)
{
    if (TryGetLocal(name, out symbol))
        return true;
 
    if (enableSearchParent && Parent != null)
        return Parent.TryGet(name, out symbol, enableSearchParent: true);
 
    symbol = default!;
    return false;
}
 
public bool TrySearchByName(string name, out IReadOnlyList<TSymbol> overloads, bool enableSearchParent = true)
{
    if (TrySearchByNameLocal(name, out overloads))
        return true;
 
    if (enableSearchParent && Parent != null)
        return Parent.TrySearchByName(name, out overloads, enableSearchParent: true);
 
    overloads = Array.Empty<TSymbol>();
    return false;
}
 
public bool TrySearchIndexByName(string name, out int tableIndex, bool enableSearchParent = true)
{
    if (TrySearchIndexByNameLocal(name, out tableIndex))
        return true;
 
    if (enableSearchParent && Parent != null)
        return Parent.TrySearchIndexByName(name, out tableIndex, enableSearchParent: true);
 
    tableIndex = -1;
    return false;
}
 
public bool TryGetOverloadIndexByName(string name, TOverload overloadKey, out int overloadIndex, bool enableSearchParent = true)
{
    if (TryGetOverloadIndexByNameLocal(name, overloadKey, out overloadIndex))
        return true;
 
    if (enableSearchParent && Parent != null)
        return Parent.TryGetOverloadIndexByName(name, overloadKey, out overloadIndex, enableSearchParent: true);
 
    overloadIndex = -1;
    return false;
}
 
// ローカルのみを見る既存ロジックを分離(既にあるならそのまま利用)
private bool TryGetLocal(string name, out TSymbol symbol) { /* 既存 */ throw new NotImplementedException(); }
private bool TrySearchByNameLocal(string name, out IReadOnlyList<TSymbol> overloads) { /* 既存 */ throw new NotImplementedException(); }
private bool TrySearchIndexByNameLocal(string name, out int tableIndex) { /* 既存 */ throw new NotImplementedException(); }
private bool TryGetOverloadIndexByNameLocal(string name, TOverload key, out int overloadIndex) { /* 既存 */ throw new NotImplementedException(); }

2) オーバーロードキーの等価性/ハッシュ問題

最小修正案: **キー型を不変(もしくは等価・ハッシュ整合)にする**

  • CommandArgumentSymbolListDictionary キーに使うなら最低限:
    • Equals と同じ定義で GetHashCode を実装
    • 内部要素(型・optional・variadic・順序)まで含めた **構造的同値** にする
    • **可変のままキーにしない**(登録後に変更されると Dictionary が壊れる)
public sealed class CommandArgumentSymbolList : IEquatable<CommandArgumentSymbolList>
{
    private readonly IReadOnlyList<ArgumentSymbol> _args;
 
    public CommandArgumentSymbolList(IReadOnlyList<ArgumentSymbol> args)
    {
        _args = args ?? throw new ArgumentNullException(nameof(args));
    }
 
    public IReadOnlyList<ArgumentSymbol> Arguments => _args;
 
    public bool Equals(CommandArgumentSymbolList? other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        if (_args.Count != other._args.Count) return false;
 
        for (int i = 0; i < _args.Count; i++)
        {
            if (!ArgumentSignatureEquals(_args[i], other._args[i]))
                return false;
        }
        return true;
    }
 
    public override bool Equals(object? obj) => Equals(obj as CommandArgumentSymbolList);
 
    public override int GetHashCode()
    {
        var hc = new HashCode();
        hc.Add(_args.Count);
        for (int i = 0; i < _args.Count; i++)
            AddArgumentSignatureHash(ref hc, _args[i]);
        return hc.ToHashCode();
    }
 
    private static bool ArgumentSignatureEquals(ArgumentSymbol a, ArgumentSymbol b)
        => a.TypeId == b.TypeId
           && a.IsOptional == b.IsOptional
           && a.IsVariadic == b.IsVariadic;
 
    private static void AddArgumentSignatureHash(ref HashCode hc, ArgumentSymbol a)
    {
        hc.Add(a.TypeId);
        hc.Add(a.IsOptional);
        hc.Add(a.IsVariadic);
    }
}
  • **注意**: 上記は「変数名」ではなく「シグネチャ」を同値定義にしています(オーバーロード識別に意味があるのは型等の並びが多い)。

より堅牢な代替案(推奨寄り): **署名文字列キー or SignatureKey 構造体**

  • Dictionary<string, ...> にしてキーを正規化済み署名にする(YAML 出力も安定)
  • 可変オブジェクトをキーにしない

署名正規化ルール(例):

  • name は辞書の外側で管理(Name→overloads)
  • 引数列: TypeId を正規化(完全修飾名/組み込み型は固定表記)
  • optional: ?、variadic: ...
  • returnType(必要なら): ->ReturnTypeId
  • 例: Int32,String?,Path...->Void
public static class SignatureKey
{
    public static string Build(IReadOnlyList<ArgumentSymbol> args, string? returnTypeId = null)
    {
        var sb = new System.Text.StringBuilder();
        for (int i = 0; i < args.Count; i++)
        {
            if (i > 0) sb.Append(',');
            sb.Append(NormalizeTypeId(args[i].TypeId));
            if (args[i].IsOptional) sb.Append('?');
            if (args[i].IsVariadic) sb.Append("...");
        }
 
        if (!string.IsNullOrEmpty(returnTypeId))
        {
            sb.Append("->");
            sb.Append(NormalizeTypeId(returnTypeId));
        }
 
        return sb.ToString();
    }
 
    private static string NormalizeTypeId(string typeId)
        => typeId.Trim(); // 最小: trim。実際は別名解決・完全修飾などをここに集約
}

**比較(要点)**

  • 最小修正(GetHashCode 実装): 変更量小、ただし「キーが可変」問題が残りやすい
  • 代替(署名キー): 将来拡張(optional/variadic/戻り値/ジェネリクス)に強く、YAML 安定化とも相性が良い

3) 引数重複チェックを参照一致から「論理キー」に変更

**方針**: Contains をやめ、Name(必要なら Position)で重複判定。

public sealed class ArgumentSymbolList
{
    private readonly List<ArgumentSymbol> _items = new();
 
    public bool Add(ArgumentSymbol arg)
    {
        // 例: 同名禁止(大文字小文字の扱いは言語仕様に合わせる)
        if (_items.Any(x => string.Equals(x.Name, arg.Name, StringComparison.Ordinal)))
            return false;
 
        _items.Add(arg);
        return true;
    }
}

CommandSymbol.AddArgument() 側で弾く場合も同様に Name をキーにする。


4) AggregateSymbolTable.Merge の副作用(TableIndex 上書き)

**影響範囲**:

  • sourcetarget が同じ Symbol インスタンスを共有
  • target.Add(symbol)symbol.TableIndex = targetIndex 等を書き換える
  • 結果: source 側で見た TableIndex が変化し、**source の整合性が壊れる**

最小修正案 A: Merge 時にクローンして Add(推奨)

public static void Merge(AggregateSymbolTable source, AggregateSymbolTable target)
{
    foreach (var sym in source.Symbols)
    {
        // `CloneForTable()` のような「テーブル所属情報を切り離す複製」を用意
        var copy = sym.CloneForTable();
        target.Add(copy);
    }
}

Symbol 基底に最小限の複製 API を追加:

public abstract class Symbol
{
    public int TableIndex { get; set; }
 
    public abstract Symbol CloneForTable();
}

最小修正案 B: TableIndex を外部管理にする(変更大きめ)

  • TableIndexSymbol から外し、SymbolHandle(TableIndex, SymbolId) のように管理
  • これは設計変更が大きいので「今すぐ」より「推奨方針」寄り

5) YAML 出力の差分安定性(同名オーバーロード排序)

**方針**: Name だけでなく **署名キー**(2) の正規化)+ 最後に Id(安定 ID)で並べる。

var ordered = symbols
    .OrderBy(x => x.Name, StringComparer.Ordinal)
    .ThenBy(x => x.SignatureKey, StringComparer.Ordinal) // 例: "Int32,String?->Void"
    .ThenBy(x => x.Id); // 例: YAML上の安定ID(GUIDではなく仕様上固定の連番など)

SignatureKey を持っていない場合は exporter 側で組み立ててもよいですが、**正規化ロジックは 1 か所**に集約してください。


3) ユニットテスト案(問題ごと)

1) Parent 検索

using Xunit;
 
public class OverloadedSymbolTableTests
{
    [Fact]
    public void TryGet_SearchesParent_WhenNotFoundLocally()
    {
        var parent = new OverloadedSymbolTable();
        parent.Add("Foo", new Symbol("Foo"));
 
        var child = new OverloadedSymbolTable { Parent = parent };
 
        var ok = child.TryGet("Foo", out var sym, enableSearchParent: true);
 
        Assert.True(ok);
        Assert.Equal("Foo", sym.Name);
    }
 
    [Fact]
    public void TryGet_DoesNotSearchParent_WhenDisabled()
    {
        var parent = new OverloadedSymbolTable();
        parent.Add("Foo", new Symbol("Foo"));
 
        var child = new OverloadedSymbolTable { Parent = parent };
 
        var ok = child.TryGet("Foo", out _, enableSearchParent: false);
 
        Assert.False(ok);
    }
}

2) キー等価性/ハッシュ

using Xunit;
 
public class CommandArgumentSymbolListTests
{
    [Fact]
    public void DictionaryKey_Works_ForStructurallyEqualSignatures()
    {
        var a1 = new CommandArgumentSymbolList(new[]
        {
            new ArgumentSymbol(typeId: "Int32", isOptional: false, isVariadic: false),
        });
 
        var a2 = new CommandArgumentSymbolList(new[]
        {
            new ArgumentSymbol(typeId: "Int32", isOptional: false, isVariadic: false),
        });
 
        var dict = new Dictionary<CommandArgumentSymbolList, int>();
        dict[a1] = 123;
 
        Assert.True(dict.TryGetValue(a2, out var v));
        Assert.Equal(123, v);
    }
}

3) 引数重複

using Xunit;
 
public class ArgumentSymbolListTests
{
    [Fact]
    public void Add_RejectsDuplicateName()
    {
        var list = new ArgumentSymbolList();
        Assert.True(list.Add(new ArgumentSymbol(name: "x")));
        Assert.False(list.Add(new ArgumentSymbol(name: "x")));
    }
}

4) Merge 副作用

using Xunit;
 
public class AggregateSymbolTableMergeTests
{
    [Fact]
    public void Merge_DoesNotMutateSourceSymbols_TableIndex()
    {
        var source = new AggregateSymbolTable();
        var sym = new SomeSymbol();
        source.Add(sym);
        var before = sym.TableIndex;
 
        var target = new AggregateSymbolTable();
        AggregateSymbolTable.Merge(source, target);
 
        Assert.Equal(before, sym.TableIndex);
    }
}

5) YAML 安定ソート

using Xunit;
 
public class YamlExportOrderTests
{
    [Fact]
    public void Export_IsStable_ForSameNameOverloads()
    {
        var s1 = new Symbol(name: "Foo", signatureKey: "Int32", id: 2);
        var s2 = new Symbol(name: "Foo", signatureKey: "String", id: 1);
 
        var ordered = new[] { s1, s2 }
            .OrderBy(x => x.Name)
            .ThenBy(x => x.SignatureKey)
            .ThenBy(x => x.Id)
            .ToArray();
 
        Assert.Equal(new[] { s2, s1 }, ordered);
    }
}

4) 推奨方針(今後の拡張で事故らない運用)

  • **オーバーロード同定は「不変の署名キー」**(SignatureKey 文字列 or 構造体)に寄せ、**可変オブジェクトを Dictionary キーにしない**
  • 親スコープ探索は SymbolTable<T> と同一の制御フローに統一し、**ローカル未ヒット時に必ず Parent 委譲**
  • 引数や変数の重複判定は参照一致禁止、**言語仕様に基づく論理キー(Name/Position/Type)**で統一
  • TableIndex のような「所属先依存の可変情報」は、可能なら外部管理か clone により参照共有の副作用を遮断
  • YAML 出力は Name + SignatureKey + 安定 Id の多段ソートで **差分安定**を保証

対応:1. OverloadedSymbolTable の親スコープ検索が効かない疑い