コンパイラ構成論ソース読み会 〜pypy〜

インストール

  • pypyをコンパイルする。 hg cloneしてきたpypy上で、ディレクトリを移動する。
     $ cd pypy/pypy/goal
  • pypyをコンパイルする。
     $ python ../../rpython/bin/rpython --lldebug -Ojit targetpypystandalone
  • lldbでデバッグしたい場合のコンパイルは以下のコマンド。
     $ python ../../rpython/bin/rpython --lldebug -Ojit targetpypystandalone

pypy-cというバイナリが出来上がる!

lldbデバッグする場合

  • 簡単なpythonのファイルを作成する。
  • 今回の場合は filetmp.py
    $ lldb -- [読む側のプログラム] tmp.py
    例 : tmp.pをpypy-cで読んでいく様子をlldbでみていく。
    $ lldb -- ./pypy-c tmp.py
    lldbのための debug symbol は /private/var/の下にできるのでしばらくすると消えてしまいます。 なので PYPY_USESSION_DIR や PYPY_USESSION_KEEP を設定すると良さそうです。

pypy interepreter をpdbデバッグする

  • lldbの場合と同様に、tmp.pyを読んでいく様子をみていく。
    $ python -m pdb pypy/bin/pyinteractive.py tmp.py
  • 各オプションについて
    • pdb で break するには
  • b <filename>:<lineno>
  • pdb で b を消すには
  • clear <breakpointid>
  • pdb で condition をかけるには
  • condition <breakpointid> <condition>
  • pdb で condition を消す
  • condition <breakpointid>
  • 毎回コマンドを実行するには
  • commands <breakpointid> の後に command を打つ 終わる時に end とか continue を書く

1日目

どこでパースしているのか探そうという話

$ lldb -- ./pypy-c tmp.py
  • ファイルを開いているはずなので、まずは open を追う。
    (lldb) b open
  • 絶対パスでpythonモジュールを読み込んでるっぽい。
  • なので先頭が/で無いものが tmp.py だとして break point に condition を付ける
  • br m -c ((char*)$rdi)[0]!=\'/\' 3.1
  • で 3.1 にある open で $rdi の先頭が / じゃないやつを止めとく
  • $rdi に引数な文字列をが残っていたりするので x $rdi とかする
  • pyinteractive.py を読む
  • パーサを読みたい

pdbデバッグしていく

   $ python -m pdb pypy/bin/pyinteractive.py tmp.py
  • break point に commnads を設定して、ファイル名を出力させながらトレースしていく。
  • ほしいファイル名はtmp.py。
   /Users/e115747/Desktop/pypy/pypy/bin/pyinteractive.py
  • このコードの関数で重要な部分らしい。
  • do_start() : Python実行用環境が作られる関数
  • doit() : 実際にコードが実行される関数
  • これらが main.run_toplevel() に渡されて実行される
   95行目  : spacce.setitem()
  • 第3引数argvがtmp.pyとなっていた
   174行目 : main.run_toplevel()
  • 第2引数がdoit()であった場合にPythonコードが実行される
   pypy/pypy/interpreter/main.py
  • 103行目 : f() が doit() と一致。
  • 一連の流れをみてみると、pythonに変換されたコードが返ってきていることがわかった。

2日目

python・pypyのコンパイラの仕組み。

  • 図&ref(blackbord.jpg)を載せる。
  • pyinteractiveがpythonを呼ぶかpypyに呼ぶかはスペースによって決まるらしい。
  • pyinteractive は Python 側にパースなども任せている様子

スペースの切り替え部分を探す。

 pypy/pypy/interpreter/main.py
  • このソースの
     space.wrap('softspace'))
  • でスペースで選択されているらしい。
  • pypyのスペースに切り替えたいなぁ...
  • この段階ではpdbで追っていくと Python VM なコードになったので VM を読むことに。
  • Python VM の frame や pyopcode(python の byte codeの様子)などを追う
  • コードの中身(関数やクラス等)を定義している
     pypy/pypy/interpreter/baseobjspace.py
  • フレームが作成される
  • フレーム : 関数の戻り値等をスタックしておいたりするやつ
     pypy/pypy/interpreter/pyframe.py
  • バイトコードの大部分(printやplus等)が書かれている
     pypy/pypy/interpreter/pyopcode.py

