このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
前回はバッカスナウア記法により文脈自由文法を記述し、さらに、その文法を 処理するパーサをプログラムで作成する方法に関して議論しました。 今回はまず、よく使われるバッカスナウア記法の拡張表記を説明します。 その後、 Python で文脈自由文法を処理するコンパイラコンパイラである Lark を紹介します。
基本的なバッカスナウア記法では右辺に終端記号、非終端記号の連接の表現し かありませんでした。 拡張バッカスナウア記法は、連接以外に正規文法の記法である、選択と繰り 返しを指定できるようにしたものです。 選択は |(縦棒) で右辺の表現を列挙します。 一方、繰り返しに関しては多くの表現方法があります。 本質的には次に示す一部が可能であれば、他の表現を実現できますが、よく使 われる表現を紹介します。
なお、このような表記を使うため、今後、文法表記で終端記号をルールに含め るときは "" (ダブルクォーテーションマーク)で囲むことにします。
足し算の文法 G1 は以下の通りでした。
これを拡張バッカスナウア記法で記述すると次のようになります。
G1は左再帰性がありました。 これを除去するには、繰り返し構造を見抜いて、繰り返し構文で記述します。
さらに、式の最後に =(イコール)記号をつけた文法を示します。 これを G3 とします。
バッカス・ナウア記法を与えて、それを処理するプログラムを出力する処理系 をコンパイラコンパイラと言います。 Python 用のコンパイラコンパイラに Larkというモジュールがあ ります。 Lark は構文解析も字句解析もできます。 Lark の基本的な利用法は、文脈自由文法を定義した文字列からパーサを生 成します。 そして、そのパーサに入力文字列を与えると、構文解析木を自動的に生成し返 します。
Lark を使用する際は、 パッケージをインポートして、Larkコンストラクタに、 Lark のパーサのソースを文字列で与え、また、開始ルールを指定すること でパーサー が作られます。 パーサのソースはPython のプログラム内に記述しても良いのですが、 Python とは別文法ですので、ここでは別ファイルで与えることにします。 拡張子を .lark にすると、主要なエディタで Lark モードがサポートされ ていて、アシストを受けることができます。
Lark 自体はパーサに文字列を与えると、構文解析木を返すものです。 その構文解析木を使用して実際に別の出力を得るために、 Visitor, Transformer, Interpreter の各クラスが用意されています。 いずれもデザインパターンのビジターとして動作しますが、 Visitor では 子ノードへのアクセスするのに対して、 Transformer は再帰的に処理をし ます。 Interpreter は逆に各子ノードは visit メソッドを明示的に呼び出すよう に作られているため、細かい処理を指定することができます。 ここでは Transformer を使用した処理を示します。
start: token*
token: TOKEN
TOKEN: /\S/
%ignore /\s/
\sは空白文字(改行なども含む)、\Sは空白以外の文字を表します。
from lark import Lark,Transformer
class MyTransformer(Transformer):
def token(self,args):
print("Hello World")
with open("rei53.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="start")
tree=parser.parse("1 2 34 567")
print(tree)
print(tree.pretty())
t=MyTransformer()
t.transform(tree)
このようにLark では文法をLarkコンストラクタに与えてパーサーを作り、 Visitorの継承クラスを作ることで、文字列 を解釈して出力を得る仕組みを作ることができます。
次章以降では、以下の順に解説をします。
Lark のドキュメントは Documentation @readthedocs にまとまっています。 ここでは、講義で使用するため、一部のみ紹介します。
EQOP : "="
NUM : /[1-9][0-9]*/
a: ( α | β)?
と記述します。
sum : NUM sum2
sum2 : ( "+" NUM sum2 )?
%ignore "\n" | "\r" | " "
PLUSOP : "+"
EQOP : "="
NUM : /[1-9][0-9]*/
start : sum EQOP
sum : NUM sum2
sum2 : ( "+" NUM sum2 )?
%ignore "\n" | "\r" | " "
from lark import Lark
with open("rei54.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="start")
tree=parser.parse("1+2+3=")
print(tree)
print(tree.pretty())
Lark の生成する構文木は次のような構造になっています。
Tree(Token('RULE', 'start'), [Tree(Token('RULE', 'sum'), [Token('NUM', '1'), Tree(Token('RULE', 'sum2'), [Token('PLUSOP', '+'), Token('NUM', '2'), Tree(Token('RULE', 'sum2'), [Token('PLUSOP', '+'), Token('NUM', '3'), Tree(Token('RULE', 'sum2'), [])])])]), Token('EQOP', '=')])
また、この構文木は lark.Tree 型で、pretty() メソッドを持っていて、 視覚的に木構造に見えるように整形した出力も可能です。
start sum 1 sum2 + 2 sum2 + 3 sum2 =
後に、構文木のフォーマットをコントロールするために、次のような ルールの記法を用いることができます。
->
をつけてその後に名前をつけ
ると、構文解釈木にはその名前が代わりに表示されます。
start : term*
?term : term1 | term3
term1: term2 -> term5
term2 : "a"
term3: TERM4 -> term6
TERM4: "b"
%ignore "\n" | "\r" | " "
from lark import Lark
with open("rei55.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="start")
tree=parser.parse("ab")
print(tree)
print(tree.pretty())
Tree(Token('RULE', 'start'), [Tree('term5', [Tree(Token('RULE','term2'), [])])1, Tree('term6', [Token('TERM4', 'b')])]) start term5 term2 term6 b
term2 の "a" はルール内に書かれた文字列なので、構文木には現れない。 term ルールは term1 または term3 に分岐しますが、子ノードを親ノード に送るだけなので、?が名前の前についているため、構文解析木に現れない。 term1 は term5, term3 は term6 として構文解析木に現れる。
Lark に付属している Transformer を用いた構文木から出力を得ることを考え ます。 Transformer クラスを継承したクラスを作成し、 メソッドとして、self と values を引数とした、ルール名をそのまま名前と したメソッドを定義します。 各メソッドでは、ルールの各要素の値がvalues の配列の要素として得られ、 最後にそこから計算した値を return で返します。
PLUSOP : "+"
EQOP : "="
NUM : /[1-9][0-9]*/
start : sum EQOP
sum : NUM sum2
sum2 : ( PLUSOP NUM sum2 )?
%ignore "\n" | "\r" | " "
from lark import Lark, Transformer
class MyTransformer(Transformer):
def start(self, values):
return values[0]
def sum(self, values):
result = int(values[0])
if len(values)==2:
result += values[1]
return result
def sum2(self, values):
if len(values)==0:
return 0
return int(values[1])+values[2]
with open("rei56.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="start")
tree=parser.parse("1+2+3+4=")
print(tree)
print(tree.pretty())
t =MyTransformer()
print(t.transform(tree))
本章では Lark による数式の処理を考えます。
足し算の文法として下記の G3 を Lark に与えます。
PLUSOP : "+"
KAZU : /[1-9][0-9]*/
wa : KAZU ( PLUSOP KAZU)* "="
%ignore "\n" | "\r" | " "
from lark import Lark,Transformer
class Show(Transformer):
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result = f"({result}{i}{j})"
return result
class Rpn(Transformer):
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result += f" {j} {i}"
return result
class Eval(Transformer):
def wa(self, values):
result = int(values[0])
for i,j in zip(values[1::2], values[2::2]):
result += int(j)
return result
with open("rei57.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="wa")
tree=parser.parse("1+2+3=")
print(tree)
print(tree.pretty())
print(Show().transform(tree))
print(Rpn().transform(tree))
print(Eval().transform(tree))
Showクラスは中置記法による表示、Rpnクラスは逆ポーランド記法による表
示、 Evalクラスは実際に式の値を計算するクラスです。
それぞれに、ルール名のメソッドを持ちます。
ルールが解釈されると、そのルールの文字列を除いた要素がvalues配列の要
素に順に置かれます。Transformer クラスでは、その値はあらかじめその先
のクラスで呼び出しが行われ、値に変換したものが得られます。
繰り返しなどで処理をする場合に、複数個をループ内で同時に使用したい場
合は、配列を個別に飛ばして組にするためzip関数を使用します。
つまり、
zip(a[0::3],a[1::3],a[2::3])
で、(a[0],a[1],a[2]),(a[3],a[4],a[5]),... というデータ構造が作られます。
次に足し算と掛け算の混ざった式を考えましょう。 計算は積項の和をとることで行いますので、 G3 を書き換えた次 の文法を G4 とします。
なお、数について、単独のルールを設けるとVisitorでメソッド呼び出しが できるようになります。 すると、文字列のままとしても、データとしての数として取り扱うこともできるよ うになります。
PLUSOP : "+"
MULOP : "*"
KAZU : /[1-9][0-9]*/
wa : seki ( PLUSOP seki)* "="
seki : num ( MULOP num)*
num: KAZU
%ignore "\n" | "\r" | " "
from lark import Lark,Transformer
class Show(Transformer):
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result = f"({result}{i}{j})"
return result
def seki(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result = f"({result}{i}{j})"
return result
def num(self, values):
return values[0]
class Rpn(Transformer):
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result += f" {j} {i}"
return result
def seki(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result += f" {j} {i}"
return result
def num(self, values):
return values[0]
class Eval(Transformer):
def wa(self, values):
result = int(values[0])
for i,j in zip(values[1::2], values[2::2]):
result += int(j)
return result
def seki(self, values):
result = int(values[0])
for i,j in zip(values[1::2], values[2::2]):
result *= int(j)
return result
def num(self, values):
return int(values[0])
with open("rei58.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="wa")
for test in ["2+3+4=","2*3*4=", "2+3*4=", "2*3+4="]:
tree=parser.parse(test)
print(tree)
print(tree.pretty())
print(Show().transform(tree))
print(Rpn().transform(tree))
print(Eval().transform(tree))
さらに括弧の処理を考えます。 括弧の中にはどんな式も入ります。一方括弧はその中の値を計算したら、計算 の対象となる値として数と同じように扱われます。 今まで「数」という終端記号を使ってきましたが、ここで「値」 というルールを導入します。 そして、値については、数かまたはカッコで括った式かを選ぶようにします。 なお、丸かっこについてはVisitorで処理する必要が無いので、構文木に現れ ないように、ルールの中に文字列として埋め込みます。
また、かっこの中に和を入れるため、=記号を和から取り除いて、スタート ルールに「和 =」とするようにします。 このルールをG5 とします。
PLUSOP : "+"
MULOP : "*"
KAZU : /[1-9][0-9]*/
?start : wa "="
wa : seki ( PLUSOP seki)*
seki : atai ( MULOP atai)*
?atai: num | "(" wa ")"
num: KAZU
%ignore "\n" | "\r" | " "
from lark import Lark,Transformer
class Show(Transformer):
def start(self,values):
return values[0]
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result = f"({result}{i}{j})"
return result
def seki(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result = f"({result}{i}{j})"
return result
def atai(self, values):
return values[0]
def num(self, values):
return values[0]
class Rpn(Transformer):
def start(self,values):
return values[0]
def wa(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result += f" {j} {i}"
return result
def seki(self, values):
result = values[0]
for i,j in zip(values[1::2], values[2::2]):
result += f" {j} {i}"
return result
def atai(self, values):
return values[0]
def num(self, values):
return values[0]
class Eval(Transformer):
def start(self,values):
return values[0]
def wa(self, values):
result = int(values[0])
for i,j in zip(values[1::2], values[2::2]):
result += int(j)
return result
def seki(self, values):
result = int(values[0])
for i,j in zip(values[1::2], values[2::2]):
result *= int(j)
return result
def atai(self, values):
return values[0]
def num(self, values):
return int(values[0])
with open("rei59.lark",encoding="utf-8") as grammar:
parser = Lark(grammar.read(),start="start")
for test in ["2+3+4=","2*3*4=", "2+3*4=", "2*3+4=", "(2+3)*4=", "2+(3*4)="]:
tree=parser.parse(test)
print(tree)
print(tree.pretty())
print(Show().transform(tree))
print(Rpn().transform(tree))
print(Eval().transform(tree))
構文解析木を受け取り、計算を行うプログラムを作成しなさい。
入力に自然数だけでなく、整数を許す数式計算プログラムを実現しなさい。