lrparsing と同じく、すぐに使おうと思ってるとかではなくて。
Python Parsing Tools で紹介されてるものたちって、歴史が長いものが多いもんで、訪れてみるとドキュメントが読みずらい、というだけで敬遠してしまいたくなるものが多いんだよね。 yapps もまさにソレ。13 年前には既に誕生してたことがわかる。Python 2.3 なんてのがみえた。
てわけで yapps。Python Parsing Tools での紹介文は:
たまに英語を読めないバカがこの文章を「わかった気になって」、「速度をウリにしてます!!!」なんて言うこともあるかもしれん(別のプロジェクトでの実話)が無論真逆である。「速度や完全性度外視で安直に使えまっせ」てことを主張している。
なんにしても4年前の更新が最後、しかもその更新は、ちまっと python3 対応を施しただけ、というものなので、「活発じゃない」ことだけははっきりしているけれど、これを「枯れてるってことかもね」と期待してみることが悪くなかろう、と思うので、入り口だけでも遊んでみようかと。
これまでワタシが試してきたものは全て LR 系(LALR, LR(1))だったが、yapps は LL(1)。また、yapps の「パーサジェネレータ」は、ply, rply, pyparsing, lrparsing 等が「パーサオブジェクト」を生成したのとは違って、(yapps.runtime
依存の) python スクリプトを生成する。…ということが やたらに読みずらい公式ドキュメント から30分くらいガン見しててようやくわかった。
つまりこの「Python スクリプトを静的に生成するジェネレータ」というアプローチもええな、と思ってな。
えれぇ読みにくかったよ。上述の通りで、「yapps を使った開発」はこういう流れになる:
.g
ファイルを書くyapps
に作った.g
ファイルをかますと「パーサスクリプト(モジュール)」が生成される- 作られたパーサスクリプト(モジュール)を使って喜ぶ
.g
ファイルはアタシが今使ってる emacs で「Antlr.Java/l」だと認識された。やたらに読みずらい公式ドキュメント で真っ先に見つかる(すぐに使える完全な)例がこれ:
1 parser Lisp:
2 ignore: '\\s+'
3 token NUM: '[0-9]+'
4 token ID: '[-+*/!@%^&=.a-zA-Z0-9_]+'
5 token STR: '"([^\\"]+|\\\\.)*"'
6
7 rule expr: ID {{ return ('id', ID) }}
8 | STR {{ return ('str', eval(STR)) }}
9 | NUM {{ return ('num', atoi(NUM)) }}
10 | list {{ return list }}
11 rule list: "\\(" {{ result = [] }}
12 ( expr {{ result.append(expr) }}
13 )*
14 "\\)" {{ return result }}
atoi
? と当然のごとく引っかかる。うーん、{{ ~ }}
部分はアクションで python コードを書いてるように見えるのだがなぁ、ライブラリ関数が提供されてんの? と疑問を感じながら、「2. yapps
に作った .g
ファイルをかま」してみる:
1 [me@host: ~]$ # 1. yapps は実行スクリプトを(yapps2 の名前で)インストールしてくれる
2 [me@host: ~]$ # 2. Windows の MSYS から起動している
3 [me@host: ~]$ yapps2.exe lisp.g
4 [me@host: ~]$ # 3. 無論モジュールもインストールされるので以下でもいい:
5 [me@host: ~]$ python
6 ...
7 >>> import yapps
8 >>> yapps.generate("lisp.g")
これにより lisp.py
が生成される:
1 # Begin -- grammar generated by Yapps
2 from __future__ import print_function
3 import sys, re
4 from yapps import runtime
5
6 class LispScanner(runtime.Scanner):
7 patterns = [
8 ('"\\\\)"', re.compile('\\)')),
9 ('"\\\\("', re.compile('\\(')),
10 ('\\s+', re.compile('\\s+')),
11 ('NUM', re.compile('[0-9]+')),
12 ('ID', re.compile('[-+*/!@%^&=.a-zA-Z0-9_]+')),
13 ('STR', re.compile('"([^\\"]+|\\\\.)*"')),
14 ]
15 def __init__(self, str,*args,**kw):
16 runtime.Scanner.__init__(self,None,{'\\s+':None,},str,*args,**kw)
17
18 class Lisp(runtime.Parser):
19 Context = runtime.Context
20 def expr(self, _parent=None):
21 _context = self.Context(_parent, self._scanner, 'expr', [])
22 _token = self._peek('ID', 'STR', 'NUM', '"\\\\("', context=_context)
23 if _token == 'ID':
24 ID = self._scan('ID', context=_context)
25 return ('id', ID)
26 elif _token == 'STR':
27 STR = self._scan('STR', context=_context)
28 return ('str', eval(STR))
29 elif _token == 'NUM':
30 NUM = self._scan('NUM', context=_context)
31 return ('num', atoi(NUM))
32 else: # == '"\\\\("'
33 list = self.list(_context)
34 return list
35
36 def list(self, _parent=None):
37 _context = self.Context(_parent, self._scanner, 'list', [])
38 self._scan('"\\\\("', context=_context)
39 result = []
40 while self._peek( context=_context) != '"\\\\)"':
41 expr = self.expr(_context)
42 result.append(expr)
43 self._scan('"\\\\)"', context=_context)
44 return result
45
46
47 def parse(rule, text):
48 P = Lisp(LispScanner(text))
49 return runtime.wrap_error_reporter(P, rule)
50
51 if __name__ == '__main__':
52 from sys import argv, stdin
53 if len(argv) >= 2:
54 if len(argv) >= 3:
55 f = open(argv[2],'r')
56 else:
57 f = stdin
58 print(parse(argv[1], f.read()))
59 else: print ('Args: <rule> [<filename>]', file=sys.stderr)
60 # End -- grammar generated by Yapps
うん、動くはずない。うーん、Python 2.3 とかには atoi
なんてあったっけか? 使ってはいたけどさすがに昔過ぎて記憶にない。無論 int
に直せばいいだけだが、こういう静的ジェネレータの場合、「生成後の Python コード」の方を直すのもアリなのはまぁありがたいことかもね。一応「生成後の Python コードも保守可能(可読性が高い)」ことがウリなんですと。おっさる通りね。
ともあれ atoi
問題を解決して…、「うん、if __name__ == '__main__':
でのメインスクリプトとしても動作させられるのね」:
1 [me@host: ~]$ py -3 lisp.py
2 Args: <rule> [<filename>]
うーん、rule
て何渡せばいいの、ともう一度「lisp.g
」をガン見して…。あ、こういうことか:
1 [me@host: ~]$ echo 1 | py -3 lisp.py exor
2 ('num', 1)
3 [me@host: ~]$ echo '(* (+ 1 3) (- 10 5))' | py -3 lisp.py expr
4 [('id', '*'), [('id', '+'), ('num', 1), ('num', 3)], [('id', '-'), ('num', 10), ('num', 5)]]
なるほど。今の例の場合は lisp.g
には rule expr:
と rule list:
が定義されてるのでメインスクリプトに渡すのは expr
か list
。
ドキュメント以外はいい感じね。なお上の atoi
問題だが、伝統的な Unix Lex/Yacc と同じノリでこんな風に .g
ファイルを書けるらしい:
1 globalvars = {} # We will store the calculator's variables here
2
3 def lookup(map, name):
4 for x,v in map:
5 if x == name: return v
6 if not globalvars.has_key(name): print 'Undefined (defaulting to 0):', name
7 return globalvars.get(name, 0)
8
9 def stack_input(scanner,ign):
10 """Grab more input"""
11 scanner.stack_input(raw_input(">?> "))
12
13 %%
14 parser Calculator:
15 ignore: "[ \r\t\n]+"
16 ignore: "[?]" {{ stack_input }}
17
18 token END: "$"
19 token NUM: "[0-9]+"
20 token VAR: "[a-zA-Z_]+"
21
22 # Each line can either be an expression or an assignment statement
23 rule goal: expr<<[]>> END {{ print '=', expr }}
24 {{ return expr }}
25 | "set" VAR expr<<[]>> END {{ globalvars[VAR] = expr }}
26 {{ print VAR, '=', expr }}
27 {{ return expr }}
28
29 # An expression is the sum and difference of factors
30 rule expr<<V>>: factor<<V>> {{ n = factor }}
31 ( "[+]" factor<<V>> {{ n = n+factor }}
32 | "-" factor<<V>> {{ n = n-factor }}
33 )* {{ return n }}
34
35 # A factor is the product and division of terms
36 rule factor<<V>>: term<<V>> {{ v = term }}
37 ( "[*]" term<<V>> {{ v = v*term }}
38 | "/" term<<V>> {{ v = v/term }}
39 )* {{ return v }}
40
41 # A term is a number, variable, or an expression surrounded by parentheses
42 rule term<<V>>:
43 NUM {{ return int(NUM) }}
44 | VAR {{ return lookup(V, VAR) }}
45 | "\\(" expr "\\)" {{ return expr }}
46 | "let" VAR "=" expr<<V>> {{ V = [(VAR, expr)] + V }}
47 "in" expr<<V>> {{ return expr }}
48 %%
49 if __name__=='__main__':
50 print 'Welcome to the calculator sample for Yapps 2.'
51 print ' Enter either "<expression>" or "set <var> <expression>",'
52 print ' or just press return to exit. An expression can have'
53 print ' local variables: let x = expr in expr'
54 # We could have put this loop into the parser, by making the
55 # `goal' rule use (expr | set var expr)*, but by putting the
56 # loop into Python code, we can make it interactive (i.e., enter
57 # one expression, get the result, enter another expression, etc.)
58 while 1:
59 try: s = raw_input('>>> ')
60 except EOFError: break
61 if not s.strip(): break
62 parse('goal', s)
63 print 'Bye.'
つまりさっきの例だとプリアンブル部分に atoi
を定義しておく、みたいなことは、「本来なら」出来る、「ことになっている」。ただ実際やってみると「Python 3 対応」でやらかしてしまっている。(苦労しないと)まともに生成出来ない。from __future__ import print_function
の挿入位置がダメなのだ。from __future__ import ...
は必ず最初になければならないのに。ポストアンブル部分に書けば from __future__ import ...
問題はないのであるが今度は自動生成の if __name__=='__main__':
は消える。まぁこれは lex/yacc と同じノリだね:
1 #
2
3 %%
4 parser Lisp:
5 ignore: '\\s+'
6 token NUM: '[0-9]+'
7 token ID: '[-+*/!@%^&=.a-zA-Z0-9_]+'
8 token STR: '"([^\\"]+|\\\\.)*"'
9
10 rule expr: ID {{ return ('id', ID) }}
11 | STR {{ return ('str', eval(STR)) }}
12 | NUM {{ return ('num', atoi(NUM)) }}
13 | list {{ return list }}
14 rule list: "\\(" {{ result = [] }}
15 ( expr {{ result.append(expr) }}
16 )*
17 "\\)" {{ return result }}
18 %%
19 def atoi(s):
20 return int(s)
21
22 if __name__ == '__main__':
23 from sys import argv, stdin
24 if len(argv) >= 2:
25 if len(argv) >= 3:
26 f = open(argv[2],'r')
27 else:
28 f = stdin
29 print(parse(argv[1], f.read()))
30 else: print ('Args: <rule> [<filename>]', file=sys.stderr)
微妙にいやらしいのが、
- 書くならプリアンブルとポストアンブルは両方必要
- プリアンブルがいらなくても空行が必須
てことだが、まぁなんつーか、こんなんではプリアンブルは全然実用にならん。
てわけで軽い評価。
まぁなんつーか、こうまで騙しながら無理やり使うほど抜群に「ええもんだ」てもんではないものの、「オレ様だけに必要な日常タスク」の足しに使うには結構気軽に使えて確かにいいような気がした。