2014/04/15


Big Sky :: プログラミング言語の作り方
Big Sky :: プログラミング言語の作り方(2)
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
Big Sky :: プログラミング言語の作り方(2)

この前の記事がなかなか人気があったので、続きを書いてみます。 Big Sky :: プログラミング言語の作り方 github の trending を見てたら面白い物を見つけた。 orangeduck...

http://mattn.kaoriya.net/software/build_your_own_programming_language2.htm
言語名を決めました。「俺言語」です。
mattn/orelang - GitHub

俺言語

https://github.com/mattn/orelang/
今回は今後を見据えてコードのブラッシュアップを行いたいと思います。

軽微な修正

前回までのコードですと、ステートメントが EOF で終了するという定義が出来ていなかったので、途中でパースエラーが起きても正常に動作してしまっていました。そこで一番外側を stmts ではなく program という識別にして「ステートメントの繰り返しがあり、最後に EOF が来る」という定義にしました。オマケで comment も足しておきました。
comment : /#[^\n]*/ ;
eof     : /$/ ;
stmt    : (<let> | <call> | <comment>) ;
program : <stmt>* <eof> ;
これで俺言語の中でコメントが打てる様になります。

リファクタリング

次に言語も決まった事なのでリファクタリングを行いました。
全ての関数に ore プレフィックスを付けました。この辺は好き好きですが付けておくと愛着が湧いたりします(どうでもいいですね)。
また今回の修正で扱える様にする型を増やして以下の様に ore_value (値を格納する構造体) を定義しました。
typedef enum {
  ORE_TYPE_NIL,
  ORE_TYPE_INT,
  ORE_TYPE_FLOAT,
  ORE_TYPE_STR,
  ORE_TYPE_FUNC,
  ORE_TYPE_CFUNC
} ore_type;

typedef mpc_ast_t* ore_func;

typedef struct _ore_value {
  ore_type t;
  union _v {
    int i;
    double d;
    char* s;
    void* c;
    ore_func f;
  } v;
} ore_value;
ORE_TYPE_CFUNC は前回追加した println の様な「C言語側の関数」を、ORE_TYPE_FUNC は俺言語内で定義した関数を格納します。
便利関数 ore_define_cfunc を用意して、println の様なコア関数の登録を簡単に出来る様にしました。 ore_value
ore_println(ore_context* ore, int num_in, ore_value* args) {
  int i;
  for (i = 0; i < num_in; i++) {
    if (i != 0) printf(", ");
    switch (args[i].t) {
      case ORE_TYPE_NIL:
        printf("nil");
        break;
      case ORE_TYPE_INT:
        printf("%d", args[i].v.i);
        break;
      case ORE_TYPE_FLOAT:
        printf("%f", args[i].v.d);
        break;
      case ORE_TYPE_STR:
        printf("%s", args[i].v.s);
        break;
      default:
        printf("<unkonwn>");
        break;
    }
  }
  printf("\n");
  return ore_value_nil();
}
この様な関数シグネチャにしておき、以下の様に登録します。
ore_context* ore = ore_new(NULL);
ore_define_cfunc(ore, "println", ore_println);
ore_eval(ore, result.output);
ore_destroy(ore);
今後、配列をサポートした際の len() 関数等も簡単に作成出来る様になりました。

今後の目論見

上記で説明した ore_println の第一引数に ore_context というのが出てきました。ore_context 構造体は以下の通り。
typedef struct _ore_context {
  khash_t(ident)* env;
  klist_t(ident)* gc;
  struct _ore_context* parent;
} ore_context;
前回グローバル変数で宣言していた env と gc を構造体内部に押しやりました。単純にかっこいいからとかではなく、こうしたのにはちゃんと理由があります。この env は ident で示す識別に対して値を格納する物、つまり「スコープ」の役割を果たします。今後、関数宣言を追加しますがその際にスコープを拘束する必要があるのです。また関数が終了するとメモリの解放が必要になります。
プログラミング言語のスコープには大きく分けて2つあります。
  • ダイナミックスコープ
  • レキシカルスコープ
