TT Lab
はじめる
学ぶ 学習パス コース

コンパイラ — 小さな言語を最初から最後まで作る

翻訳するプログラムと実行するプログラム

TT Labで続きを見る

一言でいうと

コンパイラーは、プログラムを実行せずに別の言語のプログラムに移すプログラムで、インタプリターは、プログラムを読みながらすぐに実行するプログラムです。2つは同じ前段(文字をトークンに、トークンを木に、木の意味を検査)を共有し、最後の「今計算するのか、あとで計算するコードを残すのか」で分かれます。

なぜ必要なのか

CPUはx = (a + 1) * 2を読めません。読めるのは、add、imulのような命令とレジスター番号だけです。人が書きやすい文章と、機械が実行できる命令の間の距離を、誰かが埋める必要がありますが、埋める時点が2つあります。

実務では、この2つは混ざっています。Pythonは、ソースをバイトコードにコンパイルしてから(__pycache__の.pyc)、そのバイトコードをインタプリットします。Javaは、javacがバイトコードを作り、JVMが最初はインタプリットして、よく動くメソッドだけを機械語にコンパイルします(JIT)。そのため、「この言語はコンパイル言語か」より「どの段階をいつ行うのか」のほうが、正確な問いです。

どう動くのか

コンパイラーは1つの塊ではなく、段階の列です。前の段階の出力が次の段階の入力になり、このコースのモジュールも、その列をそのままたどります。

소스 글자 ──렉서──▶ 토큰 ──파서──▶ 트리(AST) ──의미 분석──▶ 검사된 트리
   (2모듈)            (3모듈)              (4모듈)
                                              │
            ┌─────────────────────────────────┼──────────────────────────┐
            ▼                                 ▼                          ▼
     트리를 걸으며 실행               바이트코드 + 스택 VM        중간 표현 → 최적화 → x86-64
        (5모듈)                            (6모듈)                  (7·8·9모듈)

gccも、同じ列をいくつかのプログラムに分けて実行します。普段はgcc hello.cの1行に隠れて見えませんが、オプションで段階ごとに止められます。

止めるオプション 行ったこと 出力
-E プリプロセス: #includeを展開し、#defineを置き換えます .i(まだC)
-S コンパイル: Cをアセンブリにします .s(テキスト)
-c アセンブル: アセンブリを機械語にします .o(再配置可能オブジェクトファイル)
(なし) リンク: オブジェクトファイルとライブラリをつなぎ合わせます 実行ファイル

オブジェクトファイルには、まだアドレスが決まっていない名前が残ります。hello.oが呼ぶprintfはlibcにあるので、nm hello.oはその名前の前にU(undefined)を出力します。リンカーがその空欄を埋めます。リンク段階でだけ出るエラー(「undefined reference to …」)が別にある理由が、これです。コンパイルはファイル1つだけを見て、リンクはすべてを見ます。

インタプリターとコンパイラーが同じ意味を守っているかも、別に確認する必要があります。このラボの計算機は、除算をCのように0の方向へ切り捨てます(-7 / 2は-3)。Pythonの//は下へ切り下げるので(-4)、インタプリターをPythonで書きながら//を使うと、同じプログラムが、インタプリターとコンパイルした実行ファイルで違う答えを出します。2つの実装の意味が同じかどうかは、自然には守られず、実行して突き合わせてはじめてわかります。このコースの採点ツールが、ほとんどのステップで、基準実装とランダムな入力で突き合わせる理由も同じです。

現場での姿

このコースを通して作る言語は「ミニ」です。64ビット整数と真偽値、let・print・if・while・fn・return、そして優先順位のある演算子20個ほどがすべてです。小さいですが、レキサーからx86-64コードまで1本につなげて見るには十分で、すべての段階を、採点ツールが実際に実行して確かめられます。

次のラボですること

/opt/fixtures/mini/c/hello.cをgccで段階ごとに止めて4つの出力ファイルを作り、nmでオブジェクトファイルの空欄を見ます。そのあと、RPN計算機をrpn.py1つに、2通りで作ります。トークンに切って、スタックですぐに計算するインタプリター、実行せずにスタックの深さだけをたどるチェッカー、同じ計算をするCを書いてgccで焼くコンパイラーです。最後に、2つの道が同じ答えを出すかを、ランダムなプログラムで突き合わせ、どちらがいつ速いかを測ります。