新型コロナに加えて雨の日が多く在宅時間が増えています。そんな時にじっくりと一つのプロジェクトに取り組んでみるのはどうでしょう。そんな時にオススメなのは『自作プログラミング言語の開発』です。今回、手軽に自作言語が作れるツールgoyaccを利用した自作言語の作り方を紹介します。
プログラミング言語開発は積み木と似ている?
筆者は日本語プログラミング言語「なでしこ」の開発をもう15年以上開発しており、プログラミング言語の開発は、ライフワークと化しています。それでも、時々、思いつきで新しい言語を作ってみたり、いつもと違う環境で作ってみたりしています。
研究熱心なのかと言うと、そういう訳ではありません。そもそも、プログラミング言語の開発はとても面白いのです。最近も、Go言語で「なでしこ」を再実装するというプロジェクトに取り組んでいたのですが、一通りの文法を実装するだけなら、1週間程度で実装できてしまいました。
改めてゼロからプログラミング言語を作ってみて、プログラミング言語の開発は、積み木で城を建てるのかと似ているなと思いました。注意深く少しずつ積み木を積み重ねていきます。全体のバランスを見ながら、いかに高い城を作れるか考えながら、コツコツと積み上げます。
しかもプログラミング言語の開発というのは、プログラミングのスキルアップに役立ちます。まず、普段は意識することのない便利なライブラリや機構がどのように実装されているのか考察するきっかけになります。そして、実際に自分で作ってみるという作業を経験することで、プログラミングに対する姿勢を見直す機会にもなります。
Yaccを使うと比較的簡単に作れる
一つのプログラムを作るのに、いろいろな手法やツールが使えるように、プログラミング言語の開発にもいろいろな手法やツールが使えます。プログラミング言語の開発のための便利なツールがあります。それが、パーサジェネレーターと呼ばれるツールです。自作言語の文法を指定すると、その規則に応じて自動的にプログラムを自動生成してくれます。有名なパーサジェネレーターに『Yacc』があります。
Yaccは1970年に開発された歴史あるパーサジェネレーターです。BNF記法に似た構文規則を与えると、規則に応じたプログラムを自動生成してくれるという非常に便利なツールです。なお、BNFとはプログラミング言語やさまざまなマークアップ言語の文法を定義するするのに用いられています。
また、歴史あるツールであるだけあって、いろいろな言語で再実装されています。そして、Go言語で実装されたのが『goyacc』です。Go言語で書かれており、Go言語のプログラムを生成します。
本稿では、手軽に自作プログラミング言語を作ることを目的とするので、このgoyaccを用いて自作言語を作ってみましょう。
今回作るのは簡単な電卓
最初に、goyaccに慣れるために簡単な電卓を作ってみましょう。例えば以下のような計算式を与えると、計算結果を表示します。goyaccの仕組みと便利さを味わうことができるでしょう。
> 1 + 2 * 3
> 計算結果: 7
goyaccのインストール
本連載の一回目を参考にGo言語のインストールが完了していることを想定しています。まずは、goyaccをインストールしましょう。コマンドラインで以下のコマンドを実行します。
go get golang.org/x/tools/cmd/goyacc
go install golang.org/x/tools/cmd/goyacc
「最小計算言語」の生成手順を確認してみよう
今回は簡単な計算(四則演算)だけができるプログラミング言語を作ります。そのため、「最小計算言語」と名付けましょう。
ここで用意したプログラム「parser.go.y」はコメントを含めて68行です。ちょっと長いので、こちらgistのページでソースコードを確認してください。
まずは、goyaccの扱いを確認してみましょう。go installしたプログラムは、環境変数「$GOPATH」のbinディレクトリに配置されます。そのため、$GOPATH/bin/goyaccにパスを通しておくと便利です。この場合、以下のようにコマンドを実行すると、y.goというソースコードを生成します。
goyacc parser.go.y
ちなみに、もし環境変数$GOPATHを指定していない場合、Windowsだと「C:\Users\ (ユーザー名) \go\bin」にgoyacc.exeという実行ファイルが生成されています。この実行ファイルにparser.go.yをドラッグ&ドロップしてもソースコードを生成できます。
そして、生成されたGoのプログラムを実行してみましょう。ここでは、"1+2"をソースコードとして与えて計算させてみます。以下のコマンドをコマンドラインから実行します。
go run y.go "1 + 2"
すると、計算結果として3が表示されます。四則演算が可能です。
それでは、別の計算を実行してみましょう。
go run y.go "1 + 2 * 3"
他にも計算ができるので試してみましょう。ただし、今回手抜きをしているため、整数型の四則計算しかできないのに加えて、一桁の数字しか認識しません。二桁以上の数値を指定するとエラーが出ます。
無事に四則演算が可能な自作言語を完成させることができました。Go言語では手軽に実行ファイルが生成できるのがそのメリットです。実行ファイルに変換してみましょう。以下のプログラムを実行すると、Windowsならcalc.exeが作成されます。
go build -o calc
プログラムを確認してみよう
無事に実行ファイルが作成されたら、プログラム(parser.go.y)を確認してみましょう。goyaccに与える文法規則のファイルは、次のような形式となっています。以下の書式を見ると分かるように、Go言語のプログラムと合わせて文法を記述できます。
%{
// ここに初期化コード --- (*1)
%}
%union {
// ここでトークンの型を宣言 --- (*2)
}
// トークン型などの宣言 --- (*3)
%type<タイプ名> elem1 elem2 ...
%token<タイプ名> token1 token2 ...
%left token3 token4 ...
%%
// ここで文法規則 --- (*4)
%%
// ここにGo言語のプログラム --- (*5)
上記の番号に沿って実際のプログラムを確認していきましょう。(*1)の部分ではGo言語の各種宣言を記述します。パッケージ名と利用するライブラリのimportを記述します。
%{
// プログラムのヘッダを指定
package main
import (
"os"
"fmt"
)
%}
次に、(*2)の部分を見てみましょう。これは、goyaccが自動的に宣言する構造体yySymTypeをどのような構造体にするのかを指定します。なお「%union(共用体)」を指定するのは、もともとyaccの書式の名残と思われます。
%union {
num int
}
ここで、今回の自作言語では整数計算だけを行うため、int型のnumというフィールドのみ宣言しています。より複雑な言語を作る場合、いろいろな型のトークン(最小構成要素)を指定することになります。
続けて、(*3)を見てみましょう。ここでは、開発する言語で扱うトークン型や、演算の優先度などを宣言します。(*4)の部分に出てくる構文要素は全てここに列挙する必要があります。
// 利用するトークンの種類を指定
%type<num> program expr
%token<num> NUMBER
// 演算の優先度の指定
%left '+','-'
%left '*','/'
そして、一番重要なのが(*4)の部分です。ここで文法規則を指定しています。
%%
// 文法規則を指定
program
: expr
{
$$ = $1
yylex.(*Lexer).result = $$
}
expr
: NUMBER
| expr '+' expr { $$ = $1 + $3 }
| expr '-' expr { $$ = $1 - $3 }
| expr '*' expr { $$ = $1 * $3 }
| expr '/' expr { $$ = $1 / $3 }
%%
ここが肝となる部分なので、もう少し詳しい書式を紹介しましょう。波カッコ { ... } の間にGo言語のプログラムを記述します。それ以外の部分は文法の定義を記述します。
文法要素
: 定義1 { Go言語のプログラム1 }
| 定義2 { Go言語のプログラム2 }
| 定義3 { Go言語のプログラム3 }
これを確認した上で改めて実際の文法定義を見てみましょう。まず、programという要素は、exprです。そして、exprはNUMBER、あるいは、expr '+' expr、あるいは、expr '-' expr...と定義しているわけです。
ここでポイントであるのは、要素の定義の中に、要素自身を含めることができるということです。そして、その際の優先順位を(*3)の中で%left ...で指定できます。
そして、最後(*5)の部分では、goyaccで自動生成されたプログラムと組み合わせて使うプログラムを記述します。ここでは最低限、字句解析を行うために、Lexer構造体と、そのメソッドであるLex関数とError関数を定義します。
// 最低限必要な構造体を定義
type Lexer struct {
src string
index int
result int
}
// ここでトークン(最小限の要素)を一つずつ返す
func (p *Lexer) Lex(lval *yySymType) int {
for p.index < len(p.src) {
c := p.src[p.index]
p.index++
if c == '+' { return int(c) }
if c == '-' { return int(c) }
if c == '*' { return int(c) }
if c == '/' { return int(c) }
if '0' <= c && c <= '9' {
lval.num = int(c - '0')
return NUMBER
}
}
return -1
}
// エラー報告用
func (p *Lexer) Error(e string) {
fmt.Println("[error] " + e)
}
そして、Lex関数に指定するのは、ソースコードを少しずつ読み進めて、要素を返すようにします。Lex関数の戻り値には、上記(*3)で宣言したトークン名を指定します。生成されたGoのプログラムを見ると分かりますが、(*3)の部分に記述するとgoyaccが定数として宣言してくれます。なお、数値などの実際の値は、引数として与えられるyySymType構造体のフィールドに設定します。
それから、Error関数にはエラーメッセージをどのように処理するのかを指定します。
まとめ
以上、今回はgoyaccの使い方に重点をおいて、自作プログラミング言語の作り方を紹介しました。goyaccの使い方さえ覚えてしまえば、自作言語の開発がとても簡単になります。とは言え、プログラミング言語の仕組みを紹介するのは、連載の一回では足りません。次回、改良方法を紹介していきます。お楽しみに。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。