frame を作成して exec するところまでは追った。

  • execute_frame での pdb のバックトレース
 (Pdb) bt
 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/bdb.py(387)run()
 -> exec cmd in globals, locals
 <string>(1)<module>()
 pypy/pypy/bin/pyinteractive.py(204)<module>()
 -> sys.exit(main_(sys.argv))
 pypy/pypy/bin/pyinteractive.py(175)main_()
 -> verbose=interactiveconfig.verbose):
 pypy/pypy/interpreter/main.py(103)run_toplevel()
 -> f()
 pypy/pypy/bin/pyinteractive.py(159)doit()
 -> main.run_file(args[0], space=space)
 pypy/pypy/interpreter/main.py(68)run_file()
 -> run_string(istring, filename, space)
 pypy/pypy/interpreter/main.py(59)run_string()
 -> _run_eval_string(source, filename, space, False)
 pypy/pypy/interpreter/main.py(48)_run_eval_string()
 -> retval = pycode.exec_code(space, w_globals, w_globals)
 pypy/pypy/interpreter/eval.py(33)exec_code()
 -> return frame.run()
 pypy/pypy/interpreter/pyframe.py(140)run()
 -> return self.execute_frame()
 > pypy/pypy/interpreter/pyframe.py(168)execute_frame()
 -> next_instr = r_uint(self.last_instr + 1)

テストのソースを編集し、それを動かしながらデバッグしていく。

  • 直接読みたいパーツをテストしているコードを動かして、パーツのコードを読んでいくことに。
  • parser が読みたい、ということになってので、これをコピって編集する。
     pypy/interpreter/pyparser/test/test_pyparse.py
  • 編集したもの -> filehoge.py
  • hoge.pyでparseする。
     $ python hoge.py tmp.py
  • すごい文字列いっぱい出てきた!
  • ここからpdbで読んでいく。
     $ python -m pdb hoge.py tmp.py
  • textsrcの中身をprintすると、うまくtmp.pyのsourceがとれている。
 pypy/interpreter/pyparser/pyparse.py を読んだ
  • 141行目からトークナイズの部分。
 pypy/interpreter/pyparser/pytokenizer.py を読んでる。
  • アルファベット、数字、改行、コメントアウト等の判別等〜
  • endDFA
  • 決定性有限オートマトン( 状態遷移 )
  • 行末を検出するのに使われている。
  • 230行目 python_opmap はリストになっている
  • 演算子がハッシュになっている!
  • 簡単な計算の流れをみて演算子がハッシュになっているのをみた。
  • 簡単なfor文の流れをみて段落がどのように判別されているのかをみた。
  • ソースのインデント調べ終え、tree化されていた。

バイトコードが生成された。

 pypy/interpreter/test/test_compiler.py
  • バイトコードが生成された。
     $ python hogest.py tmp.py
 $ python -m pdb hogest.py tmp.py
  • ノードの塊をこれでastにするみたい。
     pypy/pypy/interpreter/astcompiler/astbuilder.py
  • 時間の関係上この日は、この部分は読まなかった。

バイトコードをディスパッチに食わせて、tmp.pyを実行できるかを試した

  • 移植した部分
 pypy/pypy/interpreter/eval.py(33)
  • ↑ 内の関数 exec_code()
 pypy/pypy/interpreter/main.py
  • ↑ 内の関数 ensure__main__(space)
  • 他にもあったかな〜〜〜〜?

3日目!

 コンパイラは、与えられたソースコードをいろいろと変換し、
 メモリ上に完全構文木(cst)にしたあと、それを抽象構文気(ast)に変換している。
  • astはpypy/interpreter/astcompiler/astbuilder.py:(52) build_ast()で作られている
  • stmt の type を見て handle していく感じ?
  • 例えば expr_node_type は以下のように handle されていった
  test -> or_test -> and_test -> not_test -> comparison -> expr -> xor_expr -> and_expr -> shift_expr -> arith_expr -> term -> factor -> power

