対象読者: コア実装に手を入れる開発者。ユーザー向けセットアップや機能紹介は IronHSP 仕様書 §11 参照。
最終更新: 2026-04-20 / ブランチ: update-hsp3net / 関連コミット: a025002b 〜 1e5a3fee
VS Code 統合は 3 つの独立した機能の集合体で、それぞれ別プロセス/別バイナリで動作します:
| 機能 | バイナリ | プロトコル | 言語 | LOC |
|---|---|---|---|---|
| デバッガ (DAP) | nhspdap.exe + hsp3debug_dap_64.dll + hsp3cl_net_dbg_64.exe | DAP over stdio + 名前付きパイプ | C# + C++ | ~800 |
| 言語サーバ (LSP) | hspls.exe | LSP over stdio | C# | ~600 |
| 色分け (TextMate) | — | JSON grammar | JSON | ~150 |
これら全てを vscode-nhsp 拡張 から起動/接続します (既存の .nhsp 拡張に .hsp 機能を追加した形)。
| パス | 役割 |
|---|---|
hsp3net/hsp3code.cpp | HSP3_DAP_MODE 下で BP フック + set_var_* export (既存ファイルへの追加) |
hsp3net/win32/hsp3cl.cpp | hsp3debug.dll ロード時の GetProcAddress を x64 bare name + x86 decorated の両対応に / hsp3cl_stop() の GetMessage ループを runmode check に変更 |
hsp3net/win32/main.cpp | HSP3_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 追加 |
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 の問題が発生しない。
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(); }
// 修正前: 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 なら即抜ける
#ifdef HSP3_DAP_MODE
// 子プロセスで stdout を redirect される時に block-buffered になって
// mes 出力が end まで届かない問題を回避
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
#endif
名前付きパイプ \\.\pipe\hsp3dap_<pid>_evt / _cmd、UTF-8 改行区切り (LF) JSON。
{"evt":"ready"}
{"evt":"stopped","reason":"entry|breakpoint|step|pause|exception","line":N,"file":"path"}
{"evt":"terminated","exit":N}
{"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 は相対パスを送ってくるため)。
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);
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)
VS Code 拡張が hspls.exe を spawn。拡張からの LSP 要求に応じて、
hspls が hspcmp64.exe を subprocess で呼び出し、出力を LSP レスポンスに変換する。
-ll / -lv / -ls)出力書式: <kind> <name> <line>:<file>
| kind | 意味 | LSP SymbolKind | Semantic type |
|---|---|---|---|
dfnc | #deffunc / #defcfunc (ユーザ定義関数) | Function (12) | function |
dexc | #func / #cfunc (外部 DLL 関数) | Method (6) | method |
dvar | 変数 (dim / sdim / 初代入) | Variable (13) | — (grammar 任せ) |
dlab | *label | Variable (13) | — |
dmac | #define / #const / #enum / #func alias | Constant (14) | 複雑 (下記) |
dmod | #module 名前 | Module (2) | — |
dmac の分類 — ファイル由来で意味が変わる| File | 実態 | Semantic type |
|---|---|---|
hspdef.as | HSP3 ランタイム定数 (screen_normal, ginfo_mx 等) | macro (ビルトイン扱い) |
その他の .as | DLL binding の #define alias / #func エイリアス | method (外部 DLL 扱い) |
.hsp | ユーザの #define / #const | function (ユーザ定義扱い) |
-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行目)
初期実装では File.WriteAllText(absPath, text) で本ファイルを上書きしていたが、
Explorer の別エディタで開いているファイルと衝突する、BOM が勝手に入る等の不具合の温床になる。
現在は同じディレクトリに foo.__hspls.hsp のサイドカー temp file を書いて
hspcmp に食わせ、出力の file フィールドを本ファイル名に rewrite した後 temp を削除する。
同じ dir に書く理由は、HSP の #include "common/..." 等の相対 path 解決を壊さないため。
LSP の textDocument/semanticTokens/full で、識別子単位の 4 分類色分けを提供。
TextMate grammar の regex では判別不能な「ユーザ定義 vs 外部 DLL」を実シンボル情報で区別する。
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: [] (未使用)
// 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;
独自 mini-lexer。行単位で walk、;/////* *//"..."/{"..."} をスキップ。
識別子は [A-Za-z_][A-Za-z_0-9@]* (@ は name@module の区切りとして含める)。
// token = [deltaLine, deltaStart, length, typeIdx, modifierMask]
// deltaLine = 前トークンとの行差
// deltaStart = 同じ行なら前トークンとの列差、違う行なら行頭からの列
// 全 token を (line, col) 昇順でソート済であること
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。
foo.__hspls.hsp) で hspcmp を実行するため、symbol の File
フィールドは temp 名で返ってくる。DocCommentParser は basename 比較するが、
temp→real のリネームが済んだ後で呼ぶ必要がある。初期実装では順序ミスで
doc が attach されない不具合があった (修正済)。
```hsp
(function) distance
```
<description>
**Parameters:**
- `name` — desc
- ...
**Returns:** ...
_— file.hsp:NN_
LSP の textDocument/signatureHelp で、関数呼び出し中に
シグネチャ + activeParameter を返す。
宣言行を 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) のような型修飾内の , を誤認しないように。
カーソル位置から source を backward scan:
( を探す () でカウント減)( とカーソル間の top-level コンマ数が activeParameter文字列リテラル "..." の中の ( / ) / , は無視。
制御構文キーワード (if/else/repeat/loop/
goto/gosub/return/end 等) は関数として
扱わない (偽陽性回避)。
統一せず 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)" (括弧あり)
LSP の ParameterInformation は label と documentation
(optional) を持つ。activeParameter のときに VS Code が下部に表示する。
各 SigParam に対応する DocParam を検索して attach:
#func 等) なら i 番目の
SigParam に i 番目の DocParam をペアリングnew vscode.ParameterInformation(label, doc)
の第 2 引数を確実に設定すること (v0 で踏んだバグ)。
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 で判定:
HSP_DECL_RX: #deffunc / #defcfunc / #func /
#cfunc / #cfuncd/#cfuncf/#cfuncst /
#comfunc / #modfunc / #modcfunc / #const /
#define / #enumHSP_VAR_DECL_RX: dim / sdim / ddim /
ldim / dim64 / wdim / dimtypeHSP_VAR_ASSIGN_RX: ^\s*([A-Za-z_]\w*)\s*=(?!=)
(== や arr(0) = 1 を除外)関数系は戻り値有無を kind で判定 (#defcfunc/#cfunc*/#modcfunc
は @return 行付き)。変数系・暗黙宣言は description 1 行のみ。
nhspdap/DapSession.SetBreakpoints で各 BP 行を簡易チェック:
| 条件 | verified | VS Code 表示 |
|---|---|---|
| 実行可能な行 | true | 🔴 赤丸 (ヒット予定) |
空行 / ; / // / # で始まる行 | false | ⚪ グレーホロー丸 + メッセージ |
実行可能行の判定は heuristic (regex ^\s*[^;/#\s] 相当)。
ブロックコメント /* */ の中身は不正確に true 返すが許容。
unverified な BP は DLL への set_bp コマンドの lines 配列に
含めない → bp_count が余分にインクリされず runtime のホットパスを
軽く保てる。
# 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
# 拡張自体は node.js なのでビルド不要
# VS Code で nhspc/vscode-nhsp/ を開いて F5 で Extension Development Host が起動
hspls.exe / nhspls.exe がロックされて再ビルド失敗する。
taskkill /F /IM hspls.exe で殺してからビルドすること (dev host は自動再起動する)。
-d だけでは DLL がロードされない。-d はデバッグ情報を .ax に埋め込むだけ。hsp3debug.dll がロードされるためには
HSPHED_BOOTOPT_DEBUGWIN が立っている必要があり、これは -w (debug window force) で立つ。
結論: デバッグ対象は必ず hspcmp64 -d -w -i foo.hsp でコンパイルする。
ERROR_BROKEN_PIPE (109) が ReadFile 直後に発生。2 本の simplex pipe に分けるのが唯一の解。
pv->pt は「文字列バッファへの char*」であって「バッファ本体」ではない。
書換は sbStrCopy(&pv->pt, new_value) で行う (可変長なので再確保が必要)。
Cannot read properties of undefined) で落ちる。
setBreakpoints 時に basename → absolute path マップを保持して、stackTrace 時に絶対化する。
fputs(mes, stdout) は end/exit まで
溜まる。setvbuf(stdout, NULL, _IONBF, 0) で runtime 起動時に無効化。
__declspec(dllexport) 変数は動く、ただし .cpp が /clr でない時のみ安全。<CompileAs>CompileAsCpp</CompileAs> でネイティブビルド指定。
/clr ファイルに dllexport 変数を書くと AppDomain 分離等で挙動が不安定。
_locked サフィックスで「lock 済前提」を明示する命名規則で統一。
languageModelTools は canBeReferencedInPrompt で
modelDescription + toolReferenceName 必須。Extension CANNOT register tool エラー。
.hsp → hsp 言語マッピングが無いと BP を置けない。contributes.languages に {id:"hsp", extensions:[".hsp"]} を
登録しないと、breakpoints: [{language:"hsp"}] が効かない。
_bp_target = 1 のような明示的な BP 行を用意してもらう想定。
dfnc でも dmac でもなく dexc。(dfnc|dlab|dvar|dmac|dmod) だけでパースすると GetTickCount 等が
未分類になる (method カラーが出ない)。dexc を必ず含める。
j:\HNWorks\... 形式の Windows パス。JSON 文字列に
直入れすると \H \t 等が無効エスケープとして扱われ、adapter の
JObject.Parse が silently fail。try/catch で continue されるためエラー
ログにも出ず、VS Code から見ると「BP ヒットしても何も起きない」状態に。
相対パスでコンパイルしていた頃は地雷が見えなかった。必ず json_esc() を通す。
g_breakpoints が空のため reason="breakpoint" に
分類できない。解決: debugini を g_start_event で待機させ、
adapter の configurationDone 受信時に {"cmd":"start","stopOnEntry":N}
を送って DLL 側が初めて force_step 要否を判断する流れに変更。ついでに
launch.json の stopOnEntry が実際に効くようになる。
OutputDataReceived /
ErrorDataReceived イベント + BeginOutputReadLine / BeginErrorReadLine で
async 読み取りにする。
-i (UTF-8 入力モード) を付けても ERROR MESSAGE は実行環境の ANSI。
.NET 側で StandardOutputEncoding = GetEncoding(ANSICodePage) で受け取り、
内部で UTF-16 に変換してから VS Code (UTF-8) へ渡す。一方 HSP ランタイム
(HSPUTF8 ビルド) の stdout は UTF-8 なので読み取り側を使い分ける。
MessageBoxA + UTF-8 バイト列はエラーメッセージ文字化けの温床。MultiByteToWideChar(CP_UTF8)
で UTF-16 変換してから MessageBoxW を使う。
parameter.documentation を送っても、VS Code 拡張側で
new vscode.ParameterInformation(label) と 1 引数だけで作ると popup に
出ない。第 2 引数 (MarkdownString) に p.documentation.value を包んで渡す。
#deffunc と #defcfunc を区別しない。dfnc として emit される。signatureHelp の書式切替 (括弧の有無)
や命令/関数の意味的差異は、source から DeclKind を直接拾って HspSymbol に
保持しておく必要がある。
x = 10 を 3 箇所で書いても hspcmp は 1 個の dvar エントリしか emit
しない (一番上)。DocCommentParser の ±2 行マッチング条件により自然と
「最初の doc 勝ち」になるが、これは HSP の semantics と一致。
後続の再代入上の ;;; は silently 無視される。
nhspc/HspLanguageServer/LspServer.cs の HandleRequest switch に case 追加HandleReferences(ps))BuildInitializeResult)nhspc/vscode-nhsp/src/hsp-lsp-client.js にラッパメソッド追加extension.js で VS Code Provider 登録
(vscode.languages.registerReferenceProvider 等)hsp3net/hsp3code.cpp に hsp3dap_set_var_<type> を export として追加hsp3debug_dap.cpp で:
g_fn_set_<type> グローバル追加debugini で GetProcAddress 解決handle_command("set_var"/"evaluate") で HSPVAR_FLAG を見て dispatchhsp3debug_dap.cpp の enum に追加LspServer.cs の TokenTypes 配列に追加 (順序 = ABI)TYPE_<X> 定数追加ClassifyIdentifier にロジック追加extension.js の SemanticTokensLegend を同じ順に揃えるhandle_command に case 追加 or debug_notice で evt 送信DapSession.HandleRequest に case 追加DebuggeeBridge.SendRequest で同期往復Initialize で true に