俺言語ではレキシカルスコープを採用する予定なので、関数宣言とその宣言時の環境を抱合せて保持する必要があります。理由は以下のJavaScriptのコードで分かるかと思います。
var count = function() {
  var c = 1;
  return function() {
    return c++;
  };
}();

console.log(count()); // 1
console.log(count()); // 2
まず c が宣言されるのは関数スコープとなります。また c の宣言はグローバルスコープを汚す物であってはなりません。
また ccount が保持する環境を参照してインクリメンタルする必要があります。
上記、ore_context 構造体で parent を保持しているのにも理由があります。
例えば c = 1;
と実行される場合、以下の処理を行う必要があります。
  • 現在のスコープからグローバルスコープに向かって c という識別で検索し、見つかれば値を 1 に更新する。
  • どこにもなければグローバルスコープに c という識別で値 1 を保存する。
その為、現在のスコープからグローバルスコープに向かって探索出来る様に parent が必要となります。今回は対応しませんが今後、上記の関数スコープを実装します。その為の準備となります。

浮動小数点のサポート

上記で ore_value に double 型を追加しています。そこで number 識別を認識した際のパース処理を以下の様に修正します。 ore_value
ore_parse_num(ore_context* ore, const char* s) {
  ore_value v;
  if (!strchr(s, '.')) {
    v.t = ORE_TYPE_INT;
    v.v.i = atoi(s);
  } else {
    v.t = ORE_TYPE_FLOAT;
    v.v.d = atof(s);
  }
  return v;
}
横着感が満載ですね。同様に演算部分も修正します。
if (is_a(t, "lexp") || is_a(t, "term")) {
  v = ore_eval(ore, t->children[0]);
  for (i = 1; i < t->children_num; i += 2) {
    char* op = t->children[i]->contents;
    ore_value rhs = ore_eval(ore, t->children[i+1]);
    switch (v.t) {
      case ORE_TYPE_INT:
        {
          int iv = rhs.t == ORE_TYPE_INT ? rhs.v.i : rhs.t == ORE_TYPE_FLOAT ? (int) rhs.v.d : 0;
          if (strcmp(op, "+") == 0) { v.v.i += iv; }
          if (strcmp(op, "-") == 0) { v.v.i -= iv; }
          if (strcmp(op, "*") == 0) { v.v.i *= iv; }
          if (strcmp(op, "/") == 0) { v.v.i /= iv; }
          if (strcmp(op, "%") == 0) { v.v.i %= iv; }
        }
        break;
      case ORE_TYPE_FLOAT:
        {
          double fv = rhs.t == ORE_TYPE_INT ? (double) rhs.v.i : rhs.t == ORE_TYPE_FLOAT ? rhs.v.d : 0;
          if (strcmp(op, "+") == 0) { v.v.d += fv; }
          if (strcmp(op, "-") == 0) { v.v.d -= fv; }
          if (strcmp(op, "*") == 0) { v.v.d *= fv; }
          if (strcmp(op, "/") == 0) { v.v.d /= fv; }
          if (strcmp(op, "%") == 0) { v.v.d = ((int) v.v.d % (int) fv); }
        }
        break;
    }
  }
  return v;
}
今後、文字列の足し算で文字列結合する処理も必要になりますね。

関数宣言

今回の修正で yacc 構文を以下の様にしました。 #define STRUCTURE \
"                                                           \n" \
"number    : /-?[0-9]+(\\.[0-9]*)?(e[0-9]+)?/ ;             \n" \
"factor    : '(' <lexp> ')'                                 \n" \
"          | <number>                                       \n" \
"          | <string>                                       \n" \
"          | <lambda>                                       \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" \
"                                                           \n" \
"lambda    : /func/                                           " \
"        '(' <ident>? (',' <ident>)* ')' '{' <stmt>* '}' ;  \n" \
"func      : /func/ <ident>                                   " \
"        '(' <ident>? (',' <ident>)* ')' '{' <stmt>* '}' ;  \n" \
"                                                           \n" \
"call      : <ident> '(' <lexp>? (',' <lexp>)* ')' ';' ;    \n" \
"comment   : /#[^\n]*/ ;                                    \n" \
"eof       : /$/ ;                                          \n" \
"stmt      : (<let> | <call> | <func> | <comment>) ;        \n" \
"program   : <stmt>* <eof> ;                                \n"
factor に lambda を、stmt に func を追加しました。引数部は ident の羅列を受け取ります。
今回は実装していないので引数の受け渡しは動作しませんが、今後「関数スコープ」が実装された際は、関数呼び出し時に新規に env を作成し、ident が示す変数名を登録してあげれば引数が渡される事になります。
最後に全体のコードを載せて置きます。
#include "mpc.h"
#include "khash.h"
#include "klist.h"

#define STRUCTURE \
"                                                           \n" \
"number    : /-?[0-9]+(\\.[0-9]*)?(e[0-9]+)?/ ;             \n" \
"factor    : '(' <lexp> ')'                                 \n" \
"          | <number>                                       \n" \
"          | <string>                                       \n" \
"          | <lambda>                                       \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" \
"                                                           \n" \
"lambda    : /func/                                           " \
"        '(' <ident>? (',' <ident>)* ')' '{' <stmt>* '}' ;  \n" \
"func      : /func/ <ident>                                   " \
"        '(' <ident>? (',' <ident>)* ')' '{' <stmt>* '}' ;  \n" \
"                                                           \n" \
"call      : <ident> '(' <lexp>? (',' <lexp>)* ')' ';' ;    \n" \
"comment   : /#[^\n]*/ ;                                    \n" \
"eof       : /$/ ;                                          \n" \
"stmt      : (<let> | <call> | <func> | <comment>) ;        \n" \
"program   : <stmt>* <eof> ;                                \n"

void ore_free(void *p);

#define is_a(t, a) (strstr(t->tag, a) != NULL)

typedef enum {
  ORE_TYPE_NIL,
  ORE_TYPE_INT,
  ORE_TYPE_FLOAT,
  ORE_TYPE_STR,
  ORE_TYPE_FUNC,
  ORE_TYPE_CFUNC
} ore_type;

typedef mpc_ast_t* ore_func;

typedef struct _ore_value {
  ore_type t;
  union _v {
    int i;
    double d;
    char* s;
    void* c;
    ore_func f;
  } v;
} ore_value;

KHASH_MAP_INIT_STR(ident, ore_value)

KLIST_INIT(ident, ore_value, ore_free)

typedef struct _ore_context {
  khash_t(ident)* env;
  klist_t(ident)* gc;
  struct _ore_context* parent;
} ore_context;

typedef ore_value (*ore_cfunc_t)(ore_context*, int, ore_value*);

ore_value ore_call(ore_context*, mpc_ast_t*);
ore_value ore_eval(ore_context*, mpc_ast_t*);

void
ore_free(void *p) {
  ore_value* v = (ore_value*) p;
  switch (v->t) {
    case ORE_TYPE_STR:
      free(v->v.s);
      break;
    case ORE_TYPE_FUNC:
      free(v->v.c);
      break;
  }
}

ore_value
ore_value_nil() {
  ore_value v = { ORE_TYPE_NIL, 0 };
  return v;
}

ore_value
ore_parse_num(ore_context* ore, const char* s) {
  ore_value v;
  if (!strchr(s, '.')) {
    v.t = ORE_TYPE_INT;
    v.v.i = atoi(s);
  } else {
    v.t = ORE_TYPE_FLOAT;
    v.v.d = atof(s);
  }
  return v;
}

ore_value
ore_parse_str(ore_context* ore, const char* s) {
  ore_value v = { ORE_TYPE_STR };
  size_t l = strlen(s) - 2;
  v.v.s = calloc(1, l + 1);
  strncpy(v.v.s, s + 1, l);
  v.v.s[l] = 0;
  *kl_pushp(ident, ore->gc) = v;
  return v;
}

