Created Updated
NotePublic

2026-08-27 22:46

wallet of satoshi に入れているsatsをセルフカストディにしなきゃらしい?リカバリフレーズなくしそうで怖いけど、まあ、しかたないか。

PrevNote

Next

No newer accessible resource.

lkjscriptの興味深いところ(供養1)

2026-09-01 12:14

lkjscriptは新しいプログラミング言語ですが、よくある新言語とは少し着眼点が異なります。

新しいプログラミング言語というと、構文や型システムが話題になりがちです。たとえばRustの所有権、Haskellの遅延評価、LispのS式などが挙げられます。しかし、lkjscriptで最初に見るべきポイントは構文ではありません。

lkjscriptの最も根本的な特徴は、プログラムの正本(authoritative source)がソースコード(テキスト)ではないという点にあります。この設計方針から、言語全体の様々な仕組みが導かれています。

プログラムをテキストとして保存しない

一般的なコンパイラでは、おおまかに次のようなパイプラインで処理を行います。

source code
    ↓
parser
    ↓
AST
    ↓
IR
    ↓
machine code

このモデルでは、ASTやIRはソースコードからいつでも再生成できる派生物にすぎません。そのため、プログラムの「正」となる情報は常にソースファイルの中にあります。

lkjscriptでは、この関係が逆転しています。

authoring request
    ↓
typed meaning graph
    ↓
compiler
    ↓
artifact

システムの中心にあるのは、typed meaning graph と呼ばれるグラフ構造です。モジュール、型、関数、フィールド、依存関係、テスト、実行ターゲットといった要素が、すべて型付きのオブジェクトとしてこのグラフに格納されています。

ソーステキストや変更要求、インデックス、コンパイラキャッシュ、実行バイナリなどは、独立した別のプログラム表現ではなく、このグラフを編集・利用するためのビューや派生物にすぎません。

これは単に「パース済みのASTをディスクに保存した」という話とも異なります。

たとえば、ある関数の名前を foo から bar に変更したとします。通常のソースコードでは、名前は関数を参照・解決するための重要な識別子です。一方、lkjscriptにおいて名前は人間やツールが対象を見つけるための手がかり(locator)として扱われます。

実体の同一性は、以下のような型付きの安定したIDによって管理されます。

mod_...
decl_...
type_...

そのため、関数名を変更しても「その関数自体の同一性」が変わるわけではありません。

プログラムを単なる巨大なテキストファイルとしてではなく、永続化された意味オブジェクトの集合として扱う点が、この設計の基本になっています。

編集はパッチではなくトランザクションとして行う

プログラムがグラフとして保持されているため、編集方法も一般的なテキストエディタのそれとは異なります。

lkjscriptに対する変更は、たとえば次のような操作として送信します。

request base=rev_...
create.module as=$notes name=notes
create.record as=$note module=$notes name=Note visibility=public
add.field as=$text record=$note name=text type=text

これは「特定の行の文字列を置換する」というパッチではなく、「指定したリビジョンをベースにモジュールを作成し、その中にレコードを作り、フィールドを追加する」というセマンティックな操作(意味的な編集)の要求です。

さらに、この変更処理は以下の2段階で行われます。

change plan
change apply

plan の段階で変更要求を正規化し、IDの割り当て、影響範囲の調査、整合性の検証、実行すべきテストの特定などを行います。この検証結果に対して plan_... というトークンが発行されます。

続く apply では、ベースとなるリビジョンが変更されていないこと、レビュー済みの論理的な効果と一致していることを確認した上で、変更を公開(コミット)します。

これはデータベースのトランザクションと非常によく似ています。

内部の保存構造も同様の発想で作られており、新しいセマンティックオブジェクトをイミュータブルな形式で書き出した後、最後に HEAD の参照をアトミックに切り替えます。これにより、編集途中の不整合な状態が外部から見えるのを防いでいます。

つまり、

edit text
→ parse
→ compile

という従来のサイクルではなく、

read revision
→ propose semantic transaction
→ validate
→ commit new revision

