# lang-review r5 — 2026-08-07

commit: `76fb4dc4` / compiler: `console-exception-rowvar-2026-08-06_5f4c762a` 世代の
stage2 (r5 のソースから自己ビルドしたもの)

r4 で挙げた所見を消化したラウンド。**観測モードは r3/r4 と同じ
maintenance-session** (タスクからのゼロ書きではなく、実在の修正作業を通した観測)
だが、r4 で固定コーパス化した `repair/` があるので **`repair_convergence` と
`diagnostics` は実測値で動いた** — 主観の再評価ではない。

再測定した軸: `repair_convergence` / `type_soundness` / `diagnostics` /
`effect_system`。残り 4 軸は `measured: false` で持ち越し。

## このラウンドで動いた issue

| # | 内容 | 状態 | 効いた軸 |
|---|---|---|---|
| #1513 | host builtin の arity / 引数型が検査されない (silent miscompile) | **closed** | type_soundness, repair_convergence |
| #1514 | 診断が構文の先頭を指す / 位置が一切付かない | **open** — B (構文先頭) と C (位置なし) を塞いだが、**A の残り 1 件 = repair ケース 08 (`handle` 適格性) が残置**。下の N1 末尾 | diagnostics, repair_convergence |
| #1515 | ローカル束縛経由の呼び出しで effect row が要求されない | **closed** | effect_system |
| #1451 | effect 付き関数を返す / struct フィールドに入れると弾かれる | **closed** | effect_system, writability |
| #1445 | Show の無い型の補間が黙って通る | **closed** | type_soundness |
| #1502 / #1507 | alias 経由の enum constructor 修飾、`vibe fmt` が `match Color::Red {` を壊す | **closed** | (writability) |
| #1520 | レジストリの宣言に検証が無い (下の N2 で新規に立てた) | **open** | — |

## repair_convergence: 3.6 → 4.8 (実測)

同じ 10 ケース、同じ採点規則 (L 位置 0/1 + A 実行可能性 0/1 + C 収束 0/2、
`score = 1 + mean`)。個別の根拠は `eval/lang-review/repair/README.md` の
append-only な更新節。

| 指標 | r4 | r5 |
|---|---|---|
| L (位置) | 3/10 | **9/10** |
| A (実行可能性) | 8/10 | **10/10** |
| C (収束) | 15/20 | **19/20** |
| 合計 | 26/40 | **38/40** |

**コーパスから silent ケースが無くなった。** 09/10 は「診断が出ないこと」自体を
測るために置いた2件で、どちらも #1513 で塞がった。残る減点は 08 (`handle`
適格性、L=0 C=1) の1件だけ。

r4 の3つの要点に対する r5 の答え:

1. 「診断は出さえすれば良い」→ **そのとおりだった。** 出るようにした2件
   (09/10) は、文言を新規に設計する必要が無く、既存の arity / 引数型
   メッセージがそのまま最良の段 (`line:col-col` スパン + 必要な編集) で出た
2. 「弱点は位置情報」→ **配線漏れだった** (N1 参照)
3. 「本当の穴は診断が無いケース」→ 塞いだ。ただし**コーパスに載っていない
   silent 種別は依然として見えない**。これは固定コーパス方式の構造的限界で、
   新種を見つけたらケースを追加していく (既存の変更は不可)

## N1. 位置情報の欠落は設計上の困難ではなく、既にある側チャネルの配線漏れだった

r4 は「同じコンパイラの中でベストとワーストが3段階に分かれているので、これは
設計の限界ではなく実装の未整備」と書いた。#1514 を実装して**その見立てが
正しかったこと**が確認できた。より強い形で言える:

- **パーサ**には ` at #<token index>` という側チャネルが既にあり、
  `parse_program_located` が line:col に解決して文言からは剥がしていた。
  01/02/04 の3つの throw が**それを付け忘れていただけ**
- **checker** には `[@off=N]` マーカーと `locate_type_error` が既にあった。
  05 (effect row 不一致) は walker の `ECall(callee, args, _)` arm が
  **その場で offset を捨てていた**うえ、compile 経路が
  `locate_type_error` を呼んでいなかった

つまり位置を出す機構は両側とも完成していて、**個々の診断がそれを使うかどうかが
規律に委ねられていた**。r4 で「3段階に分かれている」と見えたのは、規律が
守られた診断・半分守られた診断・守られていない診断の3層だった。

**含意**: 位置の質は「頑張って上げる」ものではなく、**新しい診断を足すときに
側チャネルを付け忘れないための仕組み**の問題。今は付け忘れても何も言われない。

**残る本当に難しい1件** = 08。codegen (`inline_direct_perform.vibe`) が投げる
ADR-0076 のエラーで、位置の出どころが `EHandle(body, arms)`。この AST ノードは
**offset フィールドを持たない**ので、付けるには EHandle を構築/分解している
全箇所に触る必要がある。#1514 に残置。

## N2 (major, 新規). 読まれていないレジストリ宣言は、正しさを一度も問われていない

#1513 を「手書きの表 → 中央レジストリへのフォールバック」で塞いだところ、
**過剰 reject が2件出た**。原因は別々で、直した場所も別だった:

| 行 | 症状 | 原因 | 直した場所 |
|---|---|---|---|
| `eq` | `eq(Color::Red, c)` が reject (`docs/language-tour/basics.vibe.md` が落ちた) | **宣言が誤り** — `reg_p2(CtInt, CtInt)` と書かれていたが構造的等価の builtin で多相 | **レジストリの行**を `CtUnknown` に訂正 |
| `Char::to_int` | prelude の `char.vibe` が落ちた | **consumer の正規化漏れ** — 宣言 `reg_p1(CtChar)` は正しい。`Char` と `Int` は `unify` が自由に行き来する (`char_int_compatible`) のに、head-kind へ落とす比較がその同一視を失っていた | **checker 側** (`head_fold_char`)。行は**そのまま** |

**この区別は #1519 の diff がそのまま証拠になっている** — `eq` の行は書き換えたが、
`Char::to_int` の行 (`builtin_registry.vibe:333`) には**一切触れていない**。
`CtChar -> CtInt` は API として正しく、むしろ `CtInt -> CtInt` より情報がある。

したがって N2 の主張は**片方 (`eq`) だけ**に基づく。それでも major としたのは:

- レジストリのパラメータ**型**は、今回 checker が読み始めるまで**一度も検査
  されたことがなかった** (`verify_lane_builtins` はパラメータ**数**と返り値の
  分類しか見ない)。`eq` はその状態で寝ていた嘘の実例で、しかも**隣り合う兄弟行
  `assert_eq` が `CtUnknown` と正しく書いてあった**
- 気づけたのは doctest がたまたま `eq(Color::Red, c)` を含んでいたからで、**偶然**

これは #1505 (`Http::request` の arity が checker 側だけ 2 引数) と同じ
`docs/spec/builtin-ssot-design.md` のテーマだが、**向きが違う**:

- #1505 = 同じ builtin の宣言が**2箇所にあって食い違う** → 一本化で解ける
- N2 = 宣言は**1箇所にしかないが、誰も読んでいないので嘘でも気づけない**

SSOT 化を進めるほど後者が効いてくる。→ **issue 化した (#1520)**。

**`Char::to_int` 側の教訓は別で、N1 に近い**: 新しい consumer が現れると、
既存の宣言ではなく**consumer が言語の同一視規則を再現できているか**が問われる。
`unify` が持っている規則 (`char_int_compatible`) を、head-kind という
**情報を落とす比較**に写すときに落としたのが実体。#1520 をこの方向 —
「宣言を直す」— に誘導しないよう、issue 側にも同じ注記を入れた。

## N3 (minor). `[@off=N]` マーカーの規律が型で守られていない

#1514 C で compile 経路に `emit_compile_diag_located` を足したが、これは
**マーカーを持つメッセージだけ**に `locate_type_error` を適用する。マーカーが
無いメッセージに適用すると first-occurrence ヒューリスティックに落ちて、
もっともらしい別の行を指しうるため。

つまり「自分がどこから来たかを述べている診断だけが位置を得る」という規律だが、
これは文字列の中の綴り (` [@off=`) で表現されていて、型で守られていない。
マーカーを付けたのに decode されない経路があれば **raw leak する**
(実際に #1445 の実装中に `[@off=135]` がユーザー向けメッセージに漏れた)。
N1 の「付け忘れても何も言われない」と同根。

## 軸ごとの判断

- **type_soundness 3.0 → 4.0**: r4 で 4.0 → 3.0 に下げた理由は #1513 の
  silent miscompile ただ1件で、それが閉じ、二方向ラチェットで縛られた。
  #1445 (Show の無い型の補間) も同種の穴で閉じた。**新しい ill-typed
  スイープは走らせていない** — この 4.0 は「r4 で下げた理由の消滅」であって、
  r1–r3 の 4.0 と同じ根拠 (探索して unsound が見つからなかった) ではない
- **diagnostics 3.0 → 3.5**: 位置の実測が L 3/10 → 9/10。ただし
  rubric 6 は位置以外に「parse error recovery で複数診断が出るか」も含み、
  そちらは今回測っていないので 4.0 には上げない
- **effect_system 3.0 → 3.5**: #1515 が effect が宣言から漏れる経路
  (ローカル束縛経由の呼び出し) を塞ぎ、#1451 が effect 付きクロージャを
  返す/struct に入れる形の過剰 reject を外した。健全性・書きやすさの両方向で
  動いたので +0.5。#1511 (`handle` 適格性が型検査を通り抜ける) が残るので
  4.0 にはしない
- **repair_convergence 3.6 → 4.8**: 上記
- **syntax_clarity / writability / semantics_consistency /
  concurrency_readiness**: 今回測っていない (`measured: false`)。#1451 は
  writability に効いているはずだが、tasks を再走させていないので動かさない

7軸平均 3.00 → **3.29**、8軸平均 3.08 → **3.48**。

## 次ラウンドへの申し送り

- **測っていない4軸が3ラウンド連続で持ち越し**になっている。特に
  `semantics_consistency` (2.5) は全軸で最低なのに r2 以降実測がない。
  次は tasks からのゼロ書き経路 (writability レビュアー) か意味論プローブを
  実際に走らせるラウンドにすべき
- repair コーパスは**残り 08 の1件**まで来た。ここから先はコーパスを
  増やさないとスコアが動かない = 新しい silent 種別・位置なし種別を
  見つける作業そのものが次の仕事
- N1/N3 が示すのは「診断の質を上げる」より「**質が落ちたことに気づく仕組み**」
  が足りないということ。`run_repair.sh` の二方向ラチェットは今のところ
  10 ケースしか縛っていない