ore_value
ore_println(ore_context* ore, int num_in, ore_value* args) {
  int i;
  for (i = 0; i < num_in; i++) {
    if (i != 0) printf(", ");
    switch (args[i].t) {
      case ORE_TYPE_NIL:
        printf("nil");
        break;
      case ORE_TYPE_INT:
        printf("%d", args[i].v.i);
        break;
      case ORE_TYPE_FLOAT:
        printf("%f", args[i].v.d);
        break;
      case ORE_TYPE_STR:
        printf("%s", args[i].v.s);
        break;
      default:
        printf("<unkonwn>");
        break;
    }
  }
  printf("\n");
  return ore_value_nil();
}

ore_value
ore_define_cfunc(ore_context* ore, const char* name, ore_cfunc_t c) {
  int r = 0;
  khint_t k = kh_put(ident, ore->env, name, &r);
  ore_value v = { ORE_TYPE_CFUNC };
  v.v.c = c;
  kh_value(ore->env, k) = v;
  return v;
}

ore_value
ore_call(ore_context* ore, mpc_ast_t *t) {
  khint_t k = kh_get(ident, ore->env, t->children[0]->contents);
  if (k == kh_end(ore->env)) {
    fprintf(stderr"Unknwn function '%s'\n", t->children[0]->contents);
    return ore_value_nil();
  }

  ore_value fn = kh_value(ore->env, k);
  ore_value v = ore_value_nil();
  switch (fn.t) {
    case ORE_TYPE_CFUNC:
      {
        int num_in = t->children_num / 2 - 1, n = 0, i;
        ore_value* args = (ore_value*) malloc(sizeof(ore_value) * num_in);
        for (i = 2; i < t->children_num - 2; i += 2) {
          args[n++] = ore_eval(ore, t->children[i]);
        }
        v = ((ore_cfunc_t)fn.v.c) (ore, num_in, args);
        free(args);
      }
      break;
    case ORE_TYPE_FUNC:
      // TODO: ここをちゃんと実装して引数を渡せる様にする
      v = ore_eval(ore, fn.v.f);
      break;
  }
  return v;
}

ore_value
ore_eval(ore_context* ore, mpc_ast_t* t) {
  int i, r;
  ore_value v;
  if (is_a(t, "eof") || is_a(t, "comment")) {
    return ore_value_nil();
  }
  if (is_a(t, "number")) {
    return ore_parse_num(ore, t->contents);
  }
  if (is_a(t, "string")) {
    return ore_parse_str(ore, t->contents);
  }
  if (is_a(t, "ident")) {
    khint_t k = kh_get(ident, ore->env, t->contents);
    if (k == kh_end(ore->env)) {
      return ore_value_nil();
    }
    return kh_value(ore->env, k);
  }
  if (is_a(t, "factor")) {
    return ore_eval(ore, t->children[1]);
  }
  if (is_a(t, "lexp") || is_a(t, "term")) {
    v = ore_eval(ore, t->children[0]);
    for (i = 1; i < t->children_num; i += 2) {
      char* op = t->children[i]->contents;
      ore_value rhs = ore_eval(ore, t->children[i+1]);
      switch (v.t) {
        case ORE_TYPE_INT:
          {
            int iv = rhs.t == ORE_TYPE_INT ? rhs.v.i : rhs.t == ORE_TYPE_FLOAT ? (int) rhs.v.d : 0;
            if (strcmp(op, "+") == 0) { v.v.i += iv; }
            if (strcmp(op, "-") == 0) { v.v.i -= iv; }
            if (strcmp(op, "*") == 0) { v.v.i *= iv; }
            if (strcmp(op, "/") == 0) { v.v.i /= iv; }
            if (strcmp(op, "%") == 0) { v.v.i %= iv; }
          }
          break;
        case ORE_TYPE_FLOAT:
          {
            double fv = rhs.t == ORE_TYPE_INT ? (double) rhs.v.i : rhs.t == ORE_TYPE_FLOAT ? rhs.v.d : 0;
            if (strcmp(op, "+") == 0) { v.v.d += fv; }
            if (strcmp(op, "-") == 0) { v.v.d -= fv; }
            if (strcmp(op, "*") == 0) { v.v.d *= fv; }
            if (strcmp(op, "/") == 0) { v.v.d /= fv; }
            if (strcmp(op, "%") == 0) { v.v.d = ((int) v.v.d % (int) fv); }
          }
          break;
      }
    }
    return v;
  }
  if (is_a(t, "let")) {
    khint_t k = kh_put(ident, ore->env, t->children[0]->contents, &r);
    v = ore_eval(ore, t->children[2]);
    kh_value(ore->env, k) = v;
    return v;
  }
  if (is_a(t, "func")) {
    ore_value v = { ORE_TYPE_FUNC };
    v.v.f = t->children[5];
    khint_t k = kh_put(ident, ore->env, t->children[1]->contents, &r);
    kh_value(ore->env, k) = v;
    return v;
  }
  if (is_a(t, "lambda")) {
    ore_value v = { ORE_TYPE_FUNC };
    v.v.f = t->children[4];
    return v;
  }
  if (is_a(t, "call")) {
    return ore_call(ore, t);
  }
  if (t->tag[0] == '>') {
    for (i = 0; i < t->children_num; i++) {
      ore_eval(ore, t->children[i]);
    }
    return ore_value_nil();
  }
  fprintf(stderr"Unknwn operation '%s'\n", t->tag);
  return ore_value_nil();
}