という、データベースに近いライフサイクルを持っています。

コンパイラキャッシュが壊れてもプログラムは壊れない

この「何が正本(authority)か」という分離は、コンパイラの設計にも一貫して適用されています。

lkjscriptはインクリメンタルコンパイル用のキャッシュを持っていますが、キャッシュはあくまで派生データ(derived state)にすぎません。

たとえば変更を受け入れた直後にインクリメンタルコンパイルが失敗したとしても、

accepted semantic revision

までロールバックされることはありません。プログラムの変更自体はすでに確定しているためです。

コンパイル時にキャッシュの整合性が疑わしい場合は、単にキャッシュを破棄してフルビルドで作り直せば済みます。

lkjscriptにおけるデータの位置づけを整理すると、次のようになります。

meaning graph      authoritative(正本)
compiler cache     disposable(破棄可能)
artifact           derived(派生物)
deployment         separate authority(独立した権威)
runtime handle     disposable(破棄可能)

一般的な開発環境でも「ビルドディレクトリは消去して構わない」という運用は行われますが、lkjscriptではその境界がシステムのデータモデルとして厳密に定義されています。何が失われるとプログラムそのものが壊れ、何が再生成可能なのかが明確に区別されています。

グラフ構造を直接探索・参照できる

プログラムがグラフ構造として保持されているため、コードの探索もグラフのトラバーサルとして実行できます。

lkjscriptには、通常の inspect に加えて query context というクエリインターフェースが用意されています。

lkjscript --project ./hello query context mod_... \
  --direction both --depth 2 --limit 20 --bytes 65536

このコマンドを実行すると、特定のセマンティックオブジェクトを起点にして、指定した深さまでの周辺グラフを取得できます。

単に検索用APIが用意されているだけでなく、検索対象が特定のリビジョンに固定(pin)されている点が特徴的です。ページネーション用のコンティニュエーション(続きの取得)も、リポジトリ、リビジョン、起点ノード、探索方向、深さ、ソート順などに紐づいています。

また、サーバー側で検索セッションの状態を保持しません。

revision
+ root
+ direction
+ depth
+ continuation

これらがあれば、イミュータブルな正本からいつでも全く同じ論理結果を再現できます。

これは人間向けのIDE機能として有用なだけでなく、静的解析ツールやLLMなどの機械がコードを読み込む際にも便利です。数千行のソースファイルを丸ごと読み込ませる代わりに、「この関数の周囲2ホップ分の依存関係を取得する」といったピンポイントな読み込みが可能になります。

標準ライブラリも同じ言語のグラフとして実装されている

言語処理系を設計する際、どこまでをその言語自身で書き、どこからを処理系(ホスト言語)側の組み込み機能(magic / intrinsics)とするかは常に問題になります。

lkjscriptの処理系自体はRustで書かれていますが、標準ライブラリの処理を安易にRustの組み込み関数として実装することは避けています。

わかりやすい例が fold 関数です。

list-fold-left<Item, State>(
    List<Item>,
    State,
    Function(State, Item) -> State
) -> State

これは標準パッケージの meaning graph 内で実装されています。内部では通常の再帰、list-lengthlist-get、および関数呼び出しのみを用いて記述されており、fold 専用のVM命令やRustへのコールバックなどは存在しません。

これにより、

言語自身で表現できる領域

処理系でしか実現できないプリミティブな領域

の境界が綺麗に保たれています。

標準ライブラリ自体も通常の meaning graph パッケージとして定義されているため、ユーザーのコードと全く同じコンパイラ、バリデータ、VM、リファレンスインタプリタを通過します。

処理系開発では、パフォーマンスや利便性を理由にホスト側の組み込み関数を増やしてしまいがちですが、lkjscriptではそのアプローチを自覚的に制限しています。

VMとは独立したリファレンスインタプリタの存在

実行系のアーキテクチャにも興味深い特徴があります。

lkjscriptにはバイトコードを実行するVMが存在しますが、それとは別にリファレンスインタプリタも用意されています。

