静的・バイトコード実行型言語を作る

今回から新しい言語を作ります

夏から秋にかけて、会社の仕事が大変アレな状態でして、 ずいぶん長いこと放置してしまいましてすみません。

さて、ここまで、型なし・解析木実行型の「crowbar」 という言語を作ってきましたが、 crowbarは(実用言語としてはさておき)サンプルプログラムとしては 一通りの機能が揃ったと思いますので、 放置明けで唐突ではありますが、今回から新しい言語を作ります。

言語の名前は「Diksam」です。

この名前は、「パールのようなもの」とかいったネタではなく、 私が好きな紅茶の名前です。 コーヒー言語のJavaがあるのだから、紅茶言語があってもよいのではないかと ※1。 Diksamはアッサム系の紅茶で、 コクのある重めの紅茶です。

crowbarは、変数が型を持たず、解析木を直接実行するタイプの言語でしたが、 Diksamは、静的な型付けがあり、バイトコードを実行する言語とします。

そのような言語は、crowbarのような言語に比べ、 以下のような利点があります。

バイトコードを実行する

バイトコードとは、 仮想マシン(Virtual Machine)という仮想的なCPUが実行する 機械語のようなものです。機械語はCPUが直接実行しますが、 バイトコードは、仮想マシンというプログラムが実行します。

バイトコードは、機械語同様、その実体は単なる数字の並びですが、 (これまた機械語同様)人間にとってわかりやすくするために、 各命令にニモニックという文字列表現を与えた形で表現します。

たとえばJavaの仮想マシン(JVM)が実行するバイトコードは 以下のようになります (この企画の最初の記事から再掲)。

   0:   bipush  10
   2:   istore_1
   3:   iload_1
   4:   bipush  10
   6:   if_icmpne       20
   9:   getstatic       
   12:  ldc     
   14:  invokevirtual   
   17:  goto    28
   20:  getstatic       
   23:  ldc     
   25:  invokevirtual   

人間にとってわかりやすくするために文字列表現を与えたものでさえ、 こんなワケのわからん代物では、ソースをもとにこれを生成するなど 夢のまた夢、バイトコードを生成するコンパイラなんか作れるはずがない ――と思ってしまうかもしれませんが、 実際はそんなに難しいものでもありません。 具体的にどのようにするかは以下で説明します。

式をバイトコードに変換する

crowbarはver.0.2から演算途中の値をスタックに積むようになりました (GCがポインタを確実に追跡するため)。 この時点で、バイトコードを実行する言語とほぼ同じことを実現していました。 こちらで以下のように書いた通りです。

実のところこれは、JVMのようなスタックマシンが、 バイトコードを実行していく際のスタックの動きと同じです。 つまり、ここまでやってしまえば、バイトコードインタプリタまでもう一歩です。

具体的には、解析木を深さ優先で辿って、以下のようなコードを生成します。

  1. 定数/変数の類は、値をそのままスタックに積む。
  2. 二項演算子は、左辺→右辺の順でスタックに積んだ上で、 スタックの最上位のふたつの要素について演算を行い、 その結果をスタックに積む。

たとえば、

  1 + 2 * 3 - 4

という式は、以下のような解析木に展開されます。

この解析木を帰りがけ順で辿りながら、 定数のノードでは「push 値」というバイトコードを、 演算子のノードではその値のバイトコードを出力すると、 以下のようなコードとなります(これは説明用の擬似コードです。 JavaのバイトコードでもDiksamのバイトコードでもありません)。

  1:  push 1 # 「1」をスタックに積む
  2:  push 2 # 「2」をスタックに積む
  3:  push 3 # 「3」をスタックに積む
  4:  mul    # スタックのてっぺんのふたつの要素を掛け、結果をスタックに積む
  5:  add    # スタックのてっぺんのふたつの要素を足し、結果をスタックに積む
  6:  push 4 # 「4」をスタックに積む
  7:  sub    # スタックのふたつめの要素からてっぺんの要素を引き、結果をスタックに積む

式に出てくるのは二項演算子だけではありませんが、 たとえば単項演算子でも考え方は同じです。 単項マイナス演算子であれば、「スタックのてっぺんの値を取り出し、 符号を逆転させてまたスタックに積む」という操作になります。

ところで、上記の例では整数しか扱いませんでした。 実際の言語では実数も必要ですし、「整数と実数を足す」といった 混合演算が可能です。 このような場合は、整数の側をいったん実数に変換してから 実数同士で加算を行ないます。 文字列と整数の加算もできる ※2言語では、 整数をいったん文字列に変換してから文字列の連結を行うことになります。

