hikari LSP サーバ (hikari lsp) 仕様
本書は hikari lsp の仕様を定める。起動ディスパッチ全体は hikari-command.md、言語の意味論は language-spec.md、静的検査は static-analysis.md を参照。
stdio 上で Language Server Protocol 3.x をしゃべる。vscode-hikari 拡張から spawn されることを主用途とする。
起動
$ hikari lsp [--strict]
stdin/stdout で JSON-RPC をやりとりする。ログは stderr に出る。フラグなしの既定は非 strict。
strict モードの有効化
--strict を付けると診断が strict モード で動き、健全・漸進検査 (check.md「検査内容」) に strict 規則 (static-analysis.md §3) を上乗せする (追加検出は check.md「strict 型検査」と同じ)。vscode-hikari 拡張の hikari.strict 設定 (既定 false) が ON のとき、拡張はこのフラグ付きで LSP を spawn する。
--strict が無くても、診断対象ドキュメントが header #{hikari strict} / #[hikari strict] (language-spec.md §13.1) を宣言していれば、そのドキュメントは strict 診断になる。これにより vscode-hikari の hikari.strict が false でも、strict を宣言したファイルだけは strict として検査される。
実効的な strict 判定はドキュメント単位で次のとおり (本書では 実効 strict と呼ぶ)。以降の 診断 はこの定義を参照する。
実効 strict = 「--strict 付きで起動」 OR 「ドキュメント header の strict 属性 (#{hikari strict} / #[hikari strict])」
プロトコル
- トランスポート:
Content-Length:ヘッダ付き JSON-RPC 2.0 - 起動シーケンス:
initialize→initialized→ 通常運用 →shutdown→exit
capabilities
initialize で返す主な capabilities:
| 機能 | 値 |
|---|---|
textDocumentSync |
1 (Full sync) |
semanticTokensProvider.legend |
defaultLegend (highlight pkg 由来) |
semanticTokensProvider.full |
true |
definitionProvider |
true |
referencesProvider |
true |
hoverProvider |
true |
completionProvider.triggerCharacters |
["."] |
documentFormattingProvider |
true |
codeActionProvider.codeActionKinds |
["quickfix", "source.fixAll"] |
documentSymbolProvider |
true |
renameProvider.prepareProvider |
true |
inlayHintProvider |
true |
documentHighlightProvider |
true |
serverInfo.name |
"hikari-lsp" |
サポート method
| method | 種別 | 説明 |
|---|---|---|
initialize |
request | capabilities 返却 |
shutdown |
request | shutdown 受領フラグを立てて null 応答 |
textDocument/semanticTokens/full |
request | ハイライト用 token 配列を返す |
textDocument/definition |
request | 定義位置 (go-to-definition) を返す (定義ジャンプ) |
textDocument/references |
request | 識別子の参照箇所一覧を返す |
textDocument/hover |
request | カーソル下の識別子の型情報を返す (hover) |
textDocument/completion |
request | ドット直後のメンバ補完候補を返す (補完) |
textDocument/formatting |
request | ドキュメント全体を整形した全文置換 TextEdit を返す (整形) |
textDocument/codeAction |
request | lint 自動修正の quickfix / source.fixAll を返す (コードアクション) |
textDocument/documentSymbol |
request | ファイル内のトップレベル宣言を階層シンボルで返す (シンボル一覧) |
textDocument/prepareRename |
request | カーソル位置がリネーム可能なら対象範囲を返す (リネーム) |
textDocument/rename |
request | シンボルの全出現を新名へ置換する WorkspaceEdit を返す (リネーム) |
textDocument/inlayHint |
request | 型注釈なし束縛に推論型のインラインヒントを返す (インレイヒント) |
textDocument/documentHighlight |
request | カーソル下シンボルの同名出現を返す (出現ハイライト) |
hikari/internalSource |
request | 処理系埋め込みソース (prelude.hikari 等) の仮想ドキュメント本文を返す (定義ジャンプ) |
initialized |
notification | no-op |
exit |
notification | プロセス終了 (shutdown 後ならクリーン) |
textDocument/didOpen |
notification | ドキュメント登録 |
textDocument/didChange |
notification | Full sync で本文置換 |
textDocument/didClose |
notification | ドキュメント破棄 |
$/cancelRequest |
notification | no-op (semanticTokens/full は同期完結) |
未対応 method の request にはエラー (-32601 MethodNotFound) を返す。未対応 notification は無視する。
編集途中ソースの扱い
パース失敗時のリトライ
semantic tokens のためにパースする際は、通常モードで失敗したら SkipHeader() でリトライする。#{hikari} は省略できるため通常はリトライ不要だが、#[hikari] を打ち途中のソース等でもハイライトが切れないようにする。
_test.hikari のテスト組込
ファイル名が _test.hikari で終わるドキュメントでは、hikari test がファイルスコープに供給する test / suite (std:test) と assert (std:assert) (test.md「テストライブラリ」/「収集」) を、import 無しでも builtin として意味分類する (hikari test 文脈に限った着色)。通常ファイルでは着色せず、これらは import 束縛越しに標準ライブラリメンバーとして扱う。
診断
publishDiagnostics の push
textDocument/didOpen / textDocument/didChange を受けると、当該ドキュメントを entry とした静的検査 (check.md / static-analysis.md §1) を実行し、その結果をサーバ → クライアントの push 通知 textDocument/publishDiagnostics で送る。
- クリーン時: 空配列を送って既存マーカを消す。
- push の範囲: import 先 (別ファイル) の診断は、そのファイル自身を開いたときの検査で出るため、push 対象は当該ドキュメントに属する診断のみに絞る。
- strict 規則の追加適用: 健全・漸進検査がクリーンなドキュメントに対して strict 規則 (check.md「strict 型検査」) を追加適用するのは、実効 strict が真のとき (
--strict付き起動、またはドキュメントが header#{hikari strict}/#[hikari strict]を宣言しているとき)。strict 診断も push する。
lint 警告の同梱
静的検査 (Error) に加え、パースが通るドキュメントには hikari lint (lint.md) の所見も Warning 診断として同じ push に含める (source は hikari-lint、code はルール名 — unused / self-assign / single-assign / control-juxtaposition / non-tail-recur)。構文エラーがあると lint はスキップする (構文エラーは静的検査が Error で出す)。自動修正可能な所見は code action (コードアクション) で修正できる。
hover
textDocument/hover は、カーソル下の識別子の型情報を Markdown (MarkupContent) で返す。各対象は原則として先頭行に「型」(name: 型)、続けて「形 + 一行の意味」を出す (本節では 型 + 形 + 意味 と記す)。対象を次の系統に分けて解決する。
prelude 組込名
再束縛されていない print / if / range / Some 等。ユーザ定義識別子と揃えて 型 + 形 + 意味 を出す。
- 例:
print: {Any | Unit}とprint(s)と「文字列 s を改行付きで出力する。」。 - 情報源: prelude が持つシグネチャ表で、root 常在束縛する flat 名 (prelude.md 各表の「形」「意味」列) を網羅する。
- 型の導出: 関数組込は hikari 型表記 (
{引数 | 結果}) の手書き、値束縛 (reflect/None/Less/Equal/Greater) は実値の反射 (reflect.type_of相当) で導出する。
標準ライブラリ (std:*) のメンバー
import math := "std:math" のような束縛越しの math.sqrt、import [read, write] := "std:fs" のような分解束縛名 (read) の上で、prelude と揃えた 型 + 形 + 意味 を出す。
- 例:
math.sqrt: {Float | Float}とmath.sqrt(x)と「平方根 (Float)。…」。 - モジュール識別子自体:
import math := "std:<name>"で束縛した識別子自体 (math) の上ではモジュール概要を出す。 - 情報源: std の各モジュール文書 (std/<name>.md 各表の「形」「意味」列) を基にした手書きのシグネチャ表で、各モジュール直下スロットを網羅する (サブオブジェクトのメソッド —
timeのInstant/Durationやrandomジェネレータのint/float等 — は将来対応)。
ユーザ定義識別子
束縛・スロット・その参照。静的型検査 (check.md) と同じ推論で得た型を name: 型 で出す (例: x: Int)。型が確定しない (Unknown) 位置では何も返さない。
型メソッド (prelude method, dot プロパティ・中置形)
xs.map / s.length / o.unwrap 等。レシーバ式の静的型を check.md と同じ推論で求め、確定するときその推論型を先頭行に、続けて「形 + 一行の意味」を出す。
- 例:
[1, 2, 3].mapのmap上でList(Int)とxs.map(block)と意味。 - 情報源: prelude が持つ型メソッドのシグネチャ表で、原始型ごとのメソッド (prelude.md 各型メソッド表の「形」「意味」列) を網羅する。受け手の型に依らない universal method (
compare/matches/inspect) も対象。 - 中置形も対象: 並置 dispatch
xs map { … }/v matches T(language-spec.md §8.1) の name 上でも dot 形と同じ hover を出す。universal method はレシーバ型が確定しなくても「形 + 意味」を出す。型固有メソッドは、レシーバの静的型がメソッド集合の閉じた型 (原始スカラー・List/Tuple・variant・std 不透明型 — 関数・record・Anyは対象外) に確定し、その型のメソッド表に name があるときだけ出す — これらの型の値は関数でないため並置はメソッド dispatch にしかならない (static-analysis.md §2.3 と同じ根拠)。レシーバがリテラル・複合式で束縛型を引けないケースは将来段階。 - 未確定時: レシーバ型が確定しない位置では何も返さない (中置の型固有メソッドは通常の識別子 hover に委ねる —
double nの n のような引数位置の変数参照はそのまま変数として出る)。
ユーザ定義オブジェクトのメンバ
p.x 等の dot スロット。レシーバが定義オブジェクトに帰着するとき、そのスロットの推論型を prop: 型 で出す (import 越しも解決する)。
type 定義名とその参照
type Text := … の Text、および型注釈位置の参照 x: Text / e: Text の Text 等。その定義を type Name := <基底型> で出す。
ここでは別名 Name を保たず基底 (展開) 型を見せる — 「型の中身 (定義) を示す」のが目的のため (例: type Text := {kind: String, s: String |})。type Key := Id のように右辺がさらに別の type 名なら、その参照名 (Id) で出す。
テストライブラリ組込
test / suite / assert (test.md「テストライブラリ」)。ファイル名が _test.hikari で終わるドキュメントに限り、hikari test がファイルスコープに供給する束縛 (test.md「収集」) として、prelude 組込名と揃えた 型 + 形 + 意味 を出す。
- 出力形:
test/suiteはフラット名、assertは namespace 概要、assert.equal等のメンバーはassert.<member>: 型。 - 通常ファイル:
std:test/std:assertのimport束縛越しに標準ライブラリメンバー (上記) として扱う。 - 再束縛時: ユーザがこれらの名を再束縛している場合はユーザ定義識別子として扱う。
対象外
文字列リテラルや、type 名以外を指す型注釈の位置は対象外で null を返す。
補完
textDocument/completion は、ドット (.) を trigger character として起動し、recv. / recv.<部分名> のドット直後でレシーバのメンバ候補 (CompletionItem[]) を返す。ドット文脈でない位置 (識別子の途中・行頭など) では空配列を返す。候補のフィルタ (部分名による絞り込み) はクライアント側が行うため、サーバは部分名に依らず全候補を返す。
末尾ドットのパース回避
末尾ドット (recv.) は構文上プロパティを欠くためパースが失敗する。これを避けるため、ドット直後に仮の識別子を挿入したソースを再パースして recv.<仮> の dot 式を構成し、hover と同じレシーバ解決機構で候補を集める。
レシーバの系統
レシーバを次の系統に分けて解決する。
- 標準ライブラリ (`std:*`) モジュール束縛 (
import math := "std:math"のmath.): そのモジュール直下メンバ (sqrt等、hover と同じ情報源) をFunction種別で出す。std モジュールは型メソッド・universal method を持たないため、それらは混ぜない。 - ユーザ定義オブジェクト/モジュールに帰着するレシーバ (
p./ import 束縛): そのスロットをField種別で出す (確定すればname: 型を detail に付す)。 - 静的型が確定するレシーバ (
xs./s.等): その型の record フィールド・原始型メソッド (prelude method)・universal method (matches/compare/inspect) を出す。Detailに「形」、Documentationに「一行の意味」を載せる。
サブオブジェクトのメソッドや演算子メソッド糖衣 (+ 等) は対象外。レシーバが解決できない位置では空配列を返す。
整形
textDocument/formatting は、ドキュメント全体を hikari fmt (format.md / format パッケージ) と同じ実体で整形し、ドキュメント全体を覆う 1 件の TextEdit (先頭 (0,0) から最終行末尾までを整形後ソースで置換) を返す。
- 整形オプション:
tabSize/insertSpacesは受理するが、hikari fmtが固定 2-space 整形のため無視する。 - 行幅: CLI
hikari fmtと同じく、ファイルのあるディレクトリから祖先探索したhikari.hikaのwidthを尊重する (format.md「設定ファイル」。設定が無い / ファイルパスでない URI は既定幅)。
次の場合は空配列を返してバッファを変更しない。
- 整形結果が元ソースと同一 (整形済み)
- ソースに構文エラーがある (整形中に走ってもバッファを壊さない)
- 未登録ドキュメント
コードアクション
textDocument/codeAction は、hikari lint (lint.md) の自動修正可能な所見に対する修正アクションを返す。現状の対象は control-juxtaposition (制御構造のタプル形) のみ。次の 2 種を返す。
- quickfix (電球): 要求
rangeにcontrol-juxtapositionの診断が重なるときに出す。isPreferred: true。 - source.fixAll: 保存時修正 (
editor.codeActionsOnSave) 用。ファイル内に修正対象があればrangeに依らず出す。
修正内容と条件
いずれも修正内容は同一で、hikari lint --fix (lint.md) と同じ実体 (format パッケージの制御構造並置化 + 整形) でドキュメント全体を書き換える WorkspaceEdit (全文置換 TextEdit 1 件)。行幅は 整形 と同じく hikari.hika の width を尊重する。
context.only が指定されたときは、その CodeActionKind に一致するアクションだけを返す (only の要素が対象 kind の接頭辞なら一致)。修正対象が無い / 構文エラー / 未登録ドキュメントでは空配列を返す。
シンボル一覧
textDocument/documentSymbol は、ファイル内のトップレベル宣言を DocumentSymbol[] の階層で返す (アウトライン / ブレッドクラム / シンボル移動の情報源)。各シンボルは range (宣言全体) と selectionRange (名前部分) を持ち、値がオブジェクトリテラルの宣言はその slot と body 内の束縛を children として入れ子にする。
対象と SymbolKind
name := exprの束縛・slot・分解束縛 ([a, b] := …) の各名 — 値がオブジェクト (body あり) なら Function、オブジェクト (body なし) なら Struct、大文字始まりは Constant、それ以外は slot なら Field / 束縛なら Variable。type Name := …→ Struct、enum Name := …→ Enum (slot-list 位置・body 位置のどちらの記法も対象)。
対象外
name = expr (再代入形) は、新規 mutable local の宣言と外側束縛の再代入を静的に区別できずシンボルが重複するため対象外 (明示宣言 := / 分解束縛 / slot / type・enum のみをシンボルにする)。破棄子 _ も対象外。構文が壊れていても可能な範囲でパースして部分的なシンボルを返す。未登録ドキュメントは空配列。
リネーム
textDocument/rename は、カーソル位置のシンボルの全出現 (宣言 + 参照) を新しい名前へ置換する WorkspaceEdit を返す。出現の収集は references (サポート method の逆適用) を再利用するため、bare identifier だけでなく dot property (obj.x) も揃って置換される。
リネーム可能な範囲
参照探索が現在のファイル単一のため、定義が現在のファイルにあるシンボルだけをリネーム可能とする。次はエラーを返す (VSCode はメッセージを表示)。
- 未解決 / builtin (定義が処理系内部)、定義が別ファイル (
import先) や prelude にあるシンボル — この 1 ファイルだけ書き換えると定義側と食い違うため。 - 新しい名前が hikari の識別子として不正 (空 / 数字始まり / 記号を含む)、またはリテラル語 (
true/false/nil)。
prepareRename
textDocument/prepareRename は prepareProvider を立てて公開し、リネーム前にカーソル位置がリネーム可能かを検証して対象の識別子範囲を返す (不可ならエラー)。
インレイヒント
textDocument/inlayHint は、明示的な型注釈を持たない `:=` 束縛に、静的推論した型を : T のインラインヒント (Kind=Type) として名前の直後に差し込む (例: n := 42 を n: Int := 42 のように見せる)。型は hover と同じ推論 (typecheck.TypeAt、import 越しも解決) で求める。
対象と非対象
対象は name := expr 束縛・分解束縛 ([a, b] := …) の各名・値つき slot (name := default)。次は出さない。
- 明示型注釈がある束縛 (
x: Int := …)。 - RHS が
{…}(関数/データオブジェクトリテラル) の束縛 — 型が構文上明白で: {Any | Any}のような冗長なヒントになるため。リスト[…]は要素型 (: List(Int)) に価値があるので出す。 - 推論結果が
Any(情報量ゼロ) や内部の型変数。破棄子_。
要求 range (ビューポート) 内の束縛のみを計算する。未登録ドキュメントは空配列。
有効/無効の切替
vscode-hikari 拡張の設定 hikari.inlayHints.enabled (既定 true) が OFF のとき、ヒントは返さない (空配列)。
- 初期値: 拡張は起動時に値を LSP の
initializationOptions({"inlayHints":{"enabled":<bool>}}) で渡す。 - 設定変更:
workspace/didChangeConfiguration({"settings":{"inlayHints":{"enabled":<bool>}}}) で通知する。 - 即時反映: サーバは値が変化したとき
workspace/inlayHint/refreshをクライアントへ送り、ウィンドウのリロードなしで表示/非表示を即時反映させる。
出現ハイライト
textDocument/documentHighlight は、カーソル下シンボルの同名出現 (宣言 + 参照) を現在のファイル内で返し、エディタが淡くハイライトする。出現収集は references (サポート method の「Definition の逆適用」) をそのまま再利用するため、bare identifier も dot property も揃う。Kind は付けず一律 Text 扱い (全出現を同色でハイライト)。builtin / 未解決 / 未登録ドキュメントは空配列。
定義ジャンプ
解決順
textDocument/definition は次の順で解決する。
- ユーザ定義シンボル — 束縛・slot・inner-name・分解束縛 LHS・
obj.slot・import 越し (mod.foo/ 分解束縛名の import 先透過)。importの文字列リテラル上では import 先ファイルの先頭。 - prelude self-host メンバ — rebind されていない prelude.hikari 定義の大域名・型メソッド。処理系埋め込みの prelude.hikari を指す仮想ドキュメント Location (
hikari-internal:/prelude.hikari) を返す。クライアントはカスタム requesthikari/internalSource(params{uri}→ result{content}) で本文を取得して表示する — 手元に物理ソースが無い配布バイナリ単体でも定義先を開ける。 - 処理系実装ソース — Go / hikari 実装の組込 (下記)。
処理系実装ソースの対象
item 3 の対象は次のとおり。
- flat 組込 (
print/if等) - 型語彙 (
Int/List/OneOf等の組み込み型名。型注釈・式位置とも) - 原始型メソッド (
xs.map等。ドット形に加え、レシーバ型が確定した閉じた型への中置形xs map { … }も — hover の中置形と同じ判定) - universal method (
matches等。ドット形に加え中置形v matches Tも) - std モジュールメンバ (
math.sqrt等、分解束縛名import [sqrt] := "std:math"の参照含む) - std 不透明型メソッド (
Instant/Array/SortedMap等のt.epoch_ms等)
解決条件は次のとおり。
- レシーバ型の確定: 型メソッド・不透明型メソッドはレシーバの静的型が確定する場合のみ (universal method はレシーバ型が確定しなくても全型共通の実装位置へ解決する)。
- ソースツリーの有無: 処理系ソースツリーが見つかるときだけ、その実装ソースの doc コメント位置 (Go の
//|ブロック) / 定義行 (std:math self-host の math.hikari) を指す file Location を返す。
処理系ソースツリーの探索
処理系ソースツリーは次の順で探索する (最初に internal/prelude/prelude.hikari を含むものを採用)。
$HIKARI_REPO(hikari buildの本体ソース解決 build.md「hikari リポジトリの位置解決」と同じ変数)。設定されていれば検証のみ行い、不正でも他候補へはフォールバックしない- ビルド時に記録されたソースパス — リポジトリから go build した開発ビルドならそのリポジトリ。
-trimpathビルド (配布バイナリ) では無効 - カレントディレクトリから上位へ最大 6 階層
- 実行バイナリ隣接の
../share/hikari(配布レイアウトの暫定慣例)
ソースツリーが無いときの挙動
見つからなければ 3 のフォールバックだけが無効になり、該当シンボルは定義なし (null)。ただし分解束縛した std メンバ名 (import [sqrt] := "std:math" の sqrt) の参照は分解束縛 LHS (ローカル定義) へフォールバックする — ソースを同梱しない環境でも他の解決は退行しない。
位置索引は初回要求時に発見したツリーの internal/prelude / internal/stdmod の doc コメント (//| / #|) を走査して構築し、プロセス内でキャッシュする (焼き込みではなく実ファイルから引くため、ジャンプ先ソースの行ずれと食い違わない)。