副作用のない純粋なコマンドやテストの実行時、処理系は以下の2つの経路で並行して実行を行います。

meaning graph
    ├─ compiler → bytecode VM
    └─ reference interpreter

もし両者の実行結果が一致しなければ、テストは失敗とみなされます。つまり、リファレンスインタプリタがコンパイラとVMに対するセマンティックオラクル(意味論の正しさを保証する判定器)として機能します。

これはコンパイラの検証において非常に有効です。

たとえば、

2 + 3

という式をコンパイルしてVMで実行し、5 が返ってきたとしても、それだけでコンパイラが正しいとは断言できません。コンパイラとVMが同じバグを抱えていて、たまたま結果が一致している可能性があるためです。

完全に独立して実装されたインタプリタでも同じ 5 という結果が得られるのであれば、2つの独立した実装が同じセマンティクスを満たしていることの強い根拠になります。

なお、HTTPリクエストの送受信やデータベースへの書き込みといった副作用を2重に実行するわけにはいかないため、この差分実行(differential execution)は純粋な処理に限定して適用されます。

プログラムとデプロイ設定の厳密な分離

Webアプリケーションを扱うようになると、コードベースには様々な外部環境の設定が混入しがちです。

たとえば、次のような要素です。

listen address
database path
TLS certificate
remote endpoint
secret
timeout

lkjscriptでは、これらの環境依存の情報を meaning graph に含めない設計になっています。

たとえばHTTPアプリケーションの場合、グラフ(プログラム本体)側には以下のような論理構造のみを定義します。

route
handler
request/response types
component
requirement
port
target

一方、具体的な接続先やリソース上限などはデプロイメント記述子(deployment descriptor)側に分離して配置します。

127.0.0.1:8080
/data/example
https://example.com/
TLS trust root
resource limits

さらに、ステートフルなアプリケーションが保持するデータも、セマンティックリポジトリとは明確に区別された運用データ(operational data authority)として扱われます。

概念としては、以下の3つの領域が分離されています。

program meaning
      │
      ├── artifact
      │
deployment authority
      │
operational data

アプリケーションがデータを更新してもプログラムのセマンティックリビジョンは変化しませんし、デプロイ先のアドレスを変更してもプログラム自体を書き換えたことにはなりません。

当たり前の区別に思えますが、既存の多くのフレームワークではこの境界が曖昧になりがちです。lkjscriptでは、これらを「権威(authority)の違い」として明確にモデル化しています。

capabilityによる実行時境界の制御

外部世界とのやり取りに関しても、この分離の思想に基づき capability として厳密に管理されます。

たとえばアウトバウンドのHTTP通信を行う場合でも、プログラム側から任意のURLへ自由に接続できるわけではありません。

プログラム(グラフ)側は HttpClient.get という能力の要求(requirement)を宣言し、デプロイメント側が具体的な接続先エンドポイントやTLS証明書に対する権限を付与(grant)します。

つまり、

program:
    HTTPクライアントの機能が必要である(要求)

deployment:
    この特定のエンドポイントに対するアクセス権限を与える(付与)

という関係になります。

これは依存性の注入(DI)に似ていますが、単にテストを容易にするための抽象化ではなく、外部の権威をプログラムの意味論から完全に切り離すための境界として機能しています。

現在用意されているHTTPSクライアントの実装も意図的に最小限に絞られており、リダイレクトやリトライ、プロキシ、WebSocket、任意URLへのアクセスなどは提供されていません。機能が不足しているというよりは、プログラムに許可された能力を可能な限り正確かつ安全に定義しようとした結果です。

単一バイナリによる自己完結したプラットフォーム

現在の公開版lkjscriptは、x86_64-unknown-linux-musl 向けの静的リンクバイナリとして配布されています。

この単一の実行ファイルだけで、プロジェクトの作成からビルド、実行、サーバーの立ち上げまで、一連のライフサイクルを完結して行えます。

new
status
inspect
query
change
check
build
run
serve
worker
data
package builtin

