はじめに
こんにちは、Latte72 です。
慶應義塾大学公認サークル Computer Society で低レイヤーを扱うシステム班の班長を務めることになったので、後輩たちに低レイヤー技術(特に自作言語やコンパイラ・インタプリタの実装)について興味を持ってもらうにはどうしたらいいかと考えながらこの記事を書いています。
この記事は私のサークルに入会した新入生や、プログラミング言語がどのようにして動いているのかに興味がある人、低レイヤーにあまり詳しくないけど自作言語や自作コンパイラに興味がある人たちに向けたものです。 プログラミングに関する事前知識がなくても読めるように、多くの補足をつけています。 既にプログラミングに精通している方にとっては説明が不適切に感じる部分があるかもしれません。温かい目でご覧いただき、コメント欄にてご指摘いただければありがたいです。
当初は1つの記事として公開しようと思っていたのですが、長くなりそうだったのでいくつかに分けて公開することにしました。この Part 1 の記事は、新入生に低レイヤー技術を解説する際に用語の知識を補うために、低レイヤー関連の用語をまとめています。続編の Part 2 の記事では自作言語やコンパイラ・インタプリタの実装についての紹介をしています。ぜひご覧ください!
⇓ Part 2 の記事はこちら ⇓
用語の説明
低レイヤー技術に関連する用語について解説していきます。
一部だけではイメージが掴みづらいものもあるかもしれませんが、記事全体を通して理解できれば問題ありません。少し疑問が残っていても読み進めていただいて大丈夫です。
低レイヤー
低レイヤーとはハードウェアに近い領域を指します。低レイヤー開発とは、単にコードを書くだけでなく、コンピュータの根幹部分を深く理解し、その上でシステムの基盤となる部分であるOSやコンパイラを自ら設計・実装することを指します。また、今回の記事ではあまり解説しませんが、ネットワークやセキュリティに関する技術も低レイヤーに含まれます。
OS
OS (Operation System) とはハードウェアとアプリケーションの中間層として動作するソフトウェアのことです。普段パソコンやスマホを使って生活している人でもOSの存在を意識していない人は多いのではないでしょうか。
これはOSの存在を意識しなくても使えるほど便利に設計されているという点ではとてもありがたいことなのですが、低レイヤー開発をするにあたっては少し厄介です。低レイヤー技術はOSや実際のパソコン自体の性質に強く影響を受ける分野です。そのため、開発するシステムの性質をしっかりと理解しておくことが大切です。
UNIX
UNIX は1970年代に開発されたOSで、マルチユーザー・マルチタスクの概念を初めて実現しました。シンプルで堅牢な設計が特徴であり、現在でも多くのシステムやOSの設計思想の基盤となっています。
UNIX系のOSにはいろいろな種類があり、個々のOSはPOSIX規格というものに準拠しているため、異なるUNIX系OS間での互換性が高いです。
POSIX規格
POSIX規格とは、移植性の高いソフトウェアの開発を容易にするために、主にUNIX系のOSに関して各OSが共通して持つべき仕組みを定めた規格です。
Windows
WindowsはMicrosoftが開発するOSで、GUIを中心としたユーザーフレンドリーな設計が特徴です。かなり多くの人が利用していますが、実はそのままではあまり開発には向かないOSです。これはWindowsは基本的にクローズドソースであるため、OS内部の詳細な動作が不透明で、POSIX規格にもほとんど対応していないからです。しかし安心してください。現在のWindowsには WSL という機能があり後述するLinuxをWindows上で仮想的に動作させることができます。
WSL
WSL (Windows Subsystem for Linux) はWindows上で実際のLinuxカーネルを動かす方式でとても便利です。
このWSLという機能ができる前にもWindows機でLinuxを利用する方法はありました。例えばデュアルブートと言って、1つのパソコン上にWindowsとLinuxが共存していて、起動時にどちらかを選択するという方法です。
デュアルブートはLinuxとしての動作性は優れているものの、操作ミスによりLinuxだけでなくWindowsも破壊される、WindowsとLinuxを同時に起動することができない、などの問題がありました。
これらの問題点は最近のWSLではほぼ解決されているため、開発にWindowsを利用する場合はWSLを利用するのがおすすめです。
Linux
Linux はUNIXの設計思想を引き継ぎながら、オープンソースとして発展してきたOSです。カスタマイズ性に優れ、サーバー、クラウド、組み込みシステムなど多岐にわたる用途で利用されています。
LinuxというのはたくさんのOSの総称で、個々のOSはディストリビューションと呼ばれます。具体的にはUbuntuやCentOSなどのディストリビューションがあります。
Linuxはカーネルやシステムコール、デバイスドライバなどの内部構造がオープンにされているため、低レイヤー開発者にとって理想的なプラットフォームです。デバイスドライバの開発やシステムの最適化などの実践的な学習と開発が可能です。
オープンソース
オープンソースとは、ソースコードが公開され、誰でも自由に閲覧・修正・再配布できるソフトウェア開発モデルです。
MacOS
MacOS はAppleが開発するUNIXベースのOSで、美しいGUIと安定性、高いセキュリティが特徴で、クリエイティブな分野や一般ユーザー向けに広く利用されています。Terminalや各種コマンドラインツール、UNIXライクな操作環境を提供しており、カスタマイズ性に関して制限はありますが、低レイヤー開発も可能です。
CPU / メモリ
CPU
CPU (Central Processing Unit) は、プログラムの命令を実行する演算装置です。メモリから命令を読み出し、その命令を解釈して「加算」「減算」「比較」などの演算やデータの移動などの実際の処理を行います。
機械語
機械語とはCPUが直接解釈できるデータのことです。世間的なイメージで言うと「 0
と 1
がたくさん並んでいるやつ」です。
(実際には現在利用されているほとんどのコンピュータではすべてのデータを 0
と 1
の羅列で表しています。したがって、画像も文字列もコンピュータ上では「 0
と 1
がたくさん並んでいるやつ」に該当します。)
プログラム言語は確かに機械が読みやすいように設計していますが、実際に実行するにはこのあと解説するコンパイラやインタプリタが必要であり、CPUがそのまま実行できるのは機械語だけです。
機械語の例
Wikipediaから適当に拾ってきただけなのでそんなに深い意味はないです。0111000000000001
0000000000000000
0111000000000010
0000000000000000
0001001000010000
1000000000001101
0001001000100000
1000000000001100
アセンブリ言語
アセンブリ言語とは、機械語を人間が読める形にした低レベル言語です。アセンブラを用いることで機械語に変換することができます。アセンブリ言語での一つの命令は基本的には一つの機械語の命令に対応していますが、命令によっては複数の機械語の命令に対応しているものもあります。
x86-64とarm64のアセンブリの例
以下にC言語でのプログラムと、それを変換したx86-64とarm64のアセンブリを示します。(x86-64とarm64に関しては下のアーキテクチャで解説しています。
int main() {
int x = 5;
int y = 3;
return x * y;
}
上のプログラムをx86-64のアセンブリに変換したものです。
main:
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], 5
mov dword ptr [rbp - 12], 3
mov eax, dword ptr [rbp - 8]
imul eax, dword ptr [rbp - 12]
pop rbp
ret
上のプログラムをarm64のアセンブリに変換したものです。
main:
sub sp, sp, #16
str wzr, [sp, #12]
mov w8, #5
str w8, [sp, #8]
mov w8, #3
str w8, [sp, #4]
ldr w8, [sp, #8]
ldr w9, [sp, #4]
mul w0, w8, w9
add sp, sp, #16
ret
x86-64 アセンブリの入門スライドを作成しました。よかったらご覧になってください。
実際のアセンブリ言語
もしあなたがC言語を少し書くことができるなら、以下のサイトで実際に生成されるアセンブリ言語を眺めてみるのがおすすめです。
⇒ CompilerExplorer
アーキテクチャ
アーキテクチャ とは、特定の機械語の命令の総称である命令セットを表します。命令セットはCPUごとに自由にデザインしてかまいません。しかし、機械語レベルの互換性がないと同じプログラムを動かせないので、命令セットのバリエーションはそれほど多くありません。現代のアーキテクチャはその設計思想によって、CISCとRISCの2種類に分けることができます。
CISC
CISC (Complex Instruction Set Computer) は、多くの複雑な命令が用意され、1つの命令で複数の処理を行うことができます。例としてIntelやAMDが生産し一般的なWindows機に採用されている x86-64 があります。
x86-64は、AMD64やx64などと呼ばれることもあります。
これはx86-64が、32bitのx86と呼ばれていたアーキテクチャの64bit拡張で、その開発がAMDにより行われたことに起因しています。
RISC
RISC (Reduced Instruction Set Computer) は、単純な命令セットを採用し、1つの命令あたりの処理内容が限定的です。例として2015年以降のMacに採用されている arm64 や、カリフォルニア大学バークレー校で開発されオープンソースで提供されている RISC-V があります。
メモリ
メモリとは、CPUがプログラムを実行する際に必要な命令やデータを一時的に保存するための装置です。
基本的には RAM (Random Access Memory) のことを指しますが、文脈によってはキャッシュメモリやSSDを指すこともあります。
コンパイラ / インタプリタ
プログラムを処理する方法(処理系)はその処理の方法によっていくつかの種類に分けられます。ここではその方法について解説します。
コンパイラ
コンパイラは、プログラミング言語で書かれたソースコードを一括で機械語や中間表現に変換するプログラムです。変換後のコードは独立して実行可能です。代表的な例は、C言語のコードを実行ファイルに変換する GCC や Clang です。
インタプリタ
インタプリタ は、ソースコードを逐次的に解釈し、直接実行するプログラムです。コンパイルの事前処理を必要とせず、実行時に 1 行ずつ処理します。代表例は Python や Ruby のインタプリタです。
REPL
REPL (Read-Eval-Print Loop) とは、インタプリタを使う処理系において、ユーザーとインタプリタが対話をするようにコードを実行できる仕組みです。
コードの動作やエラーを確認しながら処理することができるのでデバッグに役立ちます。
JITコンパイル
JITコンパイル (Just-In-Time Compilation) とは、プログラムのうち頻繁に実行されるコード(「ホットスポット」と呼ばれる)を先に機械語に変換しておくことで実行速度の高速化を図る技術です。具体的にはよく使われる関数やループなどがホットスポットに該当します
なお、JITコンパイルと区別する目的で通常のコンパイルを「 AOTコンパイル (Ahead-Of-Time Compilation) 」と呼ぶこともあります。
(具体的な利用例は個々のプログラミング言語の項で紹介します。)
JVM
JVM (Java Virtual Machine) とは、Javaを実行するための仮想マシンです。
Javaのソースコードはまずコンパイラによって中間表現である Javaバイトコード に変換されます。このバイトコードはプラットフォームに依存しない形式となっており、JVM上で実行されます。JVMは内部でJITコンパイルを利用しており、実行の仕組みも少しJITコンパイルに似ています。
利点 | 欠点 | |
---|---|---|
コンパイラ | ソースコード全体を事前に最適化されたネイティブコードに変換できるため、高速な実行が可能である | プラットフォームごとに再コンパイルが必要である 実行時の環境適応が難しい |
インタプリタ | ソースコードを逐次解釈するため、即時実行やデバッグが容易である 移植性が高く、プラットフォーム依存が少ない |
毎回解釈するため、実行速度が遅くなる |
JITコンパイル | 実行時に最適化を行うため、初回はインタプリタの柔軟性、後半はコンパイラの高速性を活かせる | システムが複雑化し、メモリ使用量が増加する可能性がある |
JVM | Javaバイトコードによりプラットフォーム非依存性が実現できる 内部でJITコンパイルを利用し実行時高速化を図れる |
仮想マシン層によるオーバーヘッドが存在するため、ネイティブ実行に比べ遅延が発生する |
「コンパイラ言語」 「インタプリタ言語」
多くの書籍やサイトで「コンパイラ言語」や「インタプリタ言語」という言葉が使われていますが、この使い方は誤解を招くことがあるので注意が必要です。
なぜならば「コンパイラ」や「インタプリタ」はそのプログラムをどうやってに処理するか(処理系)を表すものであり言語の基本的な構文などとは独立に考えられるべきものだからです。
一般的には C言語は「コンパイラ言語」、Python言語は「インタプリタ言語」と言われていますが、理論上は C言語のインタプリタや Python言語のコンパイラも作ることは可能です。
また、「インタプリタ言語」はJITコンパイルを利用していることがあり、これも混乱を招く可能性があります。
上記のことを踏まえたうえで、一般には「コンパイラ言語」は主な処理系がAOTコンパイラを利用するもの、「インタプリタ言語」は主な処理系がインタプリタ(一部はJITコンパイラも含む)を意味しています。
プログラミングパラダイム
プログラミングパラダイムとは、効率的なプログラムを書くためのプログラムの設計方法をいくつかのグループに分けたものです。ここでは手続き型プログラミングとオブジェクト指向について解説します。
言語によってどのプログラミングパラダイムが使いやすいかは異なります。仕様によって特定のプログラミングパラダイムを使いやすいように設計している言語もあります。しかし、どの考え方でプログラムを書くかの問題なので、「この言語ではこのプログラミングパラダイムしか使うことができない」というようなことはありません。
手続き型プログラミング
手続き型プログラミング とは、プログラムの「処理の流れ」(手続き)を中心に設計する考え方です。手続き型プログラミングではデータと処理が分離されています。あるデータを処理する際には、まずそのデータを受け取り、処理を実行し、結果を返すという一連の流れを明確に定義していきます。データをまとめるためには構造体を利用することが多いです。
手続き型プログラミングの利点と欠点
手続き型プログラミングはシンプルな構造で理解しやすいので、小規模なプログラムに向いています。しかし、グローバル変数が増えやすく、状態管理が煩雑になるので大規模なプログラムには向いていません。
オブジェクト指向
オブジェクト指向とは、データとそれに対する操作(メソッド)を一体化させたオブジェクトという概念を基本単位としてプログラムを設計する方法です。オブジェクト指向では、データとそれを扱うメソッドがひとつのまとまりとなるため、情報の再利用がしやすく、物事をモデル化するのに適した設計手法です。
クラス
クラス は、オブジェクトを生み出すための設計図として機能し、同じ種類のオブジェクトが共有する属性(変数)や処理(メソッド)を定義します。オブジェクト指向におけるメソッドは、関数とほぼ同義です。
継承
継承とは、既存のクラスを基に新しいクラスを作る仕組みで、親クラスの特徴を引き継ぎつつ新機能を追加できます。
オブジェクト指向の利点と欠点
オブジェクト指向では、継承などの柔軟で再利用性の高い設計を可能にする機能が備わっています。そのため、プログラムの拡張や保守が容易になり、複雑なシステムを効率的に管理することができます。ただし、オブジェクト指向の設計はその抽象度の高さから、シンプルなプログラムに対してはシステムが不必要に複雑になる可能性があります。
この記事を読んだ方から「オブジェクト指向の説明がわかりにくい」という声がいくつか寄せられたので、Pythonでの具体的な例を用いた解説を追加しました。
Pythonでのオブジェクト指向の例
Pythonでオブジェクト指向を利用する際のソースコードの具体的な例です。
# 基本キャラクタークラスを定義
class Character:
# オブジェクト生成時の初期化動作を定義
def __init__(self, name):
self.name = name # 名前属性
self.hp = 100 # 体力属性
# 攻撃メソッドを定義
def attack(self):
print(f"{self.name}の攻撃!")
# 基本キャラクタークラスを継承して魔法使いクラスを定義
class Wizard(Character):
# 追加メソッドとして呪文メソッドを定義
def spell(self):
print(f"{self.name}が呪文を唱えた!")
# 実際のキャラクターのオブジェクトを作成
warrior = Character("勇者") # 基本キャラ作成
mage = Wizard("魔導師") # 魔法使い作成
warrior.attack() # 「勇者の攻撃!」と表示
mage.attack() # 「魔導師の攻撃!」と表示(継承した機能)
mage.spell() # 「魔導師が呪文を唱えた!」と表示(追加機能)
上のプログラムでのクラスの設計を図を用いて簡略化すると以下のようになります。
Pythonでのオブジェクト指向について、かなり詳しくまとめているサイトを見つけたので、リンクを貼っておきます。
⇒ Pythonのオブジェクト指向プログラミングを完全理解
ガベージコレクション
ガベージコレクション (GC) は、プログラムが使用しなくなったメモリ領域を自動的に解放するメモリ管理システムです。PythonやJavaなどで利用されています。C/C++にはガベージコレクション機能がないのでmalloc
やfree
などの関数を用いて明示的にメモリ操作をしなければいけないです。一方で、ガベージコレクションがあるPythonやJavaではその必要はありません。
動的型付け / 静的型付け
プログラミング言語が持つデータ(変数)には「型」というものがあり、その変数にはどの程度の大きさのメモリを割り当てるか、そのデータの演算はどうやってに行うか、などを規定しています。具体的には整数を表すint
型や文字を表すchar
型などです。
ある変数を扱うにはその変数に型を付与しなければなりません。そのためにはどの型を付与するのかを決定する必要があります。この決定方法によりプログラミング言語は大きく2つに分けられます。
動的型付け
動的型付け とは、変数の型が実行時に決定されるプログラミング方式です。動的型付けを利用することでコードを柔軟に記述することができます。動的型付けはインタプリタと組み合わせられることが多いです。これにより同じ変数に異なる型のデータを扱う場合でも柔軟に対応できます。一方で、大規模なプロジェクトでは、変数の型が明確でないためにコードの理解や保守が難しくなってしまいます。
静的型付け
静的型付け とは、変数の型が実行前に決定され、型の整合性が事前にチェックされる方式です。静的型付けはコンパイラと組み合わされることが多いです。コンパイラが型情報を利用して最適なコード生成を行うため、実行時のパフォーマンスが向上します。また、型情報が明示されることで、プログラムの意図が明確になり、チーム開発や長期的な保守が容易になります。
C / C++
C言語
C言語とは、低レイヤー開発の定番言語で、OS開発や組み込みシステムで広く使用されています。C言語ではポインタ操作、メモリ管理など機械語に近い操作を行うことが可能です。
C言語のサンプルコード
C言語でフィボナッチ数列の10番目を求めて出力するプログラムです。
#include <stdio.h>
int fibo(int n) {
if (n < 2) {
return n;
} else {
return fibo(n - 1) + fibo(n - 2);
}
}
int main(void) {
printf("%d\n", fibo(10));
return 0;
}
C++言語
C++言語 はC言語にオブジェクト指向などの高水準な機能を追加したプログラム言語で、C言語の低レイヤー操作の利点を保持しながら抽象度の高い設計が可能です。低レイヤー開発の他にもゲームエンジンや高性能計算などに幅広く利用されます。
C++言語のサンプルコード
C++言語でフィボナッチ数列の10番目を求めて出力するプログラムです。
#include <iostream>
int fibo(int n) {
if (n < 2)
return n;
else
return fibo(n - 1) + fibo(n - 2);
}
int main() {
std::cout << fibo(10) << std::endl;
return 0;
}
C / C++ のコンパイラ
C / C++ の処理系には複数の種類があります。それらのうち有名なものを3つ紹介します。
GCC
GCC (GNU Compiler Collection) は、歴史の長いオープンソースのコンパイラで、C/C++以外にも複数の言語に対応しています。また、多くのプラットフォームで動作するため、クロスプラットフォーム開発に適しています。
Clang
Clang は、LLVMプロジェクトの一部として開発された、モダンなコンパイラで、エラーメッセージや警告が非常に分かりやすいです。
MSVC
MSVC (Microsoft Visual C++) は、Microsoftが提供する公式のC/C++コンパイラで、Visual Studioに統合されています。Windowsプラットフォーム上でのパフォーマンスやデバッグ機能に優れていますが、独自の拡張や挙動があり、コードの移植性を考慮する必要があります。
互換性について
互換性 とは、異なるハードウェアやソフトウェアが問題なく動作するように設計されていることを指します。
新しいシステムやソフトウェアが、古いバージョンのデータやファイル形式を扱えることを後方互換性、古いシステムやソフトウェアが、新しいバージョンのデータやファイル形式を扱えることを前方互換性といいます。
C++は「C with Classes」として始まり、C言語の機能を多く継承しています。
そのため、Cで記述されたコードの多くは、何らかの修正を加えればC++としても実行可能です。
Python
Python は、動的型付け言語でデバッグが容易なので、初心者でも書きやすい言語となっています。C言語の資産を利用しやすい設計となっているため、AI開発やWeb開発でも広く利用されています。
しかしながら、明示的に型を記述しないために可読性が低い、インタプリタを利用した処理系の宿命として実行速度が遅い、などの欠点もあります。
(ちなみに私が中学生の時に初めて書いたプログラミング言語もPythonでした。)
Pythonのサンプルコード
Pythonでフィボナッチ数列の10番目を求めて出力するプログラムです。
def fibo(n):
if n < 2:
return n
else:
return fibo(n - 1) + fibo(n - 2)
print(fibo(10))
CPython
CPython は、Pythonの公式の処理系でインタプリタのプログラムがC言語で記述されていることからこの名前がついています。
PyPy
PyPy は、Pythonの代替実装でJITコンパイラを採用しています。特にループ処理などの繰り返し実行されるコードでCPythonよりも高速に動作し、メモリ使用量も少ない特徴があります。ただしC拡張モジュールはPyPy専用のものとして別途に用意する必要があります。
高速化への試み
公式であるCPythonにも高速化の試みは多数実施されています。例えばPython3.13では試験的にJITコンパイラが導入されたそうです。
⇒ 「Python 3.13」で追加されたJITコンパイラとは?
JavaScript / TypeScript
JavaScript
JavaScript は、ブラウザ上で動作する動的型付け言語ですが、近年はNode.jsでサーバーサイドでも利用されています。
JavaScriptのサンプルコード
JavaScriptでフィボナッチ数列の10番目を求めて出力するプログラムです。
function fibo(n) {
if (n < 2) {
return n;
} else {
return fibo(n - 1) + fibo(n - 2);
}
}
console.log(fibo(10));
V8
V8 は、Googleが開発したJavaScriptエンジンで、ChromeブラウザやNode.jsで採用されています。JITコンパイル技術により高速な実行を実現し、JavaScriptのパフォーマンス向上に大きく貢献しました。
TypeScript
TypeScript は、JavaScriptに静的型付けを追加した上位互換言語です。Microsoftが開発・メンテナンスしており、大規模開発での型安全性やIDE支援などが特徴です。トランスパイルにより純粋なJavaScriptコードを生成します。
TypeScriptのサンプルコード
TypeScriptでフィボナッチ数列の10番目を求めて出力するプログラムです。
function fibo(n: number): number {
if (n < 2) {
return n;
} else {
return fibo(n - 1) + fibo(n - 2);
}
}
console.log(fibo(10));
トランスパイル
トランスパイル とは、特定のプログラミング言語で書かれたソースコードを、同じ抽象度レベルの別の言語(または同じ言語の異なるバージョン)に変換するプロセスです。
Rust
Rustは、メモリ安全性と並列処理の安全性を実現するために所有権システムを導入しているプログラミング言語です。
Rustでは所有権システムと借用チェッカーによりコンパイル時に厳格なチェックを行うことで、メモリリークやダングリングポインタを防ぎます。また、スレッド間での安全なメモリ共有を実現し、マルチスレッドプログラミングの難しさを軽減します。
Rustのサンプルコード
Rustでフィボナッチ数列の10番目を求めて出力するプログラムです。
fn fibo(n: u32) -> u32 {
if n < 2 {
n
} else {
fibo(n - 1) + fibo(n - 2)
}
}
fn main() {
println!("{}", fibo(10));
}
WebAssembly
WebAssembly とは、ブラウザ上でネイティブに近い速度で実行できるバイナリ形式の低レベル言語です。WASMと略記することもあります。特にRustはWebAssemblyとの親和性が高く、フロントエンドの高負荷処理(画像処理、ゲームエンジンなど)での活用が進んでいます。
Java
Javaは「 Write Once, Run Anywhere 」を掲げた言語で、バイトコードにコンパイルしてから、JVMで実行するという少し特殊な実行形態を取っています。
大規模開発に向いている言語で、大規模な企業システム(銀行など)で利用されています。
Javaのサンプルコード
Javaでフィボナッチ数列の10番目を求めて出力するプログラムです。
public class Fibonacci {
public static int fibo(int n) {
if (n < 2) {
return n;
} else {
return fibo(n - 1) + fibo(n - 2);
}
}
public static void main(String[] args) {
System.out.println(fibo(10));
}
}
JavaとJavaScript
JavaとJavaScriptは、グレープ(ぶどう)とグレープフルーツくらいの関係しかありません。名前は似ていますが、言語設計、実行環境、用途など全く異なる特性を持っています。
このような名前になった背景は若干複雑です。
JavaScriptはNetscape Communicationsという会社のエンジニアによって開発された言語で、当初は「LiveScript」という名前でした。Netscape Communications社は当時Javaを開発していたSun Microsystems社と提携をしていました。Javaは当時爆発的な人気を集めていたため、その知名度を借りるために「JavaScript」に改名されました。
LLVM
LLVM (Low Level Virtual Machine) とは、コンパイラのフロントエンド(ソースコード解析)とバックエンド(機械語生成)を分離し、中間表現(LLVM IR)を介して最適化を行うコンパイラ基盤プロジェクトです。これにより、新しいプログラミング言語の開発や既存言語の最適化が容易になります。
LLVM IR
LLVMでは、プログラムのソースコードを実行する際に、いきなりアーキテクチャ(x86-64やarm64)に固有の機械語に変換するのではなく、アセンブリ言語と同レベルの抽象化がなされたLLVM IRを経由します。LLVM IRは対応しているアーキテクチャに変換することができます。
いったんLLVM IRを経由することで、プログラミング言語の開発者は複数の種類のアーキテクチャの機械語を出力するコンパイラを書くのではなく、LLVM IRだけを出力するコンパイラを書くことに専念できます。
MLIR
MLIR (Multi-Level Intermediate Representation) は、LLVMプロジェクトから派生した新しい中間表現フレームワークです。
LLVM IRを利用すると、複数の種類のアーキテクチャの機械語を出力しなくて良くなります。しかし、LLVM IRはアセンブリ言語と同レベルの抽象化がなされている(つまりかなり複雑である)ので、複雑な構文を導入したり、適度な最適化を施したりするにはかなりの労力がかかります。また、あるプログラミング言語に対してそれを実装したとしても、他のプログラミング言語ではうまくその技術を活用することができません。
MLIRでは、プログラミング言語からLLVM IRを出力する際に更に複数の中間言語 (Dialect) を挟むことで、他の言語で既に実装された複雑な構文を導入したり、適度な最適化を施したりすることが容易になります。
MLIRのサンプルコード
MLIR の標準Dialect ( arith & scf ) を用いて、フィボナッチ数列の10番目を求めて出力するプログラムの一例です。
module {
func @fibo(%n: i32) -> i32 {
%c1 = arith.constant 1 : i32
%c2 = arith.constant 2 : i32
%cmp = arith.cmpi slt, %n, %c1 : i32
scf.if %cmp -> (i32) {
scf.yield %n : i32
} else {
%n_minus_1 = arith.subi %n, %c1 : i32
%r1 = call @fibo(%n_minus_1) : (i32) -> i32
%n_minus_2 = arith.subi %n, %c2 : i32
%r2 = call @fibo(%n_minus_2) : (i32) -> i32
%sum = arith.addi %r1, %r2 : i32
scf.yield %sum : i32
}
}
func @main() -> i32 {
%c10 = arith.constant 10 : i32
%res = call @fibo(%c10) : (i32) -> i32
return %res : i32
}
}
おわりに
低レイヤー技術や自作言語の世界は、一見難解に思えるかもしれません。しかし、小さなステップから始めれば、誰でも挑戦できます。
この記事にはプログラミングに関する事前知識がなくても読めるように補足をつけたつもりですが、もしわからない点やおかしいと思った点があればコメント欄にて気軽に教えてください。
現在、自作言語やコンパイラ・インタプリタの作成に関して更に踏み込んだ記事も執筆中です。執筆が終わりましたら、そちらも読んでいただけるとありがたいです。
この記事を気に入っていただけた場合は是非いいねとフォローをお願いします。記事執筆のモチベーションになります。