プログラミング
Hobbes – 言語と組み込みJITコンパイラ
Hobbes – A Language and Embedded JIT Compiler (github.com)
要約
Hobbesは、効率的な動的式評価、データストレージ、分析のための言語、組み込みコンパイラ、およびランタイムです。LLVM 3.3以降、cmake 3.4以降、GNU gcc 4.8以降、Linuxカーネル2.5以降が必要です。C++アプリケーションとの高性能な統合のために構築されており、メモリへの直接アクセスやリモートでのネイティブコードコンパイル・実行をサポートしますが、サンドボックス化されたランタイム環境やランタイム安全機能は提供しません。基本的な式評価、データ構造、型クラス、パターンマッチングなどをサポートしています。
全文翻訳
Hobbesは、効率的な動的式評価、データストレージ、分析のための言語、組み込みコンパイラ、およびランタイムです。
セクション
説明
ビルド方法
hobbesのビルド方法
組み込み
C++プログラムでのhobbesの使用方法
評価
基本的なhobbes式の評価
ストレージ
帯域外分析のためのデータ記録
ネットワーキング
リモートhobbesプロセスとの対話
内包表記
データ分析のためのシーケンスの変換/ソート/結合/フィルタリング/グループ化
パターンマッチング
データの効率的な分類と分解
解析
LALR(1)文法によるテキスト解析
型クラス
オーバーローディングとコンパイル時計算
非限定モジュール
カスタム制約処理のためのユーザー定義「コンパイラプラグイン」
Hobbesの使用に関する注意
Hobbesは、C/C++アプリケーションとの高性能な統合のために構築されています。Hobbesはコンパイル時チェックを提供する強く型付けされた言語ですが、サンドボックス化されたランタイム環境やランタイム安全機能は備えていません。設計上、Hobbesはメモリへの直接アクセスを許可し、配列境界チェックはありません。さらに、Hobbesはネットワーク経由(RPC)でのネイティブコードのコンパイルと実行をリモートでサポートします。この機能は、信頼できる内部ネットワーク内での使用を目的としています。このような機能を利用する場合、これらの設計上の選択を認識し、セキュリティ上の影響を理解する必要があります。
ビルド
Hobbesをビルドするには、LLVM 3.3以降、cmake 3.4以降、GNU gcc 4.8以降、およびバージョン2.5以降のLinuxカーネルが必要です。LLVM、cmake、およびg++がインストールされていれば、コードをダウンロードした後、次のコマンドを実行するだけでhobbesをビルドおよびインストールできるはずです。
$ cmake .
$ make
$ make install
LLVMをインストールした場所によっては、cmakeにLLVMのcmakeモジュールへのパスを指定しないと、これが機能しない場合があります。特に、LLVMのLLVMConfig.cmakeファイルへのパスが必要です。環境変数LLVM_DIRをそのディレクトリに設定すると、前の手順は正常に完了するはずです。
ビルドプロセスでは、静的ライブラリlibhobbes.aが生成され、C++実行可能ファイルにリンクできます(.soで使用したい場合は、ビルドで別の静的ライブラリlibhobbes-pic.aが生成されます)。さらに、ビルドプロセスでは2つのユーティリティプログラム、hiとhogが生成されます。hiプログラムはhobbes式の基本的な対話型インタプリタであり、hogプログラムはアプリケーションによって生成された構造化データを効率的にデータファイルに記録します(これらのファイルはhiによって同時にロードおよびクエリできます)。これらのプログラムのソースコードも有益である可能性があります。なぜなら、それらはhobbes APIの多くの異なる側面を示しているからです。
組み込み
簡単なC++プログラムにhobbesを組み込む方法を考えてみましょう。このコードは、hiに似た非常に基本的なシェルを実装しています。
#include <iostream>
#include <stdexcept>
#include <hobbes/hobbes.H>
int main() {
hobbes::cc c;
while (std::cin) {
std::cout << "> " << std::flush;
std::string line;
std::getline(std::cin, line);
if (line == ":q") break;
try {
c.compileFn<void()>( "print(" + line + ")" )();
} catch (std::exception& ex) {
std::cout << "*** " << ex.what();
}
std::cout << std::endl;
hobbes::resetMemoryPool();
}
return 0;
}
まず、任意の式をコンパイルするには、hobbes::ccオブジェクトを構築する必要があります。次に、例外ハンドラのコンテキストで、compileFnメソッドを使用してこのhobbes::ccオブジェクトから関数をコンパイルできます。このメソッドには、期待される戻り値の型(この場合はvoid())と、その型の値にコンパイルできると期待される文字列を指定します。コンパイルのいずれかの段階で失敗した場合(解析エラー、型不一致など)、詳細情報を含む例外がスローされます。最後に、hobbes::resetMemoryPool()を呼び出して、コンパイルされた式によって動的に割り当てられた可能性のあるメモリを解放します(つまり、コンパイルされた関数自体ではなく、コンパイルされた式によって割り当てられたメモリ。これらは、hobbes::ccが破棄されるか、hobbes::cc::releaseMachineCodeがそれらを破棄するために使用されるまで解放されません)。コンパイルされた関数がメモリを割り当てることを決定した場合、その割り当ては「メモリ領域」から行われます。メモリ領域は、割り当てが行われる動的に拡張可能なバイトシーケンスであり、hobbes::resetMemoryPool()が呼び出されると一括で解放されます。これにより、割り当てと解放が非常に効率的になりますが、「論理トランザクション」の境界を確立するにはある程度の考慮が必要です。アクティブなメモリ領域はスレッドローカルであるため、同期の問題を心配することなく、同じ関数ポインタを複数のスレッドで同時に使用できます。最後に、上記のプログラムを "test.cpp" というファイルに保存した場合、次のようにビルドできます。
$ g++ -pthread -std=c++17 -I <path-to-hobbes-headers> -I <path-to-llvm-headers> test.cpp -o test -L <path-to-hobbes-libs> -lhobbes -ldl -lrt -ltinfo -lz -L <path-to-llvm-libs> `llvm-config --libs x86asmparser x86codegen x86 mcjit passes`
明示的なパス指定は、システムにLLVMとhobbesがどのようにインストールされているかによって不要な場合があります。llvm-configプログラムのインライン呼び出しは、いくつかのライブラリを明示的にリストすることを避けるために、LLVMのユーザーには一般的です。すべてが正しく機能した場合、hobbes式を評価できる簡単なシェルが完成します。
評価
動作するシェルができたので、式を試してhobbes言語の仕組みを把握できます。まず、いくつかの典型的な定数値があります。
> 'c'
'c'
> 42
42
> 3.14159
3.14159
合計で、これらはサポートされているプリミティブ型/定数のセットです。
名前
例
説明
unit
()
C言語の'void'に似ており、単一の値を持つ自明な型
bool
false
trueまたはfalseのいずれか
char
'c'
単一のテキスト文字
byte
0Xff
単一バイト(0-255)
short
42S
2バイトの数値
int
42
4バイトの数値
long
42L
8バイトの数値
float
42.0f
4バイトの浮動小数点値
double
42.0
8バイトの浮動小数点値
これらのプリミティブは配列と組み合わせることができます。
> [1, 2, 3]
[1, 2, 3]
> "foobar"
"foobar"
> 0xdeadbeef
0xdeadbeef
レコード/タプルとも組み合わせることができます。
> {name="Jimmy", age=45, job="programmer"}
{name="Jimmy", age=45, job="programmer"}
> ("Jimmy", 45, "programmer")
("Jimmy", 45, "programmer")
またはレコード/タプルの配列と組み合わせると、テーブルとして便利に表示されます。
> [{name="Jimmy", age=45, job="programmer"}, {name="Chicken", age=40, job="programmer"}]
name age job
------- --- ----------
Jimmy 45 programmer
Chicken 40 programmer
> [("Jimmy", 45, "programmer"),("Chicken", 40, "programmer")]
Jimmy 45 programmer
Chicken 40 programmer
型をバリアントまたはサム(レコードに対するタプルと同様、「名前のない」バリアント形式)と組み合わせることができます。 "むき出し"のバリアント導入は定義上不完全であるため、明示的な型注釈を導入する必要がある場合があります。
> |food="pizza"|::|vehicle:int,food:[char]|
|food="pizza"|
> |1="pizza"|::(int+[char])
|1="pizza"|
また、バリアントはアイソレカーシブ型と組み合わせて連結リストを作成できます。これらは表示時に便利な形式を持ち、再帰型とバリアント型のボイラープレートを回避するための組み込み関数を備えています。
> roll(|1=("chicken", roll(|1=("hot dog", roll(|1=("pizza", roll(|0=()|))|))|))|) :: ^x.(()+([char]*x))
"chicken":"hot dog":"pizza":[]
> cons("chicken", cons("hot dog", cons("pizza", nil())))
"chicken":"hot dog":"pizza":[]
これらの型と型コンストラクタは、MLファミリーのプログラミング言語(SML、ocaml、Haskellなど)で一般的な「代数的データ型」を構成します。これらの型の構文と名前は、TaPLやPFPLなどの一般的な学術プログラミング言語の教科書やMLに由来しています。Haskellと同様に、hobbesは型クラスによる修飾型の形式をサポートし、ユーザー定義の制約リゾルバ(後で詳しく説明します)さえもサポートします。他の多くの用途の中でも、これにより混合型算術と型推論の両方を共存させることができます。
> 0X01+2+3.0+4L+5S
15
> (\x y z u v.x+y+z+u+v)(0X01,2,3S,4L,5.0)
15
また、hiプログラム(以前の例よりも少し複雑なインタプリタで、hobbesにバンドルされています)を使用して、このような式を評価し、それらの型を検査することもできます。