Nand to Tetrisとは何か?NANDゲートからテトリスまでコンピュータを自作する壮大なプロジェクト

目次

Nand to Tetrisとは何か?NANDゲートからテトリスまでコンピュータを自作する壮大なプロジェクト

Nand to Tetris(ナンド・トゥ・テトリス)とは、たった1つの基本論理ゲートであるNANDゲートからスタートし、最終的にはテトリスのようなゲームが動作する完全なコンピュータシステムを自作するという壮大な教育プロジェクトです。電子回路の最も基本的な部品から出発し、約12週間(3ヶ月)かけてコンピュータを作り上げます。単なる理論学習に留まらず、実際に動くシステムをゼロから構築することで、コンピュータの仕組みを実践的に学べる点が特徴です。

プロジェクト名の「Nand to Tetris」の“to”には「から〜まで」という意味が込められており、「NANDからTetrisまで」の旅路そのものがコンセプトとなっています。つまり、一番下の論理ゲート層から一番上のアプリケーション層(ゲームのテトリス)まで、コンピュータのあらゆるレイヤーを網羅的に構築・体験することを目指したプロジェクトなのです。実際、本プロジェクトの扱う範囲はハードウェア設計、コンピュータアーキテクチャ、機械語・アセンブリ言語、仮想マシン、コンパイラ、高水準言語、そして簡単なオペレーティングシステムにまで及びます。

このプロジェクトはイスラエルのコンピュータ科学者であるNoam Nisan氏とShimon Schocken氏によって提唱されました。彼らは2005年に原著『The Elements of Computing Systems』を出版し、この内容をCoursera上のオンライン講座として公開したところ、世界中で人気を博しました。日本語版は『コンピュータシステムの理論と実装』という題名で2015年に初版が出版され、2024年には改訂第2版も刊行されています。書籍とオンライン講座の二形態で提供されており、独学でも学びやすいよう教材やシミュレータも整備されています。

他の学習リソースと比べたNand to Tetrisプロジェクトならではの特徴として、コンピュータの「全体像」を自分の手で作りながら把握できる点が挙げられます。他の教材がハードウェアかソフトウェアの一部に焦点を当てるのに対し、本プロジェクトでは文字通り氷山の“海面下”まで含めて一通り体験できます。各段階ごとに習得すべき概念が明確に整理され、前の段階で作成した部品は「ブラックボックス」として次の段階で再利用できるよう設計されています。この段階的アプローチにより、初心者でも一歩一歩着実に進められ、途中で挫折しにくい工夫がなされています。また理論と実践のバランスも絶妙で、単に座学で学ぶのではなく必ず「動くもの」を作って試せるため、理解が深まりやすいのも利点です。

現代のエンジニアがNand to Tetrisに取り組む価値は非常に大きいです。ハードウェアからソフトウェアまで一貫して構築する過程を通じて、普段ソフトウェア開発だけでは意識しない低レイヤーの動作原理まで理解できるようになります。コンピュータシステムの全容を俯瞰できる「システム思考」が養われ、これによって日常の開発においてもより深い洞察が得られるでしょう。AI時代においても基盤技術への理解が強みになると言われる中、自らゼロからコンピュータを作り上げた経験は大きな自信と財産になるはずです。

Nand to Tetrisの構成と章立て: ハードウェア編・ソフトウェア編にまたがる全12章の全体像

Nand to Tetrisの教材(書籍および講座)は全12章で構成されており、大きく前半のハードウェア編(第1章〜第5章)と後半のソフトウェア編(第6章〜第12章)に分かれています。各章ごとに学ぶテーマが設定され、低レベルから高レベルへと段階的に積み上げながらコンピュータを完成させていく流れです。前半ではNANDゲートから出発してCPUやメモリなどのハードウェアを構築し、後半ではその上で動くソフトウェア(アセンブラやコンパイラ、簡易OS)を実装していきます。

まずハードウェア編の各章では、論理回路から始まりCPUとメモリに至るまで、コンピュータの物理的基盤を作ります。第1章ではブール論理に基づく基本的な論理ゲートを扱い、NANDゲートだけを使ってANDやORといったゲートを構成します。第2章ではブール算術として、半加算器・全加算器などの回路を作り、これを組み合わせて簡単な演算装置(ALU)の設計まで踏み込みます。第3章ではフリップフロップを用いた順序回路を学び、ビットやレジスタといったメモリ素子を作成して、RAMなど記憶装置の仕組みを実現します。第4章ではハードウェア作りを一旦中断し、構築したマシンが解釈するための機械語(CPUが理解できる最も基本的な命令セット)について学習します。そして第5章ではこれまでに作った論理回路、演算回路、メモリ回路をすべて組み合わせてCPU(中央処理装置)とコンピュータ全体のアーキテクチャを完成させます。

次にソフトウェア編の各章では、前半で構築したコンピュータ上で動くソフトウェア群を開発していきます。第6章では人間が読み書きしやすいアセンブリ言語のプログラムを機械語に変換するアセンブラを実装します。第7章と第8章では仮想マシン(VM)という中間層を構築します。これは高水準言語を効率よく実行するための抽象的なマシンで、本プロジェクトではスタックベースのVMを作り、これが高級言語と機械語の橋渡しをします。第9章ではJavaに似た高水準言語Jackを設計し、第10章・11章でそのJack言語のコンパイラ(言語翻訳プログラム)を2段階に分けて開発します。最後の第12章では基本的なオペレーティングシステム(OS)サービスを実装し、入出力処理やメモリ管理、グラフィックス描画などを提供する簡易OSを構築します。これにより、ハードウェアからOSに至るコンピュータの全レイヤーが出揃うことになります。

各章では単に内容を読むだけでなく、必ず実際に動作する成果物(プロジェクト)を作成する課題が課されています。章の冒頭でその回の目標や仕様が明確に示され、所定のAPIに従って部品を実装していく形式です。また各プロジェクトごとにテストスクリプトが用意されており、自分の実装が正しいかどうか自動で検証できるようになっています。例えば回路を作った章では専用のハードウェアシミュレータ上でテストスクリプトを走らせ、論理的に期待通りの出力が得られるかチェックします。プログラムを作った章でも同様に、用意されたテストを通過することで要件を満たしていることを確認できます。このようにフィードバックを得ながら進められるため、つまずきにくく学習効果が高い構成となっています。

学習ペースとしては、各章のプロジェクトに取り組むのに1〜2週間程度かかることが想定されています。全部で12章ありますので、全行程を完走するにはおよそ3ヶ月(12週間)ほどを見込むとよいでしょう。もっとも、仕事や学業と並行して進める場合は半年〜1年かけてじっくり取り組む人もいます。重要なのは時間を焦らず、自分の手で納得しながら進めることで、プロジェクトを通じて得られる知識や経験を最大限吸収することです。

ハードウェア編(第1章〜第5章): NANDから始めてCPU・メモリまで自作するコンピュータ基盤を構築する過程

ハードウェア編では、計算機を形作る物理的な基盤部分――論理回路、演算装置、記憶装置、そしてCPU――を一から作り上げます。第1章から第5章までの内容を通じて、最終的には自作の16ビットコンピュータ(Hackマシン)ハードウェアが完成することになります。このセクションではハードウェア編の各トピックについて詳しく見ていきましょう。

論理回路の作成: NANDゲートのみを用いた基本ゲート(AND/OR/NOTなど)の構築

第1章ではコンピュータの基本となる論理回路を作成します。NANDゲート1種類だけを唯一のプリミティブ(基本要素)として使用し、これを組み合わせてANDゲート、ORゲート、NOTゲート、XORゲートなどの基本的な論理回路を全て実現します。NANDゲートは「万能ゲート」と呼ばれ、適切に組み合わせれば任意の論理演算を表現できるため、まずはこのNANDのみを使って他のゲートを再現する演習から始まるのです。「なぜNANDゲート1種類から全ての回路が作れるのか?」と最初は不思議に思うかもしれませんが、実際に手を動かして組み立てることで、その仕組みを体感的に理解できるようになります。

具体的には、NANDゲートを2つ接続してNOTゲートを作ることから始め、さらにNANDを組み合わせてANDゲートやORゲートを構築します。XOR(排他的論理和)のように少し複雑な論理も、複数のNANDや前述の基本ゲートを組み合わせれば表現可能です。このようにしてブール代数でおなじみの基本論理演算回路を一通り揃え、以降の章で使う部品をまず手作りします。これらの回路は組み合わせ回路(コンビネーショナル回路)と呼ばれ、時間に依存しない現在の入力値から出力値が決まる回路です。後続の演算回路や制御回路の土台となる重要なステップと言えるでしょう。

ブール論理・ブール算術: 加算器(アダー)やALUなど算術論理回路の設計とブール演算の実装

第2章ではブール論理に基づく算術回路の設計に取り組みます。ここでは、2進数での加減算などを実現する回路を作成することで、コンピュータの計算機能の基礎を学びます。まず、1ビット同士の加算を行う半加算器(Half Adder)と繰り上がりも処理できる全加算器(Full Adder)を構築します。これらを組み合わせれば任意のビット幅の加算回路が作れるため、複数ビットのバイナリ加算器を設計します。さらに、ビット反転(NOT)や論理和・論理積(AND/OR)などの演算も組み合わせ、入力に応じて様々な演算を選択実行できるような回路を考案します。

