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

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

# インストール

  • 公式ホームページ(http://pypy.org/download.html#installing)を参考に。

    • hg clone で落としてくる。
      • $ hg clone https://bitbucket.org/pypy/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のファイルを作成する。
  • 今回の場合は &ref(tmp.py);
1
 $ lldb -- [読む側のプログラム] tmp.py

例 : tmp.pをpypy-cで読んでいく様子をlldbでみていく。

1
 $ lldb -- ./pypy-c tmp.py

lldbのための debug symbol は /private/var/の下にできるのでしばらくすると消えてしまいます。 なので PYPY_USESSION_DIR や PYPY_USESSION_KEEP を設定すると[[良さそうです。:http://doc.pypy.org/en/latest/getting-started-dev.html#where-to-start-reading-the-sources]]

# pypy interepreter をpdbデバッグする

  • lldbの場合と同様に、tmp.pyを読んでいく様子をみていく。
1
 $ 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日目

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

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

  • フレームが作成される
  • フレーム : 関数の戻り値等をスタックしておいたりするやつ

pypy/pypy/interpreter/pyopcode.py

  • バイトコードの大部分(printやplus等)が書かれている

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

  • execute_frame での pdb のバックトレース
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  (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

  • 編集したもの -> &ref(hoge.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

  • これを編集して -> &ref(hogest.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 があるはず

  • 書いた逆引きコードは以下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  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 らしい。
1
2
  p dir(syms)
  p syms.small_s

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

fileinputに潜る

  • stmt.type : 323 –> stmt

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

1
2
3
  children[0] の type --> simple_stmt
  children[0] の type --> small_stmt 
  children[0] の type --> expr_stmt

的な感じで分岐する様子

  • handle_expr_stmt

handle_expr の時点での btのログ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  /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等で再構築

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy