前述のように、Diksamはバイトコードを実行するタイプの言語です。
バイトコードを実行する言語というと他にはJavaが思い浮かびますが、 Javaでは、ソースを書いたら、javacを使ってコンパイルし クラスファイルを作成します。 このクラスファイルに、バイトコードが格納されているわけです。
Diksamの場合、クラスファイルに相当するファイルは生成しません。 コンパイラは、ソースをパースし解析木を作った後、 バイトコードをメモリ上に生成します。 そしてそのメモリ上のバイトコードを、 Diksamの仮想マシン(Diksam Virtual Machine:DVM)が実行します。
バイトコードをファイルに出力しないのは、 ファイルフォーマットを考えたりファイル出力のコードを書いたりするのが 面倒くさい、という私の都合もありますし、 ユーザにとっても、 ソースをいちいちコンパイルしなければならないという手間がありません。 コンパイルに無視できない時間がかかるような 大きなプログラムを書くようになるまでは、この方針の方が便利でしょう。
また、現状の実装においては、DVMはコンパイラが生成するバイトコードを 全面的に信頼しています。「全面的に信頼」とはどういうことかというと、 悪意のあるバイトコードを受け取ると、 DVMはクラッシュする可能性があるということです。
たとえばDVMでは文字列はヒープに確保するため、 string型の変数には文字列へのポインタが格納されていますが、 整数型が格納されているローカル変数を ある時突然string型として使用したら、 正しいアドレスで参照できずクラッシュします。
たとえばJavaでは、アプレットのように、 外部から来たバイトコードを実行しなければなりませんから、 そういう事態は許されません。 そこでJavaでは、バイトコードをロードした時点で ベリファイア(verifier)というプログラムが そのような不正なバイトコードでないかどうかを検証します。
ただ、ベリファイアを作るのも大変なので、 Diksamは当面メモリ上のバイトコードを実行する形式でいこうかと ――すみません要するにこれも手抜きです。
さて、今回作成するサンプル言語「Diksam」ですが、 ひとまず初回公開のVer.0.1では、以下のような言語とします。
Diksamで使用できるデータ型は、以下の4種類です。
前述のようにDiksamは静的な言語なので、変数には型宣言が必要です。 型宣言はCと同様以下のように書きます。
int a;
関数内で宣言された変数はローカル変数となり、 宣言されたブロック内でしか参照できません。 関数外で宣言された変数はグローバル変数です。
今のところDiksamでは変数宣言はどこにでも書けます。 Cでは、ローカル変数はブロックの先頭でしか宣言できず、 C++やJavaではこの制限がなくなって、 多くの人は「改善された」と考えているようですが、 正直私はこれが好きになれません。 途中でだらだらと変数宣言できると、 だんだんひとつの関数が長くなっていくように思うからです。 ここはそのうち直すかも。
また、「型 + 変数名」という宣言の形式も、 配列や関数型の変数が出てくるとややこしいので、 そのうち変更するかもしれません。
var a:int
のような形式の方がいいかなあ、やっぱり。
なお、C等と同様、宣言には初期化子(initializer)が書けます。
int a = 10;
DiksamにはCとは違ってトップレベルがあり、関数の外側にも文が書けます。 プログラムはトップレベルの先頭から実行されます。
制御構造としては、if, while, for, break, continue, returnが使えます。 意味はJavaやcrowbarと同じです。crowbar同様、中括弧は省略できません。 また、breakとcontinueでは(Javaと同様に)ラベルが使えます。
Diksamで使える演算子は以下の通りです。優先順位順に並べてあります。
++, --, () | インクリメント、デクリメント、関数呼び出し |
(単項の)- | 符号の反転 |
* / % | 乗算、除算、剰余 |
+ - | 加算、減算 |
= != | 等値比較。文字列も(参照ではなく値で)比較できます。 |
> >= < <= | 大小の比較。文字列の大小も比較できます(strcmp()に準ずる)。 |
&& | 論理AND演算子。手抜きのため短絡演算子にはなっていません。 |
|| | 論理OR演算子。手抜きのため短絡演算子にはなっていません。 |
= += -= *= /= %= | 代入演算子。意味はCと同じです。 |
, | カンマ演算子。左→右の順で式を評価し、右の式の値を返します。 |
関数呼び出しが演算子として定義されているということは、 関数が式である、ということです。 ということは、Cでの関数ポインタやcrowbarのクロージャのように 関数を変数に代入できたりするはずですが ――現状では、関数を格納する変数が宣言できないので そのようなことはできません。いずれ実装しようと思います。
関数定義は、たとえば以下のような形式になります。
int func(int a, double b) { int local_variable; local_variable = a + b; print("local_variable.." + local_variable + "\n"); return local_variable; }
Diksamには最初から用意されている組み込み関数があります。 一覧は以下の通りです。
print(string arg) | argを表示する。 |
以上!
って、ひとつしかないのに「一覧」もクソもないですな ……って、前と同じこと言ってますが。
コメントは、/* 〜 */のCスタイルコメントと、 //から改行までのC++スタイルのコメントが使えます。
Diksamでは、現状でも関数名は式であり、 関数呼び出しは演算子です(上の演算子一覧表にある通り)。
ということは、Cの関数ポインタやcrowbar等のクロージャのように、
my_print = print; // print関数をmy_printに代入 my_print("hello, world.\n"); // my_printを使用した呼び出し
という使い方ができるのかというと ――現状では、my_printに相当する変数を宣言する方法がないのでできません。
もちろん将来的にはできるようにするつもりではありますが、 単に「print」と書いたとき、それを「printという関数を指すナニモノカ」と いう意味とすることは、別の機能とのトレードオフになります。 「print」と書いたとき、「print()」という呼び出しと解釈する、 という方法もあるからです。
後者の解釈とすると、最初グローバル変数としていたものを、 途中で関数として置き換えても、それを参照している側は変更の必要がない、 ということになります(書き込んでいる場合はダメですが、 まともなプログラムの書き方をしている限り、 そんな個所はそんなにないでしょう)。
これは普通の関数よりオブジェクトのメソッドである場合に特に重要で、 「o.m」について、mがメソッドであれば「o.m()」と解釈する、 という規則を持ち込めば、いちいちgetterを書く必要がなくなるわけです (その場合、言語仕様としては、オブジェクトのフィールドに対して 外部からは代入不可にしておく必要があるでしょう)。
Eiffelや(それを引き継いだ)Rubyはこっちの文法を採用していて、 Diksamではどうしようかと迷ったのですが… 今のところこっちを採用しています。 もしかして私が勘違いしていて、これがトレードオフでなく、 両方取れるような方法があるようでしたら教えてくださいませ(_o_)
上の方でも書いていますが、宣言の構文は、
int a;
ではなく、
var a:int;
とした方がよかったのではないか、という思いもあります。
今のところどっちでも似たようなものですが、 配列はJava流に「int[] a;」とするとしても、 関数を代入できる変数の型宣言は、 まさかCのように「int (*a)(string str);」とするわけにもいかず。
書くなら「int(string str) a;」かなあ(JDK7でそうなるようだし)、 それぐらいなら「var a:(string str) int」の方がいいかなあ、とか考えています。