rui312/9ccを写経する-その4

https://github.com/rui314/9cc/commit/b8b3ab51ad9372a2b9f963650bbf14e714b87b85

Add Map data structure.

お、みんな大好きMapさん。これは楽しみだ。 hash関数を使うのか、open addressなのかlinked-list方式なのか!?

typedef struct {
  Vector *keys;
  Vector *vals;
} Map;

ずこー!

まあ9ccのシンプル一辺倒方針を考えればこれしかないとも言える。

Arrayを2つ並べただけ。おそらくget時はkeyを線形検索して同じindexのvalsを取る的な感じだろう。

main.c

ここでtest用のオプションが追加されている。オプションの形式はgolangとかimagemagickとかと同じ形式(由緒ある名前があるんだろうけどわからん)。

util.c

Mapの実装は想像通り。get時の検索が逆側からの検索になっている。最近セットしたものをよく使うだろうということなのかな?

if (!strcmp(map->keys->data[i], key))

個人的には、わかりやすさを目指すなら

if (strcmp(map->keys->data[i], key) == 0)

と書きたい。が、Cを書く人にはこっちのほうがわかりやすいんだろうか。

util_test.c

util系のユニットテストも書いている。この規模ならいらない気もするが。

ところでmallocは出てくるがfreeが出てこない。

この規模ならまだいらないんだろう。

https://github.com/rui314/9cc/commit/f8a1d0c6903dfe9ab816e719c70efdb27d6f0a4d

いよいよ文法が追加される。return;を追加するようだ。

token.c

なるほど、ここでmapをつかって予約語=keywordを登録する。 isalphaなどを使って続いている文字を読み取って、登録済みの予約語ならtokenを追加する。

ここまで書いてみて、ハッと気がついた。 これはお題(test.sh)を見て、先に自分の考える実装をやってみてから答え合わせ的に写経したほうが学びが深そうだ。 やってみよう。

parse.c(独自実装)

早速困った。return文をどうnode構造として解釈したらいいんだ。

テストを見るに、returnがついていたらそこで試合終了。計算結果をプロセスの終了ステータスにしたい。

この計算結果は既存の関数でできそうだ。

問題は1+2; return 3+4;みたいなパターン。

既存の考えのままだと、nodeの構造が数字と演算子しかないのでreturnとそうでない文を表現できない。

ここで音を上げて、早くもカンニングしてしまう。

なるほど、returnがないnodeとreturnのnodeを作って、これを計算結果の親としてnodeを組み上げれば良いようだ。

parse.cに

  for (;;) {
    t = tokens->data[pos];
    if (t->ty == TK_RETURN) {
      pos++;
      node = new_node(ND_RETURN, expr(), NULL);
      break;
    } else {
      node = new_node(ND_NORETURN, expr(), NULL);
    }
  }

のようなコードを追加した。

ir.c(独自実装)

noreturnなnodeは、変数もない現状では全く無駄なので、計算もせずに単純に飛ばせばいい。

  while (node->ty == ND_NORETURN) {
    node = node->lhs;
  }

あとはreturn nodeだけのはずなので、nodeの小要素でこれまで通り計算するだけ。

  assert(node->ty == ND_RETURN);
  node = node->lhs;
  int r = gen(v, node);

なんと、これだけで動いてしまった。楽しい。答え合わせもしてみよう。

token.c(答え合わせ)

これはすでに見ていたのでまあOK。答えを見てなかったらmapにkeywordを入れることを思いつけたかどうか……。

parse.c(答え合わせ)

typeが想像より多い。

  • ND_COMP_STMT => 全てのroot
  • ND_RETURN => return文
  • ND_EXPR_STMT => returnが無い文

という感じか。

Node構造体もメンバーが追加されている。

  • stmts => nodeは木構造のはずだが、配列としても用意している。なんでだろう?debug用か?
  • expr => 予想では計算結果をlhsに詰めていたが、専用にメンバーを用意したんだろう。やや富豪的。わかりやすさ重視かな?

ir.c(答え合わせ)

これまで引数で引き回していたVectorをファイルスコープの変数codeに変更している。Cはファイルスコープの変数やら関数が便利だよなあ。

ND_RETURN``ND_EXPR_STMTは予想通りとして、ND_COMP_STMTはなるほど、各文は木構造ではなく、素直に配列として持つことにしているようだ。

ここまでやって思ったが、ここまでで12 commit見てきた。 このペースでは200 commit以上を見ていくのに果てしなく時間がかかってしまう……。

飽きてきたら更新辞めると思います。

rui312/9ccを写経する-その3

https://github.com/rui314/9cc/commit/9d4e20421f140f9ad7a1d161daab088008aa5760

Fix warnings.

とのことなので大した変更ではなさそうだが、stdnoreturn.hnoreturnは知らなかった。

返り値のないvoidな関数にreturn;があってもコンパイラは特に何も言わないが、noreturnをつけていると、return;があったら

9cc.c:87:3: warning: function declared 'noreturn' has a 'return' statement
   return;
   ^~~~~~

みたいな感じで教えてくれるようだ。

https://github.com/rui314/9cc/commit/52dc210c9d22171de630eb78a3946d23b0dc727d

