You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
mul:=OrId()
add:=OrId()
term:=OrId()
termPlusMul:=AndId().And(&term).And(Plus).And(&mul)
termMinusMul:=AndId().And(&term).And(Minus).And(&mul)
add=add.Or(&termPlusMul).Or(&termMinusMul).Or(&term)
// add = term + mul | term - mul | termtermMulMul:=AndId().And(&term).And(Mul).And(&mul)
termDivMul:=AndId().And(&term).And(Div).And(&mul)
mul=mul.Or(&termMulMul).Or(&termDivMul).Or(&add)
// mul = term * mul | term / mul | addparTerm:=AndId().And(LPar).And(&mul).And(RPar)
term=term.Or(&parTerm).Or(num)
// term = ( term ) | numreturnAndId().And(&mul).And(EOF)
// mul EOF
このリポジトリの概要
なにをやっているか
GolangでCのコンパイラを書いてる
低レイヤを知りたい人のためのCコンパイラ作成入門に触発された
なぜやっているのか
低レイヤーの勉強&Golangを手に馴染ませたい
ここに書かれてる内容
コンパイラを書くにあたって考えていること
壊れにくいコードについて
コードを書いて開発してるとき、ほとんどの時間はバグとの戦いに費やされている(気がする)。
体感的には、5%コードが書いてる時間で、残りの95%がデバッグ。
しかし、最善手を打つと、デバッグにかかる時間を抑えられる=>5%でコードを書く、15%でバグりにくい設計を考える、15%でテストを書く、残りの15%がデバッグ=>トータルで50%の所要時間に!
つまるところ、テストや設計でバグを事前に捻り潰すのが大事。
バグの予防としてこのレポジトリ内で行ったていることをいくつか揚げる:
型による制約
string
などを使わない。アセンブリコードを吐く部分を例に上げて説明する。
アセンブリコードを書く部分を、素朴にやると、↓のような書き方となる。
でも、この書き方は脆弱でぶっ壊れやすい。例えば↓のようになるとただちにバグってしまう:
なので↓のように書けるようにした:
ここで、各メソッドのシグネチャーはそれぞれ以下のようになっている:
code.Ins
は、完成されたインストラクション(Fin
型)しか引数として受け付けないので、不完全なインストラクション、例えばasm.I().Mov().Rax()
をcode.Ins
を突っ込んでも、コンパイラに弾かれる。データ駆動とDSL
データ駆動
データ駆動とは、ロジックを出来るだけ薄くして出来るだけデータを使ってコードを書くことを指す。
データ駆動が良い理由は、機能の追加や変更をするとき、データの修正だけで済むので安全だからである。
一方で機能追加の際にロジックを弄る場合は、ロジックはある種何でもありなので、コードを壊す危険がある。
データ駆動は表現出来ることが抑えられているので事故が起きにくい。
文字列を字句解析するとき、素朴にやると以下のようなコードになる:
新しいトークンのタイプを追加する場合、
if
文を追加していくことになるが、それ以外なんでも追加しようと思えば出来るので危険。例えば微妙に間違った形のif
文とか、あるいはおもむろに無限に終わらないfor
文を回し始めることだって可能。一方でデータ駆動だと↓のような書き方となる。
https://github.com/sxarp/c_compiler_go/blob/master/src/tok/tokenizer.go#L96
var TPlus TokenType = TokenType{literal: "+"}
を追加していく定形作業なので事故が起きる余地が少ない。また、
TokenTypes
から、トークンナイザーを生成する関数tokenize
は、単体テスト可能な点に注意。if
文を追加する場合と違い、トークンタイプを追加するたびにテストを追加しなくても良い(追加しても良い)。ロジックにデータを流し込むことで、処理を構成出来れば、ロジックを薄く出来て単体テストもしやすくなり、結果として壊れにくいコードとなる。
DSL
データ駆動と考え方は同じで、表現力を絞ることにより事故が起きにくくなるようにする。
コード上に、必要な自由度のみが残り、余計なものが入らないので事故が起きにくくなる。
ただし、対象となるドメインの本質的な難しさは無くならない(本質的な難しさに集中できるとも言える。)
このリポジトリの例として、パーサーコンビネーターを使ったパーサーを生成するDSLを紹介する:
パーサーを以下のような感じで定義できる(実際は少し違うが/ノードをASTに追加するかどうかもオプションで取る)
DSLは
And
とOr
の実装に相当する。パースのテスト。
再帰を表現するために、
And
とOr
にはパーサーのポインターを渡すようにしている。このDSLは正しく書かないと、すぐ無限ループに入ったり、トークン列を最後までパースできなくなったりするので、DSL化したことで、問題が簡単になったわけではない(正しい生成規則を見つけるは難しい)。
Fail Fast
ちょっとでもおかしなことが起きてたら、直ちに例外(panic)を投げるようにしている。
これは本番のコードでも同様にやるべきだと考えていて、できるだけ早い段階で分かりやすい例外を吐いてクラッシュさせる方が、テスト時やデプロイ後の早い段階で検知/修正できるので、良いと思っている。
その他の開発ネタ
Golangに特有な話
直和型がない件
パーサーを書く際には型的には以下のような感じにするのが自然:
しかし、Goだと、
Fail | AST
のような型は素朴には定義できない。ワークアラウンドとして、
AST
型で定数としてFail
を定義してお茶をにごした。https://github.com/sxarp/c_compiler_go/blob/master/src/psr/parser.go#L21-L22
これでそんなに困っていはいない。
開発環境
dockerの中に全てを封じ込めている。
git pullし、make startし、make testでテストが回るようになっている。
GOPATHがない環境でも安心。
The text was updated successfully, but these errors were encountered: