前回から、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' <= c && c <= 'z' {
      lval.word = string(c)
      return WORD
    }
  }
  return -1
}
func isOperator(c rune) bool { // 演算子か
  return c == '+' || c == '-' || 
    c == '*' || c == '/' || c == '%' ||
    c == '(' || c == ')' || c == '='
}
func isDigit(c rune) bool { // 数字か
  return '0' <= c && c <= '9'
}

字句解析のLex関数が15行から50行まで増えました。前回のLex関数では1文字1トークンにしていたために簡潔でしたがちょっと長くなりました。しかしその分、多機能です。詳しく見ていきましょう。

プログラムの(*1)の部分で空白文字は読み飛ばし、(*2)で実数の値を読みます。そして、(*3)で区切り記号を識別し、(*4)で演算子記号、(*5)では変数名を読みます。もし、将来的にもっといろいろなトークンを扱いたい場合、(*2)の実数を読む処理の後ろに、同じような感じで読み取り処理を書いていくことができます。

文法規則を改良しよう

次に文法規則を改良してみましょう。まず、複数の計算式を扱えるようにし、変数定義も扱えるようにします。

%%
// program は line の繰り返し --- (*1)
program : line | program line

// line は 代入文か計算式 --- (*2)
line
  : let LF  { $$ = $1 }
  | expr LF { $$ = $1; fmt.Println("計算結果:", $1) }
  | LF

// 代入文 --- (*3)
let
  : WORD '=' expr { hensu[$1] = $3 }

// 計算式 --- (*4)
expr
  : NUMBER
  | WORD          { $$ = hensu[$1] }
  | expr '+' expr { $$ = $1 + $3 }
  | expr '-' expr { $$ = $1 - $3 }
  | expr '*' expr { $$ = $1 * $3 }
  | expr '/' expr { $$ = $1 / $3 }
  | expr '%' expr { $$ = float64(int($1) % int($3)) }
  | '(' expr ')'  { $$ = $2 }
%%

文法定義の(*1)を確認しましょう。前回見たように、goyaccの文法定義は以下のような書式となっています。

要素名
    : 定義1 { Goのプログラム1 }
    | 定義2 { Goのプログラム2 }
    | 定義3 { Goのプログラム3 }

これを踏まえて見ていきましょう。最初(*1)ではプログラムが複数の計算式を扱えるようにします。文法要素programはline、または、program lineです。つまり、lineが繰り返されるという意味になります。

(*2)の部分では、要素lineは代入文(let)または計算式(expr)であることを定義します。その際、計算式であれば計算結果を表示するように指定します。

(*3)では代入文を定義します。プログラム内の変数を管理するために、map[string]float64型のhensuというグローバル変数を定義しました。それで、ここで変数に値を代入するようにしました。

(*4)では計算式を定義します。計算式はNUMBER(数値)、WORD(変数)、四則演算、または'('計算式')'であると定義しています。ここでは、直接値を計算します。

プログラムを実行してみよう

ここまでのプログラムを、こちらにアップしました。上記のプログラムをparser.go.yという名前で保存しましょう。そして、goyaccをインストールした状態で以下のコマンドを実行します。

# goyaccでGoのプログラムを生成
goyacc parser.go.y
# プログラムを実行
go run y.go "(1 + 2) * 3"

コマンドを実行すると、『(1 + 2) * 3』が計算されて、9が表示されます。

  • Goのプログラムが生成されて計算が行われたところ

    Goのプログラムが生成されて計算が行われたところ

うまくいったら、ビルドして実行ファイルを生成してみましょう。以下のコマンドを実行すると、yという名前の実行ファイルができます。

go build y.go

いくつか計算させて遊んでみましょう。『./y "プログラム"』(※Windowsのコマンドライン環境では『y "プログラム"』)のように書いて計算を行うことができます。すると、変数計算が行われ、計算結果に23と表示されます。

./y "a=2;b=3;a*10+b"
  • 変数計算も可能

    変数計算も可能

※Windowsのコマンドライン環境での上記実行

y "a=2;b=3;a*10+b"
  • ((※Windowsのコマンドライン環境での実行)

とは言え、せっかく複数行のプログラムが書けるようになったのに、コマンドラインで使うだけではつまらないものです。対話式に使えるようになっています。

「./y」(※Windowsのコマンドライン環境では「y」)とコマンドを打ってみましょう。コマンドライン引数を与えないと対話式にプログラムを実行できます。

  • 対話式にプログラムを実行したところ

    対話式にプログラムを実行したところ

  • ((※Windowsのコマンドライン環境での実行)

自作プログラミング言語開発のまとめ

以上、簡単でしたが、自作のプログラミング言語を作ってみました。今回は少し長くなって100行ちょっとのプログラムになりましたが、変数を利用した計算もできる言語を作ることができました。

実際、オープンソースのプログラミング言語は、当然、ソースコードが公開されているので、プログラミング言語開発の参考になります。Go言語を利用して作られたプログラミング言語は、いろいろあり、Lua言語をGo言語で実装したGopherLuaや、JavaScriptをGo言語で実装したotto、そして、筆者が開発しているなでしこ3のGo言語版などがあります。

これらは、参考になるものの、規模がそれなりにあるので、なかなかソースコードを把握するのが大変です。そこで、200行未満のプログラムなら、ちょっと頑張れば理解できることでしょう。

自作言語は育てる楽しみがあります。今回紹介したように、字句解析関数のLexと文法定義部分を修正していくことで、プログラミング言語にどんどん機能を追加していくことができます。本稿を参考に、自分だけの自作言語作りに挑戦してみるのはどうでしょうか。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。