こうして完成するのがALU(Arithmetic Logic Unit、算術論理演算装置)です。ALUはコンピュータの心臓部ともいえる回路で、加算や減算、論理演算など基本的な計算処理を担います。Nand to Tetrisのプロジェクトではシンプルながら柔軟な16ビットALUを設計・実装し、必要に応じてゼロ出力や符号反転、インクリメントなどの操作も行えるよう工夫します。これにより、与えられた制御ビットの組み合わせによって様々なブール演算・ブール算術処理を実行できる汎用的なALUが出来上がります。

この章までで、論理ゲートと加算回路を組み合わせ、コンピュータが計算を行うための基礎が整いました。ALUの設計によって、外部から指示されたとおりに二つの数値の演算や比較を行うことが可能になります。次章以降では、このALUを含む各種回路を用いて、記憶装置やCPU全体の構成へと発展させていきます。

メモリの仕組みと自作: フリップフロップによる記憶素子からRAMモジュールまでメモリ回路を構築

第3章ではメモリ回路の実装に焦点を当てます。ここまで扱ってきた論理回路や算術回路は組み合わせ回路であり、入力が変われば即座に出力も変化する性質のものでした。しかしコンピュータには、計算結果やデータをあとで再利用するために「情報を記憶する」能力が必要です。この章では時間に依存して状態を保持する順序回路として、基本単位であるフリップフロップ(D型FF)を構成し、それを用いてビットやレジスタといった記憶素子を作ります。

具体的には、1ビットの情報を保持できるDフリップフロップを作成し、それを並べて16ビット幅のレジスタを実装します。レジスタはCPU内部で使われる小容量高速メモリですが、これをさらに多数集積しアドレス指定可能にしたものがRAM(Random Access Memory)です。プロジェクトではまず数ビット〜数ワード程度の小さなRAMモジュールを設計し、動作を検証しながら徐々に規模を拡大していきます。最終的には数千ワードの容量を持つ16ビットRAM(主記憶装置)を構築し、任意のメモリアドレスを指定して読み書きができるようにします。

メモリ回路を作る上では、クロック信号(時計のようなパルス)に同期してデータを保存・更新する仕組みが重要です。Dフリップフロップではクロックの立ち上がりに合わせて入力をラッチし、その値を次のクロックまで保持します。この基本原理を組み合わせ、任意のタイミングで指定したレジスタの内容を更新したり読み出したりできる制御回路を付加することで、読み書き可能なメモリが完成します。情報が電気的に保持される仕組みを回路レベルで理解できることは、本章の大きな学びの一つです。

CPU(中央処理装置)の設計・実装: ALUとレジスタを統合し機械語命令を実行できる16ビットCPUを構築・完成

第4章では一旦ハードウェア作りを中断し、コンピュータが理解する「機械語(マシン語)」について学びました。そしてその知識を踏まえ、第5章ではいよいよCPU(中央処理装置)の設計とコンピュータの全体統合を行います。ここまでに作成したALU、レジスタ、メモリをすべて組み合わせ、プログラム(命令)の実行を制御する回路を構築します。

CPU設計においてまず必要となるのは、命令セットの取り決めです。第4章で学習したHackマシンの命令セットでは、16ビット長の命令を大きく2種類に分類しています。一つはA命令(アドレス命令)と呼ばれるもので、後続の15ビットをそのままメモリアドレスや定数値として扱うシンプルな命令です。もう一つはC命令(計算命令)で、上位ビットにALUに対する演算指示(どの操作を行うかやオペランドの選択)、下位ビットに演算結果の格納先やジャンプ条件を指定するフィールドを持ちます。このように設計された機械語命令のフォーマットに従い、CPU内部では命令ビット列をデコーダで解釈し、それぞれのビットが指示する対象(ALUの操作種別、結果を書き込むレジスタやメモリ、次の実行アドレス制御など)に信号を送ります。

HackコンピュータのCPUは、プログラムカウンタ(PC)と呼ばれるレジスタを持ち、現在実行中の命令アドレスを記録します。クロックサイクルごとにこのPCの指すメモリ位置から命令を取り出し、デコーダで解析して、指定された演算をALUで実行し、結果を指定レジスタやメモリに格納します。またC命令には条件付きジャンプ(分岐)の指定も含まれており、条件が成立した場合にはPCの値を飛び先アドレスに更新し、次の実行箇所を変更します。こうして順次命令を実行していくことで、所望の計算処理が進んでいきます。

第5章の終わりまでに、16ビットのCPUとそれを支えるメモリ、入出力を含むコンピュータ全体(Hackマシン)のハードウェアが完成します。ここまで作り上げた自作コンピュータは、クロック信号に従って機械語の命令列を読み取り、計算を行い、条件分岐し、結果を保存できる「命令実行マシン」となりました。つまり、自分の手でコンピュータを一台組み上げたことになるのです。この成果物はまだ人間にとって直接プログラミングしやすいものではありませんが、次章以降でアセンブラやコンパイラといったソフトウェアを用意することで、いよいよこのコンピュータに自由に命令を与えられるようになります。

ハードウェア編の最終成果: 自作コンピュータ(Hackマシン)の完成とソフトウェア編への橋渡しとなる土台

ハードウェア編の集大成として完成したHackコンピュータは、16ビットCPU・メモリ・入出力装置(画面およびキーボード入力)を備えたシンプルな汎用コンピュータです。この時点で、機械語で記述された任意のプログラムを実行可能な環境が整いました。しかし人間が直接16進数や2進数の機械語コードを書いてプログラムするのは極めて非効率です。そこで後半のソフトウェア編では、この自作コンピュータを使いやすくするためのソフトウェア群を順に作っていきます。すなわち、人間にとって理解しやすい形式でプログラムを書き、それを機械語に翻訳して実行できるようにするための各種ツール(アセンブラ、コンパイラ、簡易OSなど)を開発していくことになります。

アセンブラ・機械語の実装: CPUの命令セットを人間に扱いやすい形にし、機械語へ翻訳するプログラムの開発

ハードウェア編で無事コンピュータ本体が完成したら、次はそのコンピュータに命令を与えるソフトウェアを作ります。まず取り組むのは、Hackコンピュータの機械語命令を人間が読み書きしやすいニーモニック形式に置き換え、それをまた機械語に翻訳するプログラム、すなわちアセンブラの実装です。ここでは機械語とアセンブリ言語の関係を確認しつつ、Hackコンピュータ用のアセンブラを自作するプロセスを見てみましょう。

機械語(マシン語)とは: CPUが解釈できる2進数(バイナリ)の命令形式

機械語(マシン語)」とは、その名の通りコンピュータの機械(ハードウェア)が直接解釈できる形式の命令コードです。典型的には0と1のビット列で表現され、人間にとっては意味のわかりづらい16進数や2進数の並びとして記述されます。ハードウェア編で完成したHackコンピュータにも専用の機械語(命令セット)があり、たとえば「@21」(10進21をアドレスレジスタAにセット)とか「D=D+A」(DレジスタにDとAの値の和を格納)といった人間向け表記の裏側では、それぞれ2進数のビット列にエンコードされた命令データが存在します。

CPU内部の回路は、このビット列を入力として受け取り、それぞれのビットの組み合わせに応じて適切な動作を行います。前述のHack CPUでは16ビットの命令中、上位1ビットで命令種別(A命令かC命令か)を判別し、残り15ビットの配置によって具体的な処理内容を理解する作りでした。機械語はコンピュータにとっての母国語とも言えるもので、この形式で与えられた命令しかCPUは実行することができません。

ただし機械語の直接のやり取りは、人間にとって非常に扱いにくいものです。プログラマが効率よく命令列を作成するために、機械語と一対一で対応するよう記号化された表現が考案されています。これをアセンブリ言語(ニーモニック)と呼びます。次の節で述べるアセンブラとは、このアセンブリ言語で書かれた命令を機械語に変換する翻訳プログラムのことです。

Hackコンピュータの命令セット: A命令(アドレス指定)とC命令(計算命令)からなるシンプルな構造

Nand to Tetrisで構築したHackコンピュータの機械語命令セットは、比較的シンプルな構造をしています。全ての命令は16ビットの固定長で、種類は2種類(A命令とC命令)のみです。A命令(Address命令)は主にアドレスや定数の設定に使われ、命令の最上位ビットが0で始まることで識別されます。残りの下位15ビットはそのまま数値(0〜32767の即値)を表し、例えば「@10」というアセンブリ命令はバイナリでは0000000000001010となり、「Aレジスタに数値10を入れる」という操作を意味します。

一方、C命令(Compute命令)は演算やデータ転送、条件分岐を行う命令で、最上位ビットが1で始まります。C命令のフォーマットはさらに細かくフィールドに分かれており、上位数ビットがALUに対する演算指定(どの計算を行うか、入力としてAレジスタかメモリかなど)、中間のビットが計算結果の格納先(Dレジスタやメモリなど)、下位のビットがジャンプ条件(例えば結果がゼロならジャンプ、正ならジャンプ等)を表します。例えばアセンブリ言語で「D=D+A」という命令は、「Dレジスタの値とAレジスタの値をALUで加算し、その結果をDレジスタに格納する」という動作で、Hack機械語では1110xxxyyzzz(※実際には各フィールドを示すビット列)といったビットパターンにエンコードされます。

