Unknownの理由は診断文言ではなく「誰がいつ埋めるか」の分類である
Unknownは二段階の間に開いた窓であるは、Unknownが⊥ではなく理由を持ち運ぶことを診断品質の観点から書いた。「なぜ不明なのか・いつ確定するのか」をユーザーに示せる、という利点である。
ただし理由の集合を実際に並べてみると、これは診断のための注釈ではない。どの理由かによって、誰がその値を埋めるのか、そもそも埋まるのかが違う。UnknownReasonは理由を記録するenumというより、未確定値の解決経路の分類である。
三つの群に割れる
substitute_placeholderは、for展開のときに値を埋める関数である。この関数がどの理由を処理するかを見ると、10個の変種が綺麗に三群に割れる。コード中のコメント自身がその区別を書いている。
| 群 | 変種 | 誰が埋めるか | いつ |
|---|---|---|---|
| for束縛のプレースホルダ | ForValue ForKey ForIndex ForValuePath | for展開 | 静的段階(パース中) |
| 関数のプレースホルダ | FnParam FnLocal | ユーザー定義関数の評価 | 静的段階(パース中) |
| 段階を跨ぐ穴 | UpstreamRef UpstreamBareRef PostCreateReadIncomplete | apply(上流または次回) | 動的段階 |
| 誤り | EmptyInterpolation | 誰も埋めない | — |
前の二群は静的段階の中で消える。for v in iterableを書いた時点では要素がまだ分からないので、パーサはForValueを置いておき、iterableが解決したらsubstitute_placeholderが実際の要素で置き換える。関数引数も同じで、関数本体をパースする時点では実引数が無いからFnParamを置き、呼び出しの評価時に埋める。つまりこれらは評価順序の都合で一時的に空いている穴であり、planに出てはいけない。
UpstreamRefは違う。同じ関数がこれを見ても何もしない — コードのコメントが「upstream and empty-interpolation unknowns are not for-expansion placeholders」と明記している。上流applyの結果は静的段階の中ではどうやっても手に入らないので、planまで生き延びて(known after upstream apply: network.vpc_id)として表示される。
「二段階の間の窓」なのは三つだけ
Unknownは二段階の間に開いた窓であるの言う窓 — 静的段階と動的段階の間に開いた、静的近似が確定できなかった箇所 — に該当するのは、実はUpstreamRef / UpstreamBareRef / PostCreateReadIncompleteの三つだけである。
残りは窓ではない。for束縛と関数のプレースホルダは、同じ静的段階の中で生まれて消える。「まだ評価していないから今は無い」だけであって、段階を跨いでいない。未知値の周りは無検査ではないが「for変数由来のUnknownなどは型が構文から決まるので窓の外である」と書いているのと同じ線が、値の側にも引ける。
同じValue::Unknownという表現に乗ってはいるが、乗っているものの素性が違う。「未確定」を表す表現が四つあるのは漸進的型付けの境界が引けていない徴候であるが「未確定を表す表現が四つある」ことを漸進的型付けの境界が引けていない徴候として挙げたが、その四つのうち「Unknown値」と数えられた一つの内部が、さらにこの線で割れている。
EmptyInterpolationは誰も埋めない
四群目は一つだけで、性質が他と違う。${}と書きかけて中身が空の状態、つまり編集途中の構文である。誰かが後で埋める値ではなく、コードがまだ書き終わっていないことの印である。
これがUnknownとして表現されているのは、LSPが${}の位置に診断を出すためと、下流のリゾルバが落ちずに済むためである。エラーとして即座に弾かず、値として持ち回って「不明」に合流させておくと、編集中の壊れたファイルでも残りの解析が進む。エディタ体験のための設計判断であって、未確定値の意味論の話ではない。
PostCreateReadIncompleteだけ時間の向きが逆
段階を跨ぐ群の三つ目は、他の二つと生まれる時点が違う。UpstreamRefはapplyの前に「この値は後で決まる」として生まれるが、PostCreateReadIncompleteはcreateが終わった後に生まれる。
由来は具体的で、CloudControlのcreate handlerが内部で複数のAWS APIを順に呼ぶ構造にある。実体を作り、activeになるまで待ち、最後にpost-create validationのreadを行う。この最終readが権限不足などで失敗すると、handlerはFailedを返すが、AWS側にリソースの実体は既に存在し続けている。実際に報告された例では、2m 47.2s待った末にAccessDeniedで失敗しているが、describe-load-balancersを叩けばState.Code = activeのLBが見つかる。
ここで属性値を「無い」ことにすると、次のplanは同じリソースをもう一度createしようとする。「作られたが読めなかった」を表現する語彙としてUnknownが再利用されている。identityは「何を同一資源と見なすか」の規則であり、stateとは独立軸であるが言う「エンジンが同じリソースとみなすもの」を保つための使い方であり、段階分離の帰結として開いた窓ではなく、部分的な失敗を記録するための窓である。
したがって「Unknownは二段階の間に開いた窓」という説明は、この変種にはそのまま当てはまらない。同じ表現が、段階分離とは別の問題にも使われている。
表現が一つであることの効き目
理由がこれだけ異種混交でありながら、Value::Unknownという一つの表現に乗っていることには意味がある。等価性の不成立がValueのPartialEqに手書きで実装されており、Unknownはどの理由であっても何とも等しくならない。#[serde(skip)]が付いているので、どの理由であってもstateには書かれない。
つまり検査は素通し、等価性は不成立 — Unknownの一貫した保守性の「検査は素通し、等価性は不成立」という規律は、理由の別に関わらず一律に効く。理由は診断とplan表示のためだけに持ち回られ、意味論は理由を見ない。10個に割れているのは人間に見せる側の都合で、差分計算から見れば全部同じ「不明」である。
分類が意味論に漏れていないことが、この設計の効き目である。理由ごとに等価性やstate書き出しの扱いが変わる設計だったら、「未確定」を表す表現が四つあるのは漸進的型付けの境界が引けていない徴候であるの指摘する境界の見えにくさはもっと深刻になっていた。
根拠の確度
UnknownReasonが10変種であること、substitute_placeholderがfor束縛の4つと関数の2つを静的段階で埋め、上流参照とEmptyInterpolationとPostCreateReadIncompleteを素通しすることは、2026-08-16時点のソースで確認済み(carina-core/src/resource/mod.rs、carina-core/src/parser/ast.rs)。#[serde(skip)]と手書きPartialEqのnever-equal不変条件も同ファイルで確認(RFC #2371)。plan表示文言はcarina-core/src/value.rsのrender_unknown。
PostCreateReadIncompleteの由来とELBv2の事例はnotes/specs/2026-06-15-partial-create-outcome-design.md(awscc#369)による。この仕様は「設計提案」ステータスであり、実装状況は未確認。変種自体はenumに存在するが、記載どおりの経路で運用されているかは追っていない。
「理由の集合は解決経路の分類である」という読み、および三群+誤りへの割り方は解釈である。コード側にこの4分類が明示されているわけではない(コメントはfor展開 / 関数 / それ以外の3分類までを書いている)。
Unknownは二段階の間に開いた窓であるが「7種の理由」と書いているのは執筆時点の変種数で、現在は10になっている。