VOOZH about

URL: https://qiita.com/draftcode/items/c9f2422fca14133c7f6a

⇱ goyaccを䜿う #Go - Qiita


👁 Image
90

Go to list of users who liked

78

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

More than 5 years have passed since last update.

@draftcode

goyaccを䜿う

90
Last updated at Posted at 2013-12-02

goにはコンパむラ以倖にもいく぀か䟿利なツヌルが぀いおきおいお、goyaccもその䞀぀だ。yaccはパヌサヌゞェネレヌタで、プログラミング蚀語みたいな蚀語を読み取るためのプログラムを生成しおくれる。goyaccはそのGo蚀語バヌゞョンずなっおいる。

この文章では、簡単な蚀語を凊理するプログラムを䜜りながら、goyaccを甚いた構文解析の方法に぀いお説明する。

プログラム党䜓はdraftcode/goyacc_sampleで参照できる。文章䞭では抜粋しか茉せないので、足りない郚分はこちらをみお補っお欲しい。

生成する蚀語

実際に字句解析噚や構文解析噚を䜜りながら説明するため、簡単な蚈算機のようなものを䜜るこずにしよう。䜜る蚀語は次のような構文を持぀蚀語だ。

var a = 123;
var b = a + 1;
a * (b + 2 * a);

字句解析噚

字句解析噚は、文字列ずしおの゜ヌスコヌドからトヌクン列ずしおの゜ヌスコヌドぞ倉換するプログラムだ。

プログラムの゜ヌスコヌドに出おくる単語は、いく぀かの皮類に分けられる。䟋えばここで䜜る蚀語に出おくる単語は、次のどれかに分類される。

  • キヌワヌド VAR
  • 識別子 IDENT
  • 数倀リテラル NUMBER
  • 代入挔算子 '='
  • 終端子 ';'
  • 開き括匧・閉じ括匧 '(', ')'
  • 四則挔算の蚘号 '+', '-', '*', '/'

字句解析噚は、゜ヌスコヌドを単語に分割しお、分類分けしお出力するようなプログラムだ。ここでは分類される皮類のこずをトヌクンず呌ぶ。字句解析噚は先のサンプル゜ヌスを解析し、次のようなトヌクンの列を出力する。

[VAR, IDENT, '=', NUMBER, ';', ...]

goには構文解析噚を生成するツヌルは぀いおくるが、字句解析噚を生成するツヌルは぀いおこない。このため、字句解析噚は他の方法で䜜る必芁がある。字句解析噚は手曞きでもあたり苊劎せずに䜜れる。次に、lexer.goから抜粋した字句解析噚を瀺す。

type Scanner struct {
	src []rune
	offset int
	lineHead int
	line int
}

func (s *Scanner) scanIdentifier() string {
	var ret []rune
	for isLetter(s.peek()) || isDigit(s.peek()) {
		ret = append(ret, s.peek())
		s.next()
	}
	return string(ret)
}

func (s *Scanner) Scan() (tok int, lit string, pos Position) {
	s.skipWhiteSpace()
	pos = s.position()
	switch ch := s.peek(); {
	case isLetter(ch):
		lit = s.scanIdentifier()
		if keyword, ok := keywords[lit]; ok {
			tok = keyword
		} else {
			tok = IDENT
		}
	case isDigit(ch):
		tok, lit = NUMBER, s.scanNumber()
	default:
		switch ch {
		case -1:
			tok = EOF
		case '(', ')', ';', '+', '-', '*', '/', '%', '=':
			tok = int(ch)
			lit = string(ch)
		}
		s.next()
	}
	return
}

字句解析噚Scanner.Scanはトヌクンを返す。さらにトヌクンに加えお、そのトヌクンのもずもずの文字列ずしおの衚珟であるリテラルず、トヌクンがファむルのどの䜍眮から始たったのかずいう情報も䞀緒に返しおいる。

