Goコンパイラをゼロから作ってセルフホスト達成するまで / How I wrote a self hosted Go compiler from scratch

7b606c5039f083d13e2d2320ce6ddcfa?s=47 DQNEO
October 28, 2019

Goコンパイラをゼロから作ってセルフホスト達成するまで / How I wrote a self hosted Go compiler from scratch

Go Conference Tokyo 2019 Autumnでの発表資料です。

7b606c5039f083d13e2d2320ce6ddcfa?s=128

DQNEO

October 28, 2019
Tweet

Transcript

  1. Goコンパイラをゼロから作って セルフホスト達成するまで @DQNEO (ドキュネオ) Go Conference Tokyo 2019.10.28

  2. 自己紹介 @DQNEO (ドキュネオ) • アメリカ版メルカリを開発 • 車輪の再発明が好き ◦ Git自作 ◦

    コンパイラ自作 ◦ JVM自作
  3. コンパイラ自作 8cc.go https://github.com/DQNEO/8cc.go Goで書いたCコンパイラ minigo https://github.com/DQNEO/minigo Goで書いたGoコンパイラ

  4. 目次 • セルフホストのデモ • Cコンパイラ (8cc) をGoに移植した話 • Goコンパイラを自作した話 •

    Go本家にコントリビュートした話
  5. セルフホストとは *.go ソース *.go minigo3 minigo1 *.go minigo2 第1世代 第2世代

    第3世代 本家Go コンパイラ
  6. 自作Goコンパイラ でセルフホスト デモ

  7. minigoの特徴 • フルスクラッチから作った (世界初?) • Lexer, Parser, CodeGenerator 手書き ◦

    (Lex/YACC/LLVMなどを使っていない) • 標準ライブラリも自作 • GNU Assemblyを吐く (x86-64 Linux専用) • 実装は約1万行
  8. アーキテクチャ minigo ソース → アセンブリ → object → 実行バイナリ GCC

    (アセンブラ、リンカ)
  9. minigoにないもの • Garbage collection • go routine • 浮動小数点 •

    マルチプラットフォーム対応 (OS,CPU)
  10. はじめる前の私 • コンパイラの知識ゼロ • Goそんなに詳しくない ◦ Tour of Go 2回挫折

    ◦ 仕事で週1触る程度 • Goがなかなか上達しない
  11. • rebuild.fm 第153回 ◦ https://rebuild.fm/153/ • rui さんが 8cc (Cコンパイラ)

    をつくった話 • めちゃくちゃ面白い きっかけ
  12. Cコンパイラ (8cc)を Goに移植してみた

  13. • コミット履歴の1件目から順番に移植 • C のコードを Go に書き換えるだけ • 楽しい✌ →

    すぐ壁にぶつかる 8cc の移植 https://github.com/rui314/8cc
  14. 8cc の移植 : 巨大コミットの壁 • ときどき1コミットがでかい (500行超) • 一気にGoに移植するのは困難 (そもそも内容が理解できない)

  15. 8cc の移植 → Cで再実装 コミット履歴を1個ずつ、 1. テスト追加分を見る (次実装するべき機能を知る) 2. 自力で、そのテストをパスする実装をCで書く

    まれに成功して大歓喜 3. 無理だったら写経 ◦ 元コミットを、自分で理解できる最小のコミットに分割 しながら写経 ◦ 「コミット分割できた」 = 「内容を理解できた」 とみなす これで巨大コミットを攻略できる
  16. 8cc の移植 : C から Go へ • 分割したコミットを C

    → Go に移植 • 労力的には ◦ 「 Cで自力で再実装 / 写経 」が 90% ◦ Go への移植は 10% • これを5ヶ月半継続 • 基本文法はほとんどカバー
  17. 8cc の移植 : CとGoを同時に学ぶ • 比較しながら学べる static char *REGS[] =

    {"rdi", "rsi", "rdx", "rcx", "r8", "r9"}; var REGS = []string{"rdi", "rsi", "rdx","rcx", "r8", "r9"} C Go
  18. 8cc の移植 : CとGoを同時に学ぶ for (;;) { int c =

    get(); if (c == EOF) return; if (c == ' ' || c == '\t') continue; for { c, err := get() if err != nil { return } if c == ' ' || c == '\t' { continue } C Go • 比較しながら学べる
  19. 8cc の移植 : CとGoを同時に学ぶ static Ast *ast_uop(int type, Ctype *ctype,

    Ast *operand) { Ast *r = malloc(sizeof(Ast)); r->type = type; r->ctype = ctype; r->operand = operand; return r; } func ast_uop(typ int, ctype *Ctype, operand *Ast) *Ast { r := &Ast{} r.typ = typ r.ctype = ctype r.operand = operand return r } C Go • 比較しながら学べる
  20. 8cc の移植 : 学んだこと • コンパイラの作り方 ◦ 体で覚えた • C言語が動く仕組み

    • アセンブリの読み書き • C と Go は驚くほど共通点が多い → 同じ方法で Goコンパイラも作れるのでは?
  21. Goコンパイラ書いてみた

  22. Goコンパイラ書いてみた • 1コミット目 .globl main main: movl $0, %eax ret

  23. Goコンパイラ書いてみた • 1日目 足し算が動いた

  24. Goコンパイラ書いてみた • 2日目 関数呼び出しが動いた

  25. • 1ヶ月 FizzBuzzが動いた • 2ヶ月 自分自身をParseできるようになった 前半はさくさく進捗

  26. • 言語仕様が意図的にそうなっている • 行頭から一単語読んだだけでモードが決定できる ◦ ”type” ,“var”, “func”など • 型は左から右に読めばOK

    ( []*int ) • 歴史的紆余曲折が少ない Go言語はパースしやすい
  27. パーサ実装で工夫したこと • 関数呼び出し履歴を可視化 • デバグが容易に

  28. • 代入文は式ではない x = 1 • ++, --も式ではない x++ •

    iotaの挙動 • 識別子解決の仕組み、universe block の役割 • defer の関数呼び出しの引数の扱い コンパイラを書くことで仕様を知る
  29. • 3ヶ月目 map, append, interfaceを実装 • 4ヶ月目 第二世代コンパイラでセグフォ地獄 • 5ヶ月目 第二世代コンパイラでセグフォ地獄 後半はめっちゃつらい

  30. 自作append func append1(x []byte, elm byte) []byte { var z

    []byte xlen := len(x) zlen := xlen + 1 if cap(x) >= zlen { z = x[:zlen] } else { var newcap int if xlen == 0 { newcap = 1 } else { newcap = xlen * 2 } z = makeSlice(zlen, newcap, 1) for i:=0;i<xlen;i++ { z[i] = x[i] } } z[xlen] = elm return z } 書籍『プログラミング言語Go』の appendのコードを拝借したら動いた
  31. 自作map 『Goのmapとheapを自作してみた』mercari.go #6 https://speakerdeck.com/dqneo/how-to-create-your-own-map-and-heap-in- go • アセンブリをめっちゃ書いた

  32. 自作malloc • 静的領域を最初にガバっと確保 • malloc()呼ぶ度に切り取って使う var heap [640485760]byte var heapTail

    *int func malloc(size int) *int { if heapTail+ size > len(heap) + heap { panic("malloc failed") } r := heapTail heapTail += size return r }
  33. 自作interface • convert時にオリジナル型を文字列化してメモリに保存 ◦ 例: “*G_NAMED(main.Token)” • type switch /

    type assertion 時に取り出して文字列比較 • メソッド呼び出しは、内部的にメソッドテーブルからmap get
  34. 第2世代コンパイラがバグだらけ *.go ソース *.go minigo1 *.go minigo2 第1世代: 普通のGoプログラム 第2世代:

    20万行のアセンブリ (バグだらけ) 本家Go コンパイラ
  35. 毎日 gdb でセグフォと戦う

  36. • 6ヶ月目 セルフホスト達成 最後は気合と根性

  37. おもしろバグ: panic • なんか第2世代コンパイラが暴走する... ◦ panic() で止まってないっぽい • func panic

    の中身が空だった • func panic を実装した ◦ Segmentation Fault ◦ バグってる... ◦ panic() で落ちる... ◦ ん? それでいいのでは  → 放置
  38. 本家Goコンパイラを読んでみた

  39. 本家Goコンパイラを読んでみた • セルフホスト達成後、Go本体を端から読んでみた • ところどころ読める ◦ 自作コンパイラと似てるけど異なる箇所が面白い • 自作で苦労した箇所が、本家ではどうなってるの? ◦

    数学の試験終了後に答え合わせする感覚
  40. • 各種データ型(slice,map等)のメモリサイズやレイアウトが気 になったので見てみた 本家Goコンパイラを読んでみた src/cmd/compile/internal/gc/align.go src/cmd/compile/internal/gc/go.go あたり

  41. case TMAP: // implemented as pointer w = int64(Widthptr) 「

    map のサイズは pointer のサイズと同じ 」 わかる〜〜 そうだよね〜〜 (自分のと同じ) 本家Go: mapのサイズ
  42. 本家Go: stringの実体 「 ポインタと文字列長の組 」 そうだったのか〜〜 (自分のとちょっと違う) // note this

    is the runtime representation // of the compilers strings. // // typedef struct // { // uchar array[8]; // pointer to data // uchar nel[4]; // number of elements // } String; var sizeof_String int // runtime sizeof(String)
  43. 本家Go: sliceの大きさ 「sliceの大きさはsizeof_Array」 えっ? case TSLICE: if t.Elem() == nil

    { break } w = int64(sizeof_Array)
  44. 本家Go: sliceの実体 // note this is the runtime representation //

    of the compilers arrays. // // typedef struct // { // uchar array[8]; // pointer to data // uchar nel[4]; // number of elements // uchar cap[4]; // allocated number of elements // } Array; var array_array int // runtime offsetof(Array,array) - same for String var array_nel int // runtime offsetof(Array,nel) - same for String var array_cap int // runtime offsetof(Array,cap) var sizeof_Array int // runtime sizeof(Array) slice のことを array と呼んでいる? 命名が紛らわしいのでは? (歴史的事情?)
  45. パッチ投げてみた • だめもと • 「郷に入れば」の精神でGerritに挑戦

  46. あっさり承認 • 現在マージ待ち • Go 1.4 に取り込まれる?

  47. まとめ • Goコンパイラを自作しました • 何かを理解するには、自作してみるのがよい • やってみれば意外とできる ◦ 時間をかければなんとかなる •

    Go本体にコントリビュートしました
  48. おまけ

  49. アセンブリの学びかた • ググる • Qiitaの記事 • 8cc/gcc に簡単なCのコードを食わせて、アセンブリ出力 を読む •

    慣れてきたら公式マニュアル (GAS, Intel CPU) • 本は読んでない ◦ 自分に合うもの (X86-64 かつ GAS かつ Linux) が なかった
  50. Intel® 64 and IA-32 Architectures Software Developer’s Manual Intel CPUマニュアルは役に立つ

    例:「複数戻り値」の実現方法が書いてある
  51. minigo func sum(a int, b int) int { return a

    + b } main.sum: push %rbp mov %rsp, %rbp push %rdi push %rsi mov -8(%rbp), %rax push %rax mov -16(%rbp), %rax mov %rax, %rcx pop %rax add %rcx, %rax leave ret ソースコード GNU assembler
  52. 8cc int sum(int a, int b) { return a +

    b; } sum: push %rbp mov %rsp, %rbp push %rdi push %rsi mov -8(%rbp), %rax mov -16(%rbp), %rcx add %rcx, %rax leave ret C言語 GNU assembler
  53. 本家Go func sum(a int, b int) int { return a

    + b } TEXT sum(SB), $0-24 MOVQ b+16(FP), AX MOVQ a+8(FP), CX ADDQ CX, AX MOVQ AX, 24(FP) RET ソースコード Plan9 assembler
  54. 参考資料 Goコンパイラをゼロから作って147日で セルフホストを達成するまで • Togetter (進捗日記) https://togetter.com/li/1357678 • Qiita (まとめ)

    https://qiita.com/DQNEO/items/2efaec18772a1ae 3c198
  55. 参考資料 (過去の発表) コンパイラ作りの魅力を語る https://speakerdeck.com/dqneo/making-compilers-is-fun コンパイラを作ってみよう (ライブコーディング) https://builderscon.io/builderscon/tokyo/2019/session/e6ed1194-9a40-4a8c-92fb -b60f39cd18dd

  56. ご清聴 ありがとう ございました