スタックとヒープの違いにはまった話

TL; DR

  • ヒープとスタックは違う
  • メモリは0と1
  • gdb楽しい

はじめに

以前に引き続き、C compilerを作っている

ポインタが思いの他簡単に実装できて感動していたら、ポインタと数字の加減算でめちゃくちゃにハマった

あっちこっちデバッグして、原因を探っていったら飛ぶように1週間ほど経っていたが、さきほど原因が判明してめっちゃ感動して、この記事を書き出した

書き上げるのに2日ぐらいかかってしまった

原因を探っていく過程で、スタックマシンとメモリについての理解が深まった

その記録

簡単なポインタの加減算

実装しようとしたのは、compiler bookのここ

「まだ配列がないので、mallocする」と書いてあったのでmalloc関数を使うのかと思いきや、mallocを使って配列をアロケートする方法がよく分からず肝心のテストが書けない

秘伝のあんちょこ、本家9ccのcommit logをたどってそれっぽい機能を実装したcommitを眺める

配列を宣言して、そこに値を代入すればいいと分かったので真似してテスト用の関数を書く

int *alloc2(int x1, int x2) {
    static int arr[2];
    arr[0] = x1;
    arr[1] = x2;
    return arr;
}

これで以下の簡単なポインタの加減算は動くようになった

int main() {
    int *p = alloc2(3, 6);
    return *(p + 1); // 6
}

ここまではそんなにかかっていない

すこし複雑なポインタの加減算

bookに書いてあるテストケース例ではテスト用関数から返り値でポインタを返すのではなく、アサインしたいポインタを渡して関数内でアサインする必要があったので、以下のテスト用関数を書いた

void allocp4(int **p, int x1, int x2, int x3, int x4) {
    int arr[4];
    arr[0] = x1;
    arr[1] = x2;
    arr[2] = x3;
    arr[3] = x4;
    *p = arr;
    return;
}

友人に指摘されるまでポインタのポインタ(int **p)という概念が出てこず、compileが通らなくてハマった

ポインタは難しい

このときにstaticが必要ないのでは?と思って、試しに外してテストを書いてみたのが、この後の大ハマりの原因になる

テストケース例のコード

int main() {
    int *p;
    allocp4(&p, 1, 2, 4, 8);
    int *q;
    q = p + 3;
    return *q; // 実行ごとに不定の値 🤔
}

明らかにポインタが指している値がおかしなことになっている

こういうときはとりあえず、シンプルなものに書き直して様子を見るに限る

シンプルにしたテスト関数

void allocp2(int **p, int x1, int x2) {
    int arr[2];
    arr[0] = x1;
    arr[1] = x2;
    *p = arr;
    return;
}

テストケース

int main() {
    int *p;
    allocp2(&p, 30, 31);
    int *q;
    q = p + 1;
    return *q;  // -> 0 🤔
}

31になってほしいが、思ったとおりにいかない🤔

加算する値を0にしても、返ってくる値は0のままで特に変化なし

加算する値を2以上にすると、実行ごとに変わる不定の値になって想定通り

もう1つシンプルにする

int main() {
    int *p;
    allocp2(&p, 30, 31);
    return *(p + 1); // -> 31 👍
}

これは想定通りに動く

動いたので1つ複雑度を上げる

int main() {
    int *p;
    int q;
    allocp2(&p, 30, 31);
    q = *(p + 1);
    return q; // -> 0 🤔
}

qをポインタではない普通の変数で宣言したが、やはり動かない

このあたりで変数宣言が実装していないのに初期化式が動いていたことを思い出して、きちんと実装し直す

しかし、結果は変わらない

ここまでで5日ぐらいかかっている

Assemblyのデバッグ

ここまでテストケースのcompileそのものは成功している

compile段階でSEGVしてくれた方が原因の特定はしやすい

思い当たる節もないので、地道にparserの出力したASTがおかしくないか、codegenでassebmblyにどのnodeから生成されたかのコメントもつけて書き出してみる

特におかしいところは見受けられない

ここまでやって打つ手がなくなったので、ついに出力されたassemblyを下の方から直接編集して途中の結果を雑にretしてどこからおかしいのか確認した

gdbを使わなかった理由は使い方がよく分からなかったため

すると、おかしくなっている行を特定できた

以下が出力されたassemblyの一部

...
    call allocp2
    push rax
    # ND_ASSIGN start gen_lbal lhs
    mov rax, rbp
    sub rax, 16
    push rax
    # ND_ASSIGN end gen_lbal lhs
    # ND_ASSIGN start gen rhs 
    # ND_DEREF start
    # gen lhs
    mov rax, rbp
    sub rax, 8
    push rax
    pop rax
    mov rax, [rax]
    push rax
    # <== A1 ==>
    # gen rhs
    push 1
    # <== A2 ==>
    pop rdi
    pop rax
    push rax
    mov rax, 4
    imul rdi, rax
    pop rax
    add rax, rdi
...

A1の時点では*(p + 1); のうち、pの値、つまりpの指すメモリアドレス(pはポインタなので)がスタックトップに積まれている

なので、以下をA1に挿入すると、pが指しているアドレスの値をretできる

    pop rax
    mov rax, [rax]
    mov rsp, rbp
    pop rbp
    ret

これは想定通りに30が返ってくる

次にA2の時点では1をスタックにpushしている

ということで、2回popして値を見れば同じように30が返ってくるはず

    pop rax
    pop rax
    mov rax, [rax]
    mov rsp, rbp
    pop rbp
    ret

ところが、結果は1

push 1 で pの指すメモリアドレスが1に書き換えられてしまっているらしい

ここまで特定できたので、ようやくgdbの使い方を少し調査してメモリアドレスの変化を観察することにした

調査に使った資料はこのあたり

おもむろにgdb ./tmpgdbを起動、disp/i $pc でステップ実行ごとに次に実行されるassemblerを表示するようにして、b mainでmainの定義位置にbreak pointを置いて、いざrunする

disasして、assemberを表示し、アドレス指定でallocp2の呼び出しと先程のA1の直前にbreak pointを設定する

最初のbreak pointでのrbpとrspのアドレスと値

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd900      0x7fffffffd900

cして、allocp2の呼び出しにいって、niして、rbpとrspを確認

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd8f0      0x7fffffffd8f0

$rbpからそれっぽいところまでメモリを10進数出力して対応を見てみる

現状の9ccsは変数のメモリを決め打ちで$rbpから8byteのoffsetで置いてるのでこんな感じ

(gdb) x/-80bd $rbp + 8
0x7fffffffd8b8: -25 -40 -1  -1  -1  127 0   0
0x7fffffffd8c0: 31  0   0   0   30  0   0   0
0x7fffffffd8c8: -8  -40 -1  -1  -1  127 0   0
0x7fffffffd8d0: 30  0   0   0   31  0   0   0 
0x7fffffffd8d8: 0   110 9   127 53  41  -38 58
0x7fffffffd8e0: 0   -39 -1  -1  -1  127 0   0
0x7fffffffd8e8: 126 81  85  85  85  85  0   0
0x7fffffffd8f0: -16 -39 -1  -1  -1  127 0   0 <- q, $rsp
0x7fffffffd8f8: -48 -40 -1  -1  -1  127 0   0 <- p
0x7fffffffd900: 0   0   0   0   0   0   0   0 <- $rbp

30と31とallocp2で配置されたらしき数字が確認できる

0x7fffffffd8f8 の値はpointerのpで10進数ではなくアドレスなので確認してみる

(gdb) x/a 0x7fffffffd8f8
0x7fffffffd8f8: 0x7fffffffd8d0

確かにアドレスが入っていて、0x7fffffffd8d0を指している

さっき出力したメモリを見ると確かに30を指している

せっかくgdbがexpressionが使えるので、$rbpからのオフセットでも見てみる

(gdb) x/a $rbp - 8
0x7fffffffd8f8: 0x7fffffffd8d0

(gdb) x/db *(int **) ($rbp - 8)
0x7fffffffd8d0: 30

この時点ではきちんと30が入っている

では cして、A1の直前のbreak pointまでいって、ni する

rbpとrspを確認

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd8d8      0x7fffffffd8d8

10進数出力

(gdb) x/-80bd $rbp + 8
0x7fffffffd8b8: -25 -40 -1  -1  -1  127 0   0
0x7fffffffd8c0: 31  0   0   0   30  0   0   0
0x7fffffffd8c8: -8  -40 -1  -1  -1  127 0   0
0x7fffffffd8d0: 30  0   0   0   31  0   0   0 <- *p
0x7fffffffd8d8: -48 -40 -1  -1  -1  127 0   0 <- $rsp
0x7fffffffd8e0: -16 -40 -1  -1  -1  127 0   0
0x7fffffffd8e8: 0   0   0   0   0   0   0   0
0x7fffffffd8f0: -16 -39 -1  -1  -1  127 0   0 <- q
0x7fffffffd8f8: -48 -40 -1  -1  -1  127 0   0 <- p
0x7fffffffd900: 0   0   0   0   0   0   0   0 <- $rbp

スタックが伸びて$rspがpの指しているポインタの直前まで来ている

(gdb) x/db *(int **) ($rbp - 8)
0x7fffffffd8d0: 30

まだこの段階では*pは30だが、大変不穏な気配

ではいざ問題のA2のinstruction、つまり、push 1に進めるためにまたniする

rbpとrspを確認

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd8d0      0x7fffffffd8d0

メモリダンプ

(gdb) x/-80bd $rbp + 8
0x7fffffffd8b8: -25 -40 -1  -1  -1  127 0   0
0x7fffffffd8c0: 31  0   0   0   30  0   0   0
0x7fffffffd8c8: -8  -40 -1  -1  -1  127 0   0
0x7fffffffd8d0: 1   0   0   0   0   0   0   0 <- *p, $rsp 🤯
0x7fffffffd8d8: -48 -40 -1  -1  -1  127 0   0
0x7fffffffd8e0: -16 -40 -1  -1  -1  127 0   0
0x7fffffffd8e8: 0   0   0   0   0   0   0   0
0x7fffffffd8f0: -16 -39 -1  -1  -1  127 0   0 <- q
0x7fffffffd8f8: -48 -40 -1  -1  -1  127 0   0 <- p
0x7fffffffd900: 0   0   0   0   0   0   0   0 <- $rbp

push 1 はcompiler bookにもあるように以下と同じ

sub rsp, 8
mov [rsp], 1

ということで、 pの指していたアドレスの値が1に書き換わってしまった

念の為、オフセットでも確認しておく

(gdb) x/db *(int **) ($rbp - 8)
0x7fffffffd8d0: 1

やはり、1になってしまっている

そして、この後は、pの指しているアドレスに対して +4 するので 結果 p + 10x7fffffffd8d4 になる

ここは、さっき1に書き換わったところなのでderefすると値が0になる

そして、qに0が代入されて0がreturnされるという挙動になっていた

ということで、stackが書き換わるのを見て「これ、stackとheapの違いというやつでは?」と気づく

staticをつける

ここまでの調査からして、自分で書いたcompiler部分ではなくテスト関数のallocp2の実装がおかしいのは明らかだったのでとりあえずそっとstaticを戻してみた

void allocp2(int **p, int x1, int x2) {
    static int arr[2];
    arr[0] = x1;
    arr[1] = x2;
    *p = arr;
    return;
}
int main() {
    int *p;
    int q;
    allocp2(&p, 30, 31);
    q = *(p + 1);
    return q; // 31 👍
}

同じようにA1の直後のダンプたち

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd8d8      0x7fffffffd8d8


(gdb) x/-80bd $rbp + 8
0x7fffffffd8b8: -25 -40 -1  -1  -1  127 0   0
0x7fffffffd8c0: -26 -40 -1  -1  -1  127 0   0
0x7fffffffd8c8: -51 83  85  85  85  85  0   0
0x7fffffffd8d0: 31  0   0   0   30  0   0   0
0x7fffffffd8d8: 32  -128    85  85  85  85  0   0 <- $rsp
0x7fffffffd8e0: -16 -40 -1  -1  -1  127 0   0
0x7fffffffd8e8: -8  -40 -1  -1  -1  127 0   0
0x7fffffffd8f0: -16 -39 -1  -1  -1  127 0   0 <- q
0x7fffffffd8f8: 32  -128    85  85  85  85  0   0 <- p
0x7fffffffd900: 0   0   0   0   0   0   0   0 <- $rbp

(gdb) x/a ($rbp - 8)
0x7fffffffd8f8: 0x555555558020 <arr.2155>

(gdb) x/db *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    30

pの指すアドレスがRBPの近く(スタック)ではなく、まったく違う場所(ヒープ)を指していることが分かる

ということで、A2の直後のダンプ

(gdb) i r rbp rsp
rbp            0x7fffffffd900      0x7fffffffd900
rsp            0x7fffffffd8d0      0x7fffffffd8d0