ore_context*
ore_new(ore_context* parent) {
  ore_context* ore = (ore_context*) malloc(sizeof(ore_context));
  ore->env = kh_init(ident);
  ore->gc = kl_init(ident);
  ore->parent = parent;
  return ore;
}

void
ore_destroy(ore_context* ore) {
  kl_destroy(ident, ore->gc);
}

int main(int argc, char **argv) {
  if (argc != 2) {
    fprintf(stderr"usage of %s: file\n", argv[0]);
    exit(0);
  }
  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* Func    = mpc_new("func");
  mpc_parser_t* Lambda  = mpc_new("lambda");
  mpc_parser_t* Call    = mpc_new("call");
  mpc_parser_t* Comment = mpc_new("comment");
  mpc_parser_t* Eof     = mpc_new("eof");
  mpc_parser_t* Stmt    = mpc_new("stmt");
  mpc_parser_t* Program = mpc_new("program");

  mpc_err_t* err = mpca_lang(MPC_LANG_DEFAULT, STRUCTURE,
      Number, Factor, String, Ident,
      Term, Lexp, Let, Lambda, Func, Call, Comment, Eof,
      Stmt, Program);
  if (err != NULL) {
    mpc_err_print(err);
    mpc_err_delete(err);
    goto leave;
  }

  mpc_result_t result;
  if (!mpc_parse_contents(argv[1], Program, &result)) {
    mpc_err_print(result.error);
    mpc_err_delete(result.error);
    goto leave;
  }

  mpc_ast_print(result.output);

  ore_context* ore = ore_new(NULL);
  ore_define_cfunc(ore, "println", ore_println);
  ore_eval(ore, result.output);
  ore_destroy(ore);

  mpc_ast_delete(result.output);

leave:
  mpc_cleanup(11,
      Number, Factor, String, Ident,
      Term, Lexp, Let, Lambda, Func, Call, Comment, Eof,
      Stmt, Program);
  return 0;
}
今回の修正で以下の俺スクリプトが動作する様になりました。 #!ore
# 計算
a = 1.0;
a = a * 2 + 1;
b = "hello";
println(b, a + 1);

# 関数宣言
func foo() {
  println(1);
}

# 関数呼出
foo();

# λ関数
c = func(){
  println("lambda");
};

# λ関数呼出
c();
次回は
  • 関数呼出への引数宣言
  • エラー処理
を行う予定です。
Posted at by



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