標準パッケージやプロジェクトテンプレートもバイナリ内に埋め込まれているため、外部のリポジトリをクローンしたり、Cargoなどのツールを別途用意したりすることなく、以下のような手順で即座に動作確認が可能です。

./lkjscript new ./site --template http --name site
./lkjscript --project ./site check
./lkjscript --project ./site build --output ./site/generated/application.lkja
./lkjscript serve --deployment ./site/service.deployment.json

これは単に「コンパイラを静的リンクして配布した」というレベルにとどまりません。コンパイラ、パッケージマネージャ、プロジェクトテンプレート、アーティファクトローダー、VM、HTTPランタイム、データストアなどが、すべて単一の配布物として統合されています。

そのためlkjscriptは、単なるプログラミング言語というよりは、公式リポジトリの表現どおり programming language and application platform と呼ぶほうが正確です。

後方互換性を捨てて設計を収束させる

開発初期の実験的プロジェクトとしての面白さとして、後方互換性を思い切って切り捨てる方針をとっている点も挙げられます。

lkjscriptは、古いリポジトリフォーマットを長期間にわたってサポートし続けるような設計にはなっていません。古くなった(obsoleteな)グラフリポジトリは明快に拒否されます。

フォールバック用の古いパーサー、レガシーモード、移行用マイグレーションコマンド、新旧形式への二重書き込みといった互換性のための仕組みは、現在の公開アーキテクチャには含まれていません。

商用言語でこのような割り切りを行うとユーザーの負担になりますが、アーキテクチャの理想形を急速に模索している段階の言語にとっては大きな利点があります。

古いモデル、新しいモデル、その間の変換レイヤーなどを同時にメンテナンスし続ける必要がないため、アーキテクチャを刷新した際には古いコードパスを完全に削除できます。これはlkjscriptのコードベース全体を通して一貫した方針です。

new model
old model
old-old model
conversion
compatibility layer

こうした歴史的経緯の積み重ねを抱え込まずに済むことが、開発速度の維持につながっています。

lkjscriptの本質的な特異性

個々の要素を分解して見ていくと、使われている要素技術自体が前代未聞というわけではありません。

イミュータブルなオブジェクトストレージ、リビジョン管理、中間表現(IR)の永続化、ケーパビリティベースのセキュリティ、リファレンスインタプリタとの差分実行といった技術は、それぞれ先行事例が存在します。

lkjscriptの特異さは、それらの技術を以下の単一の前提から徹底的に論理展開して組み上げている点にあります。

プログラムとはソースコードではなく、型付けされた意味そのものである

この前提を起点に置くことで、

  • 名前の変更と実体の同一性(ID)の分離
  • パッチではなくセマンティックトランザクションによる編集
  • 明示的なリビジョン管理
  • ソース走査ではなくグラフ探索によるクエリ
  • コンパイラキャッシュの完全な派生物化
  • 標準ライブラリのグラフ表現
  • プログラム、デプロイ、実行時データの権威の分離

といった設計判断が、必然的な帰結として一つに繋がります。

lkjscriptの面白さは、斬新な構文を考案したことにあるのではなく、普通の言語処理系が暗黙に置いている前提を、

program = source files

から、

program = accepted typed semantic graph

へと置き換え、「では言語処理系と開発環境全体を作り直すとどうなるか」をゼロベースで真摯に実験している点にあります。

その結果として、パーサーだけでなく、エディタ、バージョン管理、コンパイラ、パッケージ管理、ビルドシステム、ランタイム、デプロイ、さらにはデータベースとの境界に至るまでが再定義されています。

なお、現在の実装は完成された汎用プラットフォームというわけではありません。JIT/AOTコンパイル、安全でないコードを実行するためのサンドボックス、マルチテナント分離、アーティファクトの署名検証、分散合意形成、暗号化ストレージなどはまだ実装されていません。

現時点のlkjscriptは、多種多様な機能を広く浅く提供するのではなく、厳選された少数の機能に対して「権威(authority)とセマンティクスが矛盾なく閉じている状態」を徹底的に追求した実験的言語と見るのが自然でしょう。