Big Sky :: プログラミング言語の作り方(3)
Big Sky :: プログラミング言語の作り方(4)
Big Sky :: プログラミング言語の作り方(5)
github の trending を見てたら面白い物を見つけた。
orangeduck/BuildYourOwnLisp - GitHub手順にそってC言語で lisp を実装する手順を見せるという物なのだが、その教材の一部としてパーサのコードが含まれている。 このパーサ部分だけ別のプロジェクトとして外出しされている。
Learn C and build your own programming language in under 1000 lines of code!
https://github.com/orangeduck/BuildYourOwnLisp
orangeduck/mpc - GitHubC言語で使えて汎用性のあるパーサライブラリだ。ライブラリとはいえ組み込み手順等は作られていないので、ビルドする場合はソースを一緒にコンパイルする形になるかと思います。
A Parser Combinator library for C
https://github.com/orangeduck/mpc
今日はこれを使って四則演算計算機を作ってみたいと思います。プログラミング言語を作った事のある方には釈迦に説法ですがご勘弁下さい。
mpc では YACC 構文を扱います。YACC 構文について詳しく知りたい方は Wikipedia 等で調べて下さい。
本来であれば浮動小数も扱うべきですが今回は説明の為省きます。四則演算ですのでまず数値を定義します。mpc ではマッチングに正規表現が扱えます。
パーサライブラリによっては文字単位でしか扱えない物もあり自前でパースする必要もありますが、正規表現が使えるのはかなり便利です。
number : /-?[0-9]+/ ;
見ての通りですが、マイナス記号が0個もしくは1個あり、後続して数値の羅列があるという定義になります。次にオペレータ部を定義します。計算式ですと
1 + 3 * 4
の様に複数の計算が混じります。そこで正規表現を用いて以下の様に定義します。
term : <number> (('*' | '/' | '+' | '-' | '%') <number>)* ;
正規表現の *
を使います。これは計算式の左項(LHS)と、オペレータ記号および右項(RHS)の組み合わせが0個以上あるという定義になります。しかしここで問題が発生します(わざとらしい)。四則演算では掛け算や割り算の方が優先されるのです。またカッコが付く事でその計算も優先される事になります。つまり足し算や引き算の定義よりも、掛け算や割り算、カッコの定義を優先する必要があるのです。
まず掛け算や割り算の定義をします。
term : <factor> (('*' | '/' | '%') <factor>)* ;
そしてこの factor (number ではありません) を定義します。factor には数値、もしくはカッコで囲まれた式が入ります。ですので factor の定義は以下の様になります。
factor : '(' <lexp> ')'
| <number> ;
このカッコの中身 lexp は、term を先に計算した結果に対して足し算や引き算を実行する定義を書けば良いので以下の様に定義出来ます。
lexp : <term> (('+' | '-') <term>)* ;
まとめると
number : /-?[0-9]+/ ;
factor : '(' <lexp> ')'
| <number> ;
term : <factor> (('*' | '/' | '%') <factor>)* ;
lexp : <term> (('+' | '-') <term>)* ;
この様になります。循環していますが、この辺はパーサライブラリが宜しくやってくれます。言語の処理系では上記 number や factor 等の単位で処理を行います。この単位の構造を作ってくれるのがパーサという物で、パーサが作るこの木構造を AST (抽象構文木) と言います。
さて定義が出来たので mpc に食わせましょう。木構造のノード定義となる number や factor を作ります。
mpc_parser_t* Number = mpc_new("number");
mpc_parser_t* Factor = mpc_new("factor");
mpc_parser_t* Term = mpc_new("term");
mpc_parser_t* Lexp = mpc_new("lexp");
そしてこれを定義の順通りに mpca_lang に渡します。この辺はパーサライブラリの使い方によって様々ですのでご注意下さい。全体のコードだと以下の様になります。
#include "../mpc.h"
#define STRUCTURE \
" \n" \
" number : /-?[0-9]+/ ; \n" \
" factor : '(' <lexp> ')' \n" \
" | <number> ; \n" \
" \n" \
" term : <factor> (('*' | '/' | '%') <factor>)* ; \n" \
" lexp : <term> (('+' | '-') <term>)* ; \n"
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage of %s: expr", argv[0]);
exit(0);
}
mpc_result_t result;
mpc_parser_t* Number = mpc_new("number");
mpc_parser_t* Factor = mpc_new("factor");
mpc_parser_t* Term = mpc_new("term");
mpc_parser_t* Lexp = mpc_new("lexp");
mpc_err_t* err = mpca_lang(MPC_LANG_DEFAULT, STRUCTURE, Number, Factor , Term, Lexp);
if (err != NULL) {
mpc_err_print(err);
mpc_err_delete(err);
goto leave;
}
if (!mpc_parse("<argument>", argv[1], Lexp, &result)) {
mpc_err_print(result.error);
mpc_err_delete(result.error);
goto leave;
}
mpc_ast_print(result.output);
mpc_ast_delete(result.output);
leave:
mpc_cleanup(4, Number, Factor, Term, Lexp);
return 0;
}
このプログラムは、引数で与えられた文字列を今回定義した四則演算の定義を元にパースし、出来上がった AST を表示する物です。試しに
3 * (1 + 2)
という引数を与えると以下の様な出力が得られます。
>:
term|>:
factor|number|regex: '3'
char: '*'
factor|>:
char: '('
lexp|>:
term|factor|number|regex: '1'
char: '+'
term|factor|number|regex: '2'
char: ')'
ちゃんと動いてますね。ここまで出来たら計算処理部分を作ります。これが世に言う VM (仮想機械) と呼ばれる部分です。mpc の AST は以下のコードで表現されます。
typedef struct mpc_ast_t {
char *tag;
char *contents;
int children_num;
struct mpc_ast_t** children;
} mpc_ast_t;
ノード名は tag という文字列型で表現されています。セパレータは |
終端は :
の様です。今回ですとノード名はほぼユニークですので、簡略化の為に strstr を使います。まず数値。number は上記の定義でマッチした物そのものなので contents というメンバに入ります。今回は整数だけ扱うので atoi で。
int eval(mpc_ast_t* t) {
if (is_a(t, "number")) {
return atoi(t->contents);
}
return 0;
}
次に factor。factor の定義で lexp は2番目の枝になるので
#define is_a(t, a) (strstr(t->tag, a) != NULL)
int eval(mpc_ast_t* t) {
if (is_a(t, "number")) {
return atoi(t->contents);
}
if (is_a(t, "factor")) {
return eval(t->children[1]);
}
return 0;
}
2番目のノードを得て再起呼び出しとしました。ここを再起を使わずどう実装するかで言語処理系の高速さが決まったりします。さて最後に term と lexp ですが、どちらも左項(LHS)と、オペレータおよび右項(RHS)の組み合わせを繰り返した物、という定義ですので
#define is_a(t, a) (strstr(t->tag, a) != NULL)
int eval(mpc_ast_t* t) {
if (is_a(t, "number")) {
return atoi(t->contents);
}
if (is_a(t, "factor")) {
return eval(t->children[1]);
}
if (t->children_num >= 1) {
int x = eval(t->children[0]), i;
for (i = 1; i < t->children_num; i += 2) {
char* op = t->children[i]->contents;
int rhs = eval(t->children[i+1]);
if (strcmp(op, "+") == 0) { x += rhs; }
if (strcmp(op, "-") == 0) { x -= rhs; }
if (strcmp(op, "*") == 0) { x *= rhs; }
if (strcmp(op, "/") == 0) { x /= rhs; }
if (strcmp(op, "%") == 0) { x %= rhs; }
}
return x;
}
return 0;
}
この様なコードになりました。本当は知らないノードが来たらエラーにする等の処理が必要ですが、簡略化の為省きます。
あとはこれを呼び出します。全体のコードを掲載しておきます。
#include "../mpc.h"
#define STRUCTURE \
" \n" \
" number : /-?[0-9]+/ ; \n" \
" factor : '(' <lexp> ')' \n" \
" | <number> ; \n" \
" \n" \
" term : <factor> (('*' | '/' | '%') <factor>)* ; \n" \
" lexp : <term> (('+' | '-') <term>)* ; \n"
#define is_a(t, a) (strstr(t->tag, a) != NULL)
int eval(mpc_ast_t* t) {
if (is_a(t, "number")) {
return atoi(t->contents);
}
if (is_a(t, "factor")) {
return eval(t->children[1]);
}
if (t->children_num >= 1) {
int x = eval(t->children[0]), i;
for (i = 1; i < t->children_num; i += 2) {
char* op = t->children[i]->contents;
int rhs = eval(t->children[i+1]);
if (strcmp(op, "+") == 0) { x += rhs; }
if (strcmp(op, "-") == 0) { x -= rhs; }
if (strcmp(op, "*") == 0) { x *= rhs; }
if (strcmp(op, "/") == 0) { x /= rhs; }
if (strcmp(op, "%") == 0) { x %= rhs; }
}
return x;
}
return 0;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage of %s: expr", argv[0]);
exit(0);
}
mpc_result_t result;
mpc_parser_t* Number = mpc_new("number");
mpc_parser_t* Factor = mpc_new("factor");
mpc_parser_t* Term = mpc_new("term");
mpc_parser_t* Lexp = mpc_new("lexp");
mpc_err_t* err = mpca_lang(MPC_LANG_DEFAULT, STRUCTURE, Number, Factor , Term, Lexp);
if (err != NULL) {
mpc_err_print(err);
mpc_err_delete(err);
goto leave;
}
if (!mpc_parse("<argument>", argv[1], Lexp, &result)) {
mpc_err_print(result.error);
mpc_err_delete(result.error);
goto leave;
}
//mpc_ast_print(result.output);
printf("%d\n", eval(result.output));
mpc_ast_delete(result.output);
leave:
mpc_cleanup(4, Number, Factor, Term, Lexp);
return 0;
}
コンパイルは前述の通り mpc.c
をコンパイルリンクします。試しに実行すると
$ calc "1"
1
$ calc "1+2"
3
$ calc "3 * (1 + 2)"
9
$ calc "3 * (1 + 4 / 2)"
9
おー!ちゃんと動いています。今回は四則演算計算機を作りましたが、例えば定義でアルファベットから始まる単語を以下の様に定義し
ident : /[a-zA-Z][a-zA-Z0-9_]+/ ;
ident というタグが来たらメモリ内にある辞書からそれをキーに検索して数値を返す、という処理を書いたとしたらどうなりますか?へっ... 変数!
そうです。変数です。代入こそは出来ませんがプログラミング言語の入り口に一歩足を入れた感じがしますね。
この上記の定義および実装を繰り返し
- 変数定義
- 変数参照
- 変数代入
- 条件分岐
- 反復処理
- 関数定義
- 関数呼出
どうですか?プログラミング言語、作ってみたくなりませんか!
今回は mpc をサンプルとして使いましたが世の中には bison 等の有名な yacc 定義ツールがあります。皆さんもぜひ面白い物を作ってみて下さい。