本文へ移動
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

stdin/stdout で JSON-RPC をやりとりする。ログは stderr に出る。

静的型検査

診断は実行系とまったく同じ静的型検査 (static-analysis.md §2・検出対象は check.md「検査内容」) を動かす。ドキュメント単位・起動オプションのいずれでも水準を切り替えられない — 実行系と同じ水準を常にエディター上で見せることで、「エディターでは緑だが実行できない」という状態を作らないためである。

プロトコル

  • トランスポート: Content-Length: ヘッダー付き JSON-RPC 2.0
  • 起動シーケンス: initializeinitialized → 通常運用 → shutdownexit
  • 位置の符号化: 行・列はともに 0 始まりで、列は UTF-16 符号単位で数える (LSP 3.x の既定)。要求 (カーソル位置・範囲) と応答 (診断・定義・参照・リネーム編集・シンボル・inlay hint の範囲) の両方が同じ数え方である。識別子は ASCII 限定 (language-spec.md §1.2) なので、その行の手前に非 ASCII の綴り (文字列リテラル・コメント) が無ければ列はバイト位置と一致するが、x := "あ🎈い" + yy のように手前に非 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 に含める (sourcehikari-lintcode はルール名 — unused / self-assign / single-assign / control-juxtaposition / non-tail-recur ほか)。構文エラーがあると lint はスキップする (構文エラーは静的検査が Error で出す)。自動修正可能な所見は code action (コードアクション) で修正できる。

診断には code (診断コード)・severity (重大度)・tags (不要・非推奨) を載せる。コードと重大度の割り当ては static-analysis.mdlint.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.sqrtimport { read, write } := "std:fs" のような分解束縛名 (read) の上で、prelude と揃えた 型 + 形 + 意味 を出す。

  • : math.sqrt: {Float | Float}math.sqrt(x) と「平方根 (Float)。…」。
  • モジュール識別子自体: import math := "std:<name>" で束縛した識別子自体 (math) の上ではモジュール概要を出す。
  • 情報源: std の各モジュール文書 (std/<name>.md 各表の「形」「意味」列) を基にした手書きのシグネチャ表で、各モジュール直下スロットを網羅する。std 不透明型のメソッド (timeInstant/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].mapmap 上で 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: TextText 等。その定義を 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:assertimport 束縛越しに標準ライブラリメンバー (上記) として扱う。
  • 再束縛時: ユーザーがこれらの名を再束縛している場合はユーザー定義識別子として扱う。

対象外

文字列リテラルや、type 名以外を指す型注釈の位置は対象外で null を返す。

補完

textDocument/completion は 2 つの位置で候補 (CompletionItem[]) を返す。

どちらでもない位置 (識別子の途中・行頭など) では空配列を返す。候補のフィルター (部分名による絞り込み) はクライアント側が行うため、サーバーは部分名に依らず全候補を返す。

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.tomlwidth を尊重する (format.md「設定ファイル」。設定が無い / ファイルパスでない URI は既定幅)。

次の場合は空配列を返してバッファーを変更しない。

  • 整形結果が元ソースと同一 (整形済み)
  • ソースに構文エラーがある (整形中に走ってもバッファーを壊さない)
  • 未登録ドキュメント

コードアクション

textDocument/codeAction は、hikari lint (lint.md) の自動修正可能な所見に対する修正アクションを返す。現状の対象は control-juxtaposition (制御構造のタプル形) のみ。次の 2 種を返す。

  • quickfix (電球): 要求 rangecontrol-juxtaposition の診断が重なるときに出す。isPreferred: true
  • source.fixAll: 保存時修正 (editor.codeActionsOnSave) 用。ファイル内に修正対象があれば range に依らず出す。

修正内容と条件

いずれも修正内容は同一で、hikari lint --fix (lint.md) と同じ実体 (format パッケージの制御構造並置化 + 整形) でドキュメント全体を書き換える WorkspaceEdit (全文置換 TextEdit 1 件)。行幅は 整形 と同じく hikari.tomlwidth を尊重する。

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 := …Structenum Name := …Enum (slot-list 位置・body 位置のどちらの記法も対象)。

対象外

name = expr (代入形) は宣言ではなく既存束縛への代入なので (language-spec.md §6.1)、シンボルにするとその名前を宣言した位置と重複する。よって対象外 (宣言 := / 分解束縛 / slot / typeenum のみをシンボルにする)。破棄子 _ も対象外。構文が壊れていても可能な範囲でパースして部分的なシンボルを返す。未登録ドキュメントは空配列。

リネーム

textDocument/rename は、カーソル位置のシンボルの全出現 (宣言 + 参照) を新しい名前へ置換する WorkspaceEdit を返す。出現の収集は references (サポート method の逆適用) を再利用するため、bare identifier だけでなく dot property (obj.x) も揃って置換される。

