Skip to content

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

要約 ​

YaoXiang に統一デバッグツールチェーンを導入する。核心となる設計は一つの源、三つの消費:コンパイルフロントエンドがソースコード位置、変数名、型情報を第一級の市民として YaoXiang IR に埋め込み、インタプリタ、JIT、LLVM の三つのバックエンドがそれぞれ同一のメタデータを消費する。ユーザは 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")

三つの致命的な問題がある:

  1. コンパイラ開発のセルフホスティングが阻害される:YaoXiang で YaoXiang コンパイラを書くが、コンパイラを書く人が自分のコードをデバッグできない。セルフホスティング段階でインタラクティブなデバッグ手段がないことは行き止まりである。
  2. 三つのエンジン、ゼロのデバッグ:インタプリタ、JIT、LLVM がそれぞれ独立して動作し、問題が発生した際、ユーザは stdout に ALL TESTS PASSED が現れるかどうかを確認するしかない。 assert が失敗したのか? どの行で起きたのか、変数の値は何か、わからない。
  3. 並行処理がブラックボックス:spawn で複数のタスクが作成されるが、どのタスクがハングしたのか? 変数は誰に move されたのか? すべて運任せの推測である。

設計目標 ​

  • 統一体験:インタプリタで機能するブレークポイントが JIT でも機能し、LLVM でも一貫したソースマッピングが得られる。ユーザは基盤のエンジンの違いを意識しない。
  • 一つの源:デバッグメタデータは IR と共に流れ、重複定義せず、二つのマッピングを維持しない。
  • ゼロ侵襲:yaoxiang run --debug という一つの引数で済み、この引数を付けない場合のコンパイルと実行の動作は完全に変わらない。
  • DAP 標準:VS Code エコシステムに直接接続し、エディタプロトコルを再発明しない。

提案 ​

中核設計 ​

アーキテクチャ概要:

┌──────────────────────────────────────────────────────┐
│                    VS Code / エディタ                   │
│              DAP Client (launch.json)                 │
└────────────────────────┬─────────────────────────────┘
                         │ stdio
┌────────────────────────▼─────────────────────────────┐
│                 DAP サーバ (yx-core)                   │
│  ┌─────────┐  ┌──────────┐  ┌───────────────────┐    │
│  │ セッション│  │ブレークポイント│  │式評価エンジン     │    │
│  │ 管理     │  │ 管理      │  │                   │    │
│  └─────────┘  └──────────┘  └───────────────────┘    │
└────────────────────────┬─────────────────────────────┘
                         │ クエリ/制御
┌────────────────────────▼─────────────────────────────┐
│                ランタイムデバッグインターフェース (trait)│
│  pause / resume / step / get_frames / eval / ...    │
└────┬──────────────────┬──────────────────┬──────────┘
     │                  │                  │
┌────▼────┐    ┌───────▼───────┐    ┌─────▼──────┐
│ インタプリタ│    │ JIT (RFC-028)│    │ LLVM AOT   │
│ IRメタデータ│    │ 軽量デバッグ表│    │ IRメタデータ│
│ を直接消費 │    │ を生成       │    │ → DWARF    │
└─────────┘    └───────────────┘    └────────────┘

重要な設計判断:

  1. DAP サーバとランタイムは trait で疎結合する。サーバは基盤がインタプリタか JIT かを関知しない——DebugEngine trait を介して命令を出すだけである。各エンジンは同じ trait を独立に実装する。
  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 のアタッチを待機
    │   ├── アタッチ成功後、プログラム入口で一時停止
    │   └── インタラクティブデバッグループへ移行
    │
    └── 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 なら一時停止

三つのエンジンの実装:

インタプリタJITLLVM
ブレークポイント挿入方法実行ループで IR ノード ID をチェックJIT が機械語に int3 を挿入LLVM DWARF + ハードウェアブレークポイントを利用
条件ブレークポイント評価式を直接解釈条件式を一時的に JIT コンパイルDWARF 式スタック + 評価
性能オーバーヘッド各 IR ノードで追加のテーブルルックアップブレークポイント箇所のみオーバーヘッドほぼゼロオーバーヘッド(ハードウェアブレークポイント)

2. ステップ実行 ​

Step Over    → 現在行を実行、関数呼び出しの内部をスキップし、次の行で停止
Step Into    → 現在行の関数呼び出しの内部に入る
Step Out     → 現在関数から戻るまで実行
Continue     → 次のブレークポイントまたはプログラム終了まで実行を再開

実装ロジック:ステップ操作は本質的にテンポラリブレークポイントである。ユーザが明示的に設定したブレークポイントと同一の機構を共有し、二つのシステムではなく、一つのシステムの二つの使用方法である。

Step Over:
    現在のソース行番号 = query_line(frame)
    → 次の行にテンポラリブレークポイントを設定
    → 現在行が関数呼び出しの場合: 呼び出し点の後にテンポラリブレークポイントを設定
    → Continue → テンポラリブレークポイントヒット → 削除 → 一時停止

Step Into:
    現在呼び出し先の最初の実行可能行
    → 関数本体の最初の IR ノードのソース位置を特定
    → テンポラリブレークポイントを設定 → Continue → ヒット → 一時停止

Step Out:
    現在のスタックフレームのリターンアドレス
    → 呼び出し元の次の行を特定
    → テンポラリブレークポイントを設定 → Continue → ヒット → 一時停止