このようにHackの命令セットは極力シンプルに設計されており、2種類の命令を組み合わせるだけで十分あらゆる計算や分岐処理が記述できるようになっています。シンプルさ故に、人間が直接機械語ビット列を読み書きしようと思えばできなくはありません。しかし命令ビットと動作の対応表を逐一参照しながらでは非生産的です。そこで次に述べるアセンブリ言語とアセンブラの出番となるわけです。

アセンブラとは: アセンブリ言語を機械語に翻訳するプログラムであり、コード開発を容易にする役割を担う

アセンブラとは、前述のアセンブリ言語で記述されたソースコード(ニーモニック)を入力として受け取り、対応する機械語のバイナリコードに変換するプログラムです。人間にとって意味のある英単語や略号で書かれた命令を、コンピュータが直接実行できるビット列に翻訳する役割を果たします。例えば「@i」というニーモニックを見ればプログラマは「変数iの値をAレジスタにロードする命令だな」と理解できますが、機械にとっては111000…のようなビット列でないと処理できません。アセンブラはこのギャップを埋め、プログラマが記号的なコードで効率良くプログラムを書けるようにするための重要なソフトウェアツールです。

Hackコンピュータ用のアセンブラも、基本的な役割は他のCPU向けアセンブラと同様です。すなわち、Hackアセンブリ言語(ニーモニック形式の命令列)を入力として、対応する16ビット機械語の列に翻訳します。Nand to Tetrisの第6章では、このHackアセンブラを自分で実装する課題に取り組みます。既に機械語の命令セット仕様は決まっているので、それに沿って文字列をパースし、該当するビットパターンを出力する文字変換プログラムを作るイメージです。

ハックアセンブラの実装: ニーモニック(記号命令)を解析し、シンボル(ラベル)を解決してバイナリコードを生成

Hackアセンブラの実装では、まずテキストで書かれたニーモニック命令を読み込んで解析(パース)する処理が必要です。各行ごとに命令文を読み、@で始まるかどうかでA命令かC命令かを判定します。A命令であれば「@Xxx」の「Xxx」部分(シンボルや数値)を取得し、数値ならそのまま2進数に、ラベル名(シンボル)なら後述のシンボル表を参照して対応するアドレス値に置き換えます。C命令であれば、「Dest=Comp;Jump」の形式をそれぞれ「Dest(格納先)」「Comp(演算内容)」「Jump(ジャンプ条件)」の3要素に分解し、あらかじめ用意したビットパターン表に従って各フィールドのビット列を組み立てます。

ニーモニック中に出てくるラベル(シンボル)の処理も重要です。アセンブリ言語では「(LOOP)」のようにラベルを定義し、ジャンプ命令で「@LOOP」のように参照することでコードの分岐先を指示できます。アセンブラではまず入力コードを一巡目にパースして各ラベルが何行目(何番目の命令)かを記録し、シンボル表(テーブル)を構築します。次に二巡目で実際の命令変換を行う際、シンボル表を参照して「@LOOP」といったラベル参照を対応する数値アドレスに置き換えます。同様に「@i」のような変数シンボルも、初出時にメモリの空き領域からアドレスを割り当て、以降はそのアドレスに解決します。この二段階処理によって、可読性のために使われているラベルや変数名を正確に数値アドレスへマッピングするのです。

こうしたパースとシンボル解決の仕組みを実装し、各命令を対応するビット列(16ビットのバイナリ値)に翻訳できればアセンブラの完成です。出来上がったプログラム(アセンブラ)を使えば、Hackアセンブリ言語で書かれたソースコードから機械語のバイナリファイルを出力できます。これにより、私たちは人間に分かりやすいアセンブリコードを書くだけで、最終的にコンピュータが実行可能な機械語を生成できるようになります。

機械語プログラムの実行: Hack CPU上でアセンブリで書かれたプログラムを動かし、CPUの動作を確認

自作したアセンブラが正しく動作すれば、いよいよ自分のコンピュータ上で様々なプログラムを走らせることが可能になります。例えば、簡単なアセンブリプログラムを書いてLEDの点滅や数値計算を行わせることもできますし、もっと複雑なロジックを書けばゲームのような動作も実現できます。ハードウェア編の終盤ですでに機械語版のテストプログラムとしてアセンブリ版Pongゲーム(ボールが壁に当たって跳ね返るだけのシンプルなもの)を動作させてCPUと画面の動きを確認した人もいるでしょう。

