# lang-review r4 — 2026-08-06

commit: `24aeccb9` / compiler: `console-exception-rowvar-2026-08-06_dd5d4e10` stage2

このラウンドの目的は **8軸目 `repair_convergence` の追加と初回実測**。
「新しい言語なので初回コンパイルが通らないのは仕方ない。問題は、型検査/構文検査の
結果を見てどれだけ正確に修正できるか」という観測軸を rubric に足した。

r1–r3 の 7 軸は今回再測定していない (下の N1 で新証拠が出た `type_soundness`
を除く)。スコアファイルの `measured: false` がそれを示す。

## なぜ 8 軸目が要るのか

r3 で「writability レビュアーの観測モードには構造的な盲点がある」と書いた。
r4 はその盲点を**測れる形**にしたもの。既存 7 軸はどれも「書けたか / 正しいか」を
見ていて、**診断が出ないこと**を直接減点できる軸が無かった:

- `diagnostics` (6) は出たメッセージの質を見る軸なので、出なければ評価対象外
- `type_soundness` (4) は「通ってはいけないものが通る」を見るが、r1–r3 の
  探索は ill-typed **ユーザーコード**が中心で、builtin 呼び出しは範囲外だった

固定コーパス (`repair/`) にして二方向ラチェット (`run_repair.sh`) で縛ったので、
以降は診断の劣化も改善も CI が検出する。

## 実測結果 — repair_convergence = 3.6

10 ケース、0–4 点、`score = 1 + mean`。個別の採点根拠は
`eval/lang-review/repair/README.md`。要点は3つ:

**(1) 診断は、出さえすれば非常に良い。** 8/10 のケースで A=1 (必要な編集を
名指ししている)。#1429/#1461 の退役エラーは書くべき綴りと `vibe fmt` で直ることを
両方言うし、effect row 漏れは `hint: declare 'fn helper(...) -> T with
Stdout::write_stream'` と宣言文そのものを出す。**言語の弱点はメッセージの文面
ではない。**

**(2) 弱点は位置情報。** 10 中 7 は L=0。パターンは2つ:

- 構文の先頭を指す — `Error` が 6行24列にあるのに `line 6:1` (= `fn` の位置)。
  行は合うので追える
- **位置が一切付かない** — effect row 不一致 (05) と `handle` 適格性 (08) は
  `line:col` がそもそも出力されない

一方 06/07 (関数 arity・引数型) は `line 4:11-14` のようにスパンで該当の
呼び出しを囲む。**同じコンパイラの中でベストとワーストが3段階に分かれている**
ので、これは設計の限界ではなく実装の未整備。

**(3) 本当の穴は「診断が無い」ケース。** 09/10 は 0 点。詳細は N1。

## N1. capability builtin の呼び出しが arity も引数型も検査されない (#1513)

```vibe
fn main with Stdout {
  Stdout::write_stream(42)     // Int を String の位置に渡す
}
```

**compile が通り、実行も成功し、garbage を出力する。** 診断は一切出ない。
rubric 4 の定義そのままの silent miscompile。

```vibe
Stdout::write_stream()         // 0 引数
```

こちらは compile が通ったうえで**不正な wasm を吐く**。host が load 時に
`expected 1 elements on the stack for fallthru` で拒否する — ソース位置なし、
vibe の語彙ですらない。

範囲の実測 (未検査 / 検査あり):

| builtin | arity 検査 |
|---|---|
| `Stdout::write_char` / `Stdout::write_stream` | **なし** |
| `Env::get` / `Env::args_len` / `Env::args_get` | **なし** |
| `Stdin::read_char` / `Stdin::read_stream` | **なし** |
| `Fs::read_file` | **なし** |
| `Profiler::now_us` | あり |
| `Array::length` / `String::concat` | あり (引数型も) |

**capability かどうかでは分かれない** — `Profiler::now_us` は capability
(row `Some("Profiler")`) だが検査される。最初にこの仮説を立てて外した。

