diff --git a/compiler-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/compiler/ir/EnumizeIrBaseGenerator.kt b/compiler-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/compiler/ir/EnumizeIrBaseGenerator.kt index a78c311..c31b8a2 100644 --- a/compiler-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/compiler/ir/EnumizeIrBaseGenerator.kt +++ b/compiler-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/compiler/ir/EnumizeIrBaseGenerator.kt @@ -44,25 +44,22 @@ class EnumizeIrBaseGenerator(private val ctx: EnumizeIrContext) { } // 末端の列挙(docs/コンパイラプラグイン設計02.md §2): コンパイラの継承者リスト(FQN 順に正規化済み)を走査し、 - // 中間 sealed に到達したらその継承者リストへ再帰的に降りる。並べ替えは一切行わない(docs/コンパイラプラグイン設計02.md §3) + // 中間 sealed に到達したらその継承者リストへ再帰的に降りる。並べ替えは一切行わない(docs/コンパイラプラグイン設計02.md §3)。 + // 中間 sealed を含む全メンバーを走査順を保つ集合へ初出のみ入れ、そこから末端だけを取り出す。 + // 複数の中間 sealed を同時に実装するメンバーは経路の数だけ到達するため、この集合が + // 末端の重複掲載(kind の集合が壊れる)と中間の再展開(同じ部分木の再走査)を同時に抑える private fun collectLeaves(base: IrClass): List { - val result = mutableListOf() - collectLeavesInto(base, result, LinkedHashSet()) - return result + val members = LinkedHashSet() + collectMembersInto(base, members) + return members.filterNot { it.modality == Modality.SEALED } } - private fun collectLeavesInto( - current: IrClass, - result: MutableList, - visited: MutableSet, - ) { - if (!visited.add(current)) return + private fun collectMembersInto(current: IrClass, members: MutableSet) { for (subclassSymbol in current.sealedSubclasses) { val subclass = subclassSymbol.owner + if (!members.add(subclass)) continue if (subclass.modality == Modality.SEALED) { - collectLeavesInto(subclass, result, visited) - } else { - result.add(subclass) + collectMembersInto(subclass, members) } } } diff --git "a/docs/test/\343\202\261\343\203\274\343\202\27101-\347\224\237\346\210\220\343\201\250\345\256\237\350\241\214\346\231\202API.md" "b/docs/test/\343\202\261\343\203\274\343\202\27101-\347\224\237\346\210\220\343\201\250\345\256\237\350\241\214\346\231\202API.md" index 37d4a34..8fc0934 100644 --- "a/docs/test/\343\202\261\343\203\274\343\202\27101-\347\224\237\346\210\220\343\201\250\345\256\237\350\241\214\346\231\202API.md" +++ "b/docs/test/\343\202\261\343\203\274\343\202\27101-\347\224\237\346\210\220\343\201\250\345\256\237\350\241\214\346\231\202API.md" @@ -71,6 +71,7 @@ K9 の表記変種(import 別名・FQN・star import・typealias)と診断 | API-29 | K5=1 段 × K9=中間経由再帰追跡 | 中間 sealed class / sealed interface 経由の raw 追跡再帰で末端 companion 自動生成 | MidTrackingTest#leavesViaIntermediatesGetAutoCompanions | V3 | | API-30 | K5=中間非生成 | 中間には何も生成されず entries 非掲載(末端まで平坦化)・中間の明示 companion へも Enumish 非注入(kind 非成立)・中間型変数の asEnumish は末端 kind へ実体解決 | MidTrackingTest#intermediatesHaveNoKind | 多段の展開順→ケース03 | | API-51 | K9=スコープ順の競合解決 | raw 追跡の優先関係を競合 3 形で固定: star import 基底 vs 同一 pkg 同名非基底=同一 pkg 勝ち(entries 非所属)・同一 pkg 同名 vs 明示 import 基底=import 勝ち(所属)・明示 import vs 外側ネスト同名=ネスト勝ち(非所属) | RawTrackingTest#scopePriorityDecidesMembership | V3。表記単独形の成立→ケース04 DIA-31/32 | +| API-57 | K5=多重経路(兄弟中間の同時実装) | 複数経路で到達する末端は初出位置に 1 回だけ entries 掲載・kind は 1 つ(どちらの中間型からも同じ kind へ解決)・生成 Enumish の継承者一覧も重複せず kind-when は else 不要 | MultiPathTest#multiPathLeafIsListedOnceAtFirstOccurrence / MultiPathTest#multiPathLeafHasSingleKind | 基底は一意のため診断は非発火。展開順の規則→ケース03 §1 | ## 8. 型パラメータ(generic・K11) diff --git "a/docs/test/\343\202\261\343\203\274\343\202\27103-\351\240\206\345\272\217.md" "b/docs/test/\343\202\261\343\203\274\343\202\27103-\351\240\206\345\272\217.md" index c13f780..2e2fd36 100644 --- "a/docs/test/\343\202\261\343\203\274\343\202\27103-\351\240\206\345\272\217.md" +++ "b/docs/test/\343\202\261\343\203\274\343\202\27103-\351\240\206\345\272\217.md" @@ -11,6 +11,7 @@ FQN UTF-16 序数と DFS 入れ子展開に関する順序観測は本資料へ - 順序 = コンパイラの sealed 継承者リスト走査順(setSealedClassInheritors セッター側で ClassId FQN 文字列の UTF-16 序数昇順に正規化・大小区別・ロケール非依存)。 - プラグインは FIR / IR とも並べ替えに一切介入しない(設計00 §6.2・設計02 §3)。 - 中間 sealed はその位置で継承者リストを深さ優先に入れ子展開し、entries には末端のみが載る。 +- 複数経路で到達する末端(兄弟中間の同時実装)は初出位置に 1 回だけ載る(設計00 §6.2・設計02 §2。観測はケース01 API-57)。 - 整列キーは末端の ClassId のみ(kind が companion でも companion 名は整列に関与しない)。 ## 2. 期待列の導出 diff --git "a/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md" "b/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md" index 184e78a..4977556 100644 --- "a/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md" +++ "b/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md" @@ -26,7 +26,7 @@ Kotlin 言語仕様がこのプラグインに対して張る空間を次の次 | K2 | 基底の修飾 | expect / actual(共にエラー)・inner(sealed 不能=NOT_SEALED 合流)・型パラメータ付き・ネスト配置・可視性 | | K3 | 末端の宣言種別 | object / data object / class / data class / value class / enum class / open class / abstract class / interface / fun interface / 階層外クラスの companion(既定名・名前付き) | | K4 | 末端の修飾 | inner(エラー)・型パラメータ付き・可視性・expect/actual(言語委譲) | -| K5 | 中間 sealed | 無 / 1 段 / 多段 / 兄弟複数 / 継承者ゼロ / 可視性 / 配置(ネスト・トップレベル) | +| K5 | 中間 sealed | 無 / 1 段 / 多段 / 兄弟複数 / 多重経路(兄弟中間の同時実装) / 継承者ゼロ / 可視性 / 配置(ネスト・トップレベル) | | K6 | companion 状態 | 無し(自動生成) / 明示既定名 / 明示名前付き / 可視性 4 値 / ユーザーメンバー共存 / companion 自身が末端(外側=末端: エラー・外側=基底/中間: 成立・外側=階層外: 許容) / 非 companion のネスト宣言 `Companion` との衝突(実挙動固定) | | K7 | 配置 | 基底ネスト / 同一ファイルトップレベル / 別ファイルトップレベル / 無関係クラス内ネスト / private 外側クラス内 / 非 sealed クラス内の基底 / 多段ネスト / local(関数内。実装は FIR・チェッカー・IR の全入口で一貫除外=無診断・無生成の素通りを固定する) / 別パッケージ(sealed の言語制約エラーへ委譲) | | K8 | 可視性 | (基底/中間/末端/companion) × (public/internal/protected/private) → 実効可視性・asEnumish 3 段規則・IR-only アクセサ壁 3 形・基底より広い末端(E-2) | diff --git "a/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md" "b/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md" index ff12446..253d45f 100644 --- "a/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md" +++ "b/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md" @@ -19,7 +19,7 @@ src/test には test compilation 配線検証用の小さな @Enumize 階層(T | companionleaf | Token = sealed interface + Host.Companion(既定名 companion 末端) + WithNamed.Named(名前つき companion 末端) + data object HostA + Holder2.Zzz(名前つき・順序プローブ)。基底/中間 companion 末端の成立形は sealedbase / mid が担う | K3=companion 末端(外側=階層外)・label=宣言名・'.'(46) 序数境界 | API/ORD | | sealedbase | Task = sealed class + data class Run(`:Task()`・自動 companion) + data object Done + object Plain + protected class Inner(基底ネスト・明示 companion・基底 companion に factory と asEnumish 型付き観測メンバー=規則 1 の基底スコープ内観測) + 基底自身の companion(`companion object : Task()`)=基底 companion 末端の成立形(label="Companion") | K1=sealed class・K9 コンストラクタ呼び出し形・protected 末端・K6 基底 companion 末端 | API/VIS | | generic | `Generic` = sealed class + data class `Box` + class `Empty` + 非 generic 末端 class Fixed(`:Generic()`) ‖ Holder = 非 generic sealed interface + generic 末端 data class `Cell`(変位注釈) | K11: 型パラメータ(両方/基底のみ/末端のみ/変位)・star projection・kind の型引数非依存 | API | -| mid | RootVia = sealed interface + 中間 sealed class MidClass(companion object : RootVia=中間 companion 末端の成立形) + 中間 sealed interface MidIface(明示 companion=非実装・Enumish 非注入・kind 非成立の観測) + 各経由の companion 無し末端 | K5×K9×K6: 中間経由の raw 追跡再帰(V3)・中間へ非生成・中間 companion の非注入/末端成立の両形 | API | +| mid | RootVia = sealed interface + 中間 sealed class MidClass(companion object : RootVia=中間 companion 末端の成立形) + 中間 sealed interface MidIface(明示 companion=非実装・Enumish 非注入・kind 非成立の観測) + 各経由の companion 無し末端 ‖ MultiPath = 兄弟中間 MpLeft / MpRight + 両方を実装する末端 MpBoth(自動 companion) + 各単独経路末端 MpOnlyLeft / MpOnlyRight(RootVia の entries スナップショットを汚さないよう独立階層とする) | K5×K9×K6: 中間経由の raw 追跡再帰(V3)・中間へ非生成・中間 companion の非注入/末端成立の両形・多重経路の初出 1 回掲載 | API | | scope | 真基底 scope.target.Holder.Base(@Enumize・object Holder 内ネスト)+囮トップレベル scope.target.Base(非 sealed interface)+別 pkg 実基底 scope.other.Base(@Enumize)。競合 3 形(末端は全て scope.target 配置): (1) 明示 import Holder.Base vs 同一 pkg 囮=import 勝ち→所属 (2) scope.other.* star import vs 同一 pkg 囮=同一 pkg 勝ち→非所属・コンパイル成功 (3) 明示 import vs ホストの内側ネスト同名=ネスト勝ち→非所属 | K9: raw 追跡スコープ順の優先関係を entries 所属/非所属+コンパイル成立で固定 | API | | order.flat | FlatRoot = sealed interface + 文字境界末端(AB1/Ab2/aB3/ab4・A1/AA/A_/Az)+ 配置末端(基底ネスト Aaa・無関係クラス Box.Bbb・トップレベル Mmm/Zzz/aLower)+ 接頭辞対(Foo=class 自動 companion / FooBar=data object)+ セパレータ対(ネスト Sep.Q / トップレベル Sep0) | K7×O3: FQN UTF-16 序数の全境界を 1 スナップショット固定 | ORD | | order.mid | MidRoot = sealed interface + MidNest{NestA,NestB}(トップレベル中間×ネスト末端=FQN 順一致) + MidTop{Early,Late}(トップレベル中間×トップレベル末端=break) + MidMulti.Deep{Bottom,Wide}(多段) + MidRoot.MidIn{Outpost}(基底ネスト中間×トップレベル末端) + 直接末端 Aaa/Zzz(in-place 展開の前後) + Probe=class 末端(自動 companion)+その内側ネスト data object 末端 Bb(単純名が Companion より序数で小=entries 順と inheritors 順の分岐プローブ) | K5×O3: DFS 入れ子展開・break・多段・中間配置 2 形・inheritors 分岐を 1 スナップショット固定 | ORD | diff --git "a/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21000.md" "b/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21000.md" index 4be476f..5a274b9 100644 --- "a/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21000.md" +++ "b/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21000.md" @@ -389,6 +389,9 @@ entries = [Bbb, Aaa] 末端が中間 sealed の内側にネストしている通常の構成では、ネストした末端の FQN が中間の FQN を接頭辞に持つため結果的に FQN 序数順と一致する +- **同じ末端へ複数経路で到達する構成**(兄弟の中間 sealed を同時に実装する末端)では、 + **初出位置に 1 回だけ載る**。 + 重複の除去は並べ替えではなく、entries が kind の集合であることの帰結である - 生成 Enumish の inheritors 属性(§5.2)も同様に、収集した順のまま登録する。 属性へ格納される時点でセッター側の正規化(§6.1)が働くため、プラグイン側で整える必要はない diff --git "a/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21002.md" "b/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21002.md" index f9ccca1..f7375b4 100644 --- "a/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21002.md" +++ "b/docs/\343\202\263\343\203\263\343\203\221\343\202\244\343\203\251\343\203\227\343\203\251\343\202\260\343\202\244\343\203\263\350\250\255\350\250\21002.md" @@ -54,6 +54,8 @@ IR 側の責務は次の 3 つである: り、**真実の単一ソース**である(仮に IC 下でこの集合が壊れるとすれば、それは言語機能側のバグであり、 プラグイン固有の破損ではない) - 中間 sealed は再帰的に展開し、**末端のみ**を集める(中間 sealed 自身は entries に載せない) +- 複数の中間 sealed を同時に実装する末端は経路の数だけ到達するため、**初出位置の 1 回だけを残す**。 + ここで列挙するのは kind の**集合**であり、重複は enum 的意味論(末端 1 種類につき 1 kind)を壊す - 各末端から kind を解決する: object なら末端自身の IrClass、 それ以外(class / enum class / interface。open / abstract を含む)ならその companion の IrClass - ここで列挙するのは**基底 SI の階層 = kind の集合**である。 diff --git "a/docs/\346\246\202\350\246\201.md" "b/docs/\346\246\202\350\246\201.md" index 0531274..5960cd4 100644 --- "a/docs/\346\246\202\350\246\201.md" +++ "b/docs/\346\246\202\350\246\201.md" @@ -433,6 +433,9 @@ kind の `toString` は label getter 経由のため自動で追随するが、d 末端が中間 sealed の内側にネストしている通常の構成では、ネストした末端の FQN が中間の FQN を接頭辞に持つため、 結果的に FQN 序数順と一致する +- **末端が複数の中間 sealed を同時に実装している場合**(`class C : Mid1, Mid2`)は、 + `entries` には**最初に到達した位置に 1 回だけ**現れる(kind は末端 1 種類につき 1 つである) + **性質(enum の宣言順との違い)**: - **宣言順ではない**。宣言を並べ替えても `entries` の並びは変わらない diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpBoth.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpBoth.kt new file mode 100644 index 0000000..ba0d420 --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpBoth.kt @@ -0,0 +1,5 @@ +package io.github.projectmapk.fixtures.mid + +// 2 つの中間 sealed を同時に実装する末端(companion 明示なし = 多重経路での候補判定も観測する)。 +// MpLeft 経由の初出位置に 1 回だけ entries へ載る(docs/test/ケース01-生成と実行時API.md API-57) +class MpBoth : MpLeft, MpRight diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpLeft.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpLeft.kt new file mode 100644 index 0000000..34d7de0 --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpLeft.kt @@ -0,0 +1,4 @@ +package io.github.projectmapk.fixtures.mid + +// 兄弟中間の一方(多重経路末端 MpBoth の初出経路。docs/test/ケース01-生成と実行時API.md API-57) +sealed interface MpLeft : MultiPath diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyLeft.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyLeft.kt new file mode 100644 index 0000000..f1382a9 --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyLeft.kt @@ -0,0 +1,4 @@ +package io.github.projectmapk.fixtures.mid + +// MpLeft 単独経路の末端(MpBoth の後続 = 初出位置の対照。docs/test/ケース01-生成と実行時API.md API-57) +data object MpOnlyLeft : MpLeft diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyRight.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyRight.kt new file mode 100644 index 0000000..bf1f7b6 --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpOnlyRight.kt @@ -0,0 +1,4 @@ +package io.github.projectmapk.fixtures.mid + +// MpRight 単独経路の末端(MpBoth の重複が除かれた後に続く。docs/test/ケース01-生成と実行時API.md API-57) +data object MpOnlyRight : MpRight diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpRight.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpRight.kt new file mode 100644 index 0000000..6e3042e --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MpRight.kt @@ -0,0 +1,4 @@ +package io.github.projectmapk.fixtures.mid + +// 兄弟中間の他方(多重経路末端 MpBoth の 2 度目の到達経路。docs/test/ケース01-生成と実行時API.md API-57) +sealed interface MpRight : MultiPath diff --git a/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MultiPath.kt b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MultiPath.kt new file mode 100644 index 0000000..e540451 --- /dev/null +++ b/integration-test/producer-jvm/src/main/kotlin/io/github/projectmapk/fixtures/mid/MultiPath.kt @@ -0,0 +1,9 @@ +package io.github.projectmapk.fixtures.mid + +import io.github.projectmapk.sealedClassEnumizer.Enumize + +// 多重経路(複数の中間 sealed を同時に実装する末端)のフィクスチャ基底 +// (docs/test/ケース01-生成と実行時API.md API-57)。 +// 継承者はファイル分散: MpLeft.kt / MpRight.kt(兄弟中間)と各末端ファイル。 +// RootVia とは独立した階層とし、entries スナップショットを相互に汚さない +@Enumize sealed interface MultiPath diff --git a/integration-test/producer-jvm/src/test/kotlin/io/github/projectmapk/fixtures/mid/MultiPathTest.kt b/integration-test/producer-jvm/src/test/kotlin/io/github/projectmapk/fixtures/mid/MultiPathTest.kt new file mode 100644 index 0000000..9f3e741 --- /dev/null +++ b/integration-test/producer-jvm/src/test/kotlin/io/github/projectmapk/fixtures/mid/MultiPathTest.kt @@ -0,0 +1,42 @@ +package io.github.projectmapk.fixtures.mid + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +// 多重経路(複数の中間 sealed を同時に実装する末端)の box テスト +// (docs/test/ケース01-生成と実行時API.md §7) +class MultiPathTest { + // docs/test/ケース01-生成と実行時API.md API-57: 複数経路で到達する末端は初出位置に 1 回だけ載る。 + // 直接継承者の整列 [MpLeft, MpRight] を DFS 展開すると MpLeft 側で [MpBoth, MpOnlyLeft]・ + // MpRight 側で [MpBoth(重複), MpOnlyRight] となり、2 度目の MpBoth は落ちる + @Test + fun multiPathLeafIsListedOnceAtFirstOccurrence() { + assertEquals( + listOf("MpBoth", "MpOnlyLeft", "MpOnlyRight"), + MultiPath.Enumish.entries.map { it.label }, + ) + } + + // docs/test/ケース01-生成と実行時API.md API-57: 経路が複数でも kind は 1 つで、 + // どちらの中間型からも同じ kind へ解決される。生成 Enumish の継承者一覧も重複せず + // kind-when は else 不要で網羅する + @Test + fun multiPathLeafHasSingleKind() { + val viaLeft: MpLeft = MpBoth() + val viaRight: MpRight = MpBoth() + assertSame(MpBoth.Companion, viaLeft.asEnumish()) + assertSame(MpBoth.Companion, viaRight.asEnumish()) + assertSame(MpBoth.Companion, MultiPath.Enumish.valueOf("MpBoth")) + assertEquals( + listOf("both", "left", "right"), + MultiPath.Enumish.entries.map { kind -> + when (kind) { + MpBoth.Companion -> "both" + MpOnlyLeft -> "left" + MpOnlyRight -> "right" + } + }, + ) + } +}