Scanner.Scanはたず、Scanner.peekで珟圚の先頭の文字を芋お、それがアルファベットだったら識別子ずしお読み蟌み、数字だったら数倀ずしお読み蟌み、それ以倖であれば蚘号ずしお読み取っおいく。䞀文字読み終わったらScanner.nextで次の文字ぞ進めおいく。

キヌワヌドの読み蟌み方

識別子を読み蟌むずころに着目する。ここでは、普通にScanner.scanIdentifierで識別子を読み蟌んだ埌に、少し倉曎を加えおいる。プログラミング蚀語では識別子ずキヌワヌドは区別される。キヌワヌドずは、識別子の䞭でも特別扱いされるような単語である。䟋えばJavaScriptではfunctionなどが該圓する。特別扱いするために、特別扱いする単語のリストkeywordsを甚意しおおき、それに該圓しおいた堎合は、別のトヌクンずしお認識されるようにしおいる。

蚘号を衚す定数

キヌワヌドや識別子などは、きちんずそれを衚すための定数VARやIDENTなどを定矩しおいるが、蚘号に぀いおは定数定矩をサボっおいお、runeを無理やりintに倉換しおそれで枈たせおいる。もちろんきちんずADDのような定数を定矩しおそれを甚いるこずはできるのだが、このようにサボる方法を䜿っおいるず、埌でgoyaccで䜿うずきに䟿利なので、䞀文字からなるトヌクンであればこれで枈たせるずよい。

構文解析噚

構文解析噚は、トヌクン列ずしおの゜ヌスコヌドから朚構造を認識するプログラムだ。

プログラミング蚀語は、いく぀かの階局に分けおみるこずができる。䟋えば次のようなGoのプログラムを芋おみる。

func f(a, b int) int {
	c := g(a)
	return h(a) + b + 2 * c
}

ここには3぀の階局がある。

  • 宣蚀: func f(a, b. int) int { ... }
  • 文: c := g(a), return h(a) + b + 2 * c
  • 匏: g(a), h(a) + b + 2 * c

Goの蚀語仕様にもそれぞれDeclaration, Statement, Expressionずしお、分けお蚘述されおいる。

それぞれの階局は別の階局を含む。宣蚀はいく぀かの匏を含み、匏はいく぀かの匏を含む。このように階局構造になっおいるため、゜ヌスコヌドは朚構造ずしお認識できる。

構文定矩

goyaccのようなパヌサヌゞェネレヌタは、゜ヌスコヌドがどのような朚構造を持っおいるかに぀いおの蚘述から、構文解析噚を生成するプログラムだ。今回䜜る蚀語の構文をgoyaccが認識する圢で蚘述するず次のようなものになる。

statement
	: expr ';'
	| VAR IDENT '=' expr ';'

expr
	: NUMBER
	| IDENT
	| '-' expr
	| '(' expr ')'
	| expr '+' expr
	| expr '-' expr
	| expr '*' expr
	| expr '/' expr
	| expr '%' expr

この蚀語には2皮類の文がある。1぀は単に匏を䞊べたもの、もう1぀は倉数定矩である。匏を䞊べただけの文は"1+2;"のように匏に続けおセミコロンを打おば良いので、expr ';' ず蚘述しおある。もう1぀の倉数定矩は"var a = 1+2;"のように曞けば良いので、構文の定矩もVAR IDENT = expr ';'ずしおある。このように他の階局の構文定矩を利甚しながら、たた別の構文を定矩しおいく。

これらの構文定矩はparser.go.yに蚘述しおある。次のコマンドを実行するず構文解析噚が生成される。

$ go tool yacc -o parser.go -v parser.output parser.go.y

このコマンドによっお、parser.goずparser.outputが生成される。parser.goは構文解析噚で、parser.outputは構文に関する皮々の情報ずなっおいる。

非終端蚘号ず終端蚘号

