プログラミング言語 samplan

samplanとは

samplanはJavaで実装された簡単なプログラミング言語です。

プログラミング言語として実用的に使う、という目的ではなく、プログラミング言語の作り方を示すこと、および私自身が「言語処理系を書きたくなった」という理由で作ったものですので、言語としては、まあ実用性はありません。なにしろ使用できる組み込み関数は「print()」しかないですし、配列もないですし。

ソースはGitHubにあります。

https://github.com/kmaebashi/samplan

言語の雰囲気的なものは、ここのサンプルプログラムを見ればつかめると思います。波括弧が出てくるCやJavaに似た言語ですが、以下のような違いがあります。

  1. 宣言において、型が後置。「int hoge;」ではなくて、「var hoge int;」。
  2. if文とかで{ }を省略できない。それに伴いifの後ろの( )が不要になっている、
  3. 代入は「=」ではなく「:=」。それに伴い、等しいことを示す演算子は「==」ではなく「=」になっている。

samplanは静的な型付けを持つバイトコード実行型の言語です。ソースからコンパイルしたバイトコード※1をメモリ上に保持し、それを実行します。よって、内部的にはコンパイルしていますが、利用者がそれを意識することはありません。

処理系自体はJavaで記述しています(JDK17を使用。そんなに新しい機能は使っていませんが)。

コンパイラは、1トークン先読みの再帰下降パーサで自作しています(yaccとかJavaCCのようなパーサジェネレータは使っていません)。バイトコードを実行する仮想機械(SVM: Samplan Virtual Machine)はありがちなスタックマシンです。プログラミング言語の作り方を学びたい、という人には、何かしら参考にはなるのではないでしょうか。

プログラミング言語の作り方については、以前こちらに書いています。

プログラミング言語を作る

samplanも基本的な考え方は同じですが、上のページでは、コンパイラを作るのに、C向けの、レキシカルアナライザ生成プログラムであるlex(flex)、パーサジェネレータであるyacc(bison)を使っています。こういうツールは確かに便利なのですが、せっかくイチからプログラミング言語を作るなら、そういうツールも使わずにフルスクラッチで書いてみたい、という人もいることでしょう。また、今どきはCに詳しくない人も多いでしょうから、Javaで作ったことにも意味があると思っています。

「プログラミング言語を作る」という、マニアックな、でも楽しい趣味に目覚めた人の参考になりましたら幸いです。

samplan文法

コメント

samplanでは、「#」から改行までがコメントです。

データ型

samplanでは以下のデータ型が使用可能です。

型名意味
boolean論理型。trueまたはfalseの値を持つ。
int整数型。表現できる範囲等はJavaのintと同じ。
real実数型。表現できる範囲等はJavaのdoubleと同じ。
string文字列型。samplanに参照型はないので、初期値は空文字になっている。

変数宣言

変数は以下のように宣言します。

var hoge int;       # 初期化式なしの場合
var hoge int := 10; # 初期化式ありの場合

変数は、使うところより先に宣言されていなければいけません。

演算子

samplanでは以下の演算子が使用可能です。

No. 優先順位 演算子 機能 結合性
1 1 - 単項のマイナス。オペランドの符号を逆転させる。 右から左
2 1 ++ インクリメント。オペランドの値を1増やす。整数型の変数のみに適用可能。 右から左
3 1 -- デクリメント。オペランドの値を1減らす。整数型の変数のみに適用可能。 右から左
4 2 * 乗算。整数型と実数型に適用可能。 左から右
5 2 / 除算。整数型と実数型に適用可能。 左から右
6 3 + 加算。整数型と実数型と文字列型に適用可能。文字列は連結する。 左から右
7 3 - 減算。整数型と実数型に適用可能。 左から右
8 4 = 等値演算子。整数型と実数型と文字列型に適用可能。 左から右
9 4 != 非等値演算子。整数型と実数型と文字列型に適用可能。 左から右
10 4 > 大小比較演算子。整数型と実数型と文字列型に適用可能(文字列型は辞書順)。 左から右
11 4 >= 大小比較演算子。整数型と実数型と文字列型に適用可能(文字列型は辞書順)。 左から右
12 4 < 大小比較演算子。整数型と実数型と文字列型に適用可能(文字列型は辞書順)。 左から右
13 4 <= 大小比較演算子。整数型と実数型と文字列型に適用可能(文字列型は辞書順)。 左から右
14 5 && 論理AND。論理型のみに使用可能。 左から右
15 5 || 論理OR。論理型のみに使用可能。 左から右
16 6 := 代入。これ自体が値を持つので、「a := b := c := 0;」も可能 右から左

関数定義

以下のように書くことで関数が定義できます。

# ふたつの引数の値を加算して返す関数
function add(a real, b real) real {
  return a + b;
}

戻り値を持たない関数は、型名を省略してください。

# ふたつの引数の値を加算して表示する関数
function add2(a real, b real) {
  print("result.." + (a + b));
}

呼び出すときは、「add(2, 3)」のように書きます。

関数内ではローカル変数も使用可能です。

function sum(value int) int {
  var i int := 0;
  var sum int := 0;

  while (i <= value) {
    sum := sum + i;
    ++i;
  }

  return sum;
}

関数は、使うところより先に宣言されている必要はありません。

ローカル変数は、{ }で囲まれたブロックごとに宣言できますが、外周のブロックの変数(グローバル変数を含む)と名前が衝突してはいけません。

制御構造

if文

条件分岐はif文です。{ }が省略不可なので、elsifを導入しています。

if a = 0 {
  print("aは0だよ!");
} elsif a = 1 {
  print("aは1だよ!");
} else {
  print("aは0でも1でもないよ!");
}

while文

ループはwhile文です。今のところfor文はありません。

var i int:= 0;
while i < 10 {
  print("i.." + i);
  ++i;
}

return文

関数から戻る際にはreturn文を使います。

function add(a real, b real) real {
  return a + b;
}

戻り値がない場合は、式を省略します。

SVMインストラクションリファレンス

SVMのオペランドは、すべて整数値です。

以下の表の「スタック」欄は、インストラクション実行時のスタックの変化を示します。[ ]内には、スタックの上位にある値の型を表記しています。 右端がスタックのトップです。SVMにはbooleanはなく、内部的にはintなのですが、わかりやすくするため booleanと書いています。 交換則が効かない演算子では、順番に意味があるので、 [int1 int2]のように記述し、結果の方を[ (int - int2) ]のように記述しています。[ (int1 > int1) ]の型はbooleanです。

No. 命令 オペランド 意味 スタック
1 NOP なし 何もしない(未使用)。 -
2 PUSH_INT pushする整数値 整数をスタックにpushする。 [] → [int]
3 PUSH_REAL pushする実数値のコンスタントプールインデックス 実数をスタックにpushする。 [] → [real]
4 PUSH_STRING pushする文字列のコンスタントプールインデックス 文字列をスタックにpushする。 [] → [string]
5 PUSH_STACK_INT ローカル変数のID 整数型のローカル変数の値をスタックにpushする。 [] → [int]
6 PUSH_STACK_REAL ローカル変数のID 実数型のローカル変数の値をスタックにpushする。 [] → [real]
7 PUSH_STACK_STRING ローカル変数のID 文字列型のローカル変数の値をスタックにpushする。 [] → [string]
8 POP_STACK_INT ローカル変数のID スタックトップの整数値をローカル変数にpopする。 [int] → []
9 POP_STACK_REAL ローカル変数のID スタックトップの実数値をローカル変数にpopする。 [real] → []
10 POP_STACK_STRING ローカル変数のID スタックトップの文字列をローカル変数にpopする。 [string] → []
11 PUSH_STATIC_INT グローバル変数のID 整数型のグローバル変数の値をスタックにpushする。 [] → [int]
12 PUSH_STATIC_REAL グローバル変数のID 実数型のグローバル変数の値をスタックにpushする。 [] → [real]
13 PUSH_STATIC_STRING グローバル変数のID 文字列型のグローバル変数の値をスタックにpushする。 [] → [string]
14 POP_STATIC_INT グローバル変数のID スタックトップの整数値をグローバル変数にpopする。 [int] → []
15 POP_STATIC_REAL グローバル変数のID スタックトップの実数値をグローバル変数にpopする。 [real] → []
16 POP_STATIC_STRING グローバル変数のID スタックトップの文字列をグローバル変数にpopする。 [string] → []
17 ADD_INT なし スタックトップの二つの整数同士の加算を行い、結果をスタックに積む。 [int int] → [int]
18 ADD_REAL なし スタックトップの二つの実数同士の加算を行い、結果をスタックに積む。 [real real] → [real]
19 ADD_STRING なし スタックトップの二つの文字列同士の連結を行い、結果をスタックに積む。 [string string] → [string]
20 SUB_INT なし スタックトップの二つの整数同士の減算を行い、結果をスタックに積む。 [int1 int2] → [(int1 - int2)]
21 SUB_REAL なし スタックトップの二つの実数同士の減算を行い、結果をスタックに積む。 [real1 real2] → [(real1 - real2)]
22 MUL_INT なし スタックトップの二つの整数同士の乗算を行い、結果をスタックに積む。 [int int] → [int]
23 MUL_REAL なし スタックトップの二つの実数同士の乗算を行い、結果をスタックに積む。 [real real] → [real]
24 DIV_INT なし スタックトップの二つの整数同士の除算を行い、結果をスタックに積む。 [int1 int2] → [(int1 / int2)]
25 DIV_REAL なし スタックトップの二つの実数同士の除算を行い、結果をスタックに積む。 [real1 real2] → [(real1 / real2)]
26 MINUS_INT なし スタックトップの整数値の符号を反転する。 [int] → [int]
27 MINUS_REAL なし スタックトップの実数値の符号を反転する。 [real] → [real]
28 INCREMENT なし スタックトップの整数値に1加算する。 [int] → [int]
29 DECREMENT なし スタックトップの整数値から1減算する。 [int] → [int]
30 CAST_INT_TO_REAL なし スタックトップの整数値を実数に変換する。 [int] → [real]
31 CAST_REAL_TO_INT なし スタックトップの実数値を整数に変換する。 [real] → [int]
32 CAST_BOOLEAN_TO_STRING なし スタックトップの論理値を文字列に変換する。 [boolean] → [string]
33 CAST_INT_TO_STRING なし スタックトップの整数値を文字列に変換する。 [int] → [string]
34 CAST_REAL_TO_STRING なし スタックトップの実数値を文字列に変換する。 [real] → [string]
35 EQ_INT なし スタックトップの二つの整数同士の等値比較を行い、結果をスタックに積む。 [int int] → [boolean]
36 EQ_REAL なし スタックトップの二つの実数同士の等値比較を行い、結果をスタックに積む。 [real real] → [boolean]
37 EQ_STRING なし スタックトップの二つの文字列同士の等値比較を行い、結果をスタックに積む。 [string string] → [boolean]
38 GT_INT なし スタックトップの二つの整数同士の大小比較を行い、結果をスタックに積む。 [int1 int2] → [(int1 > int2)]
39 GT_REAL なし スタックトップの二つの実数同士の大小比較を行い、結果をスタックに積む。 [real1 real2] → [(real1 > real2)]
40 GT_STRING なし スタックトップの二つの文字列同士の大小比較を行い、結果をスタックに積む。 [string1 string2] → [(string1 > string2)]
41 GE_INT なし スタックトップの二つの整数同士の大小比較を行い、結果をスタックに積む。 [int1 int2] → [(int1 >= int2)]
42 GE_REAL なし スタックトップの二つの実数同士の大小比較を行い、結果をスタックに積む。 [real1 real2] → [(real1 >= real2)]
43 GE_STRING なし スタックトップの二つの文字列同士の大小比較を行い、結果をスタックに積む。 [string1 string2] → [(string1 >= string2)]
44 LT_INT なし スタックトップの二つの整数同士の大小比較を行い、結果をスタックに積む。 [int1 int2] → [(int1 < int2)]
45 LT_REAL なし スタックトップの二つの実数同士の大小比較を行い、結果をスタックに積む。 [real1 real2] → [(real1 < real2)]
46 LT_STRING なし スタックトップの二つの文字列同士の大小比較を行い、結果をスタックに積む。 [string1 string2] → [(string1 < string2)]
47 LE_INT なし スタックトップの二つの整数同士の大小比較を行い、結果をスタックに積む。 [int1 int2] → [(int1 <= int2)]
48 LE_REAL なし スタックトップの二つの実数同士の大小比較を行い、結果をスタックに積む。 [real1 real2] → [(real1 <= real2)]
49 LE_STRING なし スタックトップの二つの文字列同士の大小比較を行い、結果をスタックに積む。 [string1 string2] → [(string1 <= string2)]
50 NE_INT なし スタックトップの二つの整数同士の非等値比較を行い、結果をスタックに積む。 [int int] → [boolean]
51 NE_REAL なし スタックトップの二つの実数同士の非等値比較を行い、結果をスタックに積む。 [real real] → [boolean]
52 NE_STRING なし スタックトップの二つの文字列同士の非等値比較を行い、結果をスタックに積む。 [string string] → [boolean]
53 LOGICAL_AND なし スタックトップの二つの論理値同士の論理積を取り、結果をスタックに積む。 [boolean boolean] → [boolean]
54 LOGICAL_OR なし スタックトップの二つの論理値同士の論理和を取り、結果をスタックに積む。 [boolean boolean] → [boolean]
55 POP なし スタックトップの要素を捨てる。 [T] → []
56 DUPLICATE なし スタックトップの要素を複製してスタックに積む。 [T] → [T T]
57 JUMP ジャンプ先のアドレス オペランドで指定したアドレスにジャンプする。 [] → []
58 JUMP_IF_TRUE ジャンプ先のアドレス スタックトップの値がtrueであれば、オペランドで指定したアドレスにジャンプする。 [boolean] → []
59 JUMP_IF_FALSE ジャンプ先のアドレス スタックトップの値がfalseであれば、オペランドで指定したアドレスにジャンプする。 [boolean] → []
60 INVOKE 関数のID オペランドで指定された関数を呼び出す。 [] → 下記参照
61 RETURN なし 関数から復帰する。 [戻り値] → 下記参照

