RFC-034: 統一デバッグツールチェーン
概要
YaoXiang に統一されたデバッグツールチェーンを導入する。中心的な設計は一つのソース、三个の소비:コンパイルフロントエンドはソースコードの位置、変数名、型情報を一流市民として YaoXiang IR に埋め込み、インタープリタ、JIT、LLVM の3つのバックエンドが同じメタデータを消費する。ユーザーは yaoxiang run --debug で DAP(Debug Adapter Protocol)サーバーを起動し、VS Code は stdio 経由で接続し、ブレークポイント、ステップ実行、変数検査、コールスタック、式評価、並行デバッグの統一体験を得る——基盤がどの実行エンジンであっても。
動機
なぜこの機能が必要か?
現在 YaoXiang プログラムのバグを排查する手段は極限に原始的:
io.println("DEBUG: x = " + x.to_string())
io.println("DEBUG: entered branch A")3つの致命的な問題:
- コンパイラ開発の自ブートが阻止:YaoXiang で YaoXiang コンパイラを書くが、コンパイラを書く人は自分の書いたコードをデバッグできない。自ブート段階で対話式デバッグ手段がないのは袋小路だ。
- 3つのエンジン、ゼロデバッグ:インタープリタ、JIT、LLVM はそれぞれ勝手に走り、問題が発生するとユーザーは stdout に
ALL TESTS PASSEDが表示されるかどうかに頼るしかない。アサーション失敗?何行目か分からない、変数値も分からない。 - 並行はブラックボックス:
spawnが複数のタスクを作成したが、どのタスクが落ちた?変数は誰に move された?全部推測で当たるのを待つしかない。
設計目標
- 統一体験:インタープリタが通るブレークポイントは JIT 也能通过、LLVM も一貫したソースマップ。用户は基盤エンジンの違いを意識しない。
- 一つのソース:デバッグメタデータは IR と共に流れ、繰り返し定義せず、2セットのマッピングを維持しない。
- ゼロ侵襲:
yaoxiang run --debugという一つのパラメータで、このパラメータを追加しない場合はコンパイルと実行の動作が完全に変わらない。 - DAP 標準:直接 VS Code エコシステムに接続し、エディタプロトコルを再発明しない。
提案
中心設計
アーキテクチャ概要:
┌──────────────────────────────────────────────────────┐
│ VS Code / エディタ │
│ DAP Client (launch.json) │
└────────────────────────┬─────────────────────────────┘
│ stdio
┌────────────────────────▼─────────────────────────────┐
│ DAP サーバー (yx-core) │
│ ┌─────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ セッション管理 │ │ ブレークポイント管理 │ │ 式評価エンジン │ │
│ └─────────┘ └──────────┘ └───────────────────┘ │
└────────────────────────┬─────────────────────────────┘
│ クエリ/制御
┌────────────────────────▼─────────────────────────────┐
│ ランタイムデバッグインターフェース (トレイト) │
│ pause / resume / step / get_frames / eval / ... │
└────┬──────────────────┬──────────────────┬──────────┘
│ │ │
┌────▼────┐ ┌───────▼───────┐ ┌─────▼──────┐
│ インタープリタ │ │ JIT (RFC-028)│ │ LLVM AOT │
│ 直接消費 │ │ 軽量 │ │ IRメタデータ │
│ IRメタデータ │ │ デバッグ表 │ │ → DWARF │
└─────────┘ └───────────────┘ └────────────┘主要な設計上の決定事項:
- DAP サーバーとランタイムはトレイトで分離。サーバーは基盤がインタープリタか JIT化を関知しない——
DebugEngineトレイトを通じて命令を発信するだけ。各エンジンが同じトレイトを独立して実装する。 yaoxiang run --debugはインタープリタの使用を強制。デバッグは制御可能性を必要とし、パフォーマンスを必要としない。LLVM モードでは DWARF を生成して事後トレース用途(core dump / crash report)に使用し、対話式デバッグは行わない。yaoxiang runから入口検出ロジックを再利用。新しいサブコマンドを導入せず、メンタルモデルは「デバッグモードでプログラムを実行する」。
IR デバッグメタデータ
既存の YaoXiang IR にメタデータを添付し、新しい IR 種類を追加しない。すべてのメタデータはコンパイルフロントエンドの一箇所で生成し、バックエンド消費は読み取り専用:
| メタデータ | 添付点 | 説明 |
|---|---|---|
SourceLocation | 各 IR ノード | ソースファイル:行番号:列番号 |
VarName | 変数宣言/バインディングノード | ソースコード内の変数名 |
TypeAnnotation | 変数/式ノード | 推論された型(コンパイル時述語を含む) |
ScopeBoundary | ブロック/関数入口出口 | 変数のスコープのライフサイクル |
SpanInfo | spawn ノード | spawn ブロック内のタスク境界 |
起動フロー
yaoxiang run --debug file.yx
│
├── Phase 1: コンパイル(デバッグメタデータ付き)
│ ├── パース → AST
│ ├── 型チェック + コンパイル時述語検証
│ └── IR への降級(デバッグメタデータを添付)
│
├── Phase 2: インタープリタエンジンを使用
│ └── --debug モードでは --release かどうかに関係なく、インタープリタを使用
│
├── Phase 3: DAP サーバーの起動
│ ├── stdio トランスポートチャネルの初期化
│ ├── VS Code の attach を待機
│ ├── attach 成功後、プログラム入口で一時停止
│ └── 対話デバッグループに移行
│
└── Phase 4: プログラム終了 / デバッグセッション終了 → 終了各モードの差異
| モード | デバッグ方式 |
|---|---|
yaoxiang run --debug | インタープリタ強制、全機能 DAP 対話デバッグ |
yaoxiang run --release | DWARF を生成して事後トレース用途(core dump / crash report)に使用、DAP は起動しない |
yaoxiang run(通常) | デバッグメタデータなし、デバッグサポートなし |
詳細設計
1. ブレークポイント
ブレークポイントの種類:
├── ソース行ブレークポイント → コンパイルフロントエンドが位置メタデータを生成、バックエンドがクエリしてマッチング
├── 関数入口ブレークポイント → 関数呼び出し時にトリガー(第二フェーズ)
├── 条件ブレークポイント → 式評価が true の時にトリガー
└── データブレークポイント → 変数が変更された時にトリガー(第二フェーズ)ソース行ブレークポイントのコアロジック:
VS Code が送信: "file.yx:42 にブレークポイントを設定"
│
DAP サーバー:
├── IR 中のすべての SourceLocation == (file.yx, 42) のノードをクエリ
├── ランタイムに転送: "これらの IR アドレスで一時停止"
└── ランタイムが返す: ブレークポイント ID
プログラムが IR ノードを実行 → ランタイムがチェック: このノードはブレークポイントリストにあるか?
├── 通常ブレークポイント → 一時停止、DAP サーバーに通知
└── 条件ブレークポイント → 条件式を評価 → true のみ一時停止3つのエンジンの実装:
| インタープリタ | JIT | LLVM | |
|---|---|---|---|
| ブレークポイント挿入方式 | 実行ループで IR ノード ID をチェック | JIT がマシンコードに int3 を挿入 | LLVM DWARF + ハードウェアブレークポイントを利用 |
| 条件ブレークポイント評価 | 式を直接解釈 | 条件を評価するために一時的に JIT コンパイル | DWARF 式スタック + 評価 |
| パフォーマンスオーバーヘッド | 各 IR ノードでテーブル参照が1回追加 | ブレークポイント箇所のみオーバーヘッド | ほぼゼロオーバーヘッド(ハードウェアブレークポイント) |
2. ステップ実行
Step Over → 現在行を実行、関数呼び出し内部をスキップして次の行で停止
Step Into → 現在行の関数呼び出し内部に入る
Step Out → 現在関数のリターンまで実行
Continue → 次のブレークポイントまたはプログラム終了まで実行を再開実装ロジック:ステップ操作の本質は一時的なブレークポイント。ユーザー明示的なブレークポイントと同じメカニズムを共有し、2つのシステムではなく、1つのシステムの2つの使用方法。
Step Over:
現在のソース行番号 = query_line(frame)
→ 次の行に一時ブレークポイントを設定
→ 現在行が関数呼び出しの場合: 呼び出し点以降に一時ブレークポイントを設定
→ Continue → 一時ブレークポイントにヒット → 削除 → 一時停止
Step Into:
現在呼び出し対象の最初の行の実行可能位置
→ 関数本体最初の IR ノードのソース位置を探す
→ 一時ブレークポイントを設定 → Continue → ヒット → 一時停止
Step Out:
現在のスタックフレームのリターンアドレス
→ caller の次の行を探す
→ 一時ブレークポイントを設定 → Continue → ヒット → 一時停止一時ブレークポイントの4つのエッジケース処理:
- 並行帰属:一時ブレークポイントは現在のタスク ID にバインドされ、他のタスクがヒットした場合は直接無視。
- spawn ブロックの Step Over:spawn 外部で Step Over すると、spawn ブロック全体を走って後にスキップする。spawn 内部に入るデバッグは Step Into を使用すべき。
- 一時ブレークポイント未ヒット:ウォッチドッグタイムアウトを設定(30秒間ブレークポイント未ヒット)→ 強制一時停止 → VS Code に通知。同時にプログラム終了イベントを監視 → 即座にクリーンアップ。
- 同一行に複数の IR ノード:Step Over の一時ブレークポイントは
ignore_current_lineをマークし、ヒット後にソース行番号が現在行番号と等しい場合は無視して続行。
3. 変数検査とスコープ
VS Code がリクエスト: "現在のフレームの変数リスト"
│
DAP サーバー:
├── 現在一時停止点の IR ノードをクエリ
├── 現在の ScopeBoundary 内の変数バインディングを走査
│ └── 各バインディングから返す: (名前, 型, ランタイム値参照)
└── VariablesResponse を組み立て → VS Codeスコープの階層化:
┌─ Globals ───────────────────────────┐
│ モジュールレベルバインディング: 定数、型エイリアス、全域 │
├─ Locals ────────────────────────────┤
│ 現在の関数内で可視のローカル変数 │
│ ├── 引数 (関数パラメータ) │
│ └── ローカルバインディング (let / 代入) │
├─ Captured ──────────────────────────┤
│ spawn ブロック / クロージャがキャプチャした外部変数 │
│ 所有権状態を表示: move 済み / 参照共有 │
└─────────────────────────────────────┘エンジンの差異:
| エンジン | 変数値の取得 |
|---|---|
| インタープリタ | VM スタックフレームとヒープを直接読む。各値はメモリ内で明確に表現されている |
| JIT | レジスタとスタック上の値 → JIT コンパイル時に「変数 → レジスタ/スタックスロット」マップテーブルを記録する必要がある |
| LLVM | DWARF の .debug_info セクション → DW_AT_location → LLDB がネイティブサポート |
特殊型の表示:コンパイル時述語が洗練された型表示はデバッグに価値のある情報を提供する:
x: Positive(x) → 表示 "Int (x > 0 = True)"
y: Sorted(y) → 表示 "Array(Int) (ソート保証)"
result: T → ランタイムでの具体的な型を表示4. コールスタック
DAP リクエスト: StackTrace
│
返す:
┌──────────────────────────────────┐
│ #0 process_item() file.yx:42 │ ← 現在の一時停止点
│ locals: item = "hello" │
│ spawn タスク ID: task-3 │
├──────────────────────────────────┤
│ #1 main() file.yx:67 │ ← caller
│ locals: data = ["hello", ...]│
├──────────────────────────────────┤
│ #2 <entry> file.yx:1 │ ← ルート
└──────────────────────────────────┘各フレームは記録する:関数シグネチャ、呼び出し位置(ソースファイル+行番号)、ローカル変数(遅延評価)、spawn コンテキスト(タスク ID)。
インタープリタと JIT はそれぞれフレームリンクリストを維持する。フレームの取得はゼロコストではない——しかしデバッグモードはゼロオーバーヘッドを追求しない。
5. 式評価(Watch / REPL)
ユーザーはブレークポイント位置で任意の YaoXiang 式を入力:
Watch: x + y → 計算結果を返す
Watch: items[2].name → 複雑な構造にアクセス
Watch: f(x) → 関数を呼び出す(副作用リスクあり)評価戦略:
ユーザー入力式
│
├── コンパイラフロントエンドが式をパース
├── 現在のフレームコンテキストで型チェック
├── 変数値は現在のフレームから取得(読み取り専用参照)
├── 式を独立したマイクロプログラムとして実行
│ └── 外部変数の変更は不允许
│ └── spawn は不允许
│ └── IO は不允许(またはオプションで許可)
└── 結果値を返す → 元のフレーム状態は完全に不変インタープリタは天然サンドボックス:式評価は新しいサンドボックスを作成するのではなく——インタープリタ本身就是サンドボックス。式評価只是一時的にフレームを push し、使用后就销毁。正常実行と同じ VM を共有するが、いかなる副作用もコミットしない。
関数呼び出し評価:デフォルトでは許可するが、ユーザーに「この式には副作用がある可能性があります」と警告し、ユーザーの確認后才能実行。
エンジンの差異:
| エンジン | 式評価 |
|---|---|
| インタープリタ | 既存の eval コードパスを再利用し、現在のフレーム環境を注入 |
| JIT | 式を一時的にコンパイル → 現在のフレームにリンク → 実行 → 臨時代碼を破棄 |
| LLVM | サポート外——LLVM モードでは対話デバッグを行わない |
6. 並行デバッグ
タスクモデルの可視性:
DAP の threads コンセプトは YaoXiang の spawn タスクにマッピングされる。各タスクは独自のスタックフレームリンクリストと実行状態を持つ。
┌─ Threads ───────────────────────────┐
│ ● task-1 main() file.yx:10 │ ← 現在フォーカス中
│ ▶ task-2 fetch() file.yx:34 │ ← 実行中
│ ⏸ task-3 process() file.yx:56 │ ← ブレークポイントで一時停止
│ ◼ task-4 write() 終了済み │
└─────────────────────────────────────┘並行コンテキストでのブレークポイント:
| 一時停止モード | 動作 | 適用シナリオ |
|---|---|---|
stop-all(デフォルト) | あるタスクがヒット → 全タスクを一時停止 | データ競合、全域状態をデバッグ |
stop-this-only | ヒットしたタスクのみ一時停止、他は続行 | 独立タスクロジックをデバッグ |
spawn ブロックのステップセマンティクス:
spawn { // Step Over → spawn ブロック全体を実行
task_a() // Step Into → task_a に入る
task_b() // 並列実行、単独 step の影響を受けない
}7. DAP プロトコルマッピング
第一フェーズ:コアリクエスト
| DAP リクエスト | YaoXiang の意味 |
|---|---|
initialize | 能力ネゴシエーション:ブレークポイント、ステップ実行、変数、スタックフレームをサポート |
launch / attach | YaoXiang プログラムの起動/附加(--debug は attach モード) |
setBreakpoints | ソース行ブレークポイントを設定 |
configurationDone | ブレークポイント準備完了、実行開始 |
threads | すべてのアクティブな spawn タスクリストを返す |
stackTrace | 指定タスクのスタックフレームリストを返す |
scopes | 現在のフレームの変数スコープを返す |
variables | 指定スコープの変数リストを返す |
continue | 実行を再開 |
next | Step Over |
stepIn | Step Into |
stepOut | Step Out |
pause | 全タスクを中断 |
evaluate | 現在のフレームで式を評価 |
disconnect | デバッグセッションを終了 |
第二フェーズ:拡張リクエスト
| DAP リクエスト | YaoXiang の意味 |
|---|---|
setFunctionBreakpoints | 関数名ブレークポイント |
setExceptionBreakpoints | エラー/panic 時に一時停止 |
dataBreakpointInfo | データブレークポイント(変数変更トリガー) |
実装戦略
フェーズゼロ:インフラ(前置于所有フェーズ)
目標:コンパイルフロントエンドがデバッグメタデータを IR に添付。
| コンポーネント | 変更 |
|---|---|
| IR 定義 | 新しい SourceLocation、VarName、TypeAnnotation などのメタデータフィールドを追加 |
| Parser | 各 AST ノードにソース位置を記録 |
| TypeChecker | 型情報を IR ノードに添付 |
| テスト | IR dump が位置と変数情報を含むことを検証 |
ランタイムは涉及しない。
第一フェーズ:インタープリタ DAP MVP
目標:yaoxiang run --debug file.yx でブレークポイント設定、ステップ実行、変数表示が可能。
| コンポーネント | 変更 |
|---|---|
| DAP サーバー (yx-core 新モジュール) | stdio トランスポート層、コアプリクエスト処理、ブレークポイントマネージャー(ソース行 → IR ノードマッピング) |
| ランタイムデバッグ トレイト (yx-core) | DebugEngine トレイト定義(pause, resume, step, get_frames, eval, get_variables) |
| インタープリタ | 実行ループのブレークポイントチェック、一時停止/再開メカニズム、フレームリンクリスト維持、InterpreterDebugEngine 実装 |
| CLI | yaoxiang run --debug パラメータ |
受入基準:tests/yaoxiang/ 下の任意の .yx ファイルに対して、VS Code でブレークポイント設定、Step Over、変数値の表示ができる。
第二フェーズ:高度なデバッグ能力
目標:式評価、関数ブレークポイント、並行デバッグ、例外ブレークポイント。
| コンポーネント | 変更 |
|---|---|
| 式評価エンジン | マイクロプログラムコンパイル(parser + typechecker を再利用)、VM への一時フレームプッシュ、副作用分離 |
| 並行デバッグ | spawn タスクリストマッピング、ブレークポイントタスク ID バインディング、stop-all / stop-this-only 一時停止戦略 |
| 関数/例外ブレークポイント | setFunctionBreakpoints、setExceptionBreakpoints マッピング |
| VS Code 拡張 | デフォルトの launch.json テンプレートを提供 |
第三フェーズ:JIT デバッグ & LLVM DWARF
目標:JIT エンジンが DAP を再利用し、LLVM が DWARF を生成してクラッシュトレース用途に使用。
| コンポーネント | 変更 |
|---|---|
| JIT | DebugEngine トレイト実装、コンパイル時に変数→レジスタマップテーブル生成、ランタイムフレームリンクリスト、式の一時コンパイル |
| LLVM | IR デバッグメタデータ → LLVM DILocation / DISubprogram → DWARF(DAP 対話なし) |
依存関係
フェーズゼロ(IR メタデータ)
↓
フェーズ一(インタープリタ DAP MVP) ← ここから就能使用
↓
フェーズ二(高度な能力)
↓
フェーズ三(JIT + LLVM DWARF)リスク
| リスク | 緩和 |
|---|---|
| インタープリタ一時停止メカニズムの複雑さ | 複雑なステートマシンではなくシンプルな channel/signal を使用、一時停止は fetch の次の命令不让获取而已 |
| 式評価の型安全性 | 既存の typechecker を再利用、読み取り専用参照、副作用をコミットしない |
| DAP プロトコルの詳細 | debugpy / delve の実装を参照、プロトコルは成熟している |
| 並行デバッグの stop-all 活鎖 | タイムアウトメカニズム + 強制一時停止 |
权衡
优点
- 一つのソース:デバッグメタデータは1回だけ生成され、3つのエンジンが共有。「インタープリタのデバッグ情報は正しいが LLVM のは間違っている」という状況が発生しない
- ゼロ侵襲:
--debugという一つのパラメータで、このパラメータを追加しない場合の動作が完全に変わらない - DAP 標準:VS Code エコシステムに直接接続でき、カスタムエディタプロトコルやデバッガ UI を必要としない
- インタープリタ優先:デバッグは本質的にインタープリタに適している——柔軟性、制御可能性、式評価がシンプル。LLVM モードで対話デバッグを行わないのは最も実用的な選択
缺点
- デバッグモードのパフォーマンスが悪い:インタープリタは JIT/LLVM よりずっと遅い。しかしデバッグはパフォーマンスを必要としない——デバッグモードで本番負荷を実行することを期待する人はいない
- LLVM デバッグが制限付き:AOT コンパイルでは対話デバッグができず、GDB/LLDB + DWARF のみを使用可能。ただしこれは权衡:LLVM モードでは本来デバッグ動作の違いがあってはならない
- 並行一時停止が複雑:stop-all セマンティクスはインタープリタで実装するにはすべてのアクティブなタスクを走査する必要がある
替代方案
| 方案 | 为什么不选择 |
|---|---|
| 3つのエンジンがそれぞれ DAP を実装 | 3倍の仕事、3セットのバグ。「良い味付け」に反する |
| DWARF のみ使用、自己の DAP なし | インタープリタと JIT には DWARF のコンセプトがなく、LLDB は VM 内部に入れない |
| Python pdb に従ってコマンドラインデバッガを作成 | VS Code の体験はコマンドラインデバッガを完爆 |
| DAP を LSP プロセスに押し込む | ライフサイクルが完全に異なる——LSP はプロジェクトに従い、DAP はデバッグセッションに従う。プロセス分離はハード要件 |
开放问题
- [ ] 条件ブレークポイントの式構文は通常の YaoXiang と完全に一致するか?(提案:完全に一致させ、parser を再利用)
- [ ]
spawnブロック内の Step Into 動作:ユーザーが Step Into を押して spawn ブロックに入ると、複数の並列タスクのどれを表示すべきか?(提案:最初に作成されたタスクで一時停止) - [ ] VS Code 拡張:デバッグ設定は既存の
vscode-extension/ディレクトリに置くか、独立したレポジトリにするか?