ここで得られる体験は、自分が設計・実装したCPUが確かに期待通り動いていることを目の当たりにする感動です。アセンブリ言語とはいえ、自作のコンピュータがプログラムを解釈し、演算し、画面に出力している様子を見るのは大きな達成感があります。もし不具合があれば、自分で組み立てた回路やアセンブラコードをデバッグし、原因を突き止めて修正する必要があります。その過程も含め、コンピュータを作るとはどういうことかを深く理解できるでしょう。

アセンブラまで完成したことで、人間にとって扱いやすいニーモニックでプログラムを書き、それを自作マシン上で実行できる段階になりました。しかしアセンブリ言語は依然として低水準であり、大規模なプログラムを開発するには生産性が高いとは言えません。次のステップでは、もっと高級な(人間寄りの)言語でプログラムを書き、同様に自作コンピュータ上で動かすことに挑戦します。

コンパイラ・高水準言語の設計: Java風の高級言語Jackを策定し機械語に翻訳するコンパイラの構築

ソフトウェア編の後半では、いよいよ高水準言語を自作コンピュータ上で動かせるようにするための言語処理系を作ります。具体的には、本プロジェクト独自の高級プログラミング言語Jackを設計し、そのコードを機械語に翻訳するコンパイラを開発します。さらに、その高級言語から利用するための基本的なOS(オペレーティングシステム)機能も実装し、グラフィカルなアプリケーションが動く環境を整えます。ここでは高水準言語Jackの特徴と、コンパイラ作成を通じて学ぶ内容について見てみましょう。

高水準言語Jackの設計: Javaに似た文法を持つ教育用プログラミング言語を策定・作成

Nand to Tetrisで登場する高級言語Jackは、教育目的で設計されたオブジェクト指向言語です。文法や構文はJava風になっており、クラス定義やメソッド、変数、式、制御構文(if文やwhile文)など、一通りの要素を備えています。ただし実行対象が自作のHackコンピュータであることから、言語としての複雑さは必要最低限に抑えられています。例えばポインタ演算や継承など高度な機能はなく、標準ライブラリも基本的なものに限られます。

Jack言語は人間にとって理解しやすい高水準の記述力を持ちながら、背後ではHackプラットフォーム上で動作します。Jackで書かれたプログラムは、コンパイラによって最終的に機械語に翻訳されることで、自作コンピュータ上で実行可能となります。Jackは教育用言語とはいえ、変数や配列、計算処理、関数(メソッド)呼び出し、オブジェクト指向の考え方(クラスとインスタンス)など、高級言語のエッセンスを学ぶには十分な機能を持っています。

Jackにはまた、グラフィックス描画やキーボード入力などのI/O操作を行うAPI(ライブラリ)が用意されています。これは本プロジェクト内で構築する簡易OSによって提供されるもので、例えば画面にドットや文字を描画するGraphicsクラス、キーボードやマウスの入力を取得するInputクラス、メモリ確保を行うMemoryクラスなどがあります。Jackプログラマはこれらのライブラリを呼び出すことで、低レベルの煩雑な処理に踏み込まずに高水準な操作を実現できます。言い換えれば、Jack言語とそのライブラリを通じて、私たちの自作コンピュータはちょっとしたゲームやアプリケーションを記述できるプラットフォームへと昇華されるのです。

Jack言語の特徴: オブジェクト指向を採用した簡易言語でグラフィックス描画やI/O操作も可能

Jack言語の大きな特徴の一つは、シンプルながらオブジェクト指向を採用している点です。プログラマはクラスを定義し、そのクラスのインスタンス(オブジェクト)を生成して、メソッドを呼び出すことで処理を進めます。例えばゲームであれば「画面を表すScreenクラス」「ユーザー操作を扱うKeyboardクラス」「ゲーム中のキャラクターを表すクラス」などを設計し、それぞれのメソッドに役割ごとの処理を書いていきます。こうしたカプセル化により、コードの構造を分かりやすく保つことができます。

またJackは上記の通りOSレベルでいくつかの基本サービスが利用できます。そのため、Jackでコーディングすれば比較的簡単にグラフィックス表示や入出力の操作が可能です。例えば画面にテキストメッセージを表示したり、図形を描いたりといった処理も、Jack言語の命令で記述できます。実際、後述するようにJackコンパイラとOSが完成した暁には、我々の自作コンピュータ上でPongやTetrisといった簡単なゲームを動かすこともできるようになります。このように、Jackは教育プロジェクト内の言語とはいえ、実用的なプログラムを書いて実行できるだけの表現力を備えているのです。

コンパイラとは: 高級言語のコードを機械語(または中間コード)に翻訳するプログラムの役割