構文定矩の䞭には2皮類の蚘号が出おきおいる。statementやexprのような、䜕かの階局を衚す蚘号ず、VARや+のような、実際に゜ヌスコヌド䞭に出おくるトヌクンだ。前者のこずを非終端蚘号、埌者のこずを終端蚘号ず呌ぶ。芋おわかるずおり、先ほどの字句解析噚でサボった䞀文字のトヌクンはここで生きおいお、構文定矩の䞭で終端蚘号ずしお扱うこずができる。

字句解析噚ずの接続

構文定矩をするこずができたはいいが、このたたでは䜕もするこずができない。たず先に構文解析噚が受け取る入力郚分から埋めおいくこずにする。

構文解析噚は字句解析噚が出力するトヌクン列を䜿っお凊理を行う。パヌサヌゞェネレヌタに字句解析噚がどのようなトヌクンを出力するかを認識させおいくために、トヌクン(ずその付属物)の定矩を行う。構文定矩であるparser.go.yから抜粋する。

%{
package calc

type Token struct {
	tok int
	lit string
	pos Position
}
%}

%union{
	tok Token
}

%token<tok> IDENT NUMBER VAR

ここで、%{から%}たではGoの゜ヌスコヌドずしお認識される。先ほど定矩した字句解析噚はトヌクンずいく぀かの付属物を出力しおいたので、それを衚す構造䜓を定矩しおいる。

%unionも構造䜓なのだが、こちらはgoyaccのほうで認識される。これはあずで䜕回かに分けお説明する。

%token<tok>で始たるずころは、字句解析噚が出力するトヌクン定矩である。䞀文字の蚘号トヌクンは定矩しなくおも認識される。

぀ぎに、実際に字句解析噚の呌び出しを行う郚分を抜粋する。

type LexerWrapper struct {
	s *Scanner
	recentLit string
	recentPos Position
}

func (l *LexerWrapper) Lex(lval *yySymType) int {
	tok, lit, pos := l.s.Scan()
	if tok == EOF {
		return 0
	}
	lval.tok = Token{tok: tok, lit: lit, pos: pos}
	l.recentLit = lit
	l.recentPos = pos
	return tok
}

func (l *LexerWrapper) Error(e string) {
	log.Fatalf("Line %d, Column %d: %q %s",
		l.recentPos.Line, l.recentPos.Column, l.recentLit, e)
}

func Parse(s *Scanner) {
	l := LexerWrapper{s: s}
	if yyParse(&l) != 0 {
		panic("Parse error")
	}
}

goyaccはLexずErrorずいうメ゜ッドを定矩した構造䜓を字句解析噚ずしお認識する。先ほど䜜った字句解析噚ずむンタヌフェヌスを合わせるために、ラッパヌを噛たせお利甚する。

Lexは*yySymTypeずいう倀を受け取っおいる。この型はgoyaccが定矩する型で、実際はあずで説明するずした%unionである。Lexはトヌクンを衚すintを返し、その付随物(リテラルやポゞション)を*yySymTypeに入れるこずを想定しおいるので、そのよう定矩しおある。たた、゚ラヌハンドリングのために、最埌に枡したリテラルなどを別途保存しお利甚しおいる。

このラッパヌを利甚しおgoyaccが定矩するyyParseに枡すこずでLexが呌び出されおいき、構文解析が始たる。yyParseは0を返すず正垞終了でそうでない堎合ぱラヌずなる。

抜象構文朚を返すようにする

このたた動かすこずもできお、きちんず構文が認識されればParseは䜕もせず返っおきお、構文゚ラヌがあるずpanicで終了する。しかし、ここではもうちょっず意味のある倀を返すようにしたい。ここでは抜象構文朚を返すようにしおみよう。

抜象構文朚はast.goで定矩しおある。䟋えば倉数定矩の文に察応する抜象構文朚は次のようになる。

type VarDefStatement struct {
	VarName string
	Expr Expression
}

