前回から、goyaccを利用して自作プログラミング言語の作り方を紹介しています。前回はYaccの使い方を紹介するため「最小計算言語」を作ってみました。今回は複数行のプログラムに対応させ、変数も使えるように改良してみましょう。

今回作ったプログラミング言語「最小計算言語改」を実行したところ

○プログラミング言語が動くまで

さて、前回は雰囲気を味わって貰うため、プログラミング言語が動く仕組みなど、すっ飛ばして、動くものを作ってみました。ここで改めて、プログラミング言語が動く仕組みを確認してみましょう。

プログラミング言語が動くまでの手順は以下の通りです。もちろん、プログラミング言語の種類によって、それぞれの手順に差異や処理はありますが、だいたいこんな手順で動きます。

・(1)ソースコードを読む

・(2)字句解析

・(3)構文解析

・(4)コード生成

・(5)コードを実行

図にすると以下のようになります。

プログラミング言語が動くまでの手順

詳しく確認してみましょう。最初(1)でプログラムのソースコードを読み込みます。そして、(2)ソースコードを字句解析して個々のトークンに分割します。トークンというのは、数値や記号、文字列など意味のある最小のまとまりのことです。

次いで、(3)トークン列がどのように並んでいるのか調べて意味のある構文として解析します。これは、この図にあるように計算の順序を調べたり、条件分岐や繰り返し文や関数など、プログラムの構文を見極めます。

それから、(4)解析済みの構文に基づいて、プログラムがしやすい形式にコードを生成します。もし、C言語/Go言語のようなコンパイラであれば、実行可能なバイナリを生成しますし、JavaやC#などの言語であれば、仮想マシンで実行可能な中間コードを生成します。最近では、PythonやRubyなど、多くのスクリプト言語も、一度中間コードを生成し、そのコードを実行することで、プログラムが効率的に実行されるような仕組みになっています。

最後、(5)生成されたコードを実行します。なお、この図で生成されたコードは、ちょうど数式が逆ポーランド記法のように並び変えられていますが、多くの仮想マシンはこのように単純な値と命令の羅列を順に読み込んで計算処理を実行します。

○goyaccは何を自動化してくれるのか?

そして、上記の手順のうち、goyaccが引き受けてくれるのが(3)の構文解析の部分です。

前回のプログラム( --- こちら)を、なぞって紹介してみましょう。

プログラムのmain関数で、コマンドラインからソースコードを読み込みます。これが(1)のプログラムの読み込み部分に相当します。そして、(2)の字句解析ですが、ここではプログラムの最小構成単位を一文字とすることで処理を簡易化しています。goyaccでは分割したトークンを得るために、Lex関数を呼び出します。そのため、Lex関数に分割したトークンを一つずつ渡すようにプログラムを作ります。

そして(3)の構文解析はgoyaccが担当します。構文規則を記述することで、goyaccが構文解析のプログラムを自動生成します。なお、この小さなプログラミング言語では、コード生成をせず構文を解析しながら計算処理を実行しました。

自作言語の良いところは、上記のような定石のプロセスを踏襲して開発することもできますし簡易化もできるというところでしょう。

○最小計算言語に変数と関数の機能を加えよう

プログラミング言語が動くまでを確認したところで、前回の最小計算言語を改良して、実数や変数が使えるようにしてみます。これで、計算機よりちょっと便利と言えるくらいになることでしょう。

○字句解析関数を改良しよう

まず、字句解析をもう少し本格的に行い、最初に扱える数字を一桁に制限せず、実数や変数名を読み込めるように改良しましょう。そのために、Lex関数を以下のように書き換えましょう。

// トークンを一つずつ返す

func (p *Lexer) Lex(lval *yySymType) int {

for p.index < len(p.src) {

c := p.src[p.index]

// スペースなら飛ばす --- (*1)

if c == ' ' || c == '\t' {

p.index++

continue

}

// 数値の場合 --- (*2)

if isDigit(c) {

s := ""

for p.index < len(p.src) {

c = p.src[p.index]

if isDigit(c) || c == '.' {

s += string(c)

p.index++

continue

}

break

}

lval.num, _ = strconv.ParseFloat(s, 64)

return NUMBER

}

p.index++ // これ以降1文字1トークン

// プログラムの区切り記号の場合 -- (*3)

if c == ';' || c == '\n' {

return LF

}

// 演算子の場合 --- (*4)

if isOperator(c) {

return int(c)

}

// 変数の場合 --- (*5)

if 'a'