Big Sky :: プログラミング言語の作り方(3)
Big Sky :: プログラミング言語の作り方(4)
Big Sky :: プログラミング言語の作り方(5)
この前の記事がなかなか人気があったので、続きを書いてみます。
Big Sky :: プログラミング言語の作り方前回の記事では単なる四則演算しか出来ないし、「どこがプログラミング言語やねん」と言われかねないのでもう少しやってみます。 そしてどうやったらプログラミング言語っぽくなるのかを書いてみたいと思います。
github の trending を見てたら面白い物を見つけた。 orangeduck/BuildYourOwnLisp - GitHub Learn C and build your own pr...
http://mattn.kaoriya.net/software/build_your_own_programming_language.htm
前回の記事後半で変数について書きました。変数は ident とう識別で以下の様に定義します。
ident : /[a-zA-Z][a-zA-Z0-9_]*/ ;
ここで言語のシンタックス設計が必要になります。このサンプルでは以下の様なシンタックスにします。まず代入
a = 1;
b = "hello world";
計算
a = a * 2 + 1;
表示
println(a + 1);
この単位がプログラミング言語でいう statement(文) になります。また前回に作った要素は statement ではなく expression(式) となります。代入は ident が左項、右項が lexp になるので以下の様に定義出来ます。
let : <ident> '=' <lexp> ';' ;
ついでなので値を表示する関数を実装します。関数は以下の様に定義出来ます。
call : <ident> '(' <lexp>? (',' <lexp>)* ')' ';' ;
最後に let と call の2つの命令が並んでいるという定義を書きます。全体は以下の通り。
number : /-?[0-9]+/ ;
factor : '(' <lexp> ')'
| <number>
| <string>
| <ident> ;
string : /"[^\"]*"/ ;
ident : /[a-zA-Z][a-zA-Z0-9_]*/ ;
term : <factor> (('*' | '/' | '%') <factor>)* ;
lexp : <term> (('+' | '-') <term>)* ;
let : <ident> '=' <lexp> ';' ;
call : <ident> '(' <lexp>? (',' <lexp>)* ')' ';' ;
stmts : (<let> | <call>)* ;
なお文字列は本来ならばリテラルエスケープも処理すべきですが省略します。この stmts が処理すべき命令一覧になります。前回同様に、識別を mpc_new で作り parse 関数に渡します。ただしそろそろ引数ではしんどくなってきたので mpc_parse_contents を使い、ファイル名を受け取ります。
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage of %s: file", 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* String = mpc_new("string");
mpc_parser_t* Ident = mpc_new("ident");
mpc_parser_t* Term = mpc_new("term");
mpc_parser_t* Lexp = mpc_new("lexp");
mpc_parser_t* Let = mpc_new("let");
mpc_parser_t* Call = mpc_new("call");
mpc_parser_t* Stmts = mpc_new("stmts");
mpc_err_t* err = mpca_lang(MPC_LANG_DEFAULT, STRUCTURE,
Number, Factor, String, Ident, Term, Lexp, Let, Call, Stmts);
if (err != NULL) {
mpc_err_print(err);
mpc_err_delete(err);
goto leave;
}
if (!mpc_parse_contents(argv[1], Stmts, &result)) {
mpc_err_print(result.error);
mpc_err_delete(result.error);
goto leave;
}
//mpc_ast_print(result.output);
eval(result.output);
mpc_ast_delete(result.output);
leave:
mpc_cleanup(9,
Number, Factor, String, Ident, Term, Lexp, Let, Call, Stmts);
return 0;
}
変数は、あるスコープ(今回だと全部グローバル)で見た時にある識別で認識出来る物がキーとなるので変数格納領域は hash を使います。この変数格納方法は言語処理系によって分かれます。GC 管理しやすい様に管理したりもします。今回のプログラムでは GC は扱ったりはしません。まず変数には文字列もしくは数値が格納されるので以下の値型を用意しました。
#define TYPE_NIL 0
#define TYPE_NUM 1
#define TYPE_STR 2
typedef struct {
int t;
union _v {
int i;
char* s;
} v;
} V;
V V_NIL = { TYPE_NIL, 0 };
t に型識別、v の中の i もしくは s に値が入ります。そしてそれを格納する hash を定義します。hash の実装には mruby も使っている khash を使いました。attractivechaos/klib ・ GitHubなお、文字列はリテラルからダブルクォートを取り除いた物をメモリ確保して格納するので、GC とまでは行きませんが解放用に list で保持しておきます。こちらも khash と同じリポジトリに入っている klist を使います。
https://github.com/attractivechaos/klib
KHASH_MAP_INIT_STR(ident, V)
khash_t(ident) *env;
void v_free(void *p) {
V* v = (V*) p;
if (v->t == TYPE_STR)
free(v->v.s);
}
KLIST_INIT(ident, V, v_free)
klist_t(ident) *gc;
ident 識別を認識したらこの hash から ident をキーとして実値を取り出します。
if (is_a(t, "ident")) {
khint_t k = kh_get(ident, env, t->contents);
if (k == kh_end(env)) {
return V_NIL;
}
return kh_value(env, k);
}
そして代入部分は hash への push を行います。
if (is_a(t, "let")) {
int r = 0;
V v;
khint_t k = kh_put(ident, env, t->children[0]->contents, &r);
v = eval(t->children[2]);
kh_value(env, k) = v;
return v;
}
全体のコードだと以下の様になります。今回はコンパイルに mpc.h mpc.c だけでなく khash.h と klist.h も必要になります。
#include "mpc.h"
#include "khash.h"
#include "klist.h"
#define STRUCTURE \
" \n" \
" number : /-?[0-9]+/ ; \n" \
" factor : '(' <lexp> ')' \n" \
" | <number> \n" \
" | <string> \n" \
" | <ident> ; \n" \
" string : /\"[^\"]*\"/ ; \n" \
" ident : /[a-zA-Z][a-zA-Z0-9_]*/ ; \n" \
" \n" \
" term : <factor> (('*' | '/' | '%') <factor>)* ; \n" \
" lexp : <term> (('+' | '-') <term>)* ; \n" \
" let : <ident> '=' <lexp> ';' ; \n" \
" call : <ident> '(' <lexp>? (',' <lexp>)* ')' ';' ; \n" \
" stmts : (<let> | <call>)* ; \n"
#define is_a(t, a) (strstr(t->tag, a) != NULL)
#define TYPE_NIL 0
#define TYPE_NUM 1
#define TYPE_STR 2
typedef struct {
int t;
union _v {
int i;
char* s;
} v;
} V;
V V_NIL = { TYPE_NIL, 0 };
KHASH_MAP_INIT_STR(ident, V)
khash_t(ident) *env;
void v_free(void *p) {
V* v = (V*) p;
if (v->t == TYPE_STR)
free(v->v.s);
}
KLIST_INIT(ident, V, v_free)
klist_t(ident) *gc;
V eval(mpc_ast_t* t) {
int i;
if (is_a(t, "number")) {
V v = { TYPE_NUM };
v.v.i = atoi(t->contents);
return v;
}
if (is_a(t, "string")) {
V v = { TYPE_STR };
size_t l = strlen(t->contents) - 2;
v.v.s = calloc(1, l + 1);
strncpy(v.v.s, t->contents + 1, l);
v.v.s[l] = 0;
*kl_pushp(ident, gc) = v;
return v;
}
if (is_a(t, "ident")) {
khint_t k = kh_get(ident, env, t->contents);
if (k == kh_end(env)) {
return V_NIL;
}
return kh_value(env, k);
}
if (is_a(t, "factor")) {
return eval(t->children[1]);
}
if (is_a(t, "lexp") || is_a(t, "term")) {
V lhs = eval(t->children[0]);
for (i = 1; i < t->children_num; i += 2) {
char* op = t->children[i]->contents;
V rhs = eval(t->children[i+1]);
int iv = rhs.t == TYPE_NUM ? rhs.v.i : 0;
if (strcmp(op, "+") == 0) { lhs.v.i += iv; }
if (strcmp(op, "-") == 0) { lhs.v.i -= iv; }
if (strcmp(op, "*") == 0) { lhs.v.i *= iv; }
if (strcmp(op, "/") == 0) { lhs.v.i /= iv; }
if (strcmp(op, "%") == 0) { lhs.v.i %= iv; }
}
return lhs;
}
if (is_a(t, "let")) {
int r = 0;
V v;
khint_t k = kh_put(ident, env, t->children[0]->contents, &r);
v = eval(t->children[2]);
kh_value(env, k) = v;
return v;
}
if (is_a(t, "call")) {
int r = 0, v;
if (!strcmp(t->children[0]->contents, "println")) {
for (i = 2; i < t->children_num - 2; i += 2) {
if (i != 2) printf(", ");
V v = eval(t->children[i]);
switch (v.t) {
case TYPE_NIL:
printf("nil");
break;
case TYPE_NUM:
printf("%d", v.v.i);
break;
case TYPE_STR:
printf("%s", v.v.s);
break;
}
}
printf("\n");
} else {
fprintf(stderr, "Unknwn function '%s'\n", t->children[0]->contents);
}
return V_NIL;
}
for (i = 0; i < t->children_num; i++) {
eval(t->children[i]);
}
return V_NIL;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage of %s: file", 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* String = mpc_new("string");
mpc_parser_t* Ident = mpc_new("ident");
mpc_parser_t* Term = mpc_new("term");
mpc_parser_t* Lexp = mpc_new("lexp");
mpc_parser_t* Let = mpc_new("let");
mpc_parser_t* Call = mpc_new("call");
mpc_parser_t* Stmts = mpc_new("stmts");
mpc_err_t* err = mpca_lang(MPC_LANG_DEFAULT, STRUCTURE,
Number, Factor, String, Ident, Term, Lexp, Let, Call, Stmts);
if (err != NULL) {
mpc_err_print(err);
mpc_err_delete(err);
goto leave;
}
env = kh_init(ident);
gc = kl_init(ident);
if (!mpc_parse_contents(argv[1], Stmts, &result)) {
mpc_err_print(result.error);
mpc_err_delete(result.error);
goto leave;
}
mpc_ast_print(result.output);
eval(result.output);
mpc_ast_delete(result.output);
kl_destroy(ident, gc);
leave:
mpc_cleanup(9,
Number, Factor, String, Ident, Term, Lexp, Let, Call, Stmts);
return 0;
}
第一引数に渡すファイルを作ります。
a = 1;
a = a * 2 + 1;
b = "hello";
println(b, a + 1);
実行すると...
hello, 4
動きました。とりあえず
- 変数代入
- 変数参照
- 関数呼出
ここら辺からだんだんしんどくなってきますが、自分で作ったプログラミング言語は愛着もあり将来残って行く物になると思います。