このようなことを行なう場合、アプローチの方法はふたつあります。

ひとつは、スタックに積む値についてそれぞれ型タグを付け、 実行時に判定を行う方法です。 crowbarはこの方法です。 CRB_Value構造体には型タグ(typeメンバ)が付いており、 実行時にそれを見て型変換や演算を行なっていました。

もうひとつは、型の判定はすべてコンパイル時に行なってしまう方法です。

型変換が必要なら、型変換処理をコンパイル時に挟みます。 これは、下図のように解析木にキャストを挟むことで実現できます (例は、2.5と3の加算)。

この解析木から生成されるバイトコードでは、 以下のようにキャストの命令が登場します。 この例での「cast_int_to_double」という命令は、 スタックの先頭のint型の要素をdouble型に変換します。

  1:  push_double         2.5
  2:  push_int            3
  3:  cast_int_to_double
  4:  add_double

この方法では、たとえば加算であれば、それが整数同士の加算であるのか、 実数同士の加算であるのか、あるいは文字列連結であるのかは、 コンパイルが済んだ段階で完全にわかっています。 そのため、実行時に余計な判定を入れる必要がなく、 高速な実行が期待できます。 ただし、コンパイル時に変数の型がわからなければいけないので ※3、 ユーザ(プログラマ)に、変数を型付きで宣言してもらう必要があります。

制御構造をバイトコードに変換する

ifやwhileのような制御構造は、バイトコード上では、 gotoのようなジャンプ命令で表現できます。

たとえば以下のようなif文は、

  if (a = 10) {
    条件が成立した場合の処理
    条件が成立した場合の処理
    条件が成立した場合の処理
    条件が成立した場合の処理
  }
  続きの処理

以下のようなバイトコードで表現できます。

  1:  変数aの値をpush
  2:  push_int      10
  3:  eq_int           # int同士の値を比較(==)し、結果をスタックに積む
  4:  jump_if_false 9  # スタックの先頭の値がfalseなら9行目にジャンプ
  5:  条件が成立した場合の処理
  6:  条件が成立した場合の処理
  7:  条件が成立した場合の処理
  8:  条件が成立した場合の処理
  9:  続きの処理       # 4行目から、ここにジャンプする

crowbarでは、制御構造も解析木で表現し、 それを再帰で下りながらプログラムを実行していましたので、 breakやcontinueを実現するためには、再帰の深いところから (特別な戻り値を使用して)復帰する必要がありました。 バイトコード実行型の言語では、 breakやcontinueをより簡単に実現できますし、 (必要とあれば)gotoも簡単に実装可能です。

関数の実現

C等では、通常、関数呼び出しもスタックを使用します。

このあたりのことは「C言語 ポインタ完全制覇」あたりに詳しく書いたので そちらを参照してください ――と言いたいところですが一応軽く説明すると、 Cの場合、(素朴な実装では)関数呼び出しは以下の手順を踏みます。

  1. 引数を(後ろから)スタックに積む
  2. リターンアドレス等の復帰情報を積む
  3. ローカル変数分の領域を積む

Cが引数を「後ろから」スタックに積むのは、 printf()のような可変長引数を実現するためで、 今回作る言語Diksamには可変長引数はないので、 後ろから積む必然性は特にありません(実際、 Diksamでは前から積んでいます)。

リターンアドレスというのは、文字通り関数が終了した時に 復帰するアドレスを指します。 関数というものはプログラムのあちこちから呼び出されますから、 このように戻り先をスタックに積んでおかないと戻る先が わからなくなってしまうわけです。

ローカル変数を積み終わった時点でのスタックのトップを、 図では「base」という矢印で指しています。 この「base」からのオフセットで、ローカル変数や 引数を指定できます。

先程のif文のバイトコードの例において、 「変数aの値をpush」と日本語で書いたところをバイトコードにするなら、 以下のようになるでしょう(aがローカル変数の場合)。

  push_stack_int    0  # 0はその変数のbaseからのオフセット

宣言のある言語では、 グローバル変数もコンパイル時にすべて決定していますから、 番号で指定できます。 たとえばint型のグローバル変数の値をスタックに積むコードは、 以下のようになるでしょう。

  push_static_int    0  # 0はそのグローバル変数のインデックス


前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る