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が使われている