コンパイラとは、高水準言語(人間にとって読み書きしやすいプログラミング言語)で書かれたソースコードを、コンピュータが実行できる形式(機械語や中間コード)に翻訳するプログラムです。アセンブラがアセンブリ言語から機械語への翻訳者だったのに対し、コンパイラはより上位の言語から機械語への翻訳者と言えます。例えばC言語のコンパイラは、printf(“Hello”);といったソースコードを受け取り、それに対応する一連の機械語命令列を出力します。

コンパイラの役割は単なる単語の置換ではなく、ソースコードの構文解析から最適化、命令生成まで多岐にわたります。プログラミング言語の文法は複雑なため、まず字句解析(トークナイズ)と構文解析によってソースコードの構造(構文木)を理解しなければなりません。そしてその構文木をもとに、ターゲットとする機械語(あるいは仮想マシン用の中間コード)で表現できる手順に変換していきます。この際、不要な計算を省いたり効率の良い命令の組み合わせに置き換えたりする最適化を行うこともあります。

要するに、コンパイラは高級な表現を低級な表現に“噛み砕く”プログラムです。コンパイラがあるおかげで、私たちは直接機械語を書かなくても高水準言語で複雑なプログラムを作成できるわけです。Nand to Tetrisでは、自分たちで設計したJack言語用のコンパイラを自前で実装するという稀有な体験を通じて、このコンパイラの原理を学びます。

Jackコンパイラの構成: 構文解析(パーサ)とコード生成の2段階でJackプログラムを機械語に変換

Jackコンパイラの実装は大きく2つのフェーズに分かれています。第一フェーズは構文解析(パース)で、Jackのソースコードを読み取り、その文法に従ってプログラム構造を解釈します。例えばlet sum = x + y;という一文を処理する場合、「let文」→変数sumへの代入式、その右辺は二項演算式でオペランドは変数xとy、演算子は+、といった具合に、コードをツリー状の構造(抽象構文木)に分析します。Nand to Tetrisではこの解析器(パーサ)を手作りし、Jack言語のBNF(文法定義)に沿って再帰下降パーサを実装することになります。

第二フェーズはコード生成です。構文解析で得られたプログラム構造(構文木)を辿りながら、それに対応するニーモニック命令列を出力していきます。具体的には、式や文を深さ優先で走査し、各部分に相当するスタックマシン用のVMコード(または直接アセンブリ)を生成します。例えば先ほどのsum = x + yであれば、「変数xの値をスタックに積む」「変数yの値をスタックに積む」「スタックトップとその次の値を加算し、結果をスタックに積む」「その結果を変数sumにポップして書き戻す」といった一連の命令列に変換します。

Jackコンパイラでは、まず高級なJackの構文を低級なスタック操作列に分解し(構文解析とコード生成を兼ねた処理)、得られた中間コード(仮想マシン用VMコード)を、さらに前述のアセンブラや別途実装するVM変換プログラムによって最終的な機械語に落とし込みます。このように複数段階を経て高級言語が機械語になるまでを自ら実装することで、プログラミング言語処理系の内部で何が行われているのかを深く理解できるようになります。

コンパイラ実装の学び: 文法定義から低レイヤへの翻訳を体験し言語処理系の原理を理解

Jackコンパイラの開発を通じて得られる学びは非常に多岐にわたります。まず、自分で言語の文法を定義しパーサを実装することで、プログラミング言語の構文構造を体系的に理解できます。普段何気なく書いている条件分岐やループも、コンパイラの視点から見ると「非終端記号『statement』の一種であり、その生成規則は…」といった具合に捉えることができます。多少地味な作業に思えるかもしれませんが、文法をコード化する経験を積むと、エラーの原因追及やコードの読み書きにも役立つ論理的思考力が養われます。

また、高級言語で記述された処理を低レイヤの命令列に落とし込む過程を実装することで、「なぜこの高級言語構文ではこのような機械語の動きになるのか」という理解が得られます。例えば再帰呼び出しをコンパイルする際のスタックフレーム操作や、式評価順序とスタックマシンの関係など、実際に自分でコード生成してみて初めて気付くポイントも多いでしょう。これらは言語処理系やコンピュータアーキテクチャを深く理解する上で貴重な洞察となります。

さらに、Jackコンパイラと同時に簡易OSも構築することで、ソフトウェアがハードウェア資源(CPU・メモリ・入出力)をどのように抽象化し管理しているかを学べます。例えばメモリ管理(Memory.allocによるヒープ領域確保)、描画処理(Screen.drawPixelによるピクセル操作)、文字列操作や数学ライブラリの実装など、コンピュータ上で動くソフトウェアの基盤について理解が深まります。これらの知識は、日々使用する高級言語やOSが内部で行っている処理を想像する助けにもなり、エンジニアとしての幅広い素養につながります。

