VS Code 統合 内部仕様書

対象読者: コア実装に手を入れる開発者。ユーザー向けセットアップや機能紹介は IronHSP 仕様書 §11 参照。

最終更新: 2026-04-20 / ブランチ: update-hsp3net / 関連コミット: a025002b1e5a3fee

目次

1. 全体アーキテクチャ

VS Code 統合は 3 つの独立した機能の集合体で、それぞれ別プロセス/別バイナリで動作します:

機能バイナリプロトコル言語LOC
デバッガ (DAP)nhspdap.exe + hsp3debug_dap_64.dll + hsp3cl_net_dbg_64.exeDAP over stdio + 名前付きパイプC# + C++~800
言語サーバ (LSP)hspls.exeLSP over stdioC#~600
色分け (TextMate)JSON grammarJSON~150

これら全てを vscode-nhsp 拡張 から起動/接続します (既存の .nhsp 拡張に .hsp 機能を追加した形)。

VS Code │ │ DAP (stdio) LSP (stdio) ├──────────→ nhspdap.exe ├──────────→ hspls.exe │ │ │ │ │ │ Named Pipe │ │ subprocess │ │ (evt / cmd 2 本) │ │ │ ▼ │ ▼ │ hsp3debug_dap_64.dll │ hspcmp64.exe -ll -lv -ls │ │ (hsp3debug.dll として名前変更) │ (user32.as/kernel32.as も辿る) │ │ LoadLibraryA │ │ │ ▼ │ │ │ hsp3cl_net_dbg_64.exe ←───(対象 .hsp)─┘ │ ▼ .hsp ソース (エディタ表示) ← TextMate grammar (syntax coloring) ← LSP semantic tokens (上書き色分け)

2. 該当ファイル/ディレクトリマップ

