作業リポジトリ 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 - 影響: スコープ解決が破綻し、上位定義のコマンド/関数が見えない
- 優先度: **高**(意味解析/名前解決の根幹)
- 症状: ローカルに name が無いと
-
**2) オーバーロードキーの等価性/ハッシュ問題**
- 症状:
Dictionary<TOverload, ...>でキー照合が不安定(同値のはずが別物扱い、または逆) - 原因:
EqualsとGetHashCodeの不整合、内部要素が参照一致のまま、可変オブジェクトをキーにしている - 影響: 既存オーバーロード検出失敗、重複登録、探索漏れ、将来の仕様追加で破綻
- 優先度: **高**(データ構造の前提違反)
- 症状:
-
**3) 引数重複チェックが参照一致で同名を弾けない**
- 症状:
Containsが参照一致で同名別インスタンスを許してしまう - 原因:
ArgumentSymbol等がEquals未定義、またはContainsの比較子が参照一致 - 影響: 同名引数が混入し、後続フェーズで曖昧性/誤解決
- 優先度: **中〜高**
- 症状:
-
**4) AggregateSymbolTable.Merge の副作用(TableIndex 上書き)**
- 症状:
sourceのSymbolインスタンスを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) オーバーロードキーの等価性/ハッシュ問題
最小修正案: **キー型を不変(もしくは等価・ハッシュ整合)にする**
CommandArgumentSymbolListをDictionaryキーに使うなら最低限: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 上書き)
**影響範囲**:
sourceとtargetが同じ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 を外部管理にする(変更大きめ)
TableIndexをSymbolから外し、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の多段ソートで **差分安定**を保証