リネーム可能な範囲

参照探索が現在のファイル単一のため、定義が現在のファイルにあるシンボルだけをリネーム可能とする。次はエラーを返す (VSCode はメッセージを表示)。

  • 未解決 / builtin (定義が処理系内部)、定義が別ファイル (import 先) や prelude にあるシンボル — この 1 ファイルだけ書き換えると定義側と食い違うため。
  • 新しい名前が Hikari の識別子として不正 (空 / 数字始まり / 記号を含む)、またはリテラル語 (true / false / nil)。

prepareRename

textDocument/prepareRenameprepareProvider を立てて公開し、リネーム前にカーソル位置がリネーム可能かを検証して対象の識別子範囲を返す (不可ならエラー)。

インレイヒント

textDocument/inlayHint は、明示的な型注釈を持たない := 束縛に、静的推論した型を : T のインラインヒント (Kind=Type) として名前の直後に差し込む (例: n := 42n: 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 は次の順で解決する。

  1. ユーザー定義シンボル — 束縛・slot・inner-name・分解束縛 LHS・obj.slot・import 越し (mod.foo / 分解束縛名の import 先透過)。import の文字列リテラル上では import 先ファイルの先頭。"std:<mod>" はここでは解決しない — ファイル系の道に無いので、下記「標準ライブラリの着地点」が扱う。
  2. prelude self-host メンバー — rebind されていない prelude.hika 定義の大域名・型メソッド。処理系埋め込みの prelude.hika を指す仮想ドキュメント Location (hikari-internal:/prelude.hika) を返す。クライアントはカスタム request hikari/internalSource (params {uri} → result {content}) で本文を取得して表示する — 手元に物理ソースが無い配布バイナリ単体でも定義先を開ける。
  3. 標準ライブラリの着地点 — 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
等) である。同じ名前を次の順で解き、最初に当たった位置を返す。

  1. 処理系ソースツリーの .hika 実ファイルstdsrc/stdmod/ に実体を持つメンバー
    (cli / math / term/event / term/screen / time の self-host 部分)。#| doc
    ブロックの直後の束縛行 (abs := …) を指す file Location を返す。手元でソースを編集
    している最中に行が合うのはこれなので、先に来る
  2. 埋め込んだ self-host ソースの仮想ドキュメント — 同じ .hika を処理系へ焼き込んだ
    写し。hikari-internal:/std/<mod>.hika を指す Location を返し、本文は prelude と同じ
    hikari/internalSource が配る。ツリーを持たない配布バイナリ単体でもここまでは働く
  3. ランタイム層の実装行.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/clientget の実体は
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/eventnext_event の実体は
event_of) は、実体の表が持つその名前を辿って焼き込んだソースへ着地する。

分解束縛した std メンバー (import { sqrt } := "std:math"sqrt) も、dot 形と同じ
着地点へ透ける。書き方を変えただけで飛び先が変わらないようにするためで、着地点が
1 つも無いときだけ分解束縛 LHS (ローカル定義) へ落ちる。

言語そのものの組込は定義を持たない

次のものは textDocument/definitionnull を返す。

  • flat 組込 (print / if 等)
  • 型語彙 (Int / List / OneOf 等の組み込み型名。型注釈・式位置とも)
  • 原始型メソッド (xs.map 等。ドット形・中置形とも)
  • universal method (matches 等。ドット形・中置形とも)
  • 組込 enum のタグ (Option.Some 等)

散文はこれらにもある。 hover とリファレンスは処理系が持つ表から出るので、
文面は変わらない。無いのは飛び先だけである — これらの実体はランタイム層にあるが、
ランタイム層の綴りは「どの組込か」を名前で名指していない (printlnList.map
同じ工場から出る) ので、指すべき 1 行が定まらない。

近い位置を当てない。 解決できないなら null を返す。誤った位置へ飛ばすのは、飛ばない
ことより悪い。

処理系ソースツリーの探索

処理系ソースツリーは次の順で探索する (最初に stdsrc/prelude.hika を含むものを採用)。

  1. $HIKARI_REPO (hikari build の本体ソース解決 build.md「Hikari リポジトリの位置解決」と同じ変数)。設定されていれば検証のみ行い、不正でも他候補へはフォールバックしない
  2. ビルド時に記録されたソースパス — リポジトリからビルドした開発ビルドならそのリポジトリ。パスを削ってビルドした配布バイナリでは無効
  3. カレントディレクトリから上位へ最大 6 階層
  4. 実行バイナリ隣接の ../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 と同じ名前
引ける — ツリーの有無で着地点の名前が変わらない。