上の表を見る、および処理系のソースを読むにあたり、必要な情報をいくつか。

  1. 「コンスタントプールインデックス」というのは、実数及び文字列のリテラルを指すインデックスです。SVMではオペランドの型はすべて整数なので、実数や文字列はバイトコード中に直接登場させることはできません。そこで、実数や文字列のリテラルは、別途配列(コンスタントプール)で保持し、バイトコード中ではその添字で指定しています。
  2. 「ローカル変数のID」というのは、コンパイラがローカル変数(引数含む)に振った番号ですが、これはSVMに渡った時点でbaseレジスタが指す位置からのオフセットです。関数呼び出し時には、引数を前から順にスタックに積み、次に復帰情報をスタックに積み、そのまた次にローカル変数の領域を確保しますが、復帰情報が3つあるので引数のIDとローカル変数のIDの間は3離れています。数で言えば、引数1のIDが0、ローカル変数1のIDは6です。
    このIDは、コンパイル中は復帰情報部分も詰まった連番なのですが、コード生成の時点でずらしています。
  3. 「グローバル変数のID」とは、コンパイラがグローバル変数に前から順に振った連番です(先頭が0)。
  4. 「関数のID」とは、コンパイラが関数に前から順に振った連番です。samplanでは組み込み関数print()がありますが、これのIDが0であり、その続きにsamplanで書いた関数が続きます。
    しかし、コンパイラが生成する実行形式であるSvmExecutableが保持する関数の配列(SvmFunction[] functions;)には組み込み関数分を含まないので、実際に関数を参照する際には引き算する必要があります。



ひとつ上のページへ戻る | トップページへ戻る