(gdb) x/-80bd $rbp + 8
0x7fffffffd8b8: -25 -40 -1  -1  -1  127 0   0
0x7fffffffd8c0: -26 -40 -1  -1  -1  127 0   0
0x7fffffffd8c8: -51 83  85  85  85  85  0   0
0x7fffffffd8d0: 1   0   0   0   0   0   0   0 <- $rsp
0x7fffffffd8d8: 32  -128    85  85  85  85  0   0
0x7fffffffd8e0: -16 -40 -1  -1  -1  127 0   0
0x7fffffffd8e8: -8  -40 -1  -1  -1  127 0   0
0x7fffffffd8f0: -16 -39 -1  -1  -1  127 0   0 <- q
0x7fffffffd8f8: 32  -128    85  85  85  85  0   0 <- p
0x7fffffffd900: 0   0   0   0   0   0   0   0 <- $rbp

(gdb) x/a ($rbp - 8)
0x7fffffffd8f8: 0x555555558020 <arr.2155>

(gdb) x/db *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    30

最初に動いていたポインタとINTの演算も命令数が少なく、Stackが破壊されないのでたまたま動いていただけなのであった

めでたしめでたし

最後に

自分で書いてきたコンパイラだが、ポインタを扱いだしたあたりからなぜ動いてるのかうまく説明できる自信がなかった

今回のハマりはstaticを不用意に消すというポカから発生したが、結果的にgdbで遊んだりメモリの中身を見て、スタックマシンの概念をより具体的に理解できたので良かった

compiler bookのコラムにあったスタックの伸び方の話や、スタックとヒープが違うものだということがだいぶ体感できた