hanldle_expr

 どんどんif文でchildrenを取っていっている
 children が 1 の場合は children[0] を取ってきて終了
 power まできたら handle_power して handle_atom する
 おそらく、演算などがかかりそうな部分にすべて handler があって、 children が 1である(特に演算が無い場合)は次の演算をチェックする、という形で潜っていく様子

file_input

 pypy/interpreter/pyparser/data/Grammar2.7の中に、simple_stmtの構成等が書かれていた。
 pypy/interpreter/pyparser/pytoken.pyの中に、演算子に対応する一覧が書かれている。
  • type は syms の attribute として管理されているので、内部の値が分かっていてもそれが何に相当るのかは分からない
  • 例えば syms.stmt は 322 なのだけれど、 hoge.type = 320 とかだと、hoge は syms の何に相当するのか分からない
  • なので syms の attributes と 値から逆引きできるハッシュを作成して読むなどした
  • おそらく pypy にはそういう util があるはず
  • 書いた逆引きコードは以下。
 sym_tab = {}
 for attr in dir(astbuilder.syms):
     v = getattr(astbuilder.syms, attr)
     if isinstance(v, int):
         sym_tab[v] = attr
 
 def show_token(n):
 
     tokens = []
     if n > 256:
         return sym_tab[n]
     else:
         tokens = pytoken.python_tokens.items()
         for k, v in tokens:
             if n == v:
                 return k
  • ちなみに type の値が 256 以下なら pyop らしい。
 p dir(syms)
 p syms.small_s

とかも、覚えておくといいかも

fileinputに潜る

  • stmt.type : 323 --> stmt

type を識別する巨大な if-elif 文があり

 children[0] の type --> simple_stmt
 children[0] の type --> small_stmt 
 children[0] の type --> expr_stmt

的な感じで分岐する様子

  • handle_expr_stmt

handle_expr の時点での btのログ

 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/bdb.py(387)run()
 -> exec cmd in globals, locals
 <string>(1)<module>()
 /Users/e115763/files/build/pypy2/run_pypy.py(58)<module>()
 -> pycode = compile_with_astcompiler(source, "exec", StdObjSpace())
 /Users/e115763/files/build/pypy2/run_pypy.py(37)compile_with_astcompiler()
 -> ast = astbuilder.ast_from_node(space, cst, info)
 /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(12)ast_from_node()
 -> return ASTBuilder(space, node, compile_info).build_ast()
 /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(63)build_ast()
 -> stmts.append(self.handle_stmt(stmt))
 /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(626)handle_stmt()
 -> return self.handle_expr_stmt(stmt)
 /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(694)handle_expr_stmt()
 -> target_expr = self.handle_testlist(target_node)
 /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(710)handle_testlist()
 -> return self.handle_expr(tests.children[0])
 > /Users/e115763/files/build/pypy2/pypy/interpreter/astcompiler/astbuilder.py(721)handle_expr()
 -> if first_child.type in (syms.lambdef, syms.old_lambdef):
 children[1].type      : 22 --> EQUAL
   
  • y などの変数を ast 化すると Ast.NAME が返ってくる
  • +等の処理は handle_binop とかでやってる
  • ast 初期化の initialize_stateは何かのフラグ? 値が 15 とか 8 とかの決め打ちなので
  • ast.py に AST の定義が書かれているが、これは生成されたコードらしい
 生成されたコード
 pypy/interpreter/astcompiler/ast.py
 ast.pyを生成するコード
 /pypy/interpreter/astcompiler/tools/asdl_py.py
 生成の定義等が書かれているやつ← 超重要!!
 pypy/interpreter/astcompiler/tools/Python.asdl
 ソースをpypyの中で適当に書いてると環境が壊れることがあるので注意
 最悪hg clone等で再構築

添付ファイル: fileblackboard.jpg 219件 [詳細] filehoge.py 228件 [詳細] filetmp.py 230件 [詳細] filehogest.py 232件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2013-11-26 (火) 22:02:21 (1310d)