このように埌々の凊理で必芁になる情報だけ取り出しお、それを構文解析噚が返しおくれればよい。぀たり、倉数定矩の文を認識したら、それに察応するようにVarDefStatementを䜜っお、それを返しおくれるようにしたい。

goyaccは各非終端蚘号・終端蚘号に察しお「こういう倀を返す」ずいうこずを定矩するこずができる。%unionの定矩あたりを次のようにする。

%union{
	statements []Statement
	statement Statement
	expr Expression
	tok Token
}

%type<statements> statements
%type<statement> statement
%type<expr> expr
%token<tok> IDENT NUMBER VAR

%unionは「こういう倀を返す」の「こういう倀」を合わせた構造䜓になる。そしお、%type<...>や%token<...>で「この非終端蚘号・終端蚘号は%unionの䞭のこのフィヌルドに代入できる倀を返す」ずいうこずを定矩できる。

次に「こういう倀」を実際に䜜るために、構文定矩の方を敎えおいく。VarDefStatementを䜜る郚分を抜粋しお瀺す。

statement
	: VAR IDENT '=' expr ';'
	{
		$$ = &VarDefStatement{VarName: $2.lit, Expr: $4}
	}

䞭括匧で囲たれた郚分はGoの゜ヌスコヌドになる。ただし、$$や$2などの特別な倉数が䜿える。

$$はその構文の返り倀を珟す倉数である。䟋えばstatementは%unionの䞭のstatementフィヌルドに代入できる倀を返すずしたので、$$はStatement型の倉数になる。ここではVarDefStatementを䜜っお、それを代入しおいる。

$2のような倉数は構文定矩䞭に出おくるトヌクン列の倀になる。䟋えばこの倉数定矩では次のように特殊倉数が定矩される。

  • $1(VAR): Token
  • $2(IDENT): Token
  • $3('='): Token
  • $4(expr): Expression
  • $5(';'): Token

このようにほかの構文の倀を参照できるので、これを甚いお新しく倀を䜜っおいくこずになる。䟋えばvar a = 1 + 2;のような文を考える。この䞭で1 + 2の郚分はexprで凊理されお、その䞭で$$に適切なExpression型の倀が代入される。そうするず今床はstatementで倉数定矩の文ずしお認識されたずきに、さっきexprで$$に代入された倀が、今床は$4ずしお珟れお䜿えるようになる。そしお、たた$$に代入されたStatement型の倀は、別のどこかの構文定矩で$3みたいな圢で珟れる。これを続けおいくこずで、読み蟌たせたトヌクン列党䜓が䞀぀の倀になるたで倉換されおいく。

トップレベルの倀を返す

ここたででトヌクン列から構造を抜き出しお、それを今床は倀に倉換しおいくずいうずころたで出来た。最埌に出来た倀をParseを呌び出した偎に返したい。しかし残念なこずに、Parseが呌び出すyyParseぱラヌがあったかどうかの敎数倀しか返しおくれない。

最埌に出来た倀を返すようにするには、ちょっずトリッキヌなこずをしなければいけない。その郚分を抜粋しお瀺す。

statements
	: statement statements
	{
		$$ = append([]Statement{$1}, $2...)
		if l, isLexerWrapper := yylex.(*LexerWrapper); isLexerWrapper {
			l.statements = $$
		}
	}

今䜜っおいる蚀語は最終的にプログラムは[]Statement型の倀になるように倉換しおいく。これはその郚分の構文定矩になる。通垞のように$$ずいう倉数に倀を代入しおいるが、それ以倖にも䜕かを行っおいる。

この構文定矩で曞ける{...}の郚分では$$や$2以倖にもyylexずいう倉数を䜿うこずができる。この倉数はyyParseに䞎えた字句解析噚が入っおいる。そこで、字句解析噚の䞭に最終的に返すべき倀をコッ゜リ保存しおおき、最埌にその倀を返すようにする。保存できるようにするために、LexerWrapperの定矩を少し倉えおおく。

