blog.takurinton.dev

pyparsing触ってみた

2020-11-24

こんにちは

こんにちは、僕です。
最近、pyparsingというPythonのライブラリを使用していて、面白いなあと思ったので記事にしてみました。
元々自分は言語解析などに興味があって(NLPとか)、今回は形式言語解析になりますがまとめたいと思います。

pyparsingとは?

これ です。
ドキュメント

The pyparsing module is an alternative approach to creating and executing simple grammars, vs. the traditional lex/yacc approach, or the use of regular expressions. The pyparsing module provides a library of classes that client code uses to construct the grammar directly in Python code.

だそうです。英語わかんねえや。(シンプルな文章をいい感じにチャチャっとするためのモジュールだお!レキサーとかとはちゃうで、クライアントから直接やねんな。みたいなこと言ってると思います知らんけど)

要は文章をいい感じに分析する補助をしてくれます。
自分は今回プログラミング言語を作るときの補助として使用しました(本記事では触れませんが)

ジャブ

まずは簡単にパーサを作ってみようと思います。

from pyparsing import alphas, Word, Literal, nums
integer_literal = Literal("vat") + Word(alphas) + Literal("=") + Word(nums)
print(integer_literal.parseString('vat i = 10'))
>>> ['vat', 'i', '=', '10']

こんな感じでvarで変数定義、次に変数名、次に代入、次に実際に格納する値(ここではまだ格納してない)を構文解析することができました。

ちょっと実践

実践とか言ってるけど初歩中の初歩です。
if文を解析するプログラムを書いてみたいと思います。

from pyparsing import *

ident = Literal( 'IDENT')
const = Literal( 'CONST')
assign = Literal( '=' )
lparen = Literal( '(' )
rparen = Literal( ')' )

op_arith = oneOf( '+ - * / ** %' )
op_logic = oneOf( '== != <= < >= > && || !')
op_bits = oneOf( ' & | ^ ~ << >>' )
operator = op_arith ^ op_logic ^ op_bits

factor = ( Optional(lparen) + Optional( operator ) + Optional(lparen) + ( ident ^ const ) + Optional( rparen ) )
term = factor + ZeroOrMore( operator + factor )
expr = term + ZeroOrMore( operator + term )

rsv_if = Literal( 'if')
rsv_elif = Literal( 'elif')
rsv_else = Literal( 'else')

_if =  rsv_if + expr + '{' + SkipTo( '}' ) 
_elif = rsv_elif + expr + '{' + SkipTo( '}' ) 
_else = rsv_else + '{' + SkipTo( '}' ) 

if_statement = _if.setResultsName('IF')
elif_statement = _elif.setResultsName('ELIF')
else_statement = _else.setResultsName('ELSE')

statement = (if_statement ^ elif_statement ^ else_statement)

#statement
 
test='''\
if CONST > CONST { IDENT = COST }
if CONST == IDENT { IDENT = CONST EOS IDENT = IDENT EOS }
if CONST != IDENT { IDENT = CONST } else { IDENT = CONST + IDENT }
'''

r = statement
for i in r.scanString(test):
    print('{}'.format(' '.join([s for s in i[0]])))

ちょっと色々いきなり新しいものがたくさん出てきてしまいましたが、全てpyparsingの中身にあるメソッドしか使ってないので心配は要りません!

文法はPythonチックになっています。
それぞれのpyparsingのメソッドについて説明したいと思います。

  • Literal
    • リテラルを定義することができるクラス
    • 引数にリテラルを定義
    • 例えば Literal('=') とか定義すれば一般的な代入の定義ができるようになる。
  • oneOf
    • 代替のリテラルを定義することができる関数
    • ここでは演算子(式の中にいるやつ)を定義している
  • Optional
    • 指定されたものとマッチングするかどうかを調べるクラス
    • ここではそれぞれのリテラルと一致しているかどうかを調べている
    • 一般的にはここで違ったらsyntax errorを吐く
  • ZeroOrMore
    • 指定された0個以上のオプションの繰り返しを判定するクラス
    • if文はifだけでも成立するし、elifがあっても成立する、elseがなくても成立する、そのような0回以上のelif/elseを判定みたいな時に使う
  • SkipTo
    • 一致する式が見つかるまで未定義の式をスキップするクラス
    • ここではif文の{}の中にはどのような式が来るかわからない
    • そのような時に式を評価するわけではなく、とりあえずEOFが正しく終了しているかを調べるために使用している
  • setResultsName
    • ステートメントを判断するために代替テキストを定義するための関数
    • ここではIF/ELIF/ELSEを定義している
  • scanString
    • テストケースを1行ずつ回すための関数

どうでしょう!この上記の物だけでif文を構文解析できます!
また、それぞれのメソッドやクラスの説明をドキュメントで見て、簡単な言語解析の知識があればPyparsingを利用して簡単にプログラムの構文解析をすることができます。
自分がGoで作るインタプリタでコードを書いていたときはリテラルやステートメントは自分で定義しないといけなかったためとても大変でした(と言ってもグローバルな変数として格納して都度呼び出すってだけだったけど)
こうやって動的に簡単にリテラルやステートを定義できるというのはとても嬉しいことだと感じています。

まとめ

あまり内容の深い記事ではありませんでしたが、楽に構文解析をすることができることがわかったと思います。
また、これからPyparsingを使用して様々な解析を行えるという手応えを掴むことができたので良かったです。
形式言語以外でもいけそう???( ̄▽ ̄)