ぬー、またデータ構造を追加する割にはテストに変化がないのでメリットが見えないパターンっぽい。 がんばっていこう。

typedef struct {
  void **data;
  int capacity;
  int len;
} Vector;
Vector *new_vec()
void vec_push(Vector *v, void *elem)

どうやら自動拡張する配列のようだ。

なぜこう判断できたかというと、mrubyで見たことあるから、という他なくうまく説明できない……。

(void *)としているのは任意の型のポインタを使えるパターンで、pushしてlenが1増えて、capacityに到達したらcapacityを2倍にしてreallocというのが自動拡張配列の典型パターンというイメージ。

こういうデータ構造はあると何かと便利なんだろう。Rubyでも最終的なサイズとか意識せずに、Arrayに適当にオブジェクトを突っ込んだりできて便利だ。

Token *add_token(Vector *v, int ty, char *input)

なるほど、察しはついた。これまでtokens[100]と適当に100 tokenまでとしていたものを、先程の自動拡張配列Vectorを使ってtokenの拡張を自動化するようだ。

Vector *tokenize(char *p)

ここまでくれば変更は予測できる。

新たにVectorを作って、tokensを使っている部分をadd_tokenに置き換え、最後にVectorを返せば、 任意の長さのプログラムに合わせていい感じにメモリを確保した配列が手に入る。

Vector *tokens;
int pos;

Token tokens[100]は廃止されVector *tokensとして生まれ変わった。

posは0初期化しなくていいのかな?と思ったけど、グローバル変数は勝手に0初期化された気がしなくもない。

Node *number()

tokensの構造は変わったので、使っている部分を書き直す。

ついでにパースエラーだったときはposを変更しないように修正しているっぽい。

int gen_ir_sub(Vector *v, Node *node)

引数にVectorを追加。ins[1000]を廃止して自動拡張配列に置き換えるんだろう。

tokensはグローバル変数のままなのに、insはグローバル変数ではなくするようだ。

regnogen_ir_subでしか使ってないので、グローバル変数からstatic変数に変更して影響範囲を減らしている。

あとはins[inp++]となっている部分をvec_pushに置き換えるだけ。

Vector *gen_ir(Node *node)

ここでVectorを作ってgen_ir_subに渡してIR_RETURNを最後に追加してVectorを返しておしまい。

- bool used[8];
+ bool used[sizeof(regs) / sizeof(*regs)];

決め打ちだった8をregsの長さに合わせている。

- int reg_map[1000];
+ int *reg_map;

1000個決め打ちを止めている。

このサイズは元はinsの1000と合わせていた。

insの長さが可変になったので、insの代わりになったVectorからlenを引いてくればサイズは決めれそうだ。

void alloc_regs(Vector *irv)

引数が増えた。

insが廃止されたので、gen_irで作ったinsの代わりになるVectorを渡しているのだろう。

ならばinp => vector->len, ins[i] => vector->data[i]と置き換えればいいだけだ。

……、と思ったらreg_mapも決め打ちじゃなくなったんだった。

alloc_regsの外で初期化してもいいが、-1allloc_regsでしか使ってないので、alloc_regsの中で初期化したほうがわかりやすくなりそうだ。

reg_mapグローバル変数を辞めて関数間で渡したほうが更に良くなりそうだが、いずれやるんだろう。

void gen_x86(Vector *irv)

gen_x86でもinsを使っていたので、代わりになるirvを与えている。

新たに構造体が増えたが、総じてリファクタリング的な内容だった。

https://github.com/rui314/9cc/commit/ce9a6bbfbd430332666c64cd8b433043c45a6044

概念毎にファイルを分けているだけ。と思ったら少し変更がある。

tokensグローバル変数をやめてtokenize関数の返り値にしている。

かと思いきやtoken.cのファイルスコープのグローバル変数tokensは存在していて、新関数parseグローバル変数tokensにtokenizeで作った変数をそのまま割り当てている。どういうこっちゃ。

んーオブジェクト指向で考えてはいけなくて、ファイル毎にスコープ切れてるからいいじゃん的な考え方ということか。

まあこれはこれで簡素で便利と言える。

Vector系関数はutil.cなので、やはり便利構造体ということなんだろう。

https://github.com/rui314/9cc/commit/e3cda6b3777ad6937dcf51faddbbbeea733a1048

掛け算の導入のようだ。 これまで上から見ていったが、ファイル分割によってどこから見ていけばわからなくなった。

とりあえずtest.shからみて、token -> node -> irと処理順に見ていこう。

test.sh

ようやく新しいことができる。まずは掛け算っぽい。

token.c

if文の||で一つずつ書いていた部分をstrchrで纏めてしまっている。

parse.c

exprnumberの間にmul関数の層を増やしている。

これによって、mulでは掛け算があれば掛け算のNode構造を作り、掛け算がなく加減算ならばそのまま数字を返す。

つまり掛け算を優先して計算するようなNodeの木構造を作っている。んーこのへんはむずい。

難しいがとりあえず計算の優先順位は関数の層を追加して、Nodeの構造を作ってやれば、あとはir層で再帰的に処理してくれる。

なるほど、ここでようやくNode層を作ったメリットが現れた。

codegen.c

    case '*':
      printf("  mov rax, %s\n", regs[ir->rhs]);
      printf("  mul %s\n", regs[ir->lhs]);
      printf("  mov %s, rax\n", regs[ir->lhs]);
      break;

アセンブラでの掛け算はaddsubと違って、レジスタが固定されているっぽい。

参考文献としてはhttps://qiita.com/MoriokaReimen/items/4853587dcb9eb96fab62http://bttb.s1.valueserver.jp/wordpress/blog/2017/10/22/assembler_mul/がみつかるが、一次ソースはどうすれば見つかるんだろう……。

https://github.com/rui314/9cc/commit/4cd8eedb7c352380828e664950dff97a83eaeef8

割り算の追加。殆どは問題なく*のときと同じだ。 mul関数が名前がそのままなのが気になる。

    case '/':
      printf("  mov rax, %s\n", regs[ir->lhs]);
      printf("  cqo\n");
      printf("  div %s\n", regs[ir->rhs]);
      printf("  mov %s, rax\n", regs[ir->lhs]);
      break;

divはともかくcqoがググっても全然わからん……。

符号を云々なもののようだが、これがないとコンパイラFloating point exceptionと表示されてしまう。うーむ。

ともかくこのコンパイラは割り算までできるようになった。

rui312/9ccを写経する-その2

https://github.com/rui314/9cc/commit/2f62e5267a1c2874dcfa674cf8654e0cb3f189d6

コミットコメントが仰々しい感じがする。

test.sh

とりあえずここを見れば次に実装することがわかる。 コードを見る限り、そこまで追加機能はないようだが……?

int pos = 0;
enum {
  ND_NUM = 256,     // Number literal
};
typedef struct Node {
  int ty;           // Node type
  struct Node *lhs; // left-hand side
  struct Node *rhs; // right-hand side
  int val;          // Number literal
} Node;

posはプログラムの読み込み位置かな?NodeはTokenとどう違うんだろう?

Node *new_node(int op, Node *lhs, Node *rhs)
Node *new_node_num(int val)

んーまだよくわからない。

Node *number()

数字のtokenから数字のnodeを作っている?

Node *expr()

再帰的なNodeを作っているようだ。

char *regs[] = {"rdi", "rsi", "r10", "r11", "r12", "r13", "r14", "r15", NULL};

んーアセンブラの変数(レジスタ)の名前列?これは予約語なのかなあ、単純に数字が増加しているだけではないし、よくわからない。 アセンブラを少し学ぶ必要がありそうだ。

https://qiita.com/edo_m18/items/83c63cd69f119d0b9831

によるとrdiシステムコールに使われるレジスタ。でも他はそうではないっぽい。

http://milkpot.sakura.ne.jp/note/x86.html

によると汎用レジスタの一覧にすべて収まる。

おそらく、システムコールを使うときにはrdiは意味を持つが、そうでなければ汎用的に使えるレジスタ、ということかな? レジスタは、とりあえず8個使うようだ。

char *gen(Node *node)

nodeを再帰的にたどって、node毎に利用するレジスタを割り当てるっぽい。 そのうえで、node->tyからアセンブラの足し算引き算コードを作り出す。 レジスタはnodeに割り当てられたものを使う。

  printf("  mov rax, %s\n", gen(node));

これはつまりトップレベルに割り当てられたレジスタを終了ステータスレジスタに割り当てているので、 最終的にこのレジスタに全ての計算結果が返ってきているはずということか! coolだ。

ここまで写経してきて、ふと思い立って1+2+3+4+5+6+7+8の出力アセンブラを見比べてみた。

before

.intel_syntax noprefix
.global main
main:
  mov rax, 1
  add rax, 2
  add rax, 3
  add rax, 4
  add rax, 5
  add rax, 6
  add rax, 7
  add rax, 8
  ret

after

.intel_syntax noprefix
.global main
main:
  mov rdi, 1
  mov rsi, 2
  add rdi, rsi
  mov r10, 3
  add rdi, r10
  mov r11, 4
  add rdi, r11
  mov r12, 5
  add rdi, r12
  mov r13, 6
  add rdi, r13
  mov r14, 7
  add rdi, r14
  mov r15, 8
  add rdi, r15
  mov rax, rdi
  ret

んー違いはあるけど恩恵はまだなさそうな感じがする。次行ってみよう。

https://github.com/rui314/9cc/commit/dca0fdb71df98d8500170ff8178eaac0c8f9edaa

test.sh

try 153 '1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17'

んーこれまたhttps://github.com/rui314/9cc/commit/916237396164ae25557b62bf0d1ca9d9cf8c2070からのメリットが見えない感じ。 https://github.com/rui314/9cc/commit/2f62e5267a1c2874dcfa674cf8654e0cb3f189d6から予想できるのは、おそらくレジスタを有効に使って計算が長くても対応できるようにしそうなこと。

// Intermediate representation
enum {
  IR_IMM,
  IR_MOV,
  IR_RETURN,
  IR_KILL,
  IR_NOP,
};
typedef struct {
  int op;
  int lhs;
  int rhs;
} IR;