最終成果(Pong/Tetrisの動作・体験): 自作コンピュータ上でゲームを動かす達成感を味わうことができる

以上のハードウェア編・ソフトウェア編をすべて終えた暁には、「NANDからテトリスまで」の題名が示す通り、自作したコンピュータ上で実際にテトリスなどのゲームを動かすことが可能となります。最後に、その最終成果として得られる体験と今後の展望について述べます。

PongやTetrisの動作デモ: 自作コンピュータ上でグラフィックゲームを実行し、その完成度を体感

Nand to Tetrisプロジェクトのゴールの一つは、自分で作り上げたコンピュータ上で実際にアプリケーションを動作させることです。書籍では最終章まで完了すると、Java風の高級言語Jackで記述されたPongゲーム(ブロック崩しのようなシンプルなアクションゲーム)が用意されており、これを自分のコンピュータ上で実行してみることが推奨されています。Pongゲームではボールが画面内を跳ね返り、プレイヤーが操作するパドルで打ち返す動作がグラフィカルに描画されます。自作のHackコンピュータが、キーボード入力を受け取り、画面に動く図形を表示し、ゲームという形でインタラクティブに動作する様子は圧巻です。

また、プロジェクト名に冠されているテトリス(Tetris)も象徴的な存在です。テトリスそのものは書籍内で実際に作るわけではありませんが、Pongと同様にJack言語で記述すれば動かすことができるでしょう。読者や受講者の中には、Pongを発展させてテトリスやその他のオリジナルゲームを実装してみる人もいます。自分の手で設計・実装した計算機でプログラムが動くという体験、それも単純な計算ではなく画面に動きのあるゲームが動作する光景は、非常に大きな達成感をもたらします。「本当にNANDゲートから作ったコンピュータでゲームが遊べている!」という実感は、本プロジェクト最大の報酬と言えるでしょう。

全行程を終えて得られる知見: コンピュータのレイヤー構造を俯瞰し理解できたことによるエンジニアとしての成長

プロジェクトを完遂すると、コンピュータの各レイヤー(層)について統一的な視点を持てるようになります。普段ソフトウェア開発をしているだけではブラックボックスになりがちなハードウェアや言語処理系の部分も、自分の手で実装した経験に裏打ちされた理解が備わります。いわば、コンピュータという「氷山」の水面下の巨大な部分も含めて全体像を俯瞰できるようになるのです。

このような包括的な知見を得たエンジニアは、技術選択や問題解析の場面でも強みを発揮できます。例えばプログラムが遅い原因がハードウェア資源の制限にあるのか、それともコンパイラやVMの実装によるものなのか、といった切り分けを論理的に考えられるようになるでしょう。また、新しい言語やプラットフォームに触れる際も、その背後にある共通原理(論理回路やCPUの挙動、言語処理の仕組み)が頭に入っているため、理解が早くなります。Nand to Tetrisで培った「下から上まで繋がる軸」は、今後あらゆるIT技術を学ぶ上で強力な土台となるはずです。

さらなる発展: Nand to Tetrisで培った知識を土台に次の学習へのステップを踏み出す

Nand to Tetrisをやり遂げた後は、得られた知識や興味を元にさらなる探求を続けてみましょう。本プロジェクトはコンピュータシステムの広範な分野を一通りなぞったものですが、各分野を深掘りすればそれぞれが一冊の本になるような奥深い世界があります。例えばCPUアーキテクチャに興味を持ったなら、より高度なパイプラインCPUや並列処理、キャッシュメモリなどを学んでみるのも良いでしょう。コンパイラに魅力を感じたなら、最適化技法や型システムについて勉強し、自分で新たな言語やJITコンパイラを書いてみるのも面白いかもしれません。

また、Nand to Tetrisの学習コミュニティでは、多くの先人たちが自分の実装をインターネット上で公開しています。それらを参考に自分の成果物を改良したり、他の人の工夫を取り入れてみるのも良い刺激になります。加えて、第13章「さらなる冒険へ」ではありませんが、自作したコンピュータを拡張してオリジナルの機能を盛り込んでみるのも創造的な挑戦です。例えばより高解像度のディスプレイをサポートしたり、音声出力機能を追加したり、あるいはマルチタスクOSに発展させてみたりと、可能性は無限大です。

Nand to Tetrisで得た基礎知識と自信を糧に、ぜひ次なるステップへと踏み出してみてください。ゼロからコンピュータを作り上げた経験は、エンジニアとしての視野を広げ、未知の技術への挑戦を恐れない姿勢につながります。本プロジェクトを通じて培った包括的なコンピュータ観は、これから先どんな専門領域に進んでも、きっと大きな助けとなるでしょう。

資料請求

RELATED POSTS 関連記事