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
stdin/stdout で JSON-RPC をやりとりする。ログは stderr に出る。
静的型検査
診断は実行系とまったく同じ静的型検査 (static-analysis.md §2・検出対象は check.md「検査内容」) を動かす。ドキュメント単位・起動オプションのいずれでも水準を切り替えられない — 実行系と同じ水準を常にエディター上で見せることで、「エディターでは緑だが実行できない」という状態を作らないためである。
プロトコル
- トランスポート:
Content-Length:ヘッダー付き JSON-RPC 2.0 - 起動シーケンス:
initialize→initialized→ 通常運用 →shutdown→exit - 位置の符号化: 行・列はともに 0 始まりで、列は UTF-16 符号単位で数える (LSP 3.x の既定)。要求 (カーソル位置・範囲) と応答 (診断・定義・参照・リネーム編集・シンボル・inlay hint の範囲) の両方が同じ数え方である。識別子は ASCII 限定 (language-spec.md §1.2) なので、その行の手前に非 ASCII の綴り (文字列リテラル・コメント) が無ければ列はバイト位置と一致するが、
x := "あ🎈い" + yのyのように手前に非 ASCII があるとずれる ("あ🎈い"は 10 バイト・4 符号単位)。
capabilities
initialize で返す主な capabilities:
| 機能 | 値 |
|---|---|
textDocumentSync |
1 (Full sync) |
semanticTokensProvider.legend |
highlight の意味分類から組む (lsp::protocol) |
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.hika・self-host した std モジュール) の仮想ドキュメント本文を返す (定義ジャンプ) |
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 は無視する。
編集途中ソースの扱い
_test.hika のテスト組込
ファイル名が _test.hika で終わるドキュメントでは、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 対象は当該ドキュメントに属する診断のみに絞る。
- 契約系の診断も push する: 検出の根拠 (static-analysis.md §2.1) による出し分けはしない。実行を止める診断はすべてエディターに出す (起動 のとおり条件は無い)。
lint 警告の同梱
静的検査 (Error) に加え、パースが通るドキュメントには hikari lint (lint.md) の所見も診断として同じ push に含める (source は hikari-lint、code はルール名 — unused / self-assign / single-assign / control-juxtaposition / non-tail-recur ほか)。構文エラーがあると lint はスキップする (構文エラーは静的検査が Error で出す)。自動修正可能な所見は code action (コードアクション) で修正できる。
診断には code (診断コード)・severity (重大度)・tags (不要・非推奨) を載せる。コードと重大度の割り当ては static-analysis.md と lint.md が定める。
範囲は発生位置の字句の綴り全体を指す。サーバーはドキュメントを字句解析して「開始位置 → 終端位置」の索引を作り、診断の位置から引く。索引に無い位置 (合成された位置など) は 1 文字幅にフォールバックする。
hover
textDocument/hover は、カーソル下の識別子の型情報を Markdown (MarkupContent) で返す。各対象は原則として先頭行に「型」(name: 型)、続けて「形 + 一行の意味」を出す (本節では 型 + 形 + 意味 と記す)。対象を次の系統に分けて解決する。
prelude 組込名
再束縛されていない print / if / range / Some 等。ユーザー定義識別子と揃えて 型 + 形 + 意味 を出す。
- 例:
print: {Any | Unit}とprint(s)と「文字列 s を改行付きで出力する。」。 - 情報源: prelude が持つシグネチャ表で、root 常在束縛する flat 名 (prelude.md 各表の「形」「意味」列) を網羅する。
- 型の導出: 関数組込は Hikari 型表記 (
{引数 | 結果}) の手書き、値束縛 (None/Less/Equal/Greater等) は処理系側の実値を内省して導出する (LSP 実装内部の作業であり、Hikari の値ではない)。
組込型名 (型語彙)
原始型 Int / Float / String / Bool / Bytes / Range、特殊型 Unit / Any / Never / Error、組込 variant の型 Ordering / Control、型グループと構造的 interface Number / Comparable / Copyable、型コンストラクター List / Option / Result / Future / OneOf。
- 出力形: 形 + 一行の意味のみを出す (例:
List(T)と「リストの型コンストラクター。…」)。型名にとっての「型」は名前そのものなので、他の対象のような先頭の型行は置かない — 形と重複して情報が増えないため。 - 位置: 型注釈位置 (
x: Int) と値位置 (v matches Int) のどちらでも同じ hover を出す。同じ名前が同じ型を指すため体裁を揃える。 - 情報源: 型語彙を型値として束縛する箇所の doc コメントで、prelude 組込名と同じ単一情報源から引く (手書きの写しを持たない)。
- 再束縛時: ユーザーがこれらの名を再束縛している場合はユーザー定義識別子として扱う (prelude 組込名と同じ方針)。
標準ライブラリ (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 各表の「形」「意味」列) を基にした手書きのシグネチャ表で、各モジュール直下スロットを網羅する。std 不透明型のメソッド (
timeのInstant/Durationほか) は下記「型メソッド」が定める体裁で出す。record 値として供給されるサブオブジェクトのメソッド (std: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) も対象。 - 中置形も対象: 演算子表 (language-spec.md §8.3) の中置
a + b/v matches Tの演算子位置でも dot 形と同じ hover を出す。universal method はレシーバー型が確定しなくても「形 + 意味」を出す。型固有メソッドは、レシーバーの静的型がメソッド集合の閉じた型 (原始スカラー・List/Tuple・variant・std 不透明型 — 関数・record・Anyは対象外) に確定し、その型のメソッド表に name があるときだけ出す (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) で出す。
export 一覧の公開名
export { greet, n: Int } (language-spec.md §13.2) に並ぶ公開名。export は宣言であって式ではないため、型の引き方を通常の参照と分ける。
- 型注釈なし (
export { greet }): 束縛定義の推論型を通常の参照と同じname: 型で出す (例:greet: { String | String })。 - 型注釈あり (
export { n: Int }): 推論型ではなく封印契約型 (language-spec.md §17.6) を出す。importer から見えるのは注釈した型だけなので、契約型の方が宣言の意味に忠実になる。 - 型注釈自体 (
export { t: Text }のText): 通常の型参照として扱い、type定義名の hover (上記) に委ねる。 - 未定義名:
exportに並ぶが束縛が無い名では何も返さない。 exportキーワード: 識別子ではないため対象外。
export の公開名は definition / references / rename / documentHighlight でも束縛への参照として扱う。rename は公開面が壊れないよう export 一覧の同名も併せて置換する。
テストライブラリ組込
test / suite / assert (test.md「テストライブラリ」)。ファイル名が _test.hika で終わるドキュメントに限り、hikari test がファイルスコープに供給する束縛 (test.md「収集」) として、prelude 組込名と揃えた 型 + 形 + 意味 を出す。
- 出力形:
test/suiteはフラット名、assertは namespace 概要、assert.equal等のメンバーはassert.<member>: 型。 - 通常ファイル:
std:test/std:assertのimport束縛越しに標準ライブラリメンバー (上記) として扱う。 - 再束縛時: ユーザーがこれらの名を再束縛している場合はユーザー定義識別子として扱う。
対象外
文字列リテラルや、type 名以外を指す型注釈の位置は対象外で null を返す。
補完
textDocument/completion は 2 つの位置で候補 (CompletionItem[]) を返す。
- ドット直後 (
recv./recv.<部分名>): レシーバーのメンバー (レシーバーの系統) - ハンドラーの操作スロット名 (
handle { … } { <ここ> |}): そのブロックが起こす操作 (ハンドラーの操作スロット名)
どちらでもない位置 (識別子の途中・行頭など) では空配列を返す。候補のフィルター (部分名による絞り込み) はクライアント側が行うため、サーバーは部分名に依らず全候補を返す。
trigger character はドット (.) だけである。操作スロット名の位置に起動を促す記号は無く、クライアントが明示に補完を要求したときに答える。
末尾ドットのパース回避
末尾ドット (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に「一行の意味」を載せる。
サブオブジェクトのメソッドや演算子メソッド糖衣 (+ 等) は対象外。レシーバーが解決できない位置では空配列を返す。
ハンドラーの操作スロット名
handle { … } { … |} の操作スロット名の位置では、そのブロックが実際に起こす操作を Function 種別で出す。Detail に効果ラベルと操作スロットの引数個数 (操作のパラメーター数 + 継続の 1) を載せる。引数個数を引けない操作はその表示を省く。
出すのは宣言された操作だけである。効果を起こす呼び先の名前がすべて候補になるわけではない — 横取りは呼び出し地点で呼び先の実体を差し替える仕組みなので、実体が実行系に無いメンバー (相対 import・pkg: のメンバー) は名指しても捕まらない。それらを出すと、書いても働かない操作スロットの名前を勧めることになる。
ブロックが起こしていない操作は出さない。操作スロットとしては書けるが何も捕まえないためである。既に書き終えた操作スロットの名前も出さないが、カーソルが乗っているスロットの名前は残す — 綴り終えた名前の上で補完を引き直したときに候補が空になるのを避ける。
handle が書き手の束縛で覆われている場合 (language-spec.md §5 の解決順) は組込のハンドラーではないので、候補を出さない。操作スロットの位置でも閉じ括弧をまだ打っていないハンドラーはパースが通らないため、そこでは候補が出ない。
整形
textDocument/formatting は、ドキュメント全体を hikari format (format.md / format パッケージ) と同じ実体で整形し、ドキュメント全体を覆う 1 件の TextEdit (先頭 (0,0) から最終行末尾までを整形後ソースで置換) を返す。
- 整形オプション:
tabSize/insertSpacesは受理するが、hikari formatが固定 2-space 整形のため無視する。 - 行幅: CLI
hikari formatと同じく、ファイルのあるディレクトリから祖先探索したhikari.tomlの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.toml の 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 (代入形) は宣言ではなく既存束縛への代入なので (language-spec.md §6.1)、シンボルにするとその名前を宣言した位置と重複する。よって対象外 (宣言 := / 分解束縛 / 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::type_at、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 先ファイルの先頭。"std:<mod>"はここでは解決しない — ファイル系の道に無いので、下記「標準ライブラリの着地点」が扱う。 - prelude self-host メンバー — rebind されていない prelude.hika 定義の大域名・型メソッド。処理系埋め込みの prelude.hika を指す仮想ドキュメント Location (
hikari-internal:/prelude.hika) を返す。クライアントはカスタム requesthikari/internalSource(params{uri}→ result{content}) で本文を取得して表示する — 手元に物理ソースが無い配布バイナリ単体でも定義先を開ける。 - 標準ライブラリの着地点 — std モジュール・そのメンバー・不透明型メソッド (下記)。
カーソルはリテラルの綴り全体で当たる
import の文字列リテラルは、開き引用符から閉じ引用符までどの桁に置いても同じ答えを
返す。判定はトークンの綴り (開始位置と排他的終端) で行い、中身の長さでは行わない —
引用符 2 つ分ずれるうえ、エスケープを解いた中身はさらに短く、補間つき ("a${b}c") は
中身を持たないので長さが 0 になる。
レシーバーの解決は閉路で打ち切る
obj.slot の解決は、受け手の束縛の右辺 (呼び出しならその関数側) と型注釈を辿って帰着先のオブジェクトを探す。辿った先が元の束縛へ戻る形 (acc := acc(obj(acc)) の acc.x・型注釈だけを持つスロットの型が自分を指す {s: s | s.x}) は閉路なので、訪れた受け手を覚えて 2 度目に入ったら未解決 (null) として打ち切る。hover と補完も同じ解決機構を使うため同じく打ち切る。
受け手は覆いの型構築子を剥がしてから解決する
Borrowed(T) / 多重度型 (AtMostOnce / ExactlyOnce / Consuming。language-spec.md
§17.8)・効果型 (Effect / EffectConduit。同 §17.9)・refinement 型 (同 §17.7) は
基底型を覆うだけでメンバー集合を変えない。したがって受け手がこれらで包まれた位置でも、
解決は基底型で行う — 型検査のメソッド解決と同じ基底である。剥がさないと注釈を 1 つ
書き足しただけで定義ジャンプと hover・補完が黙る。
- 剥がすのは覆いだけである。
List(T)/Option(T)のような容れ物はメンバー集合が
基底と違うので剥がさない。 - 重ねた覆いは尽きるまで剥がす(
Borrowed(refinement { v: T | … })ほか)。別名を
経由した形 (type B := Borrowed(T)) も同じ。 - refinement の述語スロット名は基底のメンバーではない。
refinement { v: T | … }は
vではなくTのメンバーへ帰着する。 - 再束縛した綴りは覆いとして扱わない。
Borrowedを利用者が束縛したなら、それは
その束縛であって覆いではない (同じ規律が hover の組込説明にもかかる)。 - 束縛そのものの型表示は剥がさない。
{p: Borrowed(P) | …}のpの hover は
p: Borrowed(P)のままである — 覆いは注釈が述べた制約であって、消して見せるものでは
ない。剥がすのはメンバーを引くときだけである。
std の可変ホスト資源のハンドル (fs.File / arr.Array 等) はカタログ上それ自体が
多重度型 (ExactlyOnce(File)) なので、この規則は利用者が Borrowed と書かない位置にも
効く。
標準ライブラリの着地点は 3 段で落とす
item 3 の対象は、std モジュールそのもの (import の "std:<mod>" リテラル)・モジュール
直下メンバー (math.abs / fs.read)・不透明型メソッド (Instant / Array / SortedMap
等) である。同じ名前を次の順で解き、最初に当たった位置を返す。
- 処理系ソースツリーの
.hika実ファイル —stdsrc/stdmod/に実体を持つメンバー
(cli/math/term/event/term/screen/timeの self-host 部分)。#|doc
ブロックの直後の束縛行 (abs := …) を指す file Location を返す。手元でソースを編集
している最中に行が合うのはこれなので、先に来る - 埋め込んだ self-host ソースの仮想ドキュメント — 同じ
.hikaを処理系へ焼き込んだ
写し。hikari-internal:/std/<mod>.hikaを指す Location を返し、本文は prelude と同じ
hikari/internalSourceが配る。ツリーを持たない配布バイナリ単体でもここまでは働く - ランタイム層の実装行 —
.hikaに実体を持たないメンバー (fs.read/json.parse/
math.sqrt/Instant.year等)。実装は Rust なので、rt/src/*.rsのうちその名前を
組み立てている行を指す file Location を返す
item 3 の位置は 2 つの出所から引く。第 1 は rt/src/*.rs に現れる "<限定子>.<名前>"
ちょうどの文字列リテラルで、組込を作る位置が名前を文字列で持つことに乗っている
(試験モジュールの中は読み飛ばす)。第 2 は「メンバー → ランタイム層の値の綴り」の対応表で、
名前を工場へ渡して作るメンバー (math.sqrt) はその工場の定義行に着地する。
1 と 3 は処理系ソースツリーの有無が条件である (処理系ソースツリーの探索)。
2 は焼き込みなので条件を持たない。
import の "std:<mod>" リテラルは同じ 3 段でモジュールの実装ファイルの先頭へ
落とす。std: を名乗るだけで既知でない綴りは解決しない (null)。
公開名とランタイム層の綴りは一致しないことがある (std:http/client の get の実体は
http.get)。段 3 は「公開名 → ランタイム層の値の綴り」の対応を先に通すので、別名で
公開しているモジュールも着地する。
着地点を持たない std メンバー
std のカタログのうち、次のものだけが着地点を持たない。
- タグと定数 (
decimal.Ceiling/time.Monday/term/event.mouse_on等) — 値は
生成時に確定しており実行時に組み立てないので、指すべき 1 行が無い - 下位の名前空間そのもの (
map.sorted/map.insertion/term/event.kinds等) —
メンバーの入れ物であって実装ではない。その中のメンバー (m.sorted.of) は着地する hikari/doc.generated_notice— 値であって関数ではないので、ランタイム層が名前を持たない
この一覧は網羅である。 上記を除く全モジュール・全モジュールメンバー・全不透明型
メソッド・下位の名前空間の全メンバーは着地点を持ち、その飛び先は実在する。増えても
減っても検査が落ちる。
公開名が self-host の別名を指すメンバー (term/event の next_event の実体は
event_of) は、実体の表が持つその名前を辿って焼き込んだソースへ着地する。
分解束縛した std メンバー (import { sqrt } := "std:math" の sqrt) も、dot 形と同じ
着地点へ透ける。書き方を変えただけで飛び先が変わらないようにするためで、着地点が
1 つも無いときだけ分解束縛 LHS (ローカル定義) へ落ちる。
言語そのものの組込は定義を持たない
次のものは textDocument/definition が null を返す。
- flat 組込 (
print/if等) - 型語彙 (
Int/List/OneOf等の組み込み型名。型注釈・式位置とも) - 原始型メソッド (
xs.map等。ドット形・中置形とも) - universal method (
matches等。ドット形・中置形とも) - 組込 enum のタグ (
Option.Some等)
散文はこれらにもある。 hover とリファレンスは処理系が持つ表から出るので、
文面は変わらない。無いのは飛び先だけである — これらの実体はランタイム層にあるが、
ランタイム層の綴りは「どの組込か」を名前で名指していない (println は List.map と
同じ工場から出る) ので、指すべき 1 行が定まらない。
近い位置を当てない。 解決できないなら null を返す。誤った位置へ飛ばすのは、飛ばない
ことより悪い。
処理系ソースツリーの探索
処理系ソースツリーは次の順で探索する (最初に stdsrc/prelude.hika を含むものを採用)。
$HIKARI_REPO(hikari buildの本体ソース解決 build.md「Hikari リポジトリの位置解決」と同じ変数)。設定されていれば検証のみ行い、不正でも他候補へはフォールバックしない- ビルド時に記録されたソースパス — リポジトリからビルドした開発ビルドならそのリポジトリ。パスを削ってビルドした配布バイナリでは無効
- カレントディレクトリから上位へ最大 6 階層
- 実行バイナリ隣接の
../share/hikari(配布レイアウトの暫定慣例)
ソースツリーが無いときの挙動
見つからなければ 標準ライブラリの着地点の段
1 と 3 が無効になる。self-host した std メンバーは段 2 (焼き込んだ仮想ドキュメント) が
受けるので飛べるままで、.hika に実体を持たないメンバーは定義なし (null) になる。
分解束縛した std メンバー名の参照は着地点が 1 つも無いとき分解束縛 LHS へ落ちるので、
ソースを同梱しない環境でも他の解決は退行しない。
位置索引は初回要求時に構築してプロセス内でキャッシュする。段 1 は発見したツリーの
stdsrc/stdmod/*.hika の #| doc ブロックを、段 3 は同じツリーの rt/src/*.rs を
走査する (焼き込みではなく実ファイルから引くため、ジャンプ先ソースの行ずれと食い違わない)。
段 2 は焼き込んだソースを同じ #| doc ブロックの規則で読むので、段 1 と同じ名前で
引ける — ツリーの有無で着地点の名前が変わらない。