テンポラリブレークポイントの四つの境界ケース処理:

  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 済み / ref 共有│
└─────────────────────────────────────┘

エンジン間の差異:

エンジン変数値取得
インタプリタ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()     // 並列実行、個別のステップの影響を受けない
}

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 定義SourceLocation、VarName、TypeAnnotation などのメタデータフィールドを追加
Parser各 AST ノードがソース位置を記録
TypeChecker型情報を IR ノードに添付
テストIR dump に位置と変数情報が含まれることを検証

ランタイムは関与しない。

実装進捗(2026-09-17):

成果物状態実装形式
ソース位置完了全 76 個の Instruction バリアントが span フィールドを保持;span() メソッドには意図的にワイルドカードアームを設定せず、新規バリアントが span を持たない場合はコンパイル失敗となる。位置は 40/41 命令をカバー
変数名完了LocalSlot { name, ty, scope_depth } を FunctionBody::Code::locals に紐付け;register_local が書き込み時点で生成。.42 デバッグセグメント v2 は名前を保持し、v1 産物は後方互換で読み取り可能
グローバルスロット名完了.42 デバッグセグメント v3 は「スロット番号 → トップレベル束縛名」テーブルを保持。トップレベル束縛は Operand::Global を経由し、いかなる関数の局所名テーブルにも存在しないため、このテーブルがなければ数値のみ報告可能で変数名は報告不可。v1/v2 産物は空テーブルで補完読み取り
型情報一部スロットは既に ty を含む;TypeAnnotation 独立メタデータは未実装
dump 可視性完了dump は各命令に対し ; <file>:<line>:<col> を出力し、locals: 名前@スロット を列挙

上表で実装されたデータ形式は段階一と直接接続する:DAP のブレークポイント解析は debug_map を消費し、変数パネルは local_names(名前)と locals(型)を消費する。

第一段階:インタプリタ DAP MVP ​

目標:yaoxiang run --debug file.yx でブレークポイント設定、ステップ実行、変数表示が可能。

コンポーネント変更内容
DAP サーバ (yx-core 新規モジュール)stdio トランスポート層、中核リクエスト処理、ブレークポイントマネージャ(ソース行 → IR ノードマッピング)
ランタイムデバッグ trait (yx-core)DebugEngine trait 定義(pause, resume, step, get_frames, eval, get_variables)
インタプリタ実行ループでのブレークポイントチェック、一時停止/再開機構、フレームリンクリスト維持、InterpreterDebugEngine 実装
CLIyaoxiang run --debug 引数

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

第二段階:高度なデバッグ機能 ​

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

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

第三段階:JIT デバッグ & LLVM DWARF ​

目標:JIT エンジンが DAP を再利用、LLVM がクラッシュ遡及用 DWARF を出力。

コンポーネント変更内容
JITDebugEngine trait 実装、コンパイル時に変数→レジスタマッピング表生成、ランタイムフレームリンクリスト、式の一時コンパイル
LLVMIR デバッグメタデータ → LLVM DILocation / DISubprogram → DWARF(DAP インタラクションなし)

依存関係 ​

段階ゼロ(IR メタデータ)
    ↓
段階一(インタプリタ DAP MVP)  ← ここから使い始められる
    ↓
段階二(高度な機能)
    ↓
段階三(JIT + LLVM DWARF)

リスク ​

リスク緩和策
インタプリタの一時停止機構の複雑さ複雑なステートマシンではなく、シンプルな channel/signal を使用。一時停止は次の命令の fetch を禁止するだけ
式評価の型安全性既存の typechecker を再利用、読み取り専用参照、副作用をコミットしない
DAP プロトコルの詳細debugpy / delve の実装を参照。プロトコルは成熟している
並行デバッグの stop-all ライブロックタイムアウト機構 + 強制一時停止

トレードオフ ​

利点 ​

  • 一つの源:デバッグメタデータは一度だけ生成され、三つのエンジンで共有。「インタプリタのデバッグ情報は正しいが LLVM は間違っている」が起きない
  • ゼロ侵襲:--debug という一つの引数。この引数を付けない場合の動作は完全に変わらない
  • DAP 標準:VS Code エコシステムに直接接続。カスタムエディタプロトコルやデバッガ UI が不要
  • インタプリタ優先:デバッグは本質的にインタプリタに適している——柔軟、制御可能、式評価がシンプル。LLVM モードでインタラクティブデバッグを行わないのは最も実用的な選択

欠点 ​

  • デバッグモードの性能が低い:インタプリタは JIT/LLVM より遥かに遅い。しかしデバッグには性能は不要——誰も debug モードで本番負荷を走らせない
  • LLVM デバッグの制限:AOT コンパイルはインタラクティブデバッグができず、GDB/LLDB + DWARF のみ。しかしこれはトレードオフ:LLVM モードではそもそもデバッグ動作の差異があってはならない
  • 並行一時停止の複雑さ:stop-all セマンティクスをインタプリタで実装するには全アクティブタスクの走査が必要

代替案 ​

代替案選択しない理由
三つのエンジンがそれぞれ DAP を実装三倍の作業、三セットのバグ。「良い品味」に反する
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/ ディレクトリ下に配置するか、独立リポジトリとするか?

参考文献 ​