TypeScriptで作るコンパイラ入門
本を書きました。
本の内容や過程、技術的なポイントについて触れる記事です。
コンパイラについての知見もそうですが、「技術書をAmazonに入稿する」周りの知見も技術書を出したい方には参考になると思います。
本の概要
コンパイラに興味はあるけれど、「敷居が高そう」「C言語に馴染みが薄くてサンプルコードを読むのが大変」「アセンブリが出てきて分からなくなった」 ─そんな方のために書いた技術書です。
コンパイラ作成って興味がある方は多いと思うのですが、敷居が高いイメージあると思うんですよね。なので、可能な限り理論的な説明を噛み砕いて、プログラミング言語もTypeScriptという比較的幅広い人が扱える言語だけで完結するようにコンパイラの本を書きました。
対象はこんな方です。
- 普段はTypeScriptやJavaScriptを書いているが、低レイヤや言語処理系に興味がある
- いきなり理論書を読んで挫折した経験がある
- 手を動かしながら、コンパイラの動作を直感的に理解したい
本書では、独自言語 OtterJS を作り、そのソースコードを仮想マシンで可視化しながらコンパイル・実行する仕組みを解説します。
アセンブリやC言語の知識は不要。
TypeScriptの構文と基本的なプログラミング経験があれば、誰でもコンパイラ作りを体験できます。
本書を作ったきっかけ
この本は、「もし自分がコンパイラを初めて学ぶとき、最初に手に取る本がこれだったら良かった」と思える本を目指して作りました。
私が最初に読んだのは、中田育男 (1995) 『コンパイラ (新コンピュータサイエンス講座)』 オーム社です。
当時、コンピュータサイエンス関連の学部を出ていない文系出身の私は、どこから学び始めればいいか分からず、とりあえずそれらしい本を選びました。
この本は比較的薄い方ですが、説明が抽象的で読解力を求められ、扱っている言語の構文も現代的ではなく、前提となる挙動が分からないことが多くありました。
掲載されているプログラム例
program p;
var a, b, c;
procedure q;
var b, d;
procedure r;
var a, d, e;
begin
a := c+b;
call q;
end;
begin
call r;
c := b;
end;
begin
a := b+c;
call q;
end;
(このコード、階層どうなってるの?? begin, end? call??)
実装コードもC言語で書かれており、グローバル変数を直接書き換える処理が多かったため(コード例)、TypeScriptに慣れていた自分には追いかけるのが難しく、理解に時間がかかりました。
それでも読み切り、自分でも実装できるようになりましたが、なかなかの苦労でした。
本書制作までの経緯
この経験から、私は4年ほど前に「初心者向けのコンパイラ本を書きたい」と考えるようになりました。
理論的に体系立てられた素晴らしい本は数多くありますが、いざ自分で実装しようとすると、理論と実践の間に大きなギャップを感じます。
正確に言えば、「ギャップがある」というよりも、最初に簡単なコンパイラを作るには、抽象的すぎるアプローチは少し大げさすぎるのです。
特に戸惑ったのは、初めて読んだ本でLALR(1) やLL(1) など複数の構文解析手法が次々と登場し、それぞれの長所・短所や構文解析表の生成方法まで深掘りされていたことです。
確かに重要な知識ではありますが、「今まさに小さなコンパイラを動かしてみたい」段階の自分にとっては、頭に入る前に手が止まってしまいました。
- LALR(1)やLL(1)など、複数の解析手法が詳細に比較されている
- 文法規則や解析表の導出方法が本格的に解説されている
- しかし、最初に実装できる範囲はごくわずか
こうした高度な説明は、基礎が固まっていない段階では理解が進みにくいと感じました。
そこで、まずは最低限の実装に集中できる本から入る方が、よりスムーズに学べると判断しました。
(もちろん、理論の理解は非常に重要です。あくまで順番の問題です。)
本書の内容
本書は、TypeScriptさえ読めればコンパイラに入門できることを目指した解説書です。
アセンブリの知識も不要。従来のコンパイラ本でよく求められるC言語の知識も一切必要ありません。
事前知識はできる限り不要にし、TypeScriptの構文を理解していれば読み進められる構成にしました。
コンピューターサイエンスの理論は、必要最低限に噛み砕いて解説しています。
さらに、後述する OtterJS Viewer というツールを用意し、コンパイラの動作を視覚的に理解できるようにしています。
https://otterjscompiler.web.app/
紹介している言語
本書で扱う言語は、ほぼ JavaScript の部分集合のようなものです。
名前は OtterJS。最近カワウソ(Otter)がマイブームなので、この名前にしました。
// 関数宣言
function add(a, b) {
return a + b;
}
// 組み込み関数 print
print(add(1, 2));
// 再帰関数
function fibonacci(n) {
if (n < 2) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
for (let i = 0; i < 10; i++) {
let result = fibonacchi(i);
print(result);
}
最低限、条件分岐・ループ・関数定義・再帰関数などが扱えます。
技術的な工夫
OtterJS Viewer
今回こだわったポイントのひとつは、仮想マシンの状態を可視化するツールを作ったことです。
https://otterjscompiler.web.app/
Step ボタンで 1 行ずつコードを実行できる仕組みになっています。
これは Viewer 専用に、全く新しいコンパイラをコピペベースで作ることで実現しました。
本体のコンパイラにステップ実行機能を組み込むこともできましたが、余計なコードが混ざって学習のノイズになるのを避けています。
実装のために、本来 private にするプロパティを public に変更するなど、Viewer 専用の変更も行っています。仕組み自体は特別なものではなく、力技に近いです。
UI デザインは Angular を使用し、gemini CLI に任せてほぼ自動生成しました。
パースの可視化
パース結果を直感的に分かる形で表示するための実装は、地味に手間がかかりました。
for (let i = 0; i < 10; i++) { { 1 + 1 * 2 && 4 / 2 + 3 } }
これをパースすると、下図のように構造化されて表示されます。
実装方法としては、構文要素すべてに toString() を実装し、ブロック要素など階層を持つ要素では子要素の出力にインデントを付与します。
具体的には、toString().split("\n").map(line => " " + line) のような処理を再帰的に適用しています。
export class BlockStatement implements Node {
constructor(public statements: Node[]) {}
toString(): string {
const INDENT = ' ';
let str = '{\n';
str += this.statements
.flatMap((stmt) => stmt.toString().split('\n'))
.map((line) => INDENT + line + '\n') // 全行にインデント付与
.join('');
str += '}';
return str;
}
}
このように、Program.toString() を呼び出すと、全ノードの toString() が再帰的に呼ばれ、コードの文字列化が完成します。
ブロックスコープと return の扱い
今回特に苦戦したのは、ブロックからの return です。
function f() {
let a = 10;
{
{
return 1;
}
}
}
当初、このような「関数トップレベル以外のブロックからの return」でバグが発生しました。
一度専用のバイトコードを作ることで解消しましたが、専用のバイトコードで学習内容が本質的じゃない部分で膨らんでしまうという問題がありました。
最終的に採用したのは、{} ブロックを内部的に関数とみなし、return を再帰的に伝搬させる方法です。
function f() {
let a = 10;
// 階層1
// 隠れた return 文1(関数から抜ける)
{
// 階層2
// 隠れた return 文2(return 文1にジャンプ)
{
// 階層3
return 1; // return 文2 にジャンプ
}
}
}
ブロックの開始位置に「隠れた return 文」を仕込み、ブロックから return すると、関数を抜けるまで全ブロックの return を再帰的に呼び出します。
この方法で、1 階層分の return 処理だけで全てのケースを扱えるようになりました。
技術書を書くためのツール
Amazonで出品するにあたり、以下のツールを活用しました。
技術書をAmazonで出すノウハウが十分にWeb上で見つかるかというとそうでもないので、やったプロセスを参考に共有します。
1. Figma
本書で使用する図は Figma で作成しました。特に Figma Jam は、図形と矢印を簡単に作れるので便利です。
2. Canva
背表紙は Canva を利用しました。mm 単位でサイズ指定できるため、印刷時の寸法ズレを防げます。
当初は Figma で作っていましたが、PDF 化の際に寸法がずれ、作り直しになりました。0.5mmの違いでも許容されなかったので正確な寸法を取れるツールを使う必要があります。(判定にAIを使っているのか、たまにOKだったりたまにNGだったりする。)
3. HTML からの PDF 化
本書は全て HTML で執筆しています。CSS でページサイズやページ番号を指定できるのが利点です。
サイズを正確にmmで指定しても、裁断幅や自動で付与されるパディングなどでずれが生じてしまうので入稿前に必ずプレビュー確認が必要です。
また、実際に入稿された原稿を印刷した執筆者用のサンプルを注文できるのですが、ウェブ上での印象と本での印象は大分異なるので注意が必要です。
Web上見やすいいい感じのフォントは紙面だとダサかったり、難しいです。
微調整するたびにページのインデックスがずれたりページ区切り位置が微妙になったりするので、序盤に実際の入稿で文字サイズや余白のパターンを調整するのをおすすめします。
ページ番号とサイズ指定の例
@media print {
@page {
size: 151mm 216mm;
padding: 10mm 0;
@bottom-center {
margin-top: -5rem;
content: counter(page);
font-size: 10pt;
}
}
}
ページ区切りの例
.pdf-break {
page-break-after: always;
break-after: page;
}
入稿用 HTML
コンテンツはすべて HTML で管理しています。CSS でコードの配色や画像サイズを一括管理できるため、校正や統一感の維持が容易です。
ただし、< や > を < や > に置き換える必要があり、執筆作業はやや煩雑です。
ワードや直接 HTML 入稿も可能ですが、それぞれ一長一短があります。
特に HTML 執筆の知見はネット上でも少なく、試行錯誤が必要かもしれないです。
おわりに
「コンパイラ本を書く」は長年の目標でした。個人で書き切るのは骨が折れますが、形にできたことを嬉しく思います。
この本が、難しく見えがちなトピックを身近で楽しいものに感じさせ、実際に手を動かす人を増やす一助になれば幸いです。
Youtubeチャンネルでも紹介しました。よかったらみてください。
https://www.youtube.com/watch?v=RnWWoH0MEMU