原因は checker の fast path。`direct_call_return` は #626 の seed ヒープ制約の
ため `lookup_builtin` を呼ばずに返り値型だけを返す。arity はそこで一切見ない。
引数型は `builtin_first_arg_head` / `builtin_arg_head_rest` という**手書きの表**に
載っているものだけが検査され、この2つの表は Array / String / Bytes /
FixedArray しかカバーしていない。だから「検査される builtin の集合」は
表の保守状況であって、言語の規則ではない。

M5 (#1505、`Http::request` の arity が checker 側だけ 2 だった) と同じ根で、
**builtin の宣言が checker の中で複数の経路に分かれている**構造が再発源。
`docs/spec/builtin-ssot-design.md` のテーマ。

判定: `type_soundness` を 4.0 → 3.0 に下げる。r1–r3 を通じて 4.0 だったが、
それは builtin 呼び出しを探索していなかったから。

## C1. r3 の `handle` 適格性の表は誤りだった (訂正)

r3 は #1511 の境界を「**handle の位置 × handled body の形**の2軸」と書いた。
r4 で同じ形を現行 stage2 に食わせたところ**全て通る**:

| 形 | r3 の記録 | r4 の実測 |
|---|---|---|
| top-level `let` + `handle { ask_once() }` | ok | ok |
| top-level `let` + `handle { add(1, ask_once()) }` (`add` は top-level `fn`) | **NG** | **ok** |
| `fn` の中 + `handle { add(1, ask_once()) }` | ok | ok |

正しい境界は **handled body が呼ぶ callee の種類**だった:

| handled body | 結果 |
|---|---|
| `handle { bump(ask_once()) }` — `bump` が top-level `fn` | ok |
| `handle { bump(ask_once()) }` — `bump` が**ローカルのクロージャ** (`let bump = (x: Int) -> Int { .. }`) | **NG** |

handle の位置 (top-level `let` / `fn` の中) は**無関係**。

そしてエラー文は最初からこれを言っていた — 「direct performs, calls to named
top-level functions, or row-annotated closure literals」。r3 でこれを読み違えた。
r3 は同時に「エラー文が内部用語で行動可能でない」と書いたが、r4 の採点では
A=1 (適格な形を列挙しているので何に直すかは分かる) とした。C=1 に留まるのは
**body 内のどの呼び出しが問題かを言わない**ため — 呼び出しが複数ある body では
二分探索が要る。これが #1511 に足すべき具体的な改善要求。

訂正の経緯自体が r3 の所見の裏付けになっている: r3 の誤りは `vibe check` を
使って探索したことから来ていて、この失敗モードは 8 軸目のコーパス 09/10 が
測っているものと同じ (通ったことを正しさの証拠として扱う) 。

## スコア

| dimension | r2 | r3 | r4 | |
|---|---|---|---|---|
| syntax_clarity | 3.5 | 3.0 | 3.0 | 未再測 |
| writability | 4.0 | 3.5 | 3.5 | 未再測 |
| semantics_consistency | 2.5 | 2.5 | 2.5 | 未再測 |
| type_soundness | 4.0 | 4.0 | **3.0** | N1 |
| effect_system | 3.5 | 3.0 | 3.0 | 未再測 |
| diagnostics | 3.0 | 3.0 | 3.0 | 未再測 |
| concurrency_readiness | 3.0 | 3.0 | 3.0 | 未再測 |
| **repair_convergence** | — | — | **3.6** | 新規 |

7軸平均 3.14 → 3.00。8軸平均 3.08。

`repair_convergence` が他軸より高いのは意外に見えるが、実測の通り: 診断が
出たときの文面の質が高いので押し上がる。次ラウンドで下がるとしたら、silent
ケースをコーパスに足したときではなく (それは構成の変更で反則)、既存ケースの
位置情報が改善されないまま新しい silent 種別が見つかったときになる。

## 次にやると効くこと (優先順)

1. **N1 を塞ぐ** — fast path に arity 検査を足す。返り値型の表があるのだから
   arity だけなら追加コストは小さいはず。silent miscompile が消えるので
   `type_soundness` と `repair_convergence` の両方が上がる
2. **位置情報を付ける** — 05 (effect row 不一致) と 08 (`handle` 適格性) に
   `line:col`。この2つは既に文面が良いので、位置が付くだけで L=1 になる
3. **08 に「どの呼び出しか」を足す** — C=1 → 2
