Lambda Prelude は柔軟な反復開発と本番デプロイを分離する。インタプリタは AST をツリーウォークして素早くフィードバックを返し、build は Lambda Prelude のソースを OCaml ソースに変換して ocamlopt に渡し、真のネイティブバイナリを出力する。

独自のマシンコードバックエンドもバイトコード VM も持たない。コンパイラは Lambda Prelude の構文 / 型 / アクターモデルを OCaml の型 / 関数 / レコード / クロージャ / effect にマップする。コード生成・最適化・リンク・バイナリ配置は OCaml ツールチェーンに委ねる。

パイプライン

.lp ソース
  ├── lexer / parser
  ├── 型推論                    (プロトコル付き HM、行多相)
  ├── AOT 変換                  (AST → OCaml ソース)
  ├── ocamlopt                  (ネイティブコード生成)
  └── リンカ                    (AOT ランタイム)
.exe

言語実装は Lambda Prelude 固有の意味論 (型 / プロトコル / ディスパッチ / アクタープリミティブ) を担当する。バイナリ生成は成熟した OCaml 側に任せる。

lambda-prelude build script.lp --out binary
lambda-prelude build script.lp --emit            # 生成された OCaml を標準出力に
lambda-prelude build script.lp --out script.ml   # 生成された OCaml をファイルに

AOT バイナリが意味の正典

インタプリタと AOT バイナリの挙動が食い違ったとき、AOT バイナリが正典。インタプリタは開発ツール — 拡張が速く、機能を AOT 経路に組み込む前のプロトタイプに有用 — だが、言語としてコミットするのは AOT 経路。インタプリタが AOT と一致しない挙動を見せたら、それはインタプリタ側のバグとして扱う。

これは以下と同じ系列にある。

逆の伝統 — インタプリタを正典実行モードとする — は Python / Ruby / Lua のような動的型言語、およびイメージベース Smalltalk のもの。Lambda Prelude は表層こそ Smalltalk 風だが、本体は静的型付きで AOT ネイティブ寄りなので、静的 / コンパイル系列に並ぶ。

バイト単位の退行テスト: ほとんどのサンプルプログラムはインタプリタと AOT バイナリ両方で実行され、出力の食い違いはハーネスが拒否する。

生成されるバイナリ

lambda-prelude build はネイティブ実行ファイルを出力する。LP ソースを OCaml ソースへ変換し、ocamlopt でネイティブコンパイルする。生成バイナリには LP のランタイム(値表現・アクタースケジューラ・組込メソッド・メソッドディスパッチ経路)が一緒にコンパイルされる。静的にレシーバが特定できる送信は直接の関数呼び出しになり、特定できない送信(リフレクション / リモート / プラグインクラス)だけが実行時のメソッドディスパッチを通る。

「完全静的」専用のビルドモードも退避用の切り替えも存在しない。各呼び出しサイトは利用可能な最良の形に展開され、両種が同一バイナリ内に共存する。

ディスパッチの 4 カテゴリ

Lambda Prelude は静的型付きだが、すべての送信がコンパイル時解決を要求されるわけではない。設計上 4 つのカテゴリが共存する — 1〜3 は言語がコミットする意味境界、4 は推論の強化とともに縮んでいくエンジニアリング上の状態。

  1. 静的ディスパッチ。レシーバがコンパイル時に特定でき、直接 OCaml 呼び出しに展開される。最も多いケース。Hindley-Milner 推論はプロトコル制約付き多相とアクター行多相をカバーし、型注釈は任意。
  2. リフレクションperform:ReflectclassNameatField:put: はセレクタ / クラス名を実行時に扱うので、常に動的。
  3. 分散Remote at:for:id: はプロキシを返す。プロキシへの送信はワイヤ越しにシリアライズされ、リモート側はふつうの Lambda Prelude クラス。Remote at: url for: #Class id: のようにリテラルのクラスを渡すとプロキシは型に乗り、送信は対象クラスのプロトコルに対してコンパイル時に型検査される — 未知セレクタはコンパイルエラー。ただしディスパッチは常にワイヤ越しで、AOT がプロキシ送信を直接のローカル呼び出しに落とすことはない。非リテラルの for: はコンパイル時にクラスを名指せないので従来通り動的型付けのまま。
  4. ランタイムディスパッチ。レシーバを推論が特定できない送信 (不透明型上の多相メソッド、プラグインクラス、動的な JSON ペイロード) は、バイナリが lib/eval.ml 経由で link するランタイムディスパッチャに委ねられる。

実行モード対応表

機能インタプリタAOT
アクター基本 (spawn / send / FIFO receive / mailbox)ありあり
選択受信、受信タイムアウトありあり
Future、ask:、Future タイムアウトありあり
リンク / モニタ / 終了シグナル捕捉 / スーパーバイザありあり
境界つきソケット I/Oありあり
HTTP サーバ / クライアントありあり
TLS ソケット (Tls.Engine 駆動)ありあり
JSON、JSON-RPC、TERIOS RPC (HTTP 経由)ありあり
SQLite (組み込み)、File / OS / サブプロセス / リフレクションありあり
PostgreSQL / MariaDB / MySQL コネクション (Dynlink プラグイン)ありあり
Redis / Valkey (RESP2 wire、Dynlink プラグイン)ありあり
TORM 2-way SQL テンプレート + DAO マクロありあり
マルチドメイン spawn、クロスドメイン inbox、ワークスティーリングありあり

AOT バイナリには同じランタイムが一緒にコンパイルされているので、マルチドメインワークスティーリングスケジューラをそのまま備え、同じ Dynlink プラグインを読み込む。2 列は構造上同一になる。

クロスプラットフォームビルド

出力は Lambda Prelude のソースから直接ビルドされたネイティブバイナリで、コンテナ・Windows 開発環境・Linux デプロイ先のいずれにも適合する。

機能が完成したと判定する前に両ターゲットを通す。片方でしか動かない機能は未完成扱い。

生成コードの覗き方

--emit は変換後の OCaml ソースを標準出力に書く。--out <file>.ml を渡せばファイルに書き出せる。AOT がコードに対して実際に何をしたかを調べる標準ツール。

lambda-prelude build examples/15_counter_actor.lp --emit | less
lambda-prelude build examples/15_counter_actor.lp --out counter.ml

出力はふつうの OCaml — 型が通り、必要なところに型注釈が付き、平坦な IR ではなく入力ソースの形に近い見た目で並ぶ。