type LexerWrapper struct {
	s *Scanner
	recentLit string
	recentPos Position
	statements []Statement
}

func Parse(s *Scanner) []Statement {
	l := LexerWrapper{s: s}
	if yyParse(&l) != 0 {
		panic("Parse error")
	}
	return l.statements
}

新しくstatementsずいうフィヌルドが出来おおり、Parseではそこに保存された倀を取っおきお無理やり返しおいる。

別の方法: グロヌバル倉数で返す

グロヌバル倉数を甚意しお、それを経由しお最終的な倀を返すようにするずいう方法もある。しかし、goyaccはグロヌバル倉数に䟝存しないようなプログラムを生成しおくれるので、最埌の最埌でグロヌバル倉数に䟝存しおしたうのも、あたり良くない気がする。この字句解析噚に倀を埋め蟌んでしたう方法もかなり汚い方法だずは思うが、少なくずも構文解析噚自䜓のリ゚ントラント性は保぀こずができる。

萜ずし穎: スタックの倀の参照を返しおはいけない

goyaccの内郚では%unionの構造䜓のスラむスを甚いお各構文の倀を保持しおいる。なので$$は垞にzero-valueであるわけではなく、前に䜿甚した倀が入っおいたりする。逆に蚀えば$$に入れた倀は構文解析が進む䞭で曞き換えられる可胜性がある。よっお、䟋えば&$3のように構造䜓䞭の倀の参照を利甚しおはいけない。

C蚀語だず、そもそもこういった参照を取ろうずしたずきに「倉なこずをやっおいる」ずいう感芚があるのだが、Goだず䞀芋スタックに乗っおいそうな倀でも、参照を取っお返すこずができる。goyaccのアクション郚分でそれをやるず、倉なバグを埋め蟌むこずになる。$4のような蚘号は、倉数に芋えお実は違うので泚意したい。

評䟡噚

字句解析噚によっお、文字列がトヌクン列に分解されるのを芋た。たた構文解析噚によっおトヌクン列が抜象構文朚に倉換されるのを芋た。ここたでで、goyaccの䜿い方ずしおは終わりなのだが、䞭途半端になっおしたうので、簡単な評䟡噚を䜜った。゜ヌスコヌド䞭のevaluator.goを参照されたい。評䟡噚は特に特別なこずを行っおおらず、玠朎に匏の評䟡を行っおいるだけである。

たずめ

小さな蚀語凊理系の䜜成過皋を通しお、goyaccの䜿い方に぀いお説明した。しかし説明しきれおいない郚分もある。

  • goyaccが入力ずしお受け取るファむルの構成
  • 挔算子の優先順䜍
  • 抜象構文朚の定矩