名前からして"内部表現"……?token -> node -> irという変化だろうか? mrubyでの経験に当てはめると、MOVで変数をレジスタに割り当てたり、RETURNで関数の返り値を決めれるようにしたりという感じかな? IMMはimmediateで即値を表す?KILLは分からない。NOPは何もしない命令っぽい。

データ構造からして、命令と対象と引数でアセンブラと言うか機械命令語っぽい感じ?

IR *ins[1000];
int inp;
int regno;

Intermediateが複数のsでinsかな?これもtokens[1000]と同じく雑に1000個空間を用意しているっぽい。 同じくinsのポインタpでinp、regnoレジスタナンバーというところかな?

int gen_ir_sub(Node *node)

名前からしてirを作るサブ関数っぽい。 関数の中も見ていく。

  if (node->ty == ND_NUM) {
    int r = regno++;
    ins[inp++] = new_ir(IR_IMM, r, node->val);
    return r;
  }

レジスタ番号regnoとnodeの値node->valをIMM命令としてinsに登録してポインタinpを進める。 多分レジスタrにnode->valを割り当てるんだろうと思う。 割り当てたレジスタ番号を返すのはなんでだろう?あとでアセンブラとして書き出すのかな?

assert(node->ty == '+' || node->ty == '-');

Cのassertって個人的には好きで、バグ検知しつつここから先のコードの制限というか説明になっていて良い。 Rubyでいうとraise unless expr だと思ってる。

  int lhs = gen_ir_sub(node->lhs);
  int rhs = gen_ir_sub(node->rhs);
  ins[inp++] = new_ir(node->ty, lhs, rhs);
  ins[inp++] = new_ir(IR_KILL, rhs, 0);

node->tyは'+'とか'-'とかが入っている。これもirの命令として流用するようだ。 gen_ir_subを再帰的に使っている。たしかnode自体が再帰的にツリー状になっていたはず。 返り値はレジスタ番号だったので、lhsrhsレジスタ番号だろう。 足し算引き算の左右の値をirで纏めて、おそらく後でアセンブラに変換するんだろう。 ここでKILL命令。おそらく再帰の終わりを表している?まだわからない。

int gen_ir(Node *node) {
  int r = gen_ir_sub(node);
  ins[inp++] = new_ir(IR_RETURN, r, 0);
}

これまでのgen_ir_subと違うのは、最後にRETURN命令をつけていることだけ。おそらくこれまで再帰的に処理してきた全結果が渡ってきたレジスタ番号を、関数の返り値として設定するんだろう。

char *regs[] = {"rdi", "rsi", "r10", "r11", "r12", "r13", "r14", "r15"};
bool used[8];
int reg_map[1000];

regsは前回と変わらない。 usedとreg_mapから察するに、レジスタ番号それぞれを、使ってるor使ってないで管理するためのものなんだろう。 reg_map[1000]はinsとかに1:1で対応するんだろうか?

int alloc(int ir_reg)

mallocやallocaを連想させる名前。未割り当てのレジスタを返してほしい関数かな? 引数はレジスタ番号?中身も見ていく。

  if (reg_map[ir_reg] != -1) {
    int r = reg_map[ir_reg];
    assert(used[r]);
    return r;
  }

if文からir_regは0〜999の数字なんだろう。-1はまだ未登場の値。未使用を表すのかな? assert文から察するにused=trueのレジスタ番号rを返すっぽい。used=trueでいいのか?

  for (int i = 0; i < sizeof(regs) / sizeof(*regs); i++) {
    if (used[i])
      continue;
    used[i] = true;
    reg_map[ir_reg] = i;
    return i;
  }

regsは8つのレジスタだった。これをループで回す。条件はregs[i]でもいい気はする。もしくはusedで8って決め打ちしちゃってるしi < 8とか……。 まあわかりやすさを重視した結果かな? used=trueなら飛ばして次へ。used=falseならused=trueにしてreg_map[ir_reg]に使っているregsのindexを割り当てて番号を返す。 さっき出てきたreg_map[ir_reg]には-1は割り当てられえない。あとで初期化するのかな?それともfree的な関数が出るのかな? ともかくこの関数が返すのは、regsのindex番号のようだ。 0〜999のなにかしらの番号ir_regが与えられた時、使えるregsのindexを返す。このindexからアセンブラを書き出すんだろう。

void kill(int r) {
  assert(used[r]);
  used[r] = false;
}

usedフラグをoffにするだけ。レジスタの再利用のためかな。

void alloc_regs()

