Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Goコンパイラ書いてみた

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

自作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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

自作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 }

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

毎日 gdb でセグフォと戦う

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

本家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)

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

おまけ

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

本家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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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