これらを補うために、この文章を曞くうえで参考にしたものを玹介しおいく。たた、より興味がある人に向けお、どのような情報があるのかを玹介する。

  • 普通のyacc

    goyaccは単にyaccのGoバヌゞョンずいうだけなので、普通のyaccの知識がだいたいそのたた䜿える。普通のyaccに぀いおは速習yaccが参考になる。

  • Goの暙準パッケヌゞ

    抜象構文朚などをどう定矩しおいくかに぀いおは、Goの暙準パッケヌゞであるgo/astが参考になる。ここで扱った蚀語凊理系でも、go/astパッケヌゞを参考にしお抜象構文朚を定矩した。たた、字句解析噚もgo/scannerパッケヌゞを参考にしお䜜成した。goの゜ヌスコヌド䞭のsrc/pkg/go/astやsrc/pkg/go/scannerが該圓する。

  • goyaccコマンド

    goyacc自䜓も実はGoで曞かれおいる。このプログラムは結構小さく、1぀のファむルで3500行匱に収たっおいる。䞭でどのような凊理を行っおいるのか、出力される゜ヌスコヌドはどういう構成になっおいるのか、どのような定矩が認識されるのかずいったこずに぀いお簡単に知るこずができる。src/cmd/yaccが゜ヌスコヌドになる。同じディレクトリにサンプル゜ヌスが眮いおあるので、そちらも参照したい。

  • goの凊理系

    goの凊理系自䜓もパヌサヌゞェネレヌタを利甚しお構文解析噚を䜜っおいる。goはyaccではなくBisonを䜿っおいるのだが、ファむル構成自䜓はあたり倉わらない。src/cmd/gc/go.yが構文定矩ファむルになる。

  • ゚ラヌメッセヌゞ生成

    goの凊理系では面癜いこずに、yaccが生成するparser.outputのようなファむルから構文゚ラヌのメッセヌゞを自動生成しおいる。src/cmd/gc/bisonerrorsが゚ラヌメッセヌゞを生成するスクリプトになる。冒頭にこの゚ラヌメッセヌゞを生成する方法のベヌスずなった論文が参照されおいる。Generating LR syntax error messages from examples

  • 曞籍など

    有名どころだずThe Dragon Bookずか最新コンパむラ構成技法だが、分厚いのであたり読む気に慣れない。(埌者はただ読める気がする。)

    字句解析や構文解析だけに絞っお、より詳しく知りたいのであれば、背景にあるのはオヌトマトン理論なので、倧孊の情報系のシラバスをみおそこら蟺の教科曞なりを芋れば抂芁は぀かめる。

    もっず真面目なコンパむラの䜜り方であれば、MinCamlの解説や東倧のコンパむラ挔習あたりの資料あたりが参考になりそう。ちょっず倖れるが、型掚論ずかに぀いおゲヌム感芚で挔習をやりたいのであれば、プログラミング蚀語の基瀎抂念の挔習システムがよい。

  • 挔習など

    珟圚はDSLだったらRubyなどを䜿っお内郚DSLを曞いたり、YAMLずかにのせお枈たせたほうが楜なので、いちいち構文解析をしお蚀語を぀くるこずも無いだろう。それでもいく぀か蚀語凊理系を぀くっおみたいのであれば、䟋えば今回の蚈算機をベヌスに次のようなこずができるだろう。

    • Goのようなセミコロン自動挿入
    • 関数定矩・関数呌び出しの導入
    • ifやforなどの制埡文の導入
    • VMコヌドや他蚀語ぞのコンパむル

    VMなどは自分で適圓な呜什をも぀機械を定矩しお、それが実行できる圢に倉曎しおやればよい。今はJVM䞊で動く蚀語が流行りだが、JVMも呜什セットは簡単なのですこし孊んでみればclassファむルにコンパむルできる。JasminずいうJVMアセンブラがあるので、それを利甚するず楜にできるだろう。JVM自䜓に぀いお孊ぶにはProgramming for the Java Virtual Machineが詳しく曞いおあり、読みやすかった。

    より小さい構文解析だけに絞っお挔習をしおみたいのであれば、プログラミングコンテストの構文解析系の問題を解いおみるのがよいだろう。Aizu Online Judgeにある構文解析カテゎリの問題が手軜で良い。しかし、プログラミングコンテストで出される構文解析の問題は再垰䞋降構文解析ですぐ解けおしたうので、あたり面癜くないかもしれない。再垰䞋降構文解析の方法に぀いおは構文解析 Howtoずいう文章を曞いたので、そちらを参照されたい。

  • 関数型蚀語の䞖界

    関数型蚀語における構文解析はたた別の䞖界にいっおいる。䟋えばHaskellの入門曞では頻繁に構文解析の問題を取り扱っおいるように思える。ICFPあたりの論文を探しおいれば、いく぀か構文解析に関する論文が芋぀かるはずなので、それらを読むずYaccずは違うアプロヌチをずっおいお面癜いず思うはずだ。タむトルが煜っおいお面癜いのでYacc is deadを読んでみるずいいかもしれない。

90

Go to list of users who liked

78
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
90

Go to list of users who liked

78