2014/04/16

Recent entries from same category

  1. Zenn で Twitter bot 作成入門を書いた。
  2. プログラマーのための新しい情報共有コミュニティ Zenn で本を書いてみた。
  3. Windows ユーザは cmd.exe で生きるべき 2020年版
  4. Let's Encrypt を簡単操作できる CLI、Lego が MyDNS に対応した。
  5. golang でメモ専用コマンド「memo」作った。

Big Sky :: プログラミング言語の作り方
Big Sky :: プログラミング言語の作り方(2)
Big Sky :: プログラミング言語の作り方(3)
Big Sky :: プログラミング言語の作り方(5)
本日は関数スコープの実装と関数引数のバインディングを行います。
まず関数スコープを入れるという事は、メモリの破棄が必要になります。

しかし関数が呼び出された後、関数スコープ内のメモリを全て削除してしまうと戻り値に文字列を渡せなくなります。 func foo() {
  return "foo"; // この後スコープが削除される
}

a = foo(); // 壊れたメモリを参照
そこで GC を導入する必要があります。GC の実装には複数あます。詳しくは Wikipedia を参照下さい。
今回はその中の「参照カウント方式」を取ります。
まず ore_value に参照カウンタを付けます。
typedef struct _ore_value {
  ore_type t; // 変数種別
  union {
    int i;    // int の値
    double d; // double の値
    char* s;  // 文字列
    struct {
      void* env;    // 関数宣言時のスコープ
      int num_in;   // 引数の数(現状CFUNCのみ)
      union {
        void* c;    // CFUNCの関数ポインタ
        ore_func o; // ユーザ定義関数のステートメント
      } x;
    } f;     // 関数
  } v;
  int ref; // 参照カウンタ
} ore_value;
そして便利関数として ore_value_ref と ore_value_unref を実装します。 void
ore_value_real_free(ore_value* v) {
  switch (v->t) {
    case ORE_TYPE_STR:
      //printf("free %d, %p, %s\n", v->ref, v->v.s, v->v.s);
      free(v->v.s);
      v->v.s = NULL;
      break;
    case ORE_TYPE_FUNC:
      break;
  }
  v->t = ORE_TYPE_NIL;
}


void
ore_value_free(void *p) {
  ore_value* v = (ore_value*) p;
  ore_value_unref(v);
}

void
ore_value_ref(ore_value *v) {
  v->ref++;
}
参照カウント方式は一般的にコードが複雑になると思われがちですが、用法用量を守って正しく使うとそんなに難しくはありません。
複雑にならない様に env に登録/更新する関数を作ります。
ore_value
ore_get(ore_context* ore, const char* name) {
  if (!ore)
    return ore_value_nil();
  khint_t k;
  while (ore) {
    k = kh_get(ident, ore->env, name);
    if (k != kh_end(ore->env)) {
      return kh_value(ore->env, k);
    }
    ore = ore->parent;
  }
  return ore_value_nil();
}

void
ore_set(ore_context* ore, const char* name, ore_value v) {
  khint_t k;
  int r;
  while (ore) {
    k = kh_get(ident, ore->env, name);
    if (k != kh_end(ore->env)) {
      ore_value old = kh_value(ore->env, k);
      ore_value_unref(&old);
      kh_value(ore->env, k) = v; // update ref
      k = kh_put(ident, ore->env, name, &r);
      ore_value_ref(&v);
      kh_value(ore->env, k) = v;
      return;
    }
    if (ore->parent == NULL) {
      k = kh_put(ident, ore->env, name, &r);
      ore_value_ref(&v);
      kh_value(ore->env, k) = v;
      return;
    }
    ore = ore->parent;
  }
}

ore_value
ore_define(ore_context* ore, const char* name, ore_value v) {
  khint_t k = kh_get(ident, ore->env, name);
  int r;
  if (k != kh_end(ore->env)) {
    ore_value old = kh_value(ore->env, k);
    ore_value_unref(&old);
  }
  k = kh_put(ident, ore->env, name, &r);
  ore_value_ref(&v);
  kh_value(ore->env, k) = v;
}
前回説明した様に関数スコープが作られた後、識別から値を参照するにはグローバル方向に向かって検索する必要があるのでその実装を行いました。見つからなかった場合は nil を返していますが、言語によっては「Undefined Exception」の様なエラーを発生させる事もあります。
これを使って、変数(関数)宣言する時は ore_define、値を設定する時は ore_set、値を取得する時は ore_get を使えば良いです。
関数呼び出し時には、ore_new により新しい環境を作り、そこに引数名と受け渡された値のバインディングを行います。
{
  int num_in = t->children_num / 2 - 1, n = 0, i;
  if (fn.v.f.num_in != -1 && num_in != fn.v.f.num_in) {
    fprintf(stderr"Number of arguments mismatch: %d for %d\n",
      num_in, fn.v.f.num_in);
    ore->err = ORE_ERROR_EXCEPTION;
    return ore_value_nil();
  }
  ore_value* args = (ore_value*) malloc(sizeof(ore_value) * num_in);
  for (i = 2; i < t->children_num - 1; i += 2) {
    args[n++] = ore_eval(ore, t->children[i]);
  }
  ore_context* env = ore_new((ore_context*) fn.v.f.env);
  mpc_ast_t* stmts = NULL;
  mpc_ast_t* f = fn.v.f.x.o;
  n = 0;
  for (i = 2; i < f->children_num; i++) {
    if (is_a(f->children[i], "ident")) {
      if (n < num_in)
        ore_define(env, f->children[i]->contents, args[n++]);
    } else if (stmts == NULL && !is_a(f->children[i], "char")) {
      stmts = f->children[i];
    }
  }
  if (stmts) {
    v = ore_eval(env, stmts);
    if (ore->err != ORE_ERROR_EXCEPTION)
      ore->err = ORE_ERROR_NONE;
    *kl_pushp(ident, ore->unnamed) = v;
  }
  free(args);
  ore_destroy(env);
}
そして気を付けなければならないのが、実行時スコープです。上のコードでは、関数を呼び出したスコープで ore_new を呼び出すのではなく if (is_a(t, "func")) {
  ore_value v = { ORE_TYPE_FUNC };
  v.v.f.env = ore;
  v.v.f.num_in = -1;
  v.v.f.x.o = t;
  ore_set(ore, t->children[1]->contents, v);
  return v;
}
if (is_a(t, "lambda")) {
  ore_value v = { ORE_TYPE_FUNC };
  v.v.f.env = ore;
  v.v.f.num_in = -1;
  v.v.f.x.o = t;
  return v;
}
関数が宣言された時点での環境を保持しておき、その環境から新しい環境を作る必要があります。こうしないと #!ore
func counter() {
  var a = 0;
  return func() {
    a = a + 1;
    return a;
  };
}
count = counter();

println(count());
println(count());
この様なコードが動作しなくなります。今回の実装も以下のリポジトリから参照出来ます。
mattn/orelang - GitHub

README.md orelang 俺言語 プログラミング言語の作り方 プログラミング言語の作り方(2) プログラミング言語の作り方(3) Usage # calculation a = 1; a =...

https://github.com/mattn/orelang
そろそろエラー処理を書きたいと思います。
Posted at by