プログラミング言語の作り方

javascript/C言語/アセンブラを用い、 字句解析、構文解析、インタプリタ、コンパイラのプログラムをスクラッチから作りながら、 「プログラミング言語の作り方」を解説する。

プログラミング言語の作り方の概要

プログラミング言語を作るにはどうすれば良いのか。

ページメニュー

プログラミング言語を作るのは本当に難しいのか?

自作のプログラミング言語を作るのは、どれくらい難しいのだろうか?

プログラミング言語の作り方の解説本やwebサイトなどでは、 遠まわしで難しい説明がなされていることが多いように思う。

飛行機を作るのに、最初から戦闘機や巨大な旅客機を作る人はいない。

航空力学が分からなくても、エンジンが設計できなくても、紙飛行機は子供でも作れる。

紙飛行機だって飛行機だ。ちゃんと飛ぶ。飛べば飛行機なのだ。

そりゃ、ボーイングやエアバス社で働いている専門家なら紙飛行機ではダメだ。

自作プログラミング言語を作りたいと思う人のほどんどは、 プログラミング言語を開発しているマイクロソフト、アップル、google、oracleなどに、 勤めているプログラミング言語の専門家ではない。

例え、紙飛行機でもすぐに飛べるようなプログラミング言語を、さくっと簡単につくろう。

プログラミング言語を作る流れ

では、具体的に、独自のプログラミング言語を作るには、どうしたら良いだろうか?

自作プログラミング言語で書かれたソースコードは、 機械語や制御コードが入ったバイナリデータではなく、英数字記号でできた「単なる文字列」だ。

しかも、実生活で使われる文章のような自由度の高い曖昧なものではなく、 「限られた単語しかない、とても簡単なルールに合致した、機械的な文字列」だ。

例えば、人工知能を作る場合、人間の問いかけが多少ルールに合致していなくても、 複雑な理論とロジックで何とか意味を解釈して、 人間の意図通り動かさないといけないので、とてもハードルが高い。

しかし、プログラミング言語の場合は、ソースコードがルールにちょっとでも合致していなければ、 問答無用でエラー吐いて終了させてしまえばよく、気楽に割り切れる分、とてもハードルが低い。

つまり、「プログラミング言語を作る」とは、 「ルールに合致した機械的な文字列だけを扱う、割り切ったプログラムを作る」ことだ。

こういう割り切ったプログラムを作れば良いので、これから説明する処理も簡単なものになる。

詳細は後述するが、「インタプリタ」「コンパイラ」というのは聞いたことがあるだろう。

これら2つに共通する処理として、「字句解析」「構文解析」がある。

小難しい言い方だが、呼び名がないと説明しづらいので、この4つの用語は覚えよう。

字句解析とは

プログラミング言語で書かれたソースコードは、1つの長い文字列だ。

字句解析というと、とっつきにくいが、要は、1つの長い文字列をそのまま扱うより、 最初に、単語など、意味のある単位(トークン)に、区切っておく方が 便利だよね、というだけの話だ。

具体的に言えば、以下のような自作プログラミング言語のソースコードがあったとき、

hensuu=13-5*2;
print(hensuu);

ソースコードのファイルの中身を1つの長い文字列として、sourceという変数に読み込んだ状態が以下だ。

source = '(@hensuu=13-5*2;
print(hensuu);@)';

結局、字句解析とは、source変数をトークンで区切り、tokens配列に入れておくという簡単な文字列処理だ。

tokens = [
           (@'hensuu'@),  (@'='@),     (@'13'@),    (@'-'@),            (@'5'@),       (@'*'@),     (@'2'@),    (@';'@),
           (@'print'@),         (@'('@),    (@'hensuu'@),     (@')@)',       (@';'@) 
];

具体的な実装は、後述するが、 文字列分割するsplitのような機能や、正規表現などは、最近の言語には備わっている。

そんな機能がないC言語で、ベタに文字列分割機能を実装するにしても、 source変数を、先頭から一文字ずつ、アルファベットか数字か記号か チェックしていくだけだ。

なので、字句解析は、簡単な文字列分割機能だけで十分なのは明確だ。 複雑な理論に基づいた処理は不要だ。

構文解析とは

tokens配列を人間が見ると、何の苦労もなく、以下のようにイメージすることができる。

tokens配列を、この順番になるように変換する作業が構文解析だ。

構文解析の実装は後述するが、ポイントは、「hensuu = X」というところや、 「13 - X」というところで、「おあずけ」を食らっているところだ。

いったん処理を保留し、別の処理を先にやり、再度再開するような、おあずけを実現する手段が必要だ。

しかし、何のことはない、関数の途中呼び出しで、おあずけを実現できる。

代入を解析する関数の途中で、引き算を解析する関数を呼び、そのまた途中で、 掛け算を解析する関数を呼ぶだけだ。

つまり、掛け算の解析が終わると、10がかえってきて、引き算の解析の途中から再開し、 13-10が処理され、3が返ってきて、 代入を解析する関数が途中から再開されて、hensuuに3が代入される、といった具合だ。

ここで、この解析途中にその過程を、以下のようなtree構造のデータにしておくと、 どこを先に処理するべきかを、人間が見ても、プログラムが見ても扱いやすい。

{
    left:{
            left: 'hensuu',
            op: '=',
            right: {
                       left:'13',
                       op:'-',
                       right:{
                                 left:'5',
                                 op:'*',
                                 right:'2'
                             }
                   }
          },
    op:';'
    right:{
              left:'print',
              op:'()',
              right:'hensuu'
           }
}

名前がないと説明しづらいので、このtree構造を、抽象構文木(AST)と呼ぶ。 treeの細かい構造をどうするかは、自分の好きなように表現すれば良い。

構文解析とは、tokens配列を、astのtree構造に、変換しているだけだ。 関数で「おあずけ」を表現でき、難しい理論は不要で、淡々と実装していけばよいことが分かる。

インタプリタとは

インタプリタでは、字句解析、構文解析の後、astをそのままの流れで実行する。

astを作らずに、構文解析の途中で即実行しながらやっても良い。

しかし、扱う処理が多くなると、わけわからなくなるので、astに一度出力して置いた方が便利だ。

結局、インタプリタは、astをそのまま実行するだけだ。

コンパイラとは

コンパイラでは、字句解析、構文解析の後、astをもとに、アセンブラを出力するだけだ。 実行はしないし、できない。

出力されたアセンブラは単なるテキストファイルだ。

アセンブラのテキストファイルは、asコマンドで、機械語(バイナリ)に変換する。

この時、コンパイラは、あたかも自分がやっているかのように、as外部コマンドを叩くだけだ。 このアウトプットは、オブジェクトファイルと呼ばれる中間形式で、実行ファイルではない。

その後、リンカと呼ばれるldコマンドで、(分割コンパイルされた)複数のオブジェクトファイルを、 連結して1つの実行ファイルにする。 またしても、コンパイラは、あたかも自分がやったように、このld外部コマンドを叩くだけだ。

コンパイラとしては、実行ファイルが作られると、もう何もすることがないし、実行にかかわることもできない。

ちなみに、いざ、実行ファイルを、コマンドラインから実行すると、execシステムコールが呼ばれる。

OSは、バイナリ中で指定された、ローダーと呼ばれる外部プログラムを起動する。

ローダーは、実行ファイルや動的ライブラリなどを、メモリ上に配置して、 指定されたエントリポイントからCPUに制御を渡す。つまり実行される。

寄り道したが、結局、コンパイラは、astからアセンブラに変換するだけだ。


このページの目次へ戻るサイトの最上位へ戻る