自作言語のすすめ
ある程度プログラミングに慣れてくると、「自分で設計したオリジナル言語」を作りたくなりますよね?...ね?
自分で設計した言語を使って、ウェブのフロントエンドアプリ開発に利用できたら楽しそうです。実用性は無いに等しく、ましてやお仕事での開発で利用できるはずもありませんが、技術的な興味と自己満足で、自作言語でWebAssemblyのアプリを作ってみようという試みです。
orelang
皆さんはorelangについて覚えているでしょうか?
覚えてるも何も、知らねぇよそんなもん初めて聞いた、というかもしれません。元ネタは3年前に一部界隈で流行った、とても低機能な自作言語の記事です。
▼プログラミング言語を作る。1時間で。
https://qiita.com/shuetsu@github/items/ac21e597265d6bb906dc
制御構造は条件つきループのuntil
のみ。演算子は比較の=
と加算の+
のみという、とてもシンプルな設計で、JSONで記述します。
サンプルプログラムは次のようなものです。なんとなく雰囲気は掴めるでしょうか。
["step",
["set", "sum", 0 ],
["set", "i", 1 ],
["while", ["<=", ["get", "i"], 10],
["step",
["set", "sum", ["+", ["get", "sum"], ["get", "i"]]],
["set", "i", ["+", ["get", "i"], 1]]]],
["print", ["get", "sum"]]]
当時作成したときの都合により、until
の代わりにwhile
、演算子は=
ではなく<=
で実装していますが、大筋では変わりません。最初にsum
に0
、i
に1
を代入し、i
が10
になるまでインクリメントしながらループします。ループがまわる度にsum
にi
を足していきます。ループを抜けたときsum
の値が1から10の総和である55になっていれば正解です。最後のprint
関数は、整数値を1個表示することができます(それしかできません)
LLVM
LLVMはC系言語で利用されているコンパイラ基盤です。clangコンパイラの最も重要な部分で使われているのでお馴染みです。
CやC++など、clangコンパイラが対応している言語であればWebAssemblyのバイトコードが生成できるのなら、LLVMのAPIで生成した LLVM IR からのWebAssembly化もできるはずだよね。だったら、自作言語のWebAssembly対応も可能なのでは、というのがこの記事の趣旨です。
clangをインストールする
Ubuntu 18上での開発を想定しています。clang-6.0を使用します。
sudo apt install clang
忘れずにバージョンを確認しておきます。
soramimi@ubuntu18x64:~$ clang -v
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
...
InstalledDirの行に注意してください。上記の例ではシステムにインストールされたUbuntu標準のclangが実行されました。
もし、WebAssembly(Emscripten SDK)の開発環境だと次のようになると思います。
soramimi@ubuntu18x64:~$ source ~/emsdk/emsdk_env.sh
...
soramimi@ubuntu18x64:~$ clang -v
clang version 6.0.1 (emscripten 1.38.30 : 1.38.30)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/soramimi/emsdk/clang/e1.38.30_64bit
...
ローカル開発環境とWebAssemby開発環境を行ったり来たりしていると、時々どちらのclangが実行されているか混乱することがありますので注意が必要です。異変に気づいたらどちらのclangが使われているか確認しましょう。
which clang
ローカル開発環境でclangを使いたいときは、一度ターミナルを閉じて開き直します。
やってみる
お馴染みのプログラムで試してみます。
#include <stdio.h>
int main()
{
printf("Hello, world\n");
return 0;
}
普通に実行するなら次の通りです。
$ clang hello.c
$ ./a.out
Hello, world
$
次に LLVM IR を経由してコンパイルしてみます。
$ clang -c -S -emit-llvm hello.c -o hello.ll
.ll
という拡張子のファイルがLLVM IRです。このファイルの中を覗いてみるとアセンブリ言語っぽいですが、簡単に言うと、コンパイル中の抽象構文木を元に生成された、特定のCPUに依存しない仮想のアセンブリ言語と思っておけば良いです。
これを実行ファイルにするのはC/C++などと同じ要領でできます。
$ clang hello.ll
$ ./a.out
Hello, world
$
LLVM IRをWebAssembly化する
ここからはWebAssembly開発環境を利用しますので、Emscripten SDKのための環境変数を設定します。環境構築がまだの方は**先日の記事**を参考にしてください。
source ~/emsdk/emsdk_env.sh
次のようにしてコンパイルします。
emcc hello.ll -s WASM=1 -o hello.html
これによって生成されたhello.html
、hello.js
、hello.wasm
をウェブサーバに置いて、ウェブブラウザを開発者モードにし、hello.htmlにアクセスすると、コンソールにHello, world
が表示されます。
LLVM IRを生成する
LLVM IRを生成するのは、字句解析後に抽象構文木を構築するのとだいたい同じです。これをLLVMのAPI関数を利用して行うのですが、これがなかなか面倒です。
次のような、
; ModuleID = 'mymodule'
source_filename = "mymodule"
define i32 @main() {
entry:
ret i32 0
}
何もしないで0
を返すだけのmain
関数を出力してみましょう。
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/BasicBlock.h>
#include <llvm/IR/Instruction.h>
#include <llvm/IR/Instructions.h>
#include <llvm/IR/Constant.h>
#include <llvm/IR/Constants.h>
#include <llvm/Support/raw_os_ostream.h>
using namespace llvm;
int main()
{
LLVMContext llvmcx;
Module *module;
Function *current_function;
BasicBlock *current_block;
module = new Module("mymodule", llvmcx);
DataLayout dl(module);
// main関数を作成(戻り値int、引数なし)
current_function = Function::Create(FunctionType::get(Type::getInt32Ty(llvmcx), false), GlobalVariable::ExternalLinkage, "main", module);
// ブロックを作成
current_block = BasicBlock::Create(llvmcx, "entry", current_function);
// 関数の内容を構築
// 普通はここでブロックの中に様々な命令を構築する
// return 0
ReturnInst::Create(llvmcx, ConstantInt::get(Type::getInt32Ty(llvmcx), 0), current_block);
// LLVM IR を出力
std::string ll;
raw_string_ostream o(ll);
module->print(o, nullptr);
o.flush();
puts(ll.c_str());
return 0;
}
コンテキストを作って、モジュールを作って、ファンクションを作って、ベーシックブロックを作って、return命令を作って、文字列で出力するだけなのですが、たったそれだけのLLVM IRを生成するのに、この分量のコードが必要になります。
orelangからLLVM IRを生成する
自作言語からLLVM IRを生成すればいいのですが、それがとても骨の折れる作業です。
...という記事を、3年前に書きました。
▼Orelang(俺言語) の LLVM IR コンパイラを作ってみた
https://qiita.com/soramimi_jp/items/b7a0a9de381f3c320fe6
JSONテキストをパースして、LLVMのAPIを用いて構文木を構築し、LLVM IRを出力します。あとは、emsdkのclangにかけてコンパイルし、ウェブサーバにアップすれば完成です。
orelangコンパイラのソースコードは筆者のリポジトリにあります。
コンパイラと言っても、ソースコード埋め込みのJSONからLLVM IRを生成するだけなので、コンパイラを名乗るのはおこがましい気がしますが、コンパイラのフロントエンドということで許してください。
実行手順を示します。
soramimi@ubuntu18x64:~$ git clone https://github.com/soramimi/orec.git
Cloning into 'orec'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 32 (delta 0), reused 3 (delta 0), pack-reused 27
Unpacking objects: 100% (32/32), done.
soramimi@ubuntu18x64:~$ source ~/emsdk/emsdk_env.sh
Adding directories to PATH:
PATH += /home/soramimi/emsdk
PATH += /home/soramimi/emsdk/clang/e1.38.30_64bit
PATH += /home/soramimi/emsdk/node/8.9.1_64bit/bin
PATH += /home/soramimi/emsdk/emscripten/1.38.30
Setting environment variables:
EMSDK = /home/soramimi/emsdk
EM_CONFIG = /home/soramimi/emsdk/.emscripten
EM_CACHE = /home/soramimi/emsdk/.emscripten_cache
LLVM_ROOT = /home/soramimi/emsdk/clang/e1.38.30_64bit
EMSCRIPTEN_NATIVE_OPTIMIZER = /home/soramimi/emsdk/clang/e1.38.30_64bit/optimizer
BINARYEN_ROOT = /home/soramimi/emsdk/clang/e1.38.30_64bit/binaryen
EMSDK_NODE = /home/soramimi/emsdk/node/8.9.1_64bit/bin/node
EMSCRIPTEN = /home/soramimi/emsdk/emscripten/1.38.30
soramimi@ubuntu18x64:~$ cd orec/
soramimi@ubuntu18x64:~/orec$ make
g++ -std=c++11 -I/usr/lib/llvm-6.0/include -c -o main.o main.cpp
g++ -std=c++11 -I/usr/lib/llvm-6.0/include -c -o json.o json.cpp
g++ main.o json.o -o orec -L/usr/lib/llvm-6.0/lib `/usr/bin/llvm-config-6.0 --libs`
soramimi@ubuntu18x64:~/orec$ ./orec >test.ll
soramimi@ubuntu18x64:~/orec$ emcc test.ll -s WASM=1 -o test.html
soramimi@ubuntu18x64:~/orec$ cp test.html /var/www/html/wasmtest/index.html
soramimi@ubuntu18x64:~/orec$ cp test.js /var/www/html/wasmtest/
soramimi@ubuntu18x64:~/orec$ cp test.wasm /var/www/html/wasmtest/
HTMLファイルを生成するために emcc test.ll -s WASM=1 -o test.html
というコマンドを実行してから、test.html を index.html としてコピーしました。それ以降は emcc test.ll -o test.js
を実行すれば、.js
と.wasm
だけ生成されるようになります。これら3つのファイルをウェブサーバに配置し、ウェブブラウザからアクセスすれば、orelangの実行結果が表示されます。