はじめに
「JavaScriptのMath.Powはなぜ速いのか」という質問を読んでいたら、V8の組み込み関数に「Torque」という言語が使われているという記述を見つけました。
高速化のために、プログラミング言語処理系の中だけで使われるプログラミング言語...ロマンを感じずにはいられません。実際にTorqueを動かしてみましょう。
バージョン
V8: 記事執筆時点(2022/9/18)のmainブランチ
言語の概要
TorqueはV8組み込み関数の最適化処理を記述するために使われています。C++ではなくあえて別言語を使うのは、ロジックの可読性を上げるためだそうです。
プログラミング言語 V8 Torqueによって、V8プロジェクトに貢献する開発者は無関係な詳細実装に煩わされることなく、VMへの変更の意図に専念し変更を表現することができるようになります。この言語はECMAScriptの仕様をV8の実装に直接変換するのを容易にするように設計されましたが、同時に低レベルなV8最適化を頑健な方法(特定のオブジェクト形式のテストに基づきファストパスを作成する等)で表現できるほど強力です。
V8 Torque is a language that allows developers contributing to the V8 project to express changes in the VM by focusing on the intent of their changes to the VM, rather than preoccupying themselves with unrelated implementation details. The language was designed to be simple enough to make it easy to directly translate the ECMAScript specification into an implementation in V8, but powerful enough to express the low-level V8 optimization tricks in a robust way, like creating fast-paths based on tests for specific object-shapes.
静的型付き言語で、構文はTypeScriptに近づくように設計されたとのことです。
@export
macro PrintHelloWorld(): void {
Print('Hello world!');
}
また、ジェネリクスやunion等豊富な型操作を持ち、V8実装の組み込み型を直接触ることができます。
環境設定
リファレンスに従いました。が、元々単体で実行する想定でないものなので、実行にはかなり手間がかかりました。
面倒なのでDockerイメージ化しています。良ければご利用ください(12GBあるので注意! )。
以下イメージ作成に至るまでの環境設定の手順です。(とにかく触ってみたい!という方は読み飛ばしてください)
設定の概要
リファレンスの「Getting Started」では、テストケースにHelloWorldを追記する方法が紹介されているので、このやり方に従います。
手順は大まかには以下の通りです。
- V8をクローン、依存モジュールも最新化
- テストケースとテスト用Torqueソースコードに動かしたい処理を追記
- テスト用バイナリをビルド
- テスト実行
V8をクローン
残念ながら git clone
は使えず、専用のツールが必要です(リファレンスより)。
まずは depot_tools をインストールします。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
PATH=/depot_tools:$PATH
gclient
これで gclient
が使えるようになり、v8をクローンできるようになりました。
Pythonも後で使うので入れておきましょう。
続いてV8をクローンします。git clone
の代わりに depot_toolsの fetch
を実行します。
cd /v8
fetch v8
cd v8
# 最初はdetached headにいるので、mainに移動してからpull
git checkout main && git pull
# 依存関係をアップデート
gclient sync
テスト関数とテスト用Torqueソースコードをパッチ
続いて、今回作成したTorqueソースコードをテストから実行できるようにします。
まずは、今回追加するソースコードを実行するためのテストケースを作成します。test/cctest/torque/test-torque.cc
のテストケース一覧の末尾に追記しましょう。
// ...
namespace v8 {
namespace internal {
namespace compiler {
// ...
// 末尾のテストケースとして追記。 `Main` という名前の関数を実行するようにする
TEST(Main) {
Isolate* isolate(CcTest::InitIsolateOnce());
CodeAssemblerTester asm_tester(isolate, 0);
TestTorqueAssembler m(asm_tester.state());
{
m.Main();
m.Return(m.UndefinedConstant());
}
FunctionTester ft(asm_tester.GenerateCode(), 0);
ft.Call();
}
} // namespace compiler
} // namespace internal
} // namespace v8
次に、ソースコード自体を test/torque/test-torque.tq
へ追加します。このとき、既存のコードを消してしまうと既存のテストケースが落ちてビルド失敗するので要注意です。
namespace test {
# 関数を追加
@export
macro Main(): void {
Print('Hello world!');
}
// ... (既存のコード)
}
Dockerfileでは、sedのごり押しで無理やりコードを挿入しています。
# テストファイルのパッチ
# 内側のsedでパッチファイルの改行を"\n"に変換して1行表示
# その後外側のsedで "} // namespace compiler" の行の直前に挿入
cat test-torque.cc | sed "/\} \/\/ namespace compiler/i $(cat test-torque-patch.cc | sed -z 's/\n/\\n/g')" > test-torque.cc
# 同様に、tqファイルでも "namespace test {" の行の直後に挿入
cat test-torque.original.tq | sed "/namespace test {/a $(cat ${tq_file_path} | sed -z 's/\n/\\n/g')" > test-torque.tq
sedで置換だけでなく挿入ができることを初めて知りました(直前が sed //i
、直後が sed //a
) 。
テスト用バイナリをビルド
テストファイルにパッチを当てたのでビルドします(c++で書かれているので、そのまま実行はできません)。ビルドにはV8のビルドツール gm を利用します。リファレンスには gm x64.debug.check
の方法が紹介されていますが、こちらはテストも実行されてしまうので x64.debug.tests
(テストファイルのビルドのみ)がおすすめです。
alias gm=/v8/v8/tools/dev/gm.py
# デバッグ用ツール一式ビルド
gm x64.debug.tests
(参考)gmのオプション
$ gm -h
...
- all (build all binaries)
- tests (build test binaries)
- check (build test binaries, run most tests)
- checkall (build all binaries, run more tests)
ビルド成功すると、V8のディレクトリに out/x64.debug/cctest
が生成されます。
テスト実行
「テスト関数とテスト用Torqueソースコードをパッチ」で作成したテストケース Main
を実行します。
# cctestの引数にテストケース名を指定すると単体で実行可能
root@3083bb2e4a26:/v8/v8# out/x64.debug/cctest test-torque/Main
Hello world!
無事実行されました
FizzBuzzを動かしてみる
いよいよTorqueで遊ぶ準備が整いました。
HelloWorldだけではいまいち特徴がつかめないので、FizzBuzzを書いてみます。
(VSCodeをお使いの方は、 V8 Torque Language Supportを入れるとシンタックスハイライトが効いておススメです)
// 分かりやすいようにMainと付けたが、エントリーポイントの概念はない
@export
macro Main(): void {
for (let i: int32 = 1; i <= 30; i++) {
FizzBuzz(i);
}
}
macro FizzBuzz(n: int32): void {
if (n % 15 == 0) {
Print("fizzbuzz");
return;
}
if (n % 3 == 0) {
Print("fizz");
return;
}
if (n % 5 == 0) {
Print("buzz");
return;
}
Print(Convert<Smi>(n));
}
そこまで特殊な構文はなく、一般的な(?) FizzBuzz になりました。
ただし、数値の型の取り回しは注意が必要です。Print
は String
か Object
しか受け取れず、int32
や数値リテラル (constexpr IntegerLiteral
型)を渡すことができません。
定数と(実行時に値が変わりうる)変数の型が厳密に区別されているのがいかにも組み込み向け言語ですね。
candidates are:
Cast(implicit class Context)(MaybeObject): Smi labels CastError
Cast(implicit class Context)(Object): Smi labels CastError
Cast(class String): Smi labels CastError
上記では Smi
(small integer) に変換してから渡しています。
Smi(Object)を Print
するとデバッグプリントになるため、ちょっとFizzBuzzの見栄えは劣ります
DebugPrint: Smi: 0x1 (1)
DebugPrint: Smi: 0x2 (2)
fizz
DebugPrint: Smi: 0x4 (4)
buzz
fizz
DebugPrint: Smi: 0x7 (7)
...
ちなみに、 「FizzBuzz
関数の戻り値をUnion ( | )
にすれば純粋関数になるのでは?」という目論見は崩れました。Unionは共通の基底型が存在しない場合は使えないようです(この辺りはTypeScriptとは異なります)。
macro FizzBuzz(n: Smi): (Smi | constexpr String) {
if (n % 15 == 0) {
return "fizzbuzz";
}
if (n % 3 == 0) {
return "fizz";
}
if (n % 5 == 0) {
return "buzz";
}
return n;
}
test/torque/test-torque.tq:23:1: Torque Error: types Smi and constexpr String have no common supertype
おわりに
以上、V8の内部で動くTorque言語を動かした紹介でした。
環境設定にてこずり、言語を探索する前に力尽きてしまいました…文法や言語の特徴については、機会を改めて調べていきたいと思います。