パス役割
hsp3net/hsp3code.cppHSP3_DAP_MODE 下で BP フック + set_var_* export (既存ファイルへの追加)
hsp3net/win32/hsp3cl.cpphsp3debug.dll ロード時の GetProcAddress を x64 bare name + x86 decorated の両対応に / hsp3cl_stop() の GetMessage ループを runmode check に変更
hsp3net/win32/main.cppHSP3_DAP_MODE 下で setvbuf(stdout, _IONBF) 呼び出し
hsp3net/hsp3cl_net_dbg_64.vcxproj新規: デバッグ用ランタイムのビルド定義 (HSP3_DAP_MODE 定義、HSP_TEST_MODE なし)
plugins/win32/hsp3debug_dap/新規: DAP ブリッジ DLL (C++)。src/hsp3debug_dap.cpp に全ロジック、def で export
nhspc/nhspdap/新規: DAP アダプタ (C#)。Program.cs / DapIo.cs / DebuggeeBridge.cs / DapSession.cs
nhspc/HspLanguageServer/新規: LSP サーバ (C#)。Program.cs / LspServer.cs / HspSymbol.cs / HspcmpRunner.cs
nhspc/vscode-nhsp/syntaxes/hsp.tmLanguage.json新規: HSP3 TextMate grammar
nhspc/vscode-nhsp/src/hsp-lsp-client.js新規: VS Code 側 LSP クライアント (.hsp 用)
nhspc/vscode-nhsp/src/extension.js既存: DAP factory + LSP client 登録 + semantic tokens provider 登録を追加
nhspc/vscode-nhsp/package.json既存: debuggers (type:"hsp3net") + grammars + languages + breakpoints + activationEvents 追加

3. DAP デバッガ実装詳細

3.1 プロセス構成と起動フロー

VS Code (Debug UI) │ │ DAP: initialize → launch → setBreakpoints → configurationDone ▼ nhspdap.exe (DAP adapter) │ │ 1. LocateRuntime() で hsp3cl_net_dbg_64.exe を探す │ 2. StageDebugDll() で hsp3debug_dap_64.dll を runtime dir に │ hsp3debug.dll として配置 (copy-on-demand) │ 3. hsp3cl_net_dbg_64.exe を spawn │ 4. Child の stdout/stderr を "output" event として VS Code に流す ▼ hsp3cl_net_dbg_64.exe (runtime) │ │ 1. .ax 読込 → HSPHED_BOOTOPT_DEBUGWIN が立ってれば │ LoadLibraryA("hsp3debug.dll") で DLL ロード │ 2. debugini() 呼び出し → DLL 初期化 ▼ hsp3debug_dap_64.dll (HSP3DEBUG ハンドラ) │ │ 1. GetProcAddress(exe, "hsp3dap_bp_count") でカウンタアドレス取得 │ 2. GetProcAddress(exe, "hsp3dap_register_bp_check") でコールバック登録 │ 3. GetProcAddress(exe, "hsp3dap_force_step") を呼び dbgmode=STEPIN │ 4. pipe_thread を spawn: │ ├─ CreateNamedPipeA "\\\\.\\pipe\\hsp3dap_<pid>_evt" (outbound) │ └─ CreateNamedPipeA "\\\\.\\pipe\\hsp3dap_<pid>_cmd" (inbound) │ 5. debugini 即 return → runtime 続行 → 最初の行で STEPIN hit ▼ (nhspdap が NamedPipeClientStream で 2 本とも Connect) │ │ ↔ 双方向通信 (events / commands) ▼ ユーザが BP 設定 / 停止 / 変数編集 / 続行

3.2 2-pipe アーキテクチャの理由

/clr mixed-mode + 同一 duplex pipe の concurrent ReadFile+WriteFile は壊れる。

hsp3cl_net_dbg_64.exe は CLRSupport=true でコンパイルされており、mixed native+managed 環境。 この環境下で pipe_thread が ReadFile で blocking 中、メインスレッドから同じハンドルに WriteFile すると、 ERROR_BROKEN_PIPE (109) / ERROR_NO_DATA (232) がランダムに発生する。

解決: simplex pipe を 2 本用意 (_evt は PIPE_ACCESS_OUTBOUND、_cmd は PIPE_ACCESS_INBOUND)。 各パイプは単一スレッドしか触らない ⇒ concurrent I/O の問題が発生しない。

3.3 hsp3net ランタイム改変箇所

hsp3code.cpp

HSP3_DAP_MODE 下でのみ有効な追加 export:

// exe が export するシンボル (DLL から GetProcAddress で解決)
extern "C" __declspec(dllexport) volatile int hsp3dap_bp_count = 0;
extern "C" __declspec(dllexport) void hsp3dap_register_bp_check(fn);
extern "C" __declspec(dllexport) void hsp3dap_force_step(void);
extern "C" __declspec(dllexport) int  hsp3dap_get_var_type(const char*);
extern "C" __declspec(dllexport) int  hsp3dap_set_var_int(name, idx[], num_idx, value);
extern "C" __declspec(dllexport) int  hsp3dap_set_var_int64(...);
extern "C" __declspec(dllexport) int  hsp3dap_set_var_double(...);
extern "C" __declspec(dllexport) int  hsp3dap_set_var_str(...);   // sbStrCopy 経由
extern "C" __declspec(dllexport) int  hsp3dap_set_var_wstr(...);  // UTF-8 → UTF-16

// dispatch loop の 4 箇所に追加された条件 (HSP3_DAP_MODE 時のみ):
if (dbgmode || hsp3dap_bp_count > 0) code_dbgtrace();

// code_dbgtrace() 内で BP ヒット検知:
if (g_hsp3dap_bp_check && hsp3dap_bp_count > 0) {
    bp_hit = g_hsp3dap_bp_check(dbginfo.fname, dbginfo.line);
}
if (dbgmode || bp_hit) { runmode = STOP; msgfunc(); }

win32/hsp3cl.cpp

// 修正前: x86 stdcall decorated name のみ
dbgwin = GetProcAddress(h_dbgwin, "_debugini@16");

// 修正後: x64 bare name 優先、失敗時 x86 decorated へ fallback
dbgwin = GetProcAddress(h_dbgwin, "debugini");
if (dbgwin == NULL) dbgwin = GetProcAddress(h_dbgwin, "_debugini@16");

// hsp3cl_stop() の GetMessage ループ:
// 修正前: while(1) { GetMessage(...); if (runmode != STOP) break; }
//         → runmode が既に RUN でも GetMessage で blocking (msg 無し)
// 修正後: while (runmode == STOP) { GetMessage(...); ... }
//         → dbgnotice で runmode=RUN なら即抜ける

win32/main.cpp

#ifdef HSP3_DAP_MODE
    // 子プロセスで stdout を redirect される時に block-buffered になって
    // mes 出力が end まで届かない問題を回避
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
#endif

3.4 IPC wire protocol

名前付きパイプ \\.\pipe\hsp3dap_<pid>_evt / _cmd、UTF-8 改行区切り (LF) JSON。

DLL → adapter (events) — _evt パイプ経由

{"evt":"ready"}
{"evt":"stopped","reason":"entry|breakpoint|step|pause|exception","line":N,"file":"path"}
{"evt":"terminated","exit":N}

adapter → DLL (commands) — _cmd パイプ経由

{"cmd":"set_bp","file":"path","lines":[10,20]}      → {"resp":"bp_ack"}
{"cmd":"continue"|"step_over"|"step_in"|"step_out"|"pause"}
{"cmd":"get_vars"}                                   → {"resp":"vars","items":[...]}
{"cmd":"get_callstack"}                              → {"resp":"callstack","frames":[...]}
{"cmd":"set_var","name":"x","indices":[0,1],"value":"42"}
                                                     → {"resp":"set_var","ok":1}
{"cmd":"evaluate","expr":"iarr(1,1) = 77"}           → {"resp":"evaluate","ok":1,"result":"77"}
{"cmd":"disconnect"}

BP パスマッチングは両側で Path.GetFileName().ToLowerInvariant() に正規化して比較 (VS Code は絶対パス、HSP runtime は相対パスを送ってくるため)。

3.5 setVariable / evaluate 実装

5 種の HSP 変数型に対応。配列・モジュール変数透過。

ランタイム側の書き込みロジック

// int / int64 / double: pv->pt から要素 offset 計算して直接代入
int* arr = (int*)pv->pt;
arr[offset] = value;

// str: HSP の文字列は sbAlloc 可変長バッファで管理 (pv->pt は char*、
// 配列時は (char**)pv->master[idx] が各要素の buffer ptr)。
// sbStrCopy() で安全に再確保 (memcpy 直書きは NG)。
char** pp = (off == 0) ? &pv->pt : &((char**)pv->master)[off];
sbStrCopy(pp, (char*)value);

// wstr: UTF-8 → UTF-16 変換して固定長 wchar_t バッファへ
MultiByteToWideChar(CP_UTF8, 0, value_utf8, -1, base, elem_chars);

evaluate の式パース

DLL 側の handle_command("evaluate", ...) で簡易 LHS parser を走らせる:

expr = "iarr(1,1) = 77"
 → split by '='
 → lhs = "iarr(1,1)", rhs = "77"
 → paren split: name = "iarr", indices = [1, 1]
 → hsp3dap_get_var_type("iarr") → HSPVAR_FLAG_INT
 → hsp3dap_set_var_int("iarr", [1,1], 2, 77)

expr = "m_val@mymod = 555"
 → name = "m_val@mymod" (そのまま HSP 名前解決に渡せる)
 → hsp3dap_get_var_type("m_val@mymod") → HSPVAR_FLAG_INT
 → hsp3dap_set_var_int("m_val@mymod", NULL, 0, 555)

4. LSP サーバ (hspls) 実装詳細

4.1 プロセス構成

VS Code 拡張が hspls.exe を spawn。拡張からの LSP 要求に応じて、 hspls が hspcmp64.exe を subprocess で呼び出し、出力を LSP レスポンスに変換する。

VS Code (Language client — vscode-nhsp/src/hsp-lsp-client.js) │ LSP: initialize → textDocument/didOpen → didSave → definition etc. ▼ hspls.exe (C# LSP server — nhspc/HspLanguageServer/) │ │ didOpen / didSave 時に: │ 1. buffer を sidecar temp file に書く (foo.__hspls.hsp) │ 2. hspcmp64 -ll -lv -ls -lr -d -i <temp> を subprocess │ 3. stdout / stderr の dfnc/dvar/dmac/dmod/dexc/dlab + error N を parse │ 4. temp を削除、symbols/diagnostics を保持 │ 5. publishDiagnostics で赤波線送信 │ │ 以降の definition / hover / documentSymbol / completion / │ semanticTokens の要求はキャッシュ済 symbols を lookup するだけ ▼ hspcmp64.exe (HSP3 parser)

4.2 hspcmp 出力パース

シンボルリスト (-ll / -lv / -ls)

出力書式: <kind> <name> <line>:<file>

kind意味LSP SymbolKindSemantic type
dfnc#deffunc / #defcfunc (ユーザ定義関数)Function (12)function
dexc#func / #cfunc (外部 DLL 関数)Method (6)method
dvar変数 (dim / sdim / 初代入)Variable (13)— (grammar 任せ)
dlab*labelVariable (13)
dmac#define / #const / #enum / #func aliasConstant (14)複雑 (下記)
dmod#module 名前Module (2)

dmac の分類 — ファイル由来で意味が変わる

File実態Semantic type
hspdef.asHSP3 ランタイム定数 (screen_normal, ginfo_mx 等)macro (ビルトイン扱い)
その他の .asDLL binding の #define alias / #func エイリアスmethod (外部 DLL 扱い)
.hspユーザの #define / #constfunction (ユーザ定義扱い)

ビルトインキーワード (-lk)

出力書式: <name>\t,<kind> (例: goto\t,sys|func)。 hspls 起動時に 1 回だけ呼んで HashSet 化、Symbol テーブルに未登録な識別子の fallback 分類に使う。

診断 (エラー/警告)

書式: <file>(<line>) : error <code> : <message> (または warning)

test.hsp(2) : error 2 : 文法が間違っています (2行目)

4.3 temp file 方式

絶対にユーザの本ファイルを上書きしてはいけない。

初期実装では File.WriteAllText(absPath, text) で本ファイルを上書きしていたが、 Explorer の別エディタで開いているファイルと衝突する、BOM が勝手に入る等の不具合の温床になる。

現在は同じディレクトリに foo.__hspls.hsp のサイドカー temp file を書いて hspcmp に食わせ、出力の file フィールドを本ファイル名に rewrite した後 temp を削除する。

同じ dir に書く理由は、HSP の #include "common/..." 等の相対 path 解決を壊さないため。

5. セマンティックトークン実装

LSP の textDocument/semanticTokens/full で、識別子単位の 4 分類色分けを提供。 TextMate grammar の regex では判別不能な「ユーザ定義 vs 外部 DLL」を実シンボル情報で区別する。

Token legend (LspServer.cs)

tokenTypes: [
  "function",   // 0: user #deffunc/#defcfunc (.hsp 由来 dfnc)
  "method",     // 1: external DLL (.as 由来 dexc or dfnc)
  "macro",      // 2: HSP3 built-in (hspcmp -lk or hspdef.as dmac)
  "variable",   // 3: reserved
  "keyword",    // 4: control flow (if/else/repeat/loop/…)
]
tokenModifiers: []  (未使用)

分類アルゴリズム (LspServer.ClassifyIdentifier)

// 1. 制御構文キーワードは最優先で keyword
if (ControlKeywords.Contains(word)) return TYPE_KEYWORD;

// 2. ワークスペース symbol table lookup
if (syms.TryGetValue(word.ToLowerInvariant(), out hits)) {
    foreach (var s in hits) {
        bool isAs     = fname.EndsWith(".as");
        bool isHspdef = fname == "hspdef.as";
        switch (s.Kind) {
            case "dfnc": return (isAs && !isHspdef) ? METHOD : FUNCTION;
            case "dexc": return METHOD;
            case "dmac":
                if (isHspdef) return MACRO;        // ビルトイン定数
                if (isAs)     return METHOD;        // DLL binding
                return FUNCTION;                    // ユーザ #define
        }
    }
}

// 3. hspcmp -lk のビルトインキーワード集合
if (_builtins.Contains(word)) return TYPE_MACRO;

// 4. それ以外 — TextMate grammar に委譲
return -1;

トークナイザ (LspServer.TokenizeHsp)

独自 mini-lexer。行単位で walk、;/////* *//"..."/{"..."} をスキップ。 識別子は [A-Za-z_][A-Za-z_0-9@]* (@name@module の区切りとして含める)。

Delta encoding (LSP 仕様)

// token = [deltaLine, deltaStart, length, typeIdx, modifierMask]
// deltaLine = 前トークンとの行差
// deltaStart = 同じ行なら前トークンとの列差、違う行なら行頭からの列
// 全 token を (line, col) 昇順でソート済であること

5.5 ドキュメントコメント (;;; / ///) の仕組み

nhspc/HspLanguageServer/DocCommentParser.cs で実装。 hspcmp によるシンボル抽出のに source を 1 パス舐める。

アルゴリズム (疑似コード)

docBuf = []
for line in source.split('\n'):
    trimmed = line.trimStart()
    if trimmed.startsWith(";;;") or trimmed.startsWith("///"):
        docBuf.append(stripMarker(trimmed))
        continue
    if trimmed.isEmpty: continue               # 1行空けは許容
    if trimmed.startsWith(";") or "//":
        continue                                # 普通のコメントはスルー (重要)
    # 実行可能 or 宣言行
    m = DeclRx.match(line)   # #deffunc / #defcfunc / #func / ... / #const / #define / #enum
    if m and docBuf.nonEmpty:
        attachDocsToSymbol(name = m[2], declLine, docBuf)
    docBuf.clear()

宣言との結びつきは名前 + 行番号の近傍一致 (±2 行) で検証。hspcmp の kind ごとの行番号のズレを吸収。@param name desc / @return desc (@arg / @returns は alias) を分離、残りは description。

temp ファイル対応: LspServer は sidecar temp file (foo.__hspls.hsp) で hspcmp を実行するため、symbol の File フィールドは temp 名で返ってくる。DocCommentParser は basename 比較するが、 temp→real のリネームが済んだ後で呼ぶ必要がある。初期実装では順序ミスで doc が attach されない不具合があった (修正済)。

ホバー出力フォーマット

```hsp
(function) distance
```
<description>

**Parameters:**
- `name` — desc
- ...

**Returns:** ...

_— file.hsp:NN_

5.6 signatureHelp の実装

LSP の textDocument/signatureHelp で、関数呼び出し中に シグネチャ + activeParameter を返す。

シグネチャデータ (DocCommentParser.ExtractSigParams)

宣言行を regex で切り出し、引数リストを HspSigParam { Type, Name } の 配列に変換して HspSymbol.SigParams に保存。宣言種別 (#deffunc 等) は別フィールド DeclKind に保存 (hspcmp の Kind=dfnc だけでは #deffunc#defcfunc が区別できないため)。

// 命令形式 (名前付き)
#deffunc foo str _name, int _age
  → SigParams = [{Type:"str", Name:"_name"}, {Type:"int", Name:"_age"}]
    DeclKind = "#deffunc"

// 関数形式 (名前付き)
#defcfunc bar int _a, int _b
  → SigParams = [{Type:"int", Name:"_a"}, {Type:"int", Name:"_b"}]
    DeclKind = "#defcfunc"

// DLL バインディング (名前なし)
#func Beep "Beep" sptr, sptr
  → SigParams = [{Type:"sptr", Name:""}, {Type:"sptr", Name:""}]
    DeclKind = "#func"

local _tmp の内部 scratch パラメータは除外。文字列/括弧ネストを 考慮した top-level 分割 (DocCommentParser.SplitTopLevel) で foo(a, b) のような型修飾内の , を誤認しないように。

TryLocateCall: カーソル位置から呼び出しを特定

カーソル位置から source を backward scan:

  1. 未マッチの ( を探す () でカウント減)
  2. 見つかれば paren 形式: その直前の識別子が関数名、 ( とカーソル間の top-level コンマ数が activeParameter
  3. 無ければ statement 形式 fallback: 現在行の行頭から最初の 識別子が関数名、識別子後からカーソルまでのコンマ数が activeParameter

文字列リテラル "..." の中の ( / ) / , は無視。 制御構文キーワード (if/else/repeat/loop/ goto/gosub/return/end 等) は関数として 扱わない (偽陽性回避)。

ラベル書式 (命令 vs 関数)

統一せず DeclKind で書式切替。HSP のユーザはコードで両者を 使い分けるので、log_kv "y", y に対して log_kv(str _key, int _val) と括弧付きで出すと紛らわしい。

statementStyle = DeclKind ∈ { "#deffunc", "#func", "#modfunc", "#comfunc" }
  ↓
  命令形式:   "name type1 arg1, type2 arg2"   (括弧なし)
  関数形式:   "name(type1 arg1, type2 arg2)"  (括弧あり)

パラメータ documentation

LSP の ParameterInformationlabeldocumentation (optional) を持つ。activeParameter のときに VS Code が下部に表示する。

各 SigParam に対応する DocParam を検索して attach:

  1. 名前一致: SigParam.Name == DocParam.Name で探す
  2. 順序フォールバック: 名前なし (#func 等) なら i 番目の SigParam に i 番目の DocParam をペアリング
VS Code 拡張側で ParameterInformation に documentation を渡し忘れると サーバが送っても popup に出ない。new vscode.ParameterInformation(label, doc) の第 2 引数を確実に設定すること (v0 で踏んだバグ)。

5.7 ;;; / /// の自動テンプレ展開

VS Code 拡張の onDidChangeTextDocument で単一文字 / / ; の挿入を検知。現在行が trim 後 exactly /// / ;;; で、 直下の非空非コメント行がマッチする宣言なら vscode.SnippetString で placeholder 付きテンプレに置換。

// 再入防止フラグ _hspDocTemplateBusy で自前の edit がループしないように
// protect。setTimeout(..., 0) で change handler 終了後に defer する。
const snippet = new vscode.SnippetString(
    `${marker} \${1:説明}\n` +
    `${indent}${marker} @param ${paramName} \${2:説明}\n` +
    `${indent}${marker} @return \${3:戻り値の説明}`
);
editor.insertSnippet(snippet, markerRange);

宣言種別を regex で判定:

関数系は戻り値有無を kind で判定 (#defcfunc/#cfunc*/#modcfunc@return 行付き)。変数系・暗黙宣言は description 1 行のみ。

5.8 BP verified 判定

nhspdap/DapSession.SetBreakpoints で各 BP 行を簡易チェック:

条件verifiedVS Code 表示
実行可能な行true🔴 赤丸 (ヒット予定)
空行 / ; / // / # で始まる行false⚪ グレーホロー丸 + メッセージ

実行可能行の判定は heuristic (regex ^\s*[^;/#\s] 相当)。 ブロックコメント /* */ の中身は不正確に true 返すが許容。

unverified な BP は DLL への set_bp コマンドの lines 配列に 含めない → bp_count が余分にインクリされず runtime のホットパスを 軽く保てる。

6. ビルド手順

全部入りビルド (初回)

# C# (nhspdap / hspls / nhspls)
cd nhspc
dotnet build NhspCompiler.sln -c Release

# C++ (hsp3debug_dap.dll)
MSBuild.exe plugins/win32/hsp3debug_dap/hsp3debug_dap.vcxproj \
    -p:Configuration=Release -p:Platform=x64

# C++ (hsp3cl_net_dbg_64.exe)
MSBuild.exe hsp3net/hsp3cl_net_dbg_64.vcxproj \
    -p:Configuration=Release -p:Platform=x64

VS Code 拡張 (dev モード)

# 拡張自体は node.js なのでビルド不要
# VS Code で nhspc/vscode-nhsp/ を開いて F5 で Extension Development Host が起動
ファイルロック注意: Extension Development Host が起動中は hspls.exe / nhspls.exe がロックされて再ビルド失敗する。 taskkill /F /IM hspls.exe で殺してからビルドすること (dev host は自動再起動する)。

7. 既知の罠 (踏みやすい順)

  1. hspcmp64 の -d だけでは DLL がロードされない。
    -d はデバッグ情報を .ax に埋め込むだけ。hsp3debug.dll がロードされるためには HSPHED_BOOTOPT_DEBUGWIN が立っている必要があり、これは -w (debug window force) で立つ。 結論: デバッグ対象は必ず hspcmp64 -d -w -i foo.hsp でコンパイルする。
  2. /clr mixed-mode + 同一 duplex pipe の concurrent ReadFile + WriteFile は壊れる。
    ERROR_BROKEN_PIPE (109) が ReadFile 直後に発生。2 本の simplex pipe に分けるのが唯一の解。
  3. HSP の str 変数は sbAlloc 可変長バッファ。memcpy で直書きすると死ぬ。
    pv->pt は「文字列バッファへの char*」であって「バッファ本体」ではない。 書換は sbStrCopy(&pv->pt, new_value) で行う (可変長なので再確保が必要)。
  4. hspcmp に絶対 path を渡すと -ll/-lv/-ls の file 欄も絶対 path になる。
    basename 正規化で比較するコードが両側 (DLL の BP マッチャ、LSP の Local フィルタ) に要る。
  5. stackTrace の source.path は絶対 path 必須。
    VS Code が相対 path で source を開けず内部エラー (Cannot read properties of undefined) で落ちる。 setBreakpoints 時に basename → absolute path マップを保持して、stackTrace 時に絶対化する。
  6. 子プロセスの stdout は block-buffered になる。
    親 (nhspdap) が pipe 経由で読むとき、HSP runtime の fputs(mes, stdout) は end/exit まで 溜まる。setvbuf(stdout, NULL, _IONBF, 0) で runtime 起動時に無効化。
  7. CLR /clr exe の __declspec(dllexport) 変数は動く、ただし .cpp が /clr でない時のみ安全。
    hsp3code.cpp は <CompileAs>CompileAsCpp</CompileAs> でネイティブビルド指定。 /clr ファイルに dllexport 変数を書くと AppDomain 分離等で挙動が不安定。
  8. std::mutex は non-recursive。
    set_bp ハンドラが g_bp_mutex を保持したまま sync_bp_count_to_exe を呼び、そこで再ロックすると 例外になる。_locked サフィックスで「lock 済前提」を明示する命名規則で統一。
  9. package.json の languageModelTools は canBeReferencedInPrompt で modelDescription + toolReferenceName 必須。
    VS Code の API が変わった。legacy に無い型で Extension CANNOT register tool エラー。
  10. .hsphsp 言語マッピングが無いと BP を置けない。
    package.json の contributes.languages{id:"hsp", extensions:[".hsp"]} を 登録しないと、breakpoints: [{language:"hsp"}] が効かない。
  11. HSP runtime は空行を dispatch しない。
    BP を blank 行に設定しても絶対ヒットしない。adapter 側で「最も近い次の実行行」にスナップする 機能は未実装 (v2)。ユーザに _bp_target = 1 のような明示的な BP 行を用意してもらう想定。
  12. 外部 DLL 関数の hspcmp kind は dfnc でも dmac でもなく dexc
    正規表現 (dfnc|dlab|dvar|dmac|dmod) だけでパースすると GetTickCount 等が 未分類になる (method カラーが出ない)。dexc を必ず含める。
  13. DLL が stopped event を送る時 fname を JSON エスケープしないと死ぬ。
    dbginfo.fname は j:\HNWorks\... 形式の Windows パス。JSON 文字列に 直入れすると \H \t 等が無効エスケープとして扱われ、adapter の JObject.Parse が silently fail。try/catch で continue されるためエラー ログにも出ず、VS Code から見ると「BP ヒットしても何も起きない」状態に。 相対パスでコンパイルしていた頃は地雷が見えなかった。必ず json_esc() を通す。
  14. line 1 (最初の実行行) の BP は force_step entry stop に吸収される。
    force_step を debugini で即呼ぶと、adapter の set_bp が pipe に到達する前に debug_notice が発火し、g_breakpoints が空のため reason="breakpoint" に 分類できない。解決: debugini を g_start_event で待機させ、 adapter の configurationDone 受信時に {"cmd":"start","stopOnEntry":N} を送って DLL 側が初めて force_step 要否を判断する流れに変更。ついでに launch.json の stopOnEntry が実際に効くようになる。
  15. .NET subprocess で stdout + stderr を両方 ReadToEnd() すると deadlock。
    典型的な .NET trap。stderr が満杯 → hspcmp ブロック → stdout EOF 来ない → ReadToEnd ずっと blocking → WaitForExit すら呼べない。OutputDataReceived / ErrorDataReceived イベント + BeginOutputReadLine / BeginErrorReadLine で async 読み取りにする。
  16. hspcmp のエラー出力は UTF-8 でなく ANSI code page (日本語環境なら CP932)。
    -i (UTF-8 入力モード) を付けても ERROR MESSAGE は実行環境の ANSI。 .NET 側で StandardOutputEncoding = GetEncoding(ANSICodePage) で受け取り、 内部で UTF-16 に変換してから VS Code (UTF-8) へ渡す。一方 HSP ランタイム (HSPUTF8 ビルド) の stdout は UTF-8 なので読み取り側を使い分ける。
  17. MessageBoxA + UTF-8 バイト列はエラーメッセージ文字化けの温床。
    HSPUTF8 ビルドでは errmsg が UTF-8 バイト列。A 系 Win32 API は ANSI として解釈するので日本語環境で必ず文字化け。MultiByteToWideChar(CP_UTF8) で UTF-16 変換してから MessageBoxW を使う。
  18. LSP で didChange 時に再解析しないと hover が陳腐化する。
    didSave 限定の再解析はエディタバッファ編集や外部ツールによるディスク書換に 追従できない。didChange 内で毎回 ReparseDoc を呼ぶのが最短。hspcmp は 小規模 .hsp なら数十ms で回るので打鍵毎でも許容範囲。大規模プロジェクト なら debouncer を追加検討。
  19. signatureHelp の ParameterInformation に documentation を渡し忘れる。
    LSP 応答側で parameter.documentation を送っても、VS Code 拡張側で new vscode.ParameterInformation(label) と 1 引数だけで作ると popup に 出ない。第 2 引数 (MarkdownString) に p.documentation.value を包んで渡す。
  20. hspcmp の Kind は #deffunc#defcfunc を区別しない。
    両方とも dfnc として emit される。signatureHelp の書式切替 (括弧の有無) や命令/関数の意味的差異は、source から DeclKind を直接拾って HspSymbol に 保持しておく必要がある。
  21. HSP の変数は「最初の出現位置」が定義扱い。
    x = 10 を 3 箇所で書いても hspcmp は 1 個の dvar エントリしか emit しない (一番上)。DocCommentParser の ±2 行マッチング条件により自然と 「最初の doc 勝ち」になるが、これは HSP の semantics と一致。 後続の再代入上の ;;; は silently 無視される。

8. 拡張方法

新しい LSP メソッドを追加する

  1. nhspc/HspLanguageServer/LspServer.csHandleRequest switch に case 追加
  2. ハンドラメソッドを実装 (例: HandleReferences(ps))
  3. capabilities にプロパティ追加 (BuildInitializeResult)
  4. nhspc/vscode-nhsp/src/hsp-lsp-client.js にラッパメソッド追加
  5. extension.js で VS Code Provider 登録 (vscode.languages.registerReferenceProvider 等)

新しい変数型の書換サポート

  1. hsp3net/hsp3code.cpphsp3dap_set_var_<type> を export として追加
  2. DLL の hsp3debug_dap.cpp で:
    • 関数ポインタ型を定義
    • g_fn_set_<type> グローバル追加
    • debugini で GetProcAddress 解決
    • handle_command("set_var"/"evaluate") で HSPVAR_FLAG を見て dispatch
  3. HSPVAR_FLAG 定数を hsp3debug_dap.cpp の enum に追加

新しいセマンティックトークンカテゴリ

  1. LspServer.csTokenTypes 配列に追加 (順序 = ABI)
  2. 対応する TYPE_<X> 定数追加
  3. ClassifyIdentifier にロジック追加
  4. extension.jsSemanticTokensLegend を同じ順に揃える

新しい DAP イベント/コマンド

  1. DLL の handle_command に case 追加 or debug_notice で evt 送信
  2. adapter の DapSession.HandleRequest に case 追加
  3. adapter の DebuggeeBridge.SendRequest で同期往復
  4. 必要なら VS Code capabilities を Initialize で true に