レジスタとメモリをつかったスタックマシン、本当に人類の叡智の結晶という感じがしてすごい(こなみ

おしまい

コラム: メモリは0と1

メモリに置いてある値を解釈するという概念が難しくて、pointerの指している値を出すのにめっちゃ苦労した

例えばこれ

(gdb) x/db *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    30

これは、$rbp - 8 をintへのポインタとして解釈して、 その値をd(ecimal)でb(yte)で区切って表示する

これをこうすると謎の巨大数値がでてくる

(gdb) x/dg *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    133143986206

これは、b(yte)がg(iant word)になって、64byteで解釈されている

実体的にはこう

(gdb) x/tg *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    0000000000000000000000000001111100000000000000000000000000011110

tはbinaryなので、binary表現になった

これだと長すぎて分かりづらいので分割

(gdb) x/2tw *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    00000000000000000000000000011110    00000000000000000000000000011111

w(ord)なので32byteで区切った

もうちょい区切る

(gdb) x/8tb *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    00011110    00000000    00000000    00000000    00011111    00000000    00000000    00000000

ここまでくると一番最初の30の正体が判明して 0x555555558020 から 8byte取ってきて10進数で解釈した 00011110の値となる

ちなみに、byteでもhalfwordでもwordでも同じ

ただ、gにすると64byteになって変わる

(gdb) x/xg *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    0x0000001f0000001e

(gdb) x/dg *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    133143986206

これで、133143986206の正体も判明して、0x555555558020 から64byte取ってきて10進数で解釈するとこんな巨大な値になった

16進数だとだいぶ分かりやすい

コードを書いているとあまりに自然に10進数が使えるので忘れがちだけど、メモリがあくまで0と1を記憶しているだけのもので、工夫で10進数を扱えるようにしているというのがこれでしっくりきた

コラム2: メモリの値確認

xでメモリの値を確認するのがめっちゃ大変

上の例と同じくこう

(gdb) x/db *(int **) ($rbp - 8)
0x555555558020 <arr.2155>:    30

$rbp - 8 が指してるアドレスが知りたい

(gdb) x/a ($rbp - 8)
0x7fffffffd8f8: 0x555555558020 <arr.2155>

0x7fffffffd8f8 が指してるアドレスが指している値が知りたい

(gdb) x/a 0x7fffffffd8f8
0x7fffffffd8f8: 0x555555558020 <arr.2155>

これはアドレスそのもので、アドレスが指している値じゃない

(gdb) x/db *(0x7fffffffd8f8)
0x55558020: Cannot access memory at address 0x55558020

だめ

(gdb) x/db int 0x7fffffffd8f8
A syntax error in expression, near `0x7fffffffd8f8'.

だめ

(gdb) x/db * (int *) 0x7fffffffd8f8
0x55558020: Cannot access memory at address 0x55558020

だめ

(gdb) x/db * (int **) 0x7fffffffd8f8
0x555555558020 <arr.2155>:    30

できた

Cの有効な文法を書くのに未だに慣れない

特にポインタのポインタみたいなのが難しい

docker compose cliでecsにデプロイした話

TL; DR

  • Phoenixでサーバーを書いた
  • docker compose の ecs integrationでデプロイした
  • AWSのいろんなサービスがいい感じに起動して、便利

docker compose ecs integrationとは

2020年11月に発表されて、待望の機能が来たと結構な反響があったように思います

仕事でPhoenixのアプリを作る機会があり、そこでこのスタックを採用したのでハマったところを紹介していきます

概要、必要なこと、アーキテクチャなどはほぼ以下のドキュメントに書いてあるのでそちらをご覧ください

docs.docker.com

ふわっとまとめると、docker-compose.ymlに基づいてAWSのリソースを良い感じに用意してくれるコマンドです

ハマったところ

ECSって何、ECRって何

本当に無知でdockerをデプロイできるサービスぐらいにしかECSを理解していなかった

そのため、ECRって何という状態だった

  • ECSはElastic Container Service
  • ECRはElastic Container Registry

ECRはDocker HubみたいにDocker imageをhostingできるサービス

ECSはclusterを作成して、taskを定義して、serviceを実行するものっぽい

clusterはEC2なんかのコンピューティングリソース

taskはその名の通り仕事、実体的には単数 or 複数のdockerのcontainer定義

serviceはtaskをclusterで何個走らせるかを決める

これらを統合して、docker compose cliを使うとデプロイはこんな感じになるはず

  1. サーバーを書く
  2. buildしてDocker imageに固める
  3. ECRなどのDocker image hosting serviceにpushする
  4. docker compose up

port:80が使えない

Unix系のOSにて、デフォルトでは1024以下のportはroot権限がないと使用できないらしい

これを回避する方法は以下

  1. nginxでリバースプロキシを立てて、アプリサーバーを直接ALBに紐付けない
  2. rootユーザーでアプリサーバーを起動する
  3. port mappingする

2はセキュリティ的に推奨される感じではなさそうです

3はecs integrationではサポートされていません

このあたりはいかに詳しいです

別のcontainerで立つ以上portのabstractionは利がない & compose specがroutingに関することに関与しないのでECS integrationでもそれを踏襲する方針のようです

この事実が分かるまで、いつものlocal開発向けのdocker-compose.ymlのように80:4000をやろうとしてdocker compose upでエラーを踏んでissueを漁りドキュメントを読むというのを繰り返しました

ということで、素直に1でいきます

nginx.confを書いて、アプリサーバーと同じくbuildしてECRにpushして解決

その際に、後述のサービス検出の話でハマりました

サービス検出

英語だとService Discovery

恥ずかしながらService Discoveryが何を指しているかさっぱり分かっていなかったが少し理解した

nginxでリバースプロキシを立てたが、どうやってnginxに来た通信をpPhoenixのアプリサーバーに向けるのかという話

つまり、ECSでcontainer間通信を実現する方法が分からなくてハマりました

実はドキュメントに書いてあります

Deploying Docker containers on ECS | Docker Documentation

Services are registered automatically by the Docker Compose CLI on AWS Cloud Map during application deployment. They are declared as fully qualified domain names of the form: <service>.<compose_project_name>.local.

app.hoge-project.localのようなドメインでサービスのcontainer間で通信ができるようになります

当初やりたいことがService Discoveryであることが分かっていなかったため、いろんなクエリでググった末に以下のページにたどり着いてようやく理解できました

Amazon ECS サービスでタスク間の通信を許可する

ということでnginx.confにphoenixのアプリサーバーのドメイン名を書いて完了

Migration

これはハマったというよりもハックで乗り切った感じの話

基本的にサーバーを書いていると発生するMigrationですが、それをECSのサービスとして定義するとdocker compose cliの仕様上、ちょっと困ったことになります

docker compose convertをすると分かりますが、service.deploy.replicasに値を設定しないとECSのサービス定義のdesiredCountは1にデフォルトで設定されます

そのため、DB migrationのサービスが起動 -> 正常終了 -> desiredCountが1なので、再度起動 -> 正常終了のループを延々と繰り返します

最初はdocker compose converで生成されたCloudFormation templateをカスタマイズしようと思いましたが、上手いこといかなかったので方針を切り替えました

結果、docker compose upを走らせた後に、migrationの終了を確認してからマネジメントコンソールでNumber of Taskを0にするように手動で回避しています

この辺、上手い方法があったら教えていただけると 🙏

Command support

ecs contextでsupportされているコマンドは筆者が試した限りでは以下だけでした。

  • docker compose up
  • docker compose down
  • docker compose logs
  • docker compose convert

runが使えないので上述のDB migrationで困ったりしてました

runはsupportしてほしいという要望が既に上がっています

Add docker run support to ECS · Issue #829 · docker/compose-cli · GitHub

ちなみにdocker compose downをするとupで作成されたすべてのAWSリソース(既存のリソースは除く)が破棄されるのでお気をつけください

既存のAWSリソースの使用

ここに説明があります

Deploying Docker containers on ECS | Docker Documentation

VPCだけでいけるかと思いましたが、ドメインのヒモ付のため、LBも既存のものを使うようにしました

最後に

docker compose ecs integration、まだ荒削りな部分もありますが、production運用も十分に視野に入ってくると思います

もちろん、凝ったことをやろうとすると足りない部分も多いのでそのあたりはissueに起票したりでfeedbackしていくと良さそうです

これで、好きな言語やフレームワークでサーバーをcontainerにしてデプロイするのが簡単になりました

どんどん使っていきたいところ

おしまい

「低レイヤを知りたい人のためのCコンパイラ作成入門」をやっている話

TL; DR

  • 「低レイヤを知りたい人のためのCコンパイラ作成入門」をやっている
  • 関数定義が動くようになった
  • 「低レイヤを知りたい人のためのCコンパイラ作成入門」はいいぞ

はじめに

1年ぐらい前にこれが公開されて、ずっとやりたいと思っていたがやれてなかったので今年の2月ぐらいから地道に進めていた。

www.sigbus.info

やっとこさ関数を定義して、関数を呼び出せるようになったのでここにいたるまでの振り返り

成果物はこれ

GitHub - sasurau4/9ccs

やって楽しいポイント

loxのときのように別の言語で実装しても良かったが、Cをほとんど書いたことがなかったので、紹介されている通りCで実装した。

assemblyとかレジスタはそもそもよく分かってなかったので、基礎の基礎から説明されてて楽しい。

tokenizeらへんはloxで一度通っているのですっと頭に入ってくる。再帰下降構文解析とBFNらへんは何度やっても人類の叡智という感じがする。

スタックマシンが実際にどうやって使われてるかもようやっと少し理解できた。

makeの使い方も触れるしよい。

途中に挟んであるコラムが面白いので、コンパイラを実際に作らないでも読み物として楽しむだけでも面白い。

Cの歴史も少し触るので、現在から見ると明らかに不自然に感じる文法が採用された時代背景や思想、普段使っているJSなんかが高級言語であることがなんとなく分かる。

ハマったポイント

どのへんにハマったかをつらつら書く

ここに取り上げていないものはREADMEに書いてある。

GitHub - sasurau4/9ccs

Cのコードで困ったら9cc本体を見れば雰囲気は分かるので、それでなんとかなる。

GitHub - rui314/9cc: A Small C Compiler

アセンブラで詰まったら付録1を見たり、Compiler Explorerと自分の出力したアセンブラを比較したりすると分かる。

Compiler Explorer

アセンブラは手で書き換えて、gdbで行実行しつつ、レジスタの中身を確認するとより分かりやすかった。

SIGSEGV

友人曰く、C言語名物らしい。

初めて出たとき、例のアレじゃんとテンションが上がった。

とはいえ、それも最初だけで、実装を進めていると頻繁に出て来る上に、デバッグが非常に困難なのでこれは嫌になるわけですわという気持ちになった。

デバッグが非常に困難というのは、こいつが出ると普段JSや他の言語なんかで使うprintなど一切の出力がされなくなる。

JSのthrow errorと同じように発生したソースのlineとなんで発生したかも出るだろうし、そこまでに実行されるprintfなんかの処理は出力されるだろうと油断していたのでこれには真顔になってしまった。

仕方ないので記憶の片隅からgdbというツールでデバッグ出来たはずと思い出し、それの使い方を覚えて、ある程度はなんとかなるようになった。

gdbは便利。もう手放せなくなってしまった。

このあたりを一通り踏んだので、Rustがsafeな書き方をしているとSegmentation faultが出ないというのがどれだけありがたいかを体感できるようになった。

インクリメントの前置と後置

トークナイザを実装しているときにインクリメントの後置と+=演算子が同じ意味だと勘違いしていて、トークナイザが上手いこと動かなくなった。

これは、JSでも同じ仕様

let x = 3;
const y = x++;

console.log(`x:${x}, y:${y}`);
// expected output: "x:4, y:3"

let a = 3;
const b = ++a;

console.log(`a:${a}, b:${b}`);
// expected output: "a:4, b:4"

let p = 3;
const q = p+=1

console.log(`p:${p}, q:${q}`)
// expected output: "p:4, q:4"

インクリメントの前置と+=が同じ意味になる。

フィボナッチ数列の実行速度の比較

関数定義ができるようになって、再帰フィボナッチ数列が動くようになった記念で、以前作ったlox compilerと実行時間を比較してみた。

以前のloxの記事はこれ

Rustでlox言語のinterpreterを作っている話 - sasurau4のブログ

まずはlox版のフィボナッチ数列の実装

fun fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

9ccsのフィボナッチ数列実装

fibonacci(x) {
    if (x < 2) return x;
    return fibonacci(x - 1) + fibonacci(x - 2);
}

実装されてる言語機能がほぼ一緒なので、実装もほぼ同じものになる。

こちらが実行にかかった時間

実行関数 9ccs lox compiler
fib(20) 0.002 sec 1 sec
fib(30) 0.04 sec 56 sec
fib(38) 0.6 sec 2721 sec
fib(40) 1.5 sec -- (長すぎて未測定)
fib(50) 191 sec -- (長すぎて未測定)

9ccsはtimeコマンドで計ったのに対して、loxは実装したclock関数で測定していて、双方とも1回ずつしか実行していないので、正確な比較ではないが、ざっくり比較でもこれぐらい差がある。

引数が38と中途半端なのはloxの39の実行終了に時間がかかりすぎて、途中で打ち切ってデータが取れた最後の値が38だったため。

9ccsの方が圧倒的に早いが、それでもこの実装だと引数60で30分程度はかかりそうな気配がする。計算量の爆発怖い。

テスト

フィボナッチ数列で比較するために久しぶりにlox compilerを動かしたがfor文が動かなくなっていて悲しかった。

Cコンパイラ作成入門ではきちんとテストケースを書いて、毎回テストケースが通るように実装しているので、こういうことがない。

テスト書くの大事。

最後に

セルフホストまでは行けるかわからないが、本に紹介されているテストケースをCで書き直すまではやりたい。

とりあえず、ポインタが扱えるようになるとぐっとCっぽくなる気がするので、次はそれをやる。

おしまい。

@babel/core@7.13.7とnode-semver@7.0.0とwebpackを併用してハマった話

TL;DR

@babel/core@7.13.7以下をwebpackと一緒に使うとnode-semver@7.0.0でハマるかもしれない

はじめに

仕事でブラウザ上のエディタでmarkdownを書くとリアルタイムにReactでどんなコンテンツが表示されるかを確認できるコンテンツシミュレーターみたいなものを使っている。

このシミュレーターで書けるmarkdownは本来のmarkdownをちょっと拡張して、独自にJS書けたり、インラインでJS関数やオブジェクトの参照ができたりする。

このシミュレーターが既存のPJでは動いているのに、別のPJで新しくセットアップしようとしたら動かないという事象を調査したら、意外なところまで行ったのでその経緯と顛末を書く

発端

あるPJ(以下A PJ)でこんな感じのmdを書くとシミュレーターのコンパイルが失敗するという事象で困ってると相談を受けた。

こんにちは、${context.name}さん。

->

JavaScriptの構文エラーが発生しました
...(Trace的なやつ)

本当はこうなってほしい

こんにちは、${context.name}さん。

->

こんにちは、田中さん

コンテンツを以下のようにすると、特に問題なくシミュレーターが動くので、どうも${context.name}のところのJSの評価がうまく動かないらしい

こんにちは、世界

なぜエラーになるのか

このシミュレーターの仕組みは、エディタ上で与えられたmdコンテンツをworkerでコンパイルして、JSを評価して表示するコンテンツを生成する。

コンパイルでmarkedとbabelを使っている。

表示するためにReactのコンポーネントを書いておくという感じ。

とりあえずTraceが出てるので、宇宙より壮大なnode_modules配下のbabelされたりしたJSたちにひたすら潜ってlogを挿したりしながら、エラーメッセージをたどっていく。

これが宇宙の旅?

すると最終的に以下に辿り着いた

transform error Error: [BABEL] /[anonymous]: Cannot find module './functions/satisfies' (While processing: "base$0")
    at webpackEmptyContext (eval at ../../node_modules/@babel/core/node_modules/semver sync recursive (82363a058d1f41f6955e.worker.js:1872), <anonymous>:2:10)
    at lazyRequire (index.js?0488:5)
    at Object.get [as satisfies] (index.js?0488:12)
    at Object.assertVersion (config-api.js?bf03:91)
    at eval (index.js?8294:252)
    ...

なんだか、@babel/coreが抱えているsemverでmoduleが読み込めないらしい

node_modulesの宇宙からsemverのpackage.jsonを見つけ出してmainのindex.jsを読んでみる

const lrCache = {}
const lazyRequire = (path, subkey) => {
  const module = lrCache[path] || (lrCache[path] = require(path))
  return subkey ? module[subkey] : module
}

const lazyExport = (key, path, subkey) => {
  Object.defineProperty(exports, key, {
    get: () => {
      const res = lazyRequire(path, subkey)
      Object.defineProperty(exports, key, {
        value: res,
        enumerable: true,
        configurable: true
      })
      return res
    },
    configurable: true,
    enumerable: true
  })
}

lazyExport('re', './internal/re', 're')
...(略)
lazyExport('satisfies', './functions/satisfies')
...(略)

lazyyRequireの機構を自前で実装しているらしい。

logを差し込んで試すと、const module = lrCache[path] || (lrCache[path] = require(path))がORの右辺に落ちてrequire(path)でmoduleが見つからずにエラーになっているっぽい。

bundle時に静的に決まっていないrequireをbundlerで解決できないのはそれはそうという感じ(webpackの挙動に詳しくないので違うかもしれない)

なぜ動いているのか

エラーの原因は分かったが、今度はなぜ以前設定したPJ(以下B PJ)では今でも動いているのか謎なので同様に調査

semberのpackage.jsonのmainにあるsemver.jsがこちら

exports = module.exports = SemVer

var debug
/* istanbul ignore next */
if (typeof process === 'object' &&
    process.env &&
    process.env.NODE_DEBUG &&
    /\bsemver\b/i.test(process.env.NODE_DEBUG)) {
  debug = function () {
    var args = Array.prototype.slice.call(arguments, 0)
    args.unshift('SEMVER')
    console.log.apply(console, args)
  }
} else {
  debug = function () {}
}
...(略)

なるほど、全然違う

というかmainに書いてある値が違うのでsemverのバージョン違いが原因らしい

そのsemverはどこからやってきたのか

エラーになるA PJのsemverのversionは7.0.0

これは、@babel/core7.13.1から指定されている

エラーにならないB PJのsemverのversionは5.7.1

これは、@babel/core7.12.3から指定されている

@babel/coreが上がってるのは気づいていたが、minor versionだから大丈夫やろと油断していた

いつ上がったのか@babel/coreのリリースブログchangelogを読んでみるが、それっぽい記述がない

blameするとこいつで変わったらしい

github.com

BABEL_8でbreakingするよ的なflagが立っていて、@babel/coreのv7.13系からsemverがv7系に上がっている

これがPR

github.com

PRマージされた後に、@babel/coreとwebpackを併用するとsemverが7.0.0に固定されてissueが起こると書いてある

このコメント、ブラウザでコードエディタやってて@babel/core使ってると書いてあるので、まったく同じユースケースでびっくり

そのあとsemver@v7.1系を使うとBREAKINGなので、6.3系にダウングレードするPR出されて、まだリリースされていない。これが2日前。

github.com

ということで、次のbabelがリリースされたら解決しそう

おまけ1: semverのpreload機構

2019年12月ぐらいからちょこちょこissueにはなっていたらしい github.com

ドキュメントが更新されたりしていたが、以下のPRで廃止になっている

github.com

おまけ2: semverのsemver

github.com

node-semverのレポジトリを調査していたら、node-semverがsemverに従ってないというコメントがついていて、メンテナからするとそんなことはないよなあと思ったので貼っておく

おわりに

ということで、@babelのv7.13系の踏むのが難しいバグを踏み抜いたお話でした。

@babelのv7.13.0は2021-02-22にリリースされて、まだ4日しか経っていないのにもうv7.13.7なので、すぐにfixされるでしょう。

@babelもnode-semverももともとはnode.jsで使うことを想定されていて、ブラウザ環境で使われるのはエコシステムが発展したが故なので、この辺にハマるときはきっちりハマります。

メンテナはユースケースが複雑なので非常に大変だと思いますが、メンテしてもらっている身だとありがたい限りです。自分もブログを書いて他にハマる人がいたときの一助になればよいなと思いつつ、おしまい。

JSでnew Functionの使いどころ

はじめに

markdownを書くと、ソシャゲみたいにキャラと対話してるUIが作れるというクローズドなライブラリを仕事でメンテしています。

ADVゲームのシナリオをエンジニアじゃなくても書けるようにするやつですね。

それのメンテでJSのFunctionのconstructorを使う機会があったので、それの経緯を書き連ねていきます。

markdownでシナリオ

markdownでシナリオがどういう感じかというとこんな感じ。

# first-conversation

こんにちは、${context.name}さん。
このトークはサンプルです。

# single-select-list

元気ですか?
選択肢を選んでください。

- [元気です](multiple-select-list?mt.fine=1)
- [元気ではないです](multiple-select-list?mt.fine=2)

# multiple-select-list

複数選べる選択肢はこんな感じになります。

- [ ] [お好み焼き](?liked=お好み焼き)
- [ ] [**たこ焼き**](?liked=たこ焼き)
- [ ] [もんじゃ焼き](?liked=もんじゃ焼き)

H1ごとに段落になっていて、H1の内容は内部的なidとして使われるだけで会話の内容には影響がありません。

markdownといいつつ、独自記法を含んだmarkdownっぽい何かです。

例えば${context.name}は、対話のためのReact componentに渡すpropsの中にあるcontextオブジェクトの値を参照してこんにちは、fooさん。になります。

ここで内部的にはeval()を使っていました。

hermes

この対話エンジン、reactで動くことはもちろん、react-nativeでも、cliでも動きます。

ところで、Facebookは2019年7月にreact-native向けにhermesというJS engineを発表しました。

reactnative.dev

hermes、今のところはAndroidでopt inして始めて使えるエンジンですが、react-native-reanimatedのv2-alphaがhermesしかサポートしていなかったり、react-native本体でもiOShermesを動かせるようにするPRが作られていたりとecosystemがhermesを前提にしてきそうな動きがあります。

docs.swmansion.com github.com

もちろん、すぐにhermesがデフォルトになるということはなさそうですが、hermesをonにしたときのAndroidのパフォーマンス向上は捨てがたいです。

engineering.fb.com

ということであるプロジェクトで無邪気にhermesを有効化したんですが、react-nativeのアプリがクラッシュするようになりました。

クラッシュメッセージはUncaught ReferenceError: Property 'context' doesn't exist

なんでや。

evalの挙動

react-nativeは、普段はJavaScriptCoreSafariに積まれてるやつ)でJSを動かしています。なのでWebで動くものは基本的に動きます。

ところが、hermesはreact-native向けに新しく開発されたエンジンなので、一部機能が動きません。また、ドキュメント上でサポートから除外される機能が列挙されています。

Local mode eval() (use and introduce local variables)

github.com

犯人はこいつでした。

Local mode eval()とはなんぞやということで、困ったときのMDN。

eval 関数を eval 以外の名前を参照して呼び出すことで間接的に使用した場合、ECMAScript 5 以降ではローカルスコープではなくグローバルスコープで機能します。これは例えると、関数定義によりグローバル関数が作成されるため、評価されたコードはその呼び出されたスコープ内のローカル変数にアクセスできなくなる、ということです。

developer.mozilla.org

つまり、evalを直接呼び出したときはlocal scopeを参照しますが、別名をつけて間接的に呼び出すとglobal scopeを参照します。

そして、hermesはlocalモードでのeval呼び出しをサポートしていないので、globalに存在しないcontextというオブジェクトを呼び出そうとしてクラッシュしていました。

なるほど理解。

ということで、おもむろにhermesを手元でbuildしてMDNのサンプルのscriptを走らせるとどんな挙動をするか見てみます。

console.logが動かないのでprintに置き換え。

"use strict";
function test() {
  var x = 2, y = 4;
  print(eval('x + y')); // 直接呼び出し、ローカルスコープを使用し、結果は 6 となる
  var geval = eval; // グローバルスコープでのeval呼び出しと同等
  print(geval('x + y')); // 間接呼び出し、グローバルスコープを使用し、 x は未定義となるため ReferenceError が発生する
  (0, eval)('x + y'); // 間接的な呼び出しのもう一つの例
}
test();

これをこうじゃ

> ./build/bin/hermes test.js
test.js:25:9: warning: Direct call to eval(), but lexical scope is not supported.
  print(eval('x + y')); // 直接呼び出し、ローカルスコープを使用し、結果は 6 となる
Uncaught ReferenceError: Property 'x' doesn't exist
    at eval (JavaScript:1:1)
    at test (test.js:25:13)
    at global (test.js:30:5)

hermes、evalを直接呼んでるけど、lexical scopeはサポートされてないよとwarningを出してくれて大変親切。

MDNと違って、最初のevalがglobal scopeなので結果が6にならずにReferenceErrorになりました。

原因は分かったのでこれをfixします。

new Functionの使いどころ

MDNのevalのページにeval() を使わないでください!という項があって、そこでnew Functionする方法が書いてあります。

ということで、FunctionのMDN

developer.mozilla.org

なんとFunctionのconstructorに引数を与えるとglobal scopeで実行される関数が動的に作れるらしいです。

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// expected output: 8

つまり、これをこうしてやれば動くのでは?

const testContextProperty = new Function("context", "return context.foo");
const testCallContext = new Function("context", "return context.bar()");
const context = {
  foo: 'foo',
  bar: function() {
    return 'bar'
  }
}
print('access foo: ' + testContextProperty(context))
print('call bar: ' + testCallContext(context))

hermesで実行

> ./build/bin/hermes test.js
access foo: foo
call bar: bar

動きました 🎉

ということで、これを使ってevalを置き換えて、無事hermesでも件のライブラリが動くようになりました。

TypeScript meetup #4 Onlineの舞台裏

TL; DR

  • TypeScript meetup #4オンラインをやったよ
  • 心配してたけど、思った以上に盛り上がったよ
  • 知見を紹介するよ

はじめに

ちょっと前になりますが、TSConf JPが中止して以来、初めてのTypeScript meetupを初めてオンラインでやりました。

https://typescript-jp.connpass.com/event/177175/

COVID-19の影響で多くのミートアップ、勉強会がオンラインへと舵を切って、多くの知見が共有され始めています。

今回のミートアップでも、それらの知見が非常に役に立ったので、その一助とすべく知見を書き留めておきます。

構成

Youtube Live + OBS + Discordにした。

図にするとこんな感じ。

f:id:sasurau4:20200708231642p:plain

この構成に辿り着くまでに、検討した組み合わせ

  • Youtube Live + Google Meet + OBS -> 概ね良かったが、画面共有時のMeetのヘッダーが消せたらベストだった
  • Youtube Live + Discord + OBS -> 試してみて、良さそうだったので採用
  • Remo -> 有料だったので見送り
  • Stream Yard -> 無料だと右上にロゴが出る、既にDiscordを試していたので試していない
  • Twitch -> 既にDiscordを試していたので試していない
  • Cluster -> 配信者はスタジオ?に行かないといけないらしいので試していない

当日までの流れ

勉強会の日程を決める前に、オンラインで勉強会を開催するとなったら、どんな風に配信すればいいのかをすずさんと一緒に検証した。

Live配信するためにはYoutubeアカウントから申請して承認されないといけないというミスをやらかすも、代替アカウントでことなきを得た。他にもすずさんに色々教わりながら、無事Youtubeでお試し配信ができ、配信完全に理解した状態になった。

技術検証が終わったところで、いい感じに運営メンバーが集まれそうな日時で6/16 19:00から勉強会をすることにし、connpassで告知した。

登壇者へのdiscord参加してね連絡などをやりつつ、当日の1週間前の6/9に運営メンバーで集まって、検証しつつ、役割を決めた。

当日の役割分担は以下。

  • 司会進行: 1人
  • 配信者: 1人
  • 配信者予備(同じOBSプロファイルで待機): 1人
  • 公式Twittertweetする人: 1人
  • Youtube Liveの確認, Youtube Liveのコメント見る人: 1人
  • タイムキープ, 次の発表者への案内: 1人

計6人と贅沢な布陣だった。 1人あたりの負担が劇的に少なくなるので、初めてであればこれぐらいでいいと思う。 思ったよりもやることが多く、あまり少ないとテンパる気がする。

役割を決めた後に、Youtubeの予約枠を抑えたり、配信担当のメンバーが幕間に流す音楽を見つけたりや差し込む画像を作ってくれて、ぐっと良い感じになった。

Image from Gyazo

当日

当日は19:00本編開始としつつ、18:00に集合し、各登壇者を交えて配信テストをした。

Discordはこんな感じで構えていた。

Image from Gyazo

特に何か問題が起こることもなく、無事に配信テストも終了し18:45から配信開始した。

配信開始時、何か不具合が起こったときにすぐに反応できるようにPC1でDiscordとSlackとTwitterを開きつつ、PC2でYoutube Liveを流していた。事前の役割分担の仕方が原因で公式Twitterアカウントで登壇者の紹介を2重に投稿するミスをしたりした。

Discordの通知が鳴る影響で、運営スタッフ同士の会話はSlack、運営スタッフと登壇者との会話はDiscordという変則的な形でやっていた。

本編はこちらから

ちなみに始まってから終わるまで何か起こるんじゃないかとめちゃくちゃ緊張しながらPC1とPC2を行き来していた。

最後に運営からの話をWebsiteのLocalizationやってるよという話をちょっとさせてもらって、大きな混乱もなく配信終了した。

無事に配信終了したときの様子。

twitter.com

知見

  • 配信者の回線は強ければ強いほど良い、回線の安定が勉強会成功につながる
  • DiscordはVoice Chatに人が入室したとき、Text Chatに何かが投稿されたときに通知が鳴るのでそれが配信にめっちゃ入る
  • Youtubeにタグを入れられるので入れ忘れない
  • 公式Twitterアカウントで発言する人は1人にする
  • 控室は作っても需要がないので必要ない(登壇者もずっとお立ち台Voice Chatにいた)
  • 運営に余裕があると登壇内容を聞ける
  • Youtube Liveで流されてる映像とDiscordのリアルタイムには30秒から1分程度の遅延があるので行き来するとタイムトラベルすることになる

感想

最初に「ぜひリアクションをしてね」ってお願いしていたおかげか、かなりのリアクションがあって安心した。ちなみに同時接続数は最大437人だった。

結構な数、地方からも参加できて嬉しいという声を見かけて、やって良かった。東京に住んでいるとどうしてもこういう需要に気づきづらいので、COVID-19の効果感がある。

配信、意外とやることと考えることがあるので、Youtuberが1人で配信やってるのは本当にすごい。

勉強会についてブログを投稿してもらえるとめっちゃ嬉しい。Twitterの投稿もYoutube Liveのチャットもめっちゃ嬉しい。

qiita.com

ミートアップ実施前に参考にした記事たち

blog.cybozu.io tech-magazine.opt.ne.jp note.com engineering.linecorp.com

他にもリハと実際の環境が違ったので失敗したみたいな記事があったはずだが、思い出せなかった。。。

最後に

Youtubeを見てくれた参加者、登壇者、運営のみなさまありがとうございました。

JavaScriptで3 > a > 1と書いてしまった話

はじめに

仕事でチームメンバーみんなでTypeScriptを書いているのだが、たまにmarkdownの中のJavaScriptをいじることがある。

ある日、ある数字aが3より小さく1より大きいという条件を書く必要があった。

そのときに書いたコードがこれ

if (3 > a > 1) {
  // 省略
}

プログラミングをやっていると、こういうときは 論理演算子で2つの式を結合して3 > a && a > 1 と書くように手癖がつくものだが、小学生のときから習ってきた数学的な記法はふとしたときに顔を出す。

特に、現代のエディターによる便利な補完に慣れきってしまっている状態で一切の補完が効かない状態で油断してJSを書くと上記のようなコードが爆誕する。

今回はこれのお話。

3 > a > 1の返り値

ところで、3 > a > 1の式、実はJSのSyntaxErrorにならないのを知っていただろうか?

私はてっきり、この式は文法的に正しくないとばっかり思っていたので、上記のコードがマージされてバグとして顕在化するまで🤔となっていた。

3 > 2 && 2 > 1;
// > true

3 > 2 > 1;
// > ???

これの答えがパッと出てくるとECMAScriptチョットデキルと言える道が見えてきそう。

答えはこちら

3 > 2 && 2 > 1;
// true

3 > 2 > 1;
// false

解説はこちら

developer.mozilla.org

比較演算子は左結合で評価されるので、 3 > 2trueになって、true > 1が評価される。

trueを数字にすると1なので、1 > 1になって、falseが返り値になる。

https://www.ecma-international.org/ecma-262/#sec-abstract-relational-comparison

この記事を書くにあたってECMA-262を確認したら、こんなのもできるらしい

"hoge" > "hoge"
// false

"hogedayo" > "hoge"
// true

JS、秘孔がたくさんあるので、面白い。

やはり仕様書は正義。ちゃんと読めてるのか不安だが。。。

3 > a > 1を禁止したい

さすがに、3 > a > 1を許容していたらつらいので、linterで止める方向性で解決策を模索した。

とりあえず、markdownの中のJSにlintするのはこいつでいけそうというのをチームメンバーが見つけてくれたので採用。

github.com

そうしたら、「ban-consective-comparison-operator」とか「ban-continous-comparison-operator」とかでeslintのruleがあるだろうと探してみたが、見つからない。

eslint本家のrepositoryのissueも探してみたが話題にすら上がっていない。ついでに、3rd partyのlint ruleも探したがやっぱり見つからない。

世の中の人は3 > a > 1とか書かないらしい。

これは仕方ないので空きを見つけて自作して、チームメンバーの@1natsuが公開しているルール集に突っ込んでおくかという話になった。

github.com

コミュニティに聞いてみた

自作する方向にはなったが、コミュニティに聞いてないなと思い、dicorsdのcommunityのhelpに投げてみた。

eslintのcommunity chat、もともとgitterを使っていたはずだが、いつの間にかdicordに移行していたらしい。

こちらが聞いてみた結果。

gyazo.com

質問を投げて1分でチームメンバーから返答がついてびっくりした。

やはり、調べたとおり直接禁止するルールはないらしいが、no-restricted-syntaxなるruleを使えばいけるらしい。

聞いたことがないruleだったので、ドキュメントを見てみた。

eslint.org

要はASTでqueryができる範囲においてあらゆるruleを代替できる、まさに銀の弾丸

これ見たとき、ASTが分かれば、好きなルール書けるじゃんと結構興奮した。

例では、FunctionExpressionWithStatementin演算子を禁止している。

eslintに限らず、この手のDX系のplugin機構を持つライブラリはuserがなんにでも使える何かを用意せず、そういうことがやりたいならAPIを叩くイメージがあったので目からうろこだった。

もちろん、ASTでqueryができる範囲なのでno-unused-varsとかは代替できない(はず)。

ということでさっそく禁止してみることにした。

禁止してみる

最近のフロントエンドエンジニアは当たり前のようにASTを読んだり書けるらしいので、おもむろにASTExplorerを開く。

知りたいASTはこちら。

astexplorer.net

これで、ASTは分かったので、queryするだけ。

eslintのplugin機構に詳しくなかったり、ASTってなんじゃという人はこちらがおすすめ。日本語も完備されている。

github.com

これをやると完全に「これ進研ゼミでやったやつだ」になれる。

今回はこの辺。

https://github.com/Quramy/eslint-plugin-tutorial/tree/057e4cbc907d0d571996452639a9b5470bf197fa/guide/20_dive_into_ast#build-selector https://eslint.org/docs/developer-guide/selectors

CSSっぽいselectorを書けるらしい。

ここで試せるので、試行錯誤しながら書いていく。

http://estools.github.io/esquery/

出来上がったqueryはこちら

BinaryExpression[operator=/>|>=|<|<=/] > BinaryExpression

連続したBinaryExpressionをすべて禁止すると1 + 1とかまで禁止されてしまうので比較演算子だけに絞っている。

実際に禁止してみたところ🎉

f:id:sasurau4:20200622114438p:plain

ちなみに、eslintのデフォルトがES5であることを知らなくて、しばらくconstでparse errorが発生して🤔になっていた。

いつもは雑にeslint-plugin-standardを入れているので、最新のECMAScriptが使えるということを知った。

github.com

おまけ

TypeScriptではどうなるかというとこうなる。

twitter.com

f:id:sasurau4:20200622115237p:plain

www.typescriptlang.org

結論

やっぱりTypeScriptは最高