Skip to content

RFC-034: 統一デバッグツールチェーン

概要

YaoXiang に統一されたデバッグツールチェーンを導入する。中心的な設計は一つのソース、三个の소비:コンパイルフロントエンドはソースコードの位置、変数名、型情報を一流市民として YaoXiang IR に埋め込み、インタープリタ、JIT、LLVM の3つのバックエンドが同じメタデータを消費する。ユーザーは yaoxiang run --debug で DAP(Debug Adapter Protocol)サーバーを起動し、VS Code は stdio 経由で接続し、ブレークポイント、ステップ実行、変数検査、コールスタック、式評価、並行デバッグの統一体験を得る——基盤がどの実行エンジンであっても。

動機

なぜこの機能が必要か?

現在 YaoXiang プログラムのバグを排查する手段は極限に原始的:

yaoxiang
io.println("DEBUG: x = " + x.to_string())
io.println("DEBUG: entered branch A")

3つの致命的な問題:

  1. コンパイラ開発の自ブートが阻止:YaoXiang で YaoXiang コンパイラを書くが、コンパイラを書く人は自分の書いたコードをデバッグできない。自ブート段階で対話式デバッグ手段がないのは袋小路だ。
  2. 3つのエンジン、ゼロデバッグ:インタープリタ、JIT、LLVM はそれぞれ勝手に走り、問題が発生するとユーザーは stdout に ALL TESTS PASSED が表示されるかどうかに頼るしかない。アサーション失敗?何行目か分からない、変数値も分からない。
  3. 並行はブラックボックス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   │
└─────────┘    └───────────────┘    └────────────┘

主要な設計上の決定事項

  1. DAP サーバーとランタイムはトレイトで分離。サーバーは基盤がインタープリタか JIT化を関知しない——DebugEngine トレイトを通じて命令を発信するだけ。各エンジンが同じトレイトを独立して実装する。
  2. yaoxiang run --debug はインタープリタの使用を強制。デバッグは制御可能性を必要とし、パフォーマンスを必要としない。LLVM モードでは DWARF を生成して事後トレース用途(core dump / crash report)に使用し、対話式デバッグは行わない。
  3. yaoxiang run から入口検出ロジックを再利用。新しいサブコマンドを導入せず、メンタルモデルは「デバッグモードでプログラムを実行する」。

IR デバッグメタデータ

既存の YaoXiang IR にメタデータを添付し、新しい IR 種類を追加しない。すべてのメタデータはコンパイルフロントエンドの一箇所で生成し、バックエンド消費は読み取り専用:

メタデータ添付点説明
SourceLocation各 IR ノードソースファイル:行番号:列番号
VarName変数宣言/バインディングノードソースコード内の変数名
TypeAnnotation変数/式ノード推論された型(コンパイル時述語を含む)
ScopeBoundaryブロック/関数入口出口変数のスコープのライフサイクル
SpanInfospawn ノード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 --releaseDWARF を生成して事後トレース用途(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つのエンジンの実装

インタープリタJITLLVM
ブレークポイント挿入方式実行ループで 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つのエッジケース処理

  1. 並行帰属:一時ブレークポイントは現在のタスク ID にバインドされ、他のタスクがヒットした場合は直接無視。
  2. spawn ブロックの Step Over:spawn 外部で Step Over すると、spawn ブロック全体を走って後にスキップする。spawn 内部に入るデバッグは Step Into を使用すべき。
  3. 一時ブレークポイント未ヒット:ウォッチドッグタイムアウトを設定(30秒間ブレークポイント未ヒット)→ 強制一時停止 → VS Code に通知。同時にプログラム終了イベントを監視 → 即座にクリーンアップ。
  4. 同一行に複数の IR ノード:Step Over の一時ブレークポイントは ignore_current_line をマークし、ヒット後にソース行番号が現在行番号と等しい場合は無視して続行。

3. 変数検査とスコープ

VS Code がリクエスト: "現在のフレームの変数リスト"

DAP サーバー:
    ├── 現在一時停止点の IR ノードをクエリ
    ├── 現在の ScopeBoundary 内の変数バインディングを走査
    │   └── 各バインディングから返す: (名前, 型, ランタイム値参照)
    └── VariablesResponse を組み立て → VS Code

スコープの階層化

┌─ Globals ───────────────────────────┐
│  モジュールレベルバインディング: 定数、型エイリアス、全域   │
├─ Locals ────────────────────────────┤
│  現在の関数内で可視のローカル変数           │
│  ├── 引数 (関数パラメータ)                │
│  └── ローカルバインディング (let / 代入)          │
├─ Captured ──────────────────────────┤
│  spawn ブロック / クロージャがキャプチャした外部変数      │
│  所有権状態を表示: move 済み / 参照共有  │
└─────────────────────────────────────┘

エンジンの差異

エンジン変数値の取得
インタープリタVM スタックフレームとヒープを直接読む。各値はメモリ内で明確に表現されている
JITレジスタとスタック上の値 → JIT コンパイル時に「変数 → レジスタ/スタックスロット」マップテーブルを記録する必要がある
LLVMDWARF の .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 / attachYaoXiang プログラムの起動/附加(--debug は attach モード)
setBreakpointsソース行ブレークポイントを設定
configurationDoneブレークポイント準備完了、実行開始
threadsすべてのアクティブな spawn タスクリストを返す
stackTrace指定タスクのスタックフレームリストを返す
scopes現在のフレームの変数スコープを返す
variables指定スコープの変数リストを返す
continue実行を再開
nextStep Over
stepInStep Into
stepOutStep Out
pause全タスクを中断
evaluate現在のフレームで式を評価
disconnectデバッグセッションを終了

第二フェーズ:拡張リクエスト

DAP リクエストYaoXiang の意味
setFunctionBreakpoints関数名ブレークポイント
setExceptionBreakpointsエラー/panic 時に一時停止
dataBreakpointInfoデータブレークポイント(変数変更トリガー)

実装戦略

フェーズゼロ:インフラ(前置于所有フェーズ)

目標:コンパイルフロントエンドがデバッグメタデータを IR に添付。

コンポーネント変更
IR 定義新しい SourceLocationVarNameTypeAnnotation などのメタデータフィールドを追加
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 実装
CLIyaoxiang run --debug パラメータ

受入基準tests/yaoxiang/ 下の任意の .yx ファイルに対して、VS Code でブレークポイント設定、Step Over、変数値の表示ができる。

第二フェーズ:高度なデバッグ能力

目標:式評価、関数ブレークポイント、並行デバッグ、例外ブレークポイント。

コンポーネント変更
式評価エンジンマイクロプログラムコンパイル(parser + typechecker を再利用)、VM への一時フレームプッシュ、副作用分離
並行デバッグspawn タスクリストマッピング、ブレークポイントタスク ID バインディング、stop-all / stop-this-only 一時停止戦略
関数/例外ブレークポイントsetFunctionBreakpointssetExceptionBreakpoints マッピング
VS Code 拡張デフォルトの launch.json テンプレートを提供

第三フェーズ:JIT デバッグ & LLVM DWARF

目標:JIT エンジンが DAP を再利用し、LLVM が DWARF を生成してクラッシュトレース用途に使用。

コンポーネント変更
JITDebugEngine トレイト実装、コンパイル時に変数→レジスタマップテーブル生成、ランタイムフレームリンクリスト、式の一時コンパイル
LLVMIR デバッグメタデータ → 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/ ディレクトリに置くか、独立したレポジトリにするか?

参考文献