初めての RPython Toolchain

タイトルは釣りっぽく見えるかもしれませんが, 理論にばっかり興味あったので, RPython をちゃんと触るのは初めてです.

発端

理論派で手を動かすのが億劫な俺が実装書いてみようと思った動機は, PyPy Sudden Death Calendar 27日目 - JVM Backend に完敗した件を受けて にある話にあります. ここでは ootype によって JVM 用に変換されたクラスがしっちゃかめっちゃかだ, ということを追っています. なんでこんなことになっているのか分からないので, まずは簡単なインタプリタを例に RPython Toolchain の動きを追っていく計画です.

プログラム仕様

このプログラムの仕様はすごく簡単で, 各行の先頭に暗黙の echo があるものとして動作します. プログラミング言語 ECHO とでも呼びましょうか.

実装

さっきのブログエントリの筆者でもある shoma さんが翻訳されている PyPy を使ってインタプリタを書く を参考に作りました.

"""
echo.py

programming language ECHO
implemented on top of PyPy

by cocoatomo
"""

import sys
import os

try:
    from pypy.rlib.jit import JitDriver
except ImportError:
    class JitDriver(object):
        def __init__(self, **kw): pass
        def jit_merge_point(self, **kw): pass
        def can_enter_jit(self, **kw): pass

jitdriver = JitDriver(greens=['pc', 'program'], reds=[])

def mainloop(program):
    pc = 0

    while pc < len(program):
        jitdriver.jit_merge_point(program=program, pc=pc)

        line = program[pc]

        # instead of print
        os.write(1, line)
        os.write(1, '\n')

        pc += 1

def parse(program):
    return program.split('\n')

def run(fp):
    program_contents = ''
    while True:
        read = os.read(fp, 4096)
        if len(read) == 0:
            break
        program_contents += read
    os.close(fp)

    program = parse(program_contents)
    mainloop(program)

def entry_point(argv):
    if len(argv) < 2:
        print("too few arguments. program file name needed.")
        return 1

    run(os.open(argv[1], os.O_RDONLY, 0777))
    return 0

def target(*args):
    return entry_point, None

def jitpolicy(driver):
    from pypy.jit.codewriter.policy import JitPolicy
    return JitPolicy()

if __name__ == "__main__":
    entry_point(sys.argv)

なんの変哲も無い処理系です.

PyPy は pypy-ja で fork している レポジトリ から取ってきました.

$ python ./pypyja/pypy/translator/goal/translate.py --opt=jit echo.py

とすると長々とビルドが走り, MBA によって俺の腿が温まりました.

$ cat input.echo
begin
1
2
3
4
5
end

$ ./echo-c input.echo
begin
1
2
3
4
5
end

と無事に期待通りの動作をします.

実はこの JIT を追加したインタプリタの生成で, 1つ失敗をしました.

jitdriver = JitDriver(greens=['pc', 'program'], reds=[])

の行を

jitdriver = JitDriver(greens=['program', 'pc'], reds=[])

と書いて変換したところ以下のようなエラーでコケました.

[translation:ERROR] Error:
[translation:ERROR]  Traceback (most recent call last):
[translation:ERROR]    File "./pypyja/pypy/translator/goal/translate.py", line 308, in main
[translation:ERROR]     drv.proceed(goals)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/translator/driver.py", line 809, in proceed
[translation:ERROR]     return self._execute(goals, task_skip = self._maybe_skip())
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/translator/tool/taskengine.py", line 116, in _execute
[translation:ERROR]     res = self._do(goal, taskcallable, *args, **kwds)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/translator/driver.py", line 286, in _do
[translation:ERROR]     res = func()
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/translator/driver.py", line 397, in task_pyjitpl_lltype
[translation:ERROR]     backend_name=self.config.translation.jit_backend, inline=True)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/metainterp/warmspot.py", line 43, in apply_jit
[translation:ERROR]     **kwds)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/metainterp/warmspot.py", line 187, in __init__
[translation:ERROR]     self.find_portals()
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/metainterp/warmspot.py", line 244, in find_portals
[translation:ERROR]     self.split_graph_and_record_jitdriver(*jit_merge_point_pos)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/metainterp/warmspot.py", line 259, in split_graph_and_record_jitdriver
[translation:ERROR]     graph.startblock = support.split_before_jit_merge_point(*jmpp)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/codewriter/support.py", line 78, in split_before_jit_merge_point
[translation:ERROR]     greens_v, reds_v = decode_hp_hint_args(portalop)
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/codewriter/support.py", line 107, in decode_hp_hint_args
[translation:ERROR]     return (_sort(greens_v, True), _sort(reds_v, False))
[translation:ERROR]    File "/Users/tomohiko/MyWorks/Python/PyPy/pypyja/pypy/jit/codewriter/support.py", line 104, in _sort
[translation:ERROR]     assert lst == lst2
[translation:ERROR]  AssertionError
[translation] start debugger...
> /path/to/PyPy/pypyja/pypy/jit/codewriter/support.py(104)_sort()
-> assert lst == lst2
(Pdb+) lst
[program_0, pc_0]
(Pdb+) lst2
[pc_0, program_0]
(Pdb+)

中身見てみると program と pc の順序が逆になっているので, 上のような修正を行って無事 JIT 付きインタプリタが生成できました.

考察

順序に依存しているのは解せないところではありますが, 暗黙のルールとして「ローカル変数」「引数」の順に並べなくてはいけないのかな. と予想しています. (資料やソースまでは追っていません.)

実装の都合を考えれば順序が固定されていた方が楽なのは分かりますが, たったこれだけのために生成処理がコケるのは痛いですね. ローカル変数と引数に同じ名前の変数が出てきたときにはどうなるんでしょうね.

特に今追う必要は無いと思っていますが, もうちょい実装詳細について知る必要が出てきたら思い出して調べてみたいと思います.

(追記)

shoma さんがコケた原因を教えてくれました.

コケたのは pypyja/pypy/jit/codewriter/support.py の中にある decode_hp_hint_args 関数の assert lst == lst2 という assert 文でした. この直前に

# a crash here means that you have to reorder the variable named in
# the JitDriver.  Indeed, greens and reds must both be sorted: first
# all INTs, followed by all REFs, followed by all FLOATs.

というコメントがあり, greens や reds のリストの要素となる変数名は,「整数, 参照, 浮動小数」の順で JitDriver に渡さなければいけないとのことでした.

それでもこの都合の理由が分かりませんが, 条件が判明しただけでも良しとしましょう.

余談

translate.py を MacBook Air で走らせると暖まれますよ.

Comments

blog comments powered by Disqus

Licenses