title: Perl6の内部表現
author: Takahiro Shimizu
profile:
lang: Japanese
## このセッションの内容
- Perl6の主要な実装であるRakudoの内部構造を探ります
- Rakudoの内部で利用されているVMや, Perl6のサブセットなどについて探索します
- スクリプト言語で主に使われているバイトコードインタプリタの気持ちになります
## 内容
- Perl6とは?
- スクリプト言語処理系の動き
- Perl6の内部構造
- NQP
- MoarVM
- NQPとMoarVMのバイトコード対応
- バイトコードインタプリタのC言語実装
- MoarVMの詳細
- MoarVMのバイトコード実行
- まとめ
## Perl6とは
- 当初Perl5の時期バージョンとして開発されていたプログラミング言語
- 現在は別の言語として開発がそれぞれ進んでいる
- 仕様と実装が分離しており, 現在はテストが仕様となっている
- 実装は歴史上複数存在しているが,主流な実装はRakudo
- 言語的にはスクリプト言語であり, 漸進的型付き言語
- 動作環境は、独自のVMのMoarVM, JVM、一部JavaScript上で動作する
## 現在のPerl6
- 現在のバージョンは `6.d`
- [ブラウザ上で実行可能な環境](https://perl6.github.io/6pad/)が存在する
- [IDE](https://commaide.com/)が開発されている
- WebApplicationFrameworkなども開発されており、 Perl5のモジュールを移行したものがいくつか存在する
- 日本では趣味のプロダクト以外社会では使用されていない
- 海外では実際に使われているケースも存在する
- 処理速度では一部Perl5に勝っているが、それでも大分遅い
## [参考]Perl5のソースコード
- Perl5時代
− スカラ、配列、ハッシュの3種類
- それぞれの変数への参照であるリファレンスが使用可能
```perl
use ustrict;
use warnings;
my $scalar_value = "hello!";
print "$scalar_value\n";
my @array = (1..10);
print "$array[0]\n";
my %hash = ( this_is_key => "this_is_value");
print "$hash{this_is_key}\n";
my $hash_ref = \%hash;
print "$hash_ref->{this_is_key}\n";
```
## Perl6のソースコード概要
- Perl5の文法とは比較的変更が多い
- 雰囲気は似ている
- 変数がオブジェクトと化した事により, 変数からsayメソッドを呼ぶことが可能
```
my $str_value = 'hello world!';
$str_value.say; # hello world!
```
- Perl5と同様に,変数にはデフォルトでは型がないような振る舞いをする
```
my $sample_value = 'hello world!';
$sample_value.say; # hello world!
$sample_value = '31';
$sample_value.say; # 31
say($sample_value * 3);
```
## Perl6の言語的な特徴
- 漸進的型付き言語である為, 型を強制することも可能となる
```
my Int $int_value = 31;
$int_value = "hello"; # Compile error!
```
```
$ perl6 type_invalid.p6
Type check failed in assignment to $int_value; expected Int but got Str ("hello")
in block at type_invalid.p6 line 4
```
## Perl6の言語的な特徴
- 型を独自に定義することも可能
- 入力の型によって実行する関数を変える事などができる
```perl6
my subset Fizz of Int where * %% 3;
my subset Buzz of Int where * %% 5;
my subset FizzBuzz of Int where Fizz&Buzz;
my subset Number of Int where none Fizz|Buzz;
proto sub fizzbuzz ($) { * }
multi sub fizzbuzz (FizzBuzz) { "FuzzBuzz" }
multi sub fizzbuzz (Fizz) { "Fizz" }
multi sub fizzbuzz (Buzz) { "Buzz" }
multi sub fizzbuzz (Number $number) { $number }
fizzbuzz($_).say for 1..15;
```
- 型を利用したFizzBuzz
## スクリプト言語
- Perl6は現状コンパイルすることはできない
- スクリプト言語の分類
- 現在広く使われているスクリプト言語(Perl,Python,Ruby...)などとPerl6の構成は類似している
- 今回はPerl6の実装を追いながら、最近のスクリプト言語処理系の大まかな実装を理解する
## スクリプト言語処理系
- スクリプト言語は入力として与えられたソースコードを、 直接評価せずにバイトコードにコンパイルする形式が主流となっている
- その為スクリプト言語の実装は大きく2つで構成されている
- バイトコードに変換するフロントエンド部分
- バイトコードを解釈する仮想機械
## Perl6以外のスクリプト言語
- 現在使われているプロセスVMは言語に組み込まれているものが多い
- JVMやElixirなどのVMは複数の言語で使用されている
- Java
- JVM
- Ruby
- YARV
- Python
- PythonVM
- Erlang
- Elixir
- BEAM
## Perl6の処理系の構成
- Perl6の処理系で現在主流なものはRakudoと呼ばれる実装である(歴史上複数存在する)
- Rakudoは3つのレイヤーから構成されている
- Perl6インタプリタ
- Perl6インタプリタを記述するPerl6のサブセットNQP
- Perl6のバイトコードを解釈するMoarVM
- Perl6/NQPがフロントエンドに相当し、MoarVMがバックエンドに相当する
## Rakudoの構成図
![](fig/Rakudo_System_overview.png)
(http://brrt-to-the-future.blogspot.com/2015/03/advancing-jit-compiler.html)
## Perl6とNQP
- NQP(NotQuitPerl Perl)
- Perl6のサブセット。Perl6っぽい言語
- Perl6、 NQP自体がNQPで記述されている
- NQPもNQPで記述されている為、 セルフビルド(自分自身で自分自身をコンパイルする)を行う
- NQPはPerl6の文法をベースにしているが、 制約がいくつか存在する
- 元々はPerl6の主力実装がParrotだった時代に登場
- 文法がアップデートされており、当時の資料は古くなっている
```
my $value := "hello!";
say($value);
```
## NQPスクリプト
- 変数は束縛 `:=` を使う
− 関数の間に空白を入れてはいけない
- 再帰呼び出しを使うフィボナッチ数列
```
#! nqp
sub fib($n) {
$n < 2 ?? $n !! fib($n-1) + fib($n - 2);
}
my $N := 29;
my $z := fib($N);
nqp::say("fib($N) = " ~ fib($N));
```
## NQPスクリプト(nまでの整数の和)
```perl6
sub add_test($n){
my $sum := 0;
while ( $n > 1) {
$sum := $sum + $n;
--$n;
}
return $sum;
}
say(add_test(10000));
```
## NQP
- NQPはPerl6の中で一番レイヤーが低い言語
- その為、 実行するVMのオペコード(処理単位)を使用することができる
- NQPオペコードは、 Perl6の内部の抽象構文木でも使用されている
- また、 Perl6と同様に型を指定することが可能
```perl6
sub add_test(int $n){
mu $sum := 0;
while nqp::isgt_i($n,1) {
$sum := nqp::add_i($sum,$n);
$n := nqp::sub_i($n,1);
}
return $sum;
}
```
## NQPとMoarVM
- NQPは実行する際にMoarVM/JVMが必要となる
- NQPコンパイラが各VMに対応したバイトコードに変換する
- MoarVMの場合は、MoarVMのバイナリ moar に、 NQPのインタプリタのバイトコードをライブラリや入力として与える
## Perl6のVM
- MoarVM, JVM , JavaScriptが選択可能
- メインで開発されているのはMoarVMであり、 他のVMは機能が実装されていないものが存在する
- `rakudo-star` というPerl6のパッケージ環境では、 MoarVMがデフォルトでインストールされる
## MoarVM
- Metamodel On A Runtime
- C言語で記述されているPerl6専用の仮想機械
- レジスタマシン
- 型情報を持つレジスタに対しての演算として処理される
- Rubyなどはスタックマシンとして実装されている
- Unicodeのサポートや、LuaJITなどを利用したJITコンパイルなども可能
- Perl6やNQPは、MoarVMに対してライブラリなどを設定して起動する
## バイトコード
- Perl6も、Rakudo/NQPはバイトコードに変換され、 バイトコードをVMが実行する
- Perl6/NQPはバイトコードにコンパイルすることが可能
- 直接実行することはできない
```
$nqp --target=mbc --output=fib.moarvm fib.nqp
```
## バイトコード
- バイナリ形式で表現される為、 VMがどのように読み取るかでバイトコードの意味が異なる
- スクリプト言語系のVMは、 VMという名前の通り、 計算機をエミュレートしている
- その為、通常のCPUのストア命令などに相当する命令が実装されている
- スクリプト言語は、その命令の実行を繰り返すことでプログラムを評価する
- スクリプト言語で重要なバイトコード表現は、「仮想機械がどの命令を実行するか」のバイトコード
- CPUに対するアセンブラの数値に対応する
- どういった構成なのかは仮想機械によって異なる
## バイトコードとMoarVM
- MoarVMバイトコードはMoarVMの実行バイナリ `moar` でディスアセンブルすることが可能
```
annotation: add_test.nqp:1
00003 const_i64_16 loc_2_int, 0
00004 hllboxtype_i loc_3_obj
00005 box_i loc_3_obj, loc_2_int, loc_3_obj
00006 set loc_1_obj, loc_3_obj
label_1:
00007 decont loc_3_obj, loc_0_obj
00008 smrt_numify loc_4_num, loc_3_obj
00009 const_i64_16 loc_2_int, 1
00010 coerce_in loc_5_num, loc_2_int
00011 gt_n loc_2_int, loc_4_num, loc_5_num
00012 unless_i loc_2_int, label_2(00031)
00013 osrpoint
annotation: add_test.nqp:3
00014 decont loc_3_obj, loc_1_obj
00015 smrt_numify loc_5_num, loc_3_obj
00016 decont loc_3_obj, loc_0_obj
00017 smrt_numify loc_4_num, loc_3_obj
00018 add_n loc_4_num, loc_5_num, loc_4_num
00019 hllboxtype_n loc_3_obj
00020 box_n loc_3_obj, loc_4_num, loc_3_obj
00021 set loc_1_obj, loc_3_obj
00022 decont loc_3_obj, loc_0_obj
00023 smrt_numify loc_4_num, loc_3_obj
00024 coerce_ni loc_6_int, loc_4_num
00025 const_i64_16 loc_7_int, 1
00026 sub_i loc_7_int, loc_6_int, loc_7_int
00027 hllboxtype_i loc_3_obj
00028 box_i loc_3_obj, loc_7_int, loc_3_obj
00029 set loc_0_obj, loc_3_obj
00030 goto label_1(00007)
```
## NQPとバイトコードの対応
```
say(add_test(10000));
```
```
annotation: add_test.nqp:1
label_1:
00020 getlex_no loc_7_obj, '&say'
00021 decont loc_7_obj, loc_7_obj
00022 const_s loc_3_str, '&add_test'
00023 getlexstatic_o loc_8_obj, loc_3_str
00024 decont loc_8_obj, loc_8_obj
00025 const_i64_16 loc_5_int, 10000
00026 prepargs Callsite_1
00027 arg_i 0, loc_5_int
00028 invoke_o loc_8_obj, loc_8_obj
00029 prepargs Callsite_0
00030 arg_o 0, loc_8_obj
00031 invoke_v loc_7_obj
00032 null loc_7_obj
00033 return_o loc_7_obj
```
- Perl6の変数は直接実態を参照せず、中身が入っているコンテナを参照するようになっている。
- その為 `decont` 命令で、コンテナの中身をレジスタに設定する必要がある
- `const_i64_16` などは64bitの数という意味で、 `int` 型としてレジスタに登録している
- `prepargs` で引数の確認を行い, `invoke_o` で実際にサブルーチンに移行する
## NQPとバイトコードの対応
```
my $sum := 0;
```
```
annotation: add_test.nqp:1
00003 const_i64_16 loc_2_int, 0
00004 hllboxtype_i loc_3_obj
00005 box_i loc_3_obj, loc_2_int, loc_3_obj
00006 set loc_1_obj, loc_3_obj
```
- まず `loc_2` レジスタをint型の整数0で初期化する
- 変数 `$sum` はint型の指定がないので、 obj型で登録しなければならない
- その為, 整数として登録された `loc_2` から、 obj型に一旦キャストし、 `loc_3` レジスタに設定したものを、 `loc_1` レジスタに設定する
## NQPとバイトコードの対応
```
while ( $n > 1) {
```
```
label_1:
00007 decont loc_3_obj, loc_0_obj
00008 smrt_numify loc_4_num, loc_3_obj
00009 const_i64_16 loc_2_int, 1
00010 coerce_in loc_5_num, loc_2_int
00011 gt_n loc_2_int, loc_4_num, loc_5_num
00012 unless_i loc_2_int, label_2(00031)
00013 osrpoint
```
− 比較にもint型の指定がない為、 `num` 型にキャストし、 `num` 型のレジスタでの大小を比較する
- 比較命令は `gt_n` であり、 結果により `unless_i` 命令で、別のラベルにジャンプする
## decode命令
```
while ( $n > 1) {
```
```
00007 decont loc_3_obj, loc_0_obj
```
![](fig/decont_perl6_loc3.svg)
- 変数 `$n` と 整数 `1` を大小比較する為、 まず `$n` から値を取り出す
- とりだした時点では、何の型で使うかは決定していない為、 obj型として判定する
## smrt_nomify
```
while ( $n > 1) {
```
```
00008 smrt_numify loc_4_num, loc_3_obj
```
- `smrt_numify` はレジスタ上のオブジェクトを、 num型に変換し、 別のレジスタに登録する命令
- 今回の整数の比較では、 int型の強制がない為、 数値として比較するためにnum型にキャストしている
![](fig/perl6_num_convert.svg)
## MoarVMのバイトコードインタプリタ部分
MoarVMなどの言語処理系のバイトコードインタプリタは次のことを繰り返している
1. 入力されたバイトコード列から命令に対応する部分を読み取る
2. 読み込んだ数値から、 対応する命令を取得する
3. 命令部分を実行する
4. バイトコード列を次に進め、繰り返す
- この部分の実装は大体次のような処理をしている
## 巨大なswitch文を使うケース
- 命令に対応するバイトコードを数値に変換できるようにし、 switch-case文で分岐させる
- 実行のたびにループで先頭に戻り、次の命令を計算する必要があるので低速
```
```
## Cコンパイラのラベルgotoを使うケース
- 巨大なcase文とループではなく、 次の命令の実行場所に直接jmpで移動する
- 次の命令に対応するラベルを取得する必要があるが、 ループする必要がなく高速
- ラベルgotoであり、 Cコンパイラの拡張機能として搭載されている
- gccおよびLLVM/clangには実装されている
```
```
## MoarVMでは
- ラベルgotoが利用できる場合は利用する
- 使えないコンパイラの場合は、 switch文を利用する
- この判断はマクロで処理をしている
− 一般的にはラベルgotoの方が高速である為、他のスクリプト言語でもラベルgotoが使われている