長いので見ていこう。allocとどう違うんだ。

  for (int i = 0; i < inp; i++) {
    IR *ir = ins[i];

inpはinsのindexだった。これまで進んだins全てでループしているっぽい。

    switch (ir->op) {

命令毎になんかする。

    case IR_IMM:
      ir->lhs = alloc(ir->lhs);
      break;

allocはとにかく使えるregsのindexを返す関数(という予想)だった。 IR_IMMgen_ir_subで出てきた。lhsはregnoというインクリメントする謎の値だった。 この時点では使えるかどうかわからないから使えるレジスタindexを割り当てているんだろう。 ここから、おそらくalloc_regs関数はalloc関数のirレベルで適用するものと予想。

    case IR_MOV:
    case '+':
    case '-':
      ir->lhs = alloc(ir->lhs);
      ir->rhs = alloc(ir->rhs);
      break;

IR_MON'+''-'が一緒になっているのに面食らったが、おそらく全て左右の2つのレジスタを使う命令なんだろう。

    case IR_RETURN:
      kill(reg_map[ir->lhs]);
      break;

IR_RETURNは関数の返り値と予想している。killは使ってないフラグを設定するんだった。 関数の返り値は再利用すると思われるんだがkillするからには再利用はしない。予想は外れていることになる。 これまでのcommitからアセンブラの最後のプロセスの終了ステータスを設定するやつか?

    case IR_KILL:
      kill(reg_map[ir->lhs]);
      ir->op = IR_NOP;
      break;

IR_KILLgen_ir_subの最後に登場していた。 例えば足し算では左と右のレジスタの値を足して左のレジスタに入れる。そうすると右側のレジスタは再利用可能になる。ということかな。

void gen_x86()

なまえからしアセンブラを吐きそうな感じだ(勘)。またまた見ていく。

  for (int i = 0; i < inp; i++) {
    IR *ir = ins[i];

alloc_regsと同じくinsのループのようだ。

switch (ir->op) {

ちなみに実行順はmain関数を見るにgen_ir->alloc_regs->gen_x86のようだから予想は合っている。 ここでの命令は全てalloc_regsが終わったあとの世界のようだ。

    case IR_IMM:
      printf("  mov %s, %d\n", regs[ir->lhs], ir->rhs);
      break;

IR_IMMは使えるレジスタ左に即値のnode-valが右に入っていた。 これでmov reg1 123のように出力されるんだろう。

    case IR_MOV:
      printf("  mov %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;

単純に右から左へのコピー。変数とかを使えるようにするための布石か? ならこのテストコードではこの命令自体がいらないような……。作るところもないし。

    case IR_RETURN:
      printf("  mov rax, %s\n", regs[ir->lhs]);
      printf("  ret\n");
      break;

これは予想通り最終的な結果をプロセスの終了ステータスに割り当てている。

    case '+':
      printf("  add %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;
    case '-':
      printf("  sub %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;

左右に使えるレジスタ番号が入っているので、ここで使えることが保証される。

    case IR_NOP:
      break;

IR_KILLとかが変化してIR_NOPになったものだろう。 何もしないことが便利なときもある。

int main(int argc, char **argv)

一応main関数も見ていこう。

  for (int i = 0; i < sizeof(reg_map) / sizeof(*reg_map); i++)
    reg_map[i] = -1;

やはりreg_mapallocで予想したとおり-1で初期化されていた。

  tokenize(argv[1]);
  Node* node = expr();

  gen_ir(node);
  alloc_regs();

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");
  gen_x86();

やはりtoken -> node -> irと変換していき、レジスタ割当を行って、アセンブラを吐いておわり。

token = 空白など余計な文字を除いた文字の列

node = 構文的な計算順序の組み立て

ir = このプログラミング言語VM的なもの

ということかな。

んー、やはりテストコードからは、実装が先の世界に行き過ぎてる感がある。

アセンブラはこうなった

.intel_syntax noprefix
.global main
main:
  mov rdi, 1
  mov rsi, 2
  add rdi, rsi
  mov rsi, 3
  add rdi, rsi
  mov rsi, 4
  add rdi, rsi
  mov rsi, 5
  add rdi, rsi
  mov rsi, 6
  add rdi, rsi
  mov rsi, 7
  add rdi, rsi
  mov rsi, 8
  add rdi, rsi
  mov rsi, 9
  add rdi, rsi
  mov rsi, 10
  add rdi, rsi
  mov rsi, 11
  add rdi, rsi
  mov rsi, 12
  add rdi, rsi
  mov rsi, 13
  add rdi, rsi
  mov rsi, 14
  add rdi, rsi
  mov rsi, 15
  add rdi, rsi
  mov rsi, 16
  add rdi, rsi
  mov rsi, 17
  add rdi, rsi
  mov rax, rdi
  ret

rui314/9ccを写経する-その1

8ccという有名なCコンパイラがあるが、これを書いたrui314さんが新たに9ccというリポジトリを上げていた。

https://note.mu/ruiu/n/n00ebc977fd60 を読むに、これは8ccをさらにわかりやすく、Cコンパイラ自作の教材として作っているものに違いないと勝手に判断し、写経してみることにした。 写経なので、書いているコードは同じ。自分が理解していった記録をここに残す。

僕のスペックは、普段はRubyでWebアプリを書くお仕事をしている。 Cコンパイラは一切書いたことはなく、Brainf*ckぐらいは書いたことがあるレベルだ。

ではスタート。

https://github.com/rui314/9cc/commit/56e94442ae8844688d5390851e5b29ba0c946e2f

9cc.c

main関数でprintfしているだけのコードのようだが、いきなりアセンブラっぽいコードを発見。 アセンブラは一切書いたことがないのでこれが初アセンブラだ。

.intel_syntax noprefixググるアセンブラのお作法としての記法を宣言するコードのようだ。アセンブラには複数の記法があるのだろう。

.global main
main:

なんとなくだけど関数宣言みたいなものかな?Cで言うmain関数宣言みたいなものだろう。

  mov rax, %d
  ret

うーんこの辺は全然わからん。多分変数raxに数字を入れてreturn的な感じか?

Makefile

9cc: 9cc.c

なんとたったこれだけで cc 9cc.c -o 9cc taskを書いたことになるようだ。 *.c taskがdefaultで用意されてるのかな?深くはわからない。

test.sh

テストコードっぽい。

  ./9cc "$input" > tmp.s

これから作るCコンパイラ9ccに引数をあたえて標準出力を書き出す。 いまのところ単にアセンブラをprintfするコードだったので、tmp.sにはアセンブラのコードが書き出されるのだろう。

  gcc -static -o tmp tmp.s

これはmacでは実行不可能だった。-staticgcc特有のオプションっぽい? clangでも同じことができなくはないだろうけど、コードの読み替えなどはしたくなかったのでDockerでlinux環境を用意した。

Dockerfile

FROM ubuntu:18.10

RUN apt-get update
RUN apt-get install -y make gcc lld

docker-compose.yml

version: "3"
services:
  9cc:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/9cc
    working_dir: /9cc

これで写経したコードが

docker-compose run 9cc

で動くようになった。docker便利。

  ./tmp
  actual="$?"

おそらく出力されたアセンブラを元に、仮の実行バイナリtmpを作って実行しているんだろう。 $?はプロセスの終了ステータスだった。あのアセンブラは、終了ステータスをセットするものだったようだ。 この終了ステータスが入力と同じならOKとしている。

初っ端から初アセンブラを書いた。新言語習得である。

https://github.com/rui314/9cc/commit/916237396164ae25557b62bf0d1ca9d9cf8c2070

いきなり足し算引き算の登場。どうやって実装するんだろう。おそらくアセンブラコードを拡張していくはずだ。

9cc.c

まず最初に数字を読み込むが、strtolは初めて知った。 第一引数にポインタを渡したら勝手に文字列を数字として読み取ってくれるっぽい。 第二引数にポインタのポインタを渡すと、読み取り終わり位置までポインタが指す位置を進めておいてくれるようだ。便利。 テストコードから察するに、これで20のような2桁の場合でも読み込めるようだ。 次は1文字ずつ読み込んで、+だったらaddアセンブラ命令、-だったらsubアセンブラ命令を書き出すようだ。 たぶんこれで足し算引き算になるんだろう。

test.sh

写経通りやると動いた。 試しに123456のような数字を書いてみると、どうやら結果が255を超えることができない。 多分終了ステータスの最大値だと思われるが、このコンパイラは現状255が限界のようだ。

https://github.com/rui314/9cc/commit/42adde9b5e0ed09c0d76bb84da38d136be8390c1

おそらく空白を入れることができるようにする変更。これまでは空白文字を入れるとSEGVしていた。 ついでにtokenizeということで、空白を除去したプログラム構文を作り出すんだろう。本格的になってきた。

9cc.c

enum {
  TK_NUM = 256, // Number literal
  TK_EOF,       // End marker
};

tokenの種類を定義しているようだけど、何故256から始まるんだろう?0からじゃだめなのかな?とりあえずこのまま進める。

typedef struct {
  int ty;      // Token type
  int val;     // Number literal
  char *input; // Token string (for error reporting)
} Token;

tokenの構造体のようだ。tokenのty(種類)とval(値)とinput(入力文字)。valがint型なので、おそらく数字の足し引きしかできないだろう。これでいいんだ感満載である。

Token tokens[100];

これは正直ちょっと笑ってしまった。 あまりに単純で「これでいいんだw」というズコーな感じと、単純なところから始めることへの徹底されたこだわりまで感じる。 極限まで研ぎ澄まされた職人芸という感じだ。

void tokenize(char *p)

これも結局は1文字ずつ読み込んでif文で分岐しているだけ。 「これでいいんだ」と何回思わせるんだ。職人芸だ。

tyに直接'+''-'を入れている。 そうか、tyに直接文字列が入ることを想定すると、どの文字ともぶつからないようにするために TK_NUM = 256 となっていたんだな。

main関数ではこのtokenを使って、やはりif文で分岐してアセンブラを書き出す。

空白を飛ばすだけにしてはやや仰々しいが、これぐらいはいいでしょと飛ばしているんだろう。

test.sh

無事、空白を無視することができるようになった。

embulk-decoder-execつくった

github.com

経緯

現在fluentdからlzoファイル形式で圧縮して定期的に溜めてるJSONデータが既にある。 これを別のストレージにサッと移せたらできること広がりそうだなーと考えた。

問題点

bulk処理といえばembulk、ということでembulkを触ってみて、どうやら圧縮ファイルを展開するのはdecoderと言うらしいことがわかった。 decoderをinputのオプションとして行うことで、あとにつながる処理に渡すようだ。

embulkはpluginアーキテクチャだと分かっていたので、embulk-decoder-lzoがすでにあるかな?と思ったらあった。

github.com

これでいけるじゃんと思って組み込んでみたが、以下のエラーが出てきた。

Error: java.io.IOException: Compressed with incompatible lzo version: 0x20a0 (expected 0x2050)

よくわからないけど、lzo形式のversionの違いでincompatibilityがあるっぽい? そして手元のデータは0x20a0の方で、0x2050は展開しているライブラリの表示のようだ。

github.com

んーということはこのライブラリをlzo version0x20a0対応すればいいのか? しかしJavaもlzoプロトコルも全くと言っていいほどわからない。 ましてやversion upにともなう互換性はどうすればいいんだとかも全くわからない。

提案手法

俺はlzoファイルを展開したいだけなんだ!

$ cat in.lzo | lzop -dc > out.json

したいだけなんだ!

と思って、「embulkから任意の外部プロセスを立ててdecodeするplugin」というアイデアに至った。

かくしてembulkのpluginを書こうとしたが、どうやらdecoder pluginは現状javaでしか書けないっぽい。 なんとかがんばってjrubyで書けるようにする道もあるかもしれないが、どうせならjavaに挑戦してみることにした。

javaは新卒のころにだましだまし書いてた記憶がおぼろげにある。 まだプログラミングの楽しさが分かっていなかったときのことだ。多分8年前?

そんなわけで全くの初心者ではないが、ほぼ初心者と言っていいだろう。

embulkにはpluginの雛形を生成するコマンドがあったので使ってみる。

$ embulk new java-decoder exec

これで生成されたjavaのコードをなんとか読んでみると、どうやらjavaではrubyで言うIOInputStreamとかOutputStreamという名前で扱われているようだった。

embulkのdecoder pluginはinputから渡ってきたInputStreamに対して、readメソッドを実装したclassのオブジェクトを返すと、embulkがよしなに読み込んでくれるようだ。 このclassにロジックを書けばいい。

参考はgzipとbzip2のdecode pluginがcoreにあった。

embulk/GzipFileDecoderPlugin.java at 627afede81ff547eda0db8a06a0d5fd53c8d586c · embulk/embulk · GitHub

コードは大変短い。java.util.zip.GZIPInputStreamというやつは、名前から察するに組み込みのclassっぽかった。

(このjavaは1ファイル1classでimportすることで使えるみたいなのも最初は戸惑ったがgolangみたいなもんだろと思ったら読めた。)

java.util.zip.GZIPInputStreamは最終的にはInputStreamを継承している。

どうやらこのInputStream classをつかったインターフェースはjavaの世界ではかなり一般的なもののようだ。 javaは割と堅いイメージを持ってたけど、特定のインターフェースさえ実装すればいいのはgolangっぽくて柔軟な印象を得た。javaいいじゃん。時代はjava

そんなこんなでjavaの標準的な動かし方を調べつつ、任意のコマンド指定でプロセスを立てて、stdinとstdoutをpipeから渡してやる実装ができた。 javaの作法はよくわからないので、Threadを立ててprocessのstdinに対してwirteしまくり、いつでもstdoutからreadしてねと言う感じで実装した。

試しにS3 inputに対してlzopで展開してPostgreSQLにoutputするサンプルを書いてみたら、指定のs3 dir以下のファイルを全部読み込んで展開して書き出しまでちゃんと動いた。

$ cat tmp.yml
in:
  type: s3
  bucket: bucket
  path_prefix: path
  auth_method: default
  decoders:
    - type: exec
      mode: pipe
      command: lzop -dc
  parser:
    type: jsonl
    columns:
      ...

out:
  type: postgresql
  database: test
  host: postgres
  port: 5432
  user: postgres
  password: password
  mode: replace
  table: test
  column_options:
    ...

(なんかmodeオプションとかいらない気もするが、pipe以外の使い方もあるかなと思ってこうなってる。。。)

考察

かくしてjavaのlzoライブラリに依存することなく、手元のlzopコマンドで動くなら大丈夫な状態ができた。 こんな感じで、ガンガン外部コマンドやパイプに頼る実装はunixっぽくて好き。

この仕組を応用すれば、encoderも作れそうだなあ(予定がない)。

まとめ

「で、これはproductionで使えるの?」と言うと、実はこれを書き出した時点で、自分のやっていたプロジェクトが一旦ペンディングになった。。。 僕の実装が予想より遅すぎたためである。面目ない。

いったんプロジェクトから離れはするが、必ずここに戻ってくる。 そういう誓いを立てるためにも、このpluginは取りあえず動く状態にしたのでインターネットに保存しておくのでどなたかのお役にもたてれば。

必ず戻ってくるぞ!

logs

RubyKaigi2018 in 仙台に行ってきた

rubykaigi.org

RubyKaigiは京都も広島も行っていなくて、仙台で3年ぶりの参加だった。

どのセッションも裏番組が面白そうすぎて、血涙を流しながら見にいっていた。

セッションを聞いて「こんな事ができたんだ」「それならこんな事もできるかな」みたいにアレコレ考えるのが楽しかった。

明日から使えるtipsを学ぶというよりも、自分の考えを拡張するため、あんまり話が想像できないセッションにも積極的に参加した。

どのセッションも面白かったというほかないんだけど、これだけはどうしても内に秘めたままにできない……。

「どうして俺は発表できないんだッ!くやしいッ!!!」

また次の機会にがんばります。RubyKaigi2018に関わったすべての皆様に感謝。

Logs

After Kaigi

終わってからは家族で観光した。

家族ばかり撮ってたのでネットに上げられるような写真はあまりない。

はじめてfluent-pluginを書いた

ようするに

github.com

fluentdでちょっと溜めて、postgresにbulk insertするやつです。

そもそも

fluentdが何をするやつなのかいまいちよく分かっていなかった。 「ログを転送する……。それで??」みたいな。ふわっとした理解だった。

いろいろ調べていくうちに、「この考え方だとすんなり理解できるな」というポイントを発見した。

Linuxで言うIOモデルをプロセス化したやつ」

とイメージすることで全体を理解しやすくなった。 更に雑に言うと「ちょっと溜めてなんかするやつ」である。

「何でも出来る」と言われてもよくわからなかったけど、inputをちょっと溜めて(buffer)、変換してoutputするイメージを掴むことで、fluentdの動作イメージがわいた。

例えば、アプリケーションの各プロセスで1レコードずつDBにinsertするとwrite負荷が高いけど、一箇所にある程度溜めてからDBにbulk insertすれば、DBへの接続は溜めた分まとめてできるので負荷が減る。fluentdならこれができる。

アプリケーションプロセスからのinsertが1、batch処理を100としたら、5とか10の単位で処理できる。 fluentdによってエンジニアが使える武器が増えた感じがする。

加えて「n秒に1回」みたいなこともできるので、マイクロバッチ処理も出来る。「何でも出来るちょっとしたサーバー」としても使いやすい。

postgres

今回はギョームで紆余曲折を経てpostgresを使うことにしたが、結構なデータ量を扱うのでinsert負荷が懸念された。 そこで、既に弊社でヘビーに使われているfluentdのラインにpostgresにもデータを送る経路を追加して、ある程度まとめてinsertしようと考えた。

ちょっと探したところ、 https://github.com/uken/fluent-plugin-postgres というpluginがすでにあるが、与えられたsqlをprepareして1レコードずつexec_preparedするやつだったので、sqlが自由にかけて柔軟ではあるけど、パフォーマンスに懸念があった。(実際に図ったところ、データ量によっては10倍〜100倍の差があった。)

あとは https://github.com/choplin/fluent-plugin-pgjson はテーブルスキーマが固定だったので要件に合わない。

そこで fluent-plugin-mysqlのbulkのやつ のpostgres版を作ってみたのが経緯。

本日production入りして、弊社の流量でもキビキビinsertしているっぽい。

fluent-plugin

他のpluginを参考にしつつ、結構雰囲気で書けた。 output-pluginなら、writeメソッドをよしなに実装すればいい。

ただ、formatメソッドの有無でchunkの挙動が変わってくるあたりに歴史的経緯を感じたのがちょっとハマりポイント。 1 plugin 1 classにしているところとかよく設計されているなあと思った。

testは https://docs.fluentd.org/v1.0/articles/plugin-test-code を参考に書いてみたけどうまく動かなくて力技で書いた。 (dirverからeventsが取れなかった)

豆知識

ON CONFLICT DO UPDATE

postgresではinsert文にON CONFLICT DO UPDATEをつけることでupsertできる。

しかしながら、同じinsert文内でON CONFLICTに指定したkeyが重複すると、postgres側では「どっちやねん」となってinsertに失敗するようだった。

https://www.postgresql.jp/document/9.6/html/sql-insert.html

ON CONFLICT DO UPDATE句のあるINSERTは「決定論的な」文です。 これは、そのコマンドが既存のどの行に対しても、2回以上影響を与えることが許されない、ということを意味します。 これに反する状況が発生した時は、カーディナリティ違反のエラーが発生します。 挿入されようとする行は、競合解決インデックスあるいは制約により制限される属性の観点で、複製されてはなりません。

これではunique indexを貼っている場合に不便……。 かと思いきや、多分fluentdからinsertするからには履歴テーブルとして扱うのがベストプラクティスなんだろうと思う。

送り側としては、受け側の事情は気にしたくない。(ただでさえ、既に複数のストレージに様々な事情でデータをpostしているのだ) ガンガン同じデータをpostしてもいいようにしたほうが運用が楽そうだと考えた。 そこで、unique indexはすべて外して、送られてくるがままを受け入れるようにした。

定期的に重複するデータを消したりする必要も出てくるかもしれないが(無視できる量なら大丈夫だけど)、多分エラーだ何だで困るより楽だと思う。

最大values数

postgresはprepareするとき$1 $2のような記号を使うが、これは$65535が最大。 つまり insert文で送れるvaluesは65535個までのようだ。

下記commitによると、16-bit unsigned integerでいまのところ固定のようだった。

https://github.com/postgres/postgres/commit/f86e6ba40c9cc51c81fe1cf650b512ba5b19c86b

pluginでは65535を超えそうなら、クエリを分割するように工夫しているので安心。

反省

  • fluentd自体に触れるのが初めてだったので、簡単な機能なのにdeployまで結構時間がかかってしまった。
  • あとから気がついたけど https://github.com/fluent/fluent-plugin-sql を使っても同じことはできたっぽい?