2014/04/14


Big Sky :: プログラミング言語の作り方
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
https://github.com/attractivechaos/klib
なお、文字列はリテラルからダブルクォートを取り除いた物をメモリ確保して格納するので、GC とまでは行きませんが解放用に list で保持しておきます。こちらも khash と同じリポジトリに入っている klist を使います。 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(ba + 1);
実行すると... hello, 4 動きました。とりあえず
  • 変数代入
  • 変数参照
  • 関数呼出
は出来ました。計算しか出来ないプログラムですが、プログラミング言語の作り方をイメージして頂けるのではと思います。
ここら辺からだんだんしんどくなってきますが、自分で作ったプログラミング言語は愛着もあり将来残って行く物になると思います。
Posted at by



2014/04/11


Big Sky :: プログラミング言語の作り方(2)
Big Sky :: プログラミング言語の作り方(3)
Big Sky :: プログラミング言語の作り方(4)
Big Sky :: プログラミング言語の作り方(5)
github の trending を見てたら面白い物を見つけた。
orangeduck/BuildYourOwnLisp - GitHub

Learn C and build your own programming language in under 1000 lines of code!

https://github.com/orangeduck/BuildYourOwnLisp
手順にそってC言語で lisp を実装する手順を見せるという物なのだが、その教材の一部としてパーサのコードが含まれている。 このパーサ部分だけ別のプロジェクトとして外出しされている。
orangeduck/mpc - GitHub

A Parser Combinator library for C

https://github.com/orangeduck/mpc
C言語で使えて汎用性のあるパーサライブラリだ。ライブラリとはいえ組み込み手順等は作られていないので、ビルドする場合はソースを一緒にコンパイルする形になるかと思います。
今日はこれを使って四則演算計算機を作ってみたいと思います。プログラミング言語を作った事のある方には釈迦に説法ですがご勘弁下さい。

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 というタグが来たらメモリ内にある辞書からそれをキーに検索して数値を返す、という処理を書いたとしたらどうなりますか?

へっ... 変数!


そうです。変数です。代入こそは出来ませんがプログラミング言語の入り口に一歩足を入れた感じがしますね。
この上記の定義および実装を繰り返し
  • 変数定義
  • 変数参照
  • 変数代入
  • 条件分岐
  • 反復処理
  • 関数定義
  • 関数呼出
などを実装するとプログラミング言語になっていくわけです。あの Ruby もこの様な手続きで開発されています。
どうですか?プログラミング言語、作ってみたくなりませんか!
今回は mpc をサンプルとして使いましたが世の中には bison 等の有名な yacc 定義ツールがあります。皆さんもぜひ面白い物を作ってみて下さい。
Posted at by



2014/04/09


先日、golang でスクリプト言語を書いた訳ですが
Big Sky :: Pure Golang でスクリプト言語の VM 書いた。
http://mattn.kaoriya.net/software/anko/20140328210749.htm
言語仕様もだいたい決まって、github で 70star 近く頂いてて、テストも書けてきて、ダンゴも表示出来て、そろそろ実用的になったんじゃないかと個人的に思ったりはしているのですが、誰もコントリビュートしてくれないという、悲しい状況が続いております。
あんまりなので、インストールしなくてもすぐさま試せる様に playground を作ってみました。
Anko Playground
http://play-anko.appspot.com/
golang の playground とほぼ同じです。(アニメーション機能等はありません)
これで誰でも Anko を試せる様になったのではないかと思います。
Posted at by