Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

DQNEO
October 28, 2019

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

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

DQNEO

October 28, 2019
Tweet

More Decks by DQNEO

Other Decks in Technology

Transcript

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

    View Slide

  2. 自己紹介
    @DQNEO (ドキュネオ)
    ● アメリカ版メルカリを開発
    ● 車輪の再発明が好き
    ○ Git自作
    ○ コンパイラ自作
    ○ JVM自作

    View Slide

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

    View Slide

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

    View Slide

  5. セルフホストとは
    *.go
    ソース
    *.go
    minigo3
    minigo1
    *.go minigo2
    第1世代
    第2世代
    第3世代
    本家Go
    コンパイラ

    View Slide

  6. 自作Goコンパイラ
    でセルフホスト
    デモ

    View Slide

  7. minigoの特徴
    ● フルスクラッチから作った (世界初?)
    ● Lexer, Parser, CodeGenerator 手書き
    ○ (Lex/YACC/LLVMなどを使っていない)
    ● 標準ライブラリも自作
    ● GNU Assemblyを吐く (x86-64 Linux専用)
    ● 実装は約1万行

    View Slide

  8. アーキテクチャ
    minigo
    ソース → アセンブリ → object → 実行バイナリ
    GCC (アセンブラ、リンカ)

    View Slide

  9. minigoにないもの
    ● Garbage collection
    ● go routine
    ● 浮動小数点
    ● マルチプラットフォーム対応 (OS,CPU)

    View Slide

  10. はじめる前の私
    ● コンパイラの知識ゼロ
    ● Goそんなに詳しくない
    ○ Tour of Go 2回挫折
    ○ 仕事で週1触る程度
    ● Goがなかなか上達しない

    View Slide

  11. ● rebuild.fm 第153回
    ○ https://rebuild.fm/153/
    ● rui さんが 8cc (Cコンパイラ) をつくった話
    ● めちゃくちゃ面白い
    きっかけ

    View Slide

  12. Cコンパイラ (8cc)を
    Goに移植してみた

    View Slide

  13. ● コミット履歴の1件目から順番に移植
    ● C のコードを Go に書き換えるだけ
    ● 楽しい✌
    → すぐ壁にぶつかる
    8cc の移植
    https://github.com/rui314/8cc

    View Slide

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

    View Slide

  15. 8cc の移植 → Cで再実装
    コミット履歴を1個ずつ、
    1. テスト追加分を見る (次実装するべき機能を知る)
    2. 自力で、そのテストをパスする実装をCで書く
    まれに成功して大歓喜
    3. 無理だったら写経
    ○ 元コミットを、自分で理解できる最小のコミットに分割
    しながら写経
    ○ 「コミット分割できた」 = 「内容を理解できた」
    とみなす
    これで巨大コミットを攻略できる

    View Slide

  16. 8cc の移植 : C から Go へ
    ● 分割したコミットを C → Go に移植
    ● 労力的には
    ○ 「 Cで自力で再実装 / 写経 」が 90%
    ○ Go への移植は 10%
    ● これを5ヶ月半継続
    ● 基本文法はほとんどカバー

    View Slide

  17. 8cc の移植 : CとGoを同時に学ぶ
    ● 比較しながら学べる
    static char *REGS[] = {"rdi", "rsi", "rdx", "rcx", "r8", "r9"};
    var REGS = []string{"rdi", "rsi", "rdx","rcx", "r8", "r9"}
    C
    Go

    View Slide

  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
    ● 比較しながら学べる

    View Slide

  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
    ● 比較しながら学べる

    View Slide

  20. 8cc の移植 : 学んだこと
    ● コンパイラの作り方
    ○ 体で覚えた
    ● C言語が動く仕組み
    ● アセンブリの読み書き
    ● C と Go は驚くほど共通点が多い
    → 同じ方法で Goコンパイラも作れるのでは?

    View Slide

  21. Goコンパイラ書いてみた

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  26. ● 言語仕様が意図的にそうなっている
    ● 行頭から一単語読んだだけでモードが決定できる
    ○ ”type” ,“var”, “func”など
    ● 型は左から右に読めばOK ( []*int )
    ● 歴史的紆余曲折が少ない
    Go言語はパースしやすい

    View Slide

  27. パーサ実装で工夫したこと
    ● 関数呼び出し履歴を可視化
    ● デバグが容易に

    View Slide

  28. ● 代入文は式ではない x = 1
    ● ++, --も式ではない x++
    ● iotaの挙動
    ● 識別子解決の仕組み、universe block の役割
    ● defer の関数呼び出しの引数の扱い
    コンパイラを書くことで仕様を知る

    View Slide

  29. ● 3ヶ月目 map, append, interfaceを実装
    ● 4ヶ月目 第二世代コンパイラでセグフォ地獄
    ● 5ヶ月目 第二世代コンパイラでセグフォ地獄
    後半はめっちゃつらい

    View Slide

  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;iz[i] = x[i]
    }
    }
    z[xlen] = elm
    return z
    }
    書籍『プログラミング言語Go』の
    appendのコードを拝借したら動いた

    View Slide

  31. 自作map
    『Goのmapとheapを自作してみた』mercari.go #6
    https://speakerdeck.com/dqneo/how-to-create-your-own-map-and-heap-in-
    go
    ● アセンブリをめっちゃ書いた

    View Slide

  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
    }

    View Slide

  33. 自作interface
    ● convert時にオリジナル型を文字列化してメモリに保存
    ○ 例: “*G_NAMED(main.Token)”
    ● type switch / type assertion 時に取り出して文字列比較
    ● メソッド呼び出しは、内部的にメソッドテーブルからmap get

    View Slide

  34. 第2世代コンパイラがバグだらけ
    *.go
    ソース
    *.go minigo1
    *.go minigo2
    第1世代:
    普通のGoプログラム
    第2世代:
    20万行のアセンブリ
    (バグだらけ)
    本家Go
    コンパイラ

    View Slide

  35. 毎日 gdb でセグフォと戦う

    View Slide

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

    View Slide

  37. おもしろバグ: panic
    ● なんか第2世代コンパイラが暴走する...
    ○ panic() で止まってないっぽい
    ● func panic の中身が空だった
    ● func panic を実装した
    ○ Segmentation Fault
    ○ バグってる...
    ○ panic() で落ちる...
    ○ ん? それでいいのでは  → 放置

    View Slide

  38. 本家Goコンパイラを読んでみた

    View Slide

  39. 本家Goコンパイラを読んでみた
    ● セルフホスト達成後、Go本体を端から読んでみた
    ● ところどころ読める
    ○ 自作コンパイラと似てるけど異なる箇所が面白い
    ● 自作で苦労した箇所が、本家ではどうなってるの?
    ○ 数学の試験終了後に答え合わせする感覚

    View Slide

  40. ● 各種データ型(slice,map等)のメモリサイズやレイアウトが気
    になったので見てみた
    本家Goコンパイラを読んでみた
    src/cmd/compile/internal/gc/align.go
    src/cmd/compile/internal/gc/go.go
    あたり

    View Slide

  41. case TMAP: // implemented as pointer
    w = int64(Widthptr)
    「 map のサイズは pointer のサイズと同じ 」
    わかる〜〜 そうだよね〜〜
    (自分のと同じ)
    本家Go: mapのサイズ

    View Slide

  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)

    View Slide

  43. 本家Go: sliceの大きさ
    「sliceの大きさはsizeof_Array」
    えっ?
    case TSLICE:
    if t.Elem() == nil {
    break
    }
    w = int64(sizeof_Array)

    View Slide

  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 と呼んでいる?
    命名が紛らわしいのでは? (歴史的事情?)

    View Slide

  45. パッチ投げてみた
    ● だめもと
    ● 「郷に入れば」の精神でGerritに挑戦

    View Slide

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

    View Slide

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

    View Slide

  48. おまけ

    View Slide

  49. アセンブリの学びかた
    ● ググる
    ● Qiitaの記事
    ● 8cc/gcc に簡単なCのコードを食わせて、アセンブリ出力
    を読む
    ● 慣れてきたら公式マニュアル (GAS, Intel CPU)
    ● 本は読んでない
    ○ 自分に合うもの (X86-64 かつ GAS かつ Linux) が
    なかった

    View Slide

  50. Intel® 64 and IA-32 Architectures Software Developer’s Manual
    Intel CPUマニュアルは役に立つ
    例:「複数戻り値」の実現方法が書いてある

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  54. 参考資料
    Goコンパイラをゼロから作って147日で
    セルフホストを達成するまで
    ● Togetter (進捗日記)
    https://togetter.com/li/1357678
    ● Qiita (まとめ)
    https://qiita.com/DQNEO/items/2efaec18772a1ae
    3c198

    View Slide

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

    View Slide

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

    View Slide