2000行くらいのphpスクリプトをpythonに移植したいのですが、出来るだけ自動的に変換したいと思い、少し試行錯誤してみました。(単に単調に手を動かすのが嫌だという話もあります。)
- 1. 正規表現でチャレンジ
- 2. token_get_allでチャレンジ(字句解析結果を使う)
- 3. phcでチャレンジ(構文解析結果を使う)
1. 正規表現でチャレンジ
まず初めに試してみたのは正規表現を使って小さな変更を何度も繰り返して行う方法です。一行ずつソースコードを読み込んでセミコロンを消してみたり、ブレースを消してみたりと取っ付きは良く、意気揚々と書き始めて以下のようになりました。
#!/usr/bin/perl
use strict;
use warnings;
use Perl6::Say;
use Text::Trim;
my $indentUnit = " ";
my $currentIndent = 0;
while(<>) {
my $line = trim($_);
my $delta = countIndent($line);
my $result = convertLine($line);
if ($delta >= 0) {
say $indentUnit x $currentIndent . $result if $result;
$currentIndent += $delta;
}
else {
$currentIndent += $delta;
say $indentUnit x $currentIndent . $result if $result;
}
}
sub countIndent {
my $line = shift;
my $indent = 0;
# 1文字ずつ走査して{}でindentを増減させる
my $inSQ = 0; # in Single Quatation
my $inDQ = 0; # in Double Quatation
for my $c (split(//, $line)) {
if (!$inSQ and !$inDQ) {
$indent++ if $c eq '{';
$indent-- if $c eq '}';
}
$inSQ = !$inSQ if $c eq "'";
$inDQ = !$inDQ if $c eq '"';
}
return $indent;
}
sub convertLine {
my $line = shift;
$line =~ s/^< \?php$//g; # phpブロック開始削除
$line =~ s/^\?>$//g; # phpブロック終了削除
$line =~ s/\s*;$//g; # 行末のセミコロン削除
# ブロックの処理
$line =~ s/function (\w+)\(([^)]*)\)\s*{/def $1($2):/; # メソッド
$line =~ s/if\s*\(([^)]+)\)\s*{/if $1:/; # if
$line =~ s/while\s*\(([^)]+)\)\s*{/while $1:/; # while
$line =~ s/^}$//g; # 閉じブロック
# 正規表現で変換しにくい部分を処理
my $inSQ = 0; # in Single Quatation
my $inDQ = 0; # in Double Quatation
my $result = "";
for my $c (split(//, $line)) {
if (!$inSQ and !$inDQ) {
# コーテーションの外側
$result .= ($c eq '$') ? "" : $c;
}
else {
# コーテーションの内側
$result .= $c;
}
}
return $result;
}
ところが最初の勢いはすぐに無くなってしまいました。明らかに破綻している気がしたからです。理由は幾つもあるのですが、やはり各行が独立している訳ではないので、行をまたぐ変換をするにはフラグだらけの酷い事になりそうだったからです。あとはコメントの存在がやっかいで、正規表現に一致した箇所がコメントか否かを一々判断するのは無理だと思いました。(そう判断したのは僕が正規表現力が足りないという事もあります。正規表現マスターなら見事に変換プログラムを書けるかもしれません。)
2. token_get_allでチャレンジ
さて正規表現での変換を諦めて、じゃあ次はどうすれば良いのか考えました。
先ほどのプログラムを書いていて思ったのは、やはりソースコードをある程度解析する必要があるという事です。例えば”if”という文字列があった時に、それがif構文のifなのか、コメント中のifなのか、変数名のifなのか、正規表現で正面からいきなりこれらの問題を解決するのは流石にキツいので、このifはif構文のifですよという事が先に解っている状態にしてから変換をかけたいです。
という事で少し調べてみると、phpにはtoken_get_allというメソッドが用意されている事がわかりました。このメソッドをにphpのスクリプトを文字列で渡すと、解析してtoken列として返却されます。いわゆる字句解析を行ってくれる訳です。例えば以下のような訳の解らないプログラムを渡すと、(T_IF, T_VARIABLE, T_COMMENT, T_PRINT, T_CONSTANT)の様にtoken列として返却されます。これで先ほどのこのifは何のifなのかという問題は解決されました。
< ?php
# 以下のヒアドキュメント中のphpコードから
$code = <<< PHP
PHP;
$tokens = token_get_all($code);
foreach ($tokens as $token) {
if (is_array($token))
print token_name($token[0]) . "\n";
}
/* 以下のような出力を得られる (T_WHITESPACEは消去)
T_OPEN_TAG
T_IF
T_VARIABLE
T_COMMENT
T_PRINT
T_CONSTANT_ENCAPSED_STRING
T_CLOSE_TAG
*/
?>
これは行けるかもしれないと、再度意気揚々とコードを書き始めたのですが、またまたすぐに止めてしまいました。正規表現で書くよりは見通しの良いプログラムになりそうだったのですが、字句解析されたトークンを元に変換を考えた場合、先ほどと同じで、各トークンが独立でないので、トークンをまたぐ変換を行う為には多くの状態変数が必要になります。この状態変数がおそらくは複雑すぎて、僕には到底扱える気がしませんでした。字句解析だけでも不十分なのです。
3. phcでチャレンジ
ここまでで解ったのはプログラムを変換するには、最低でもプログラムの構文木を得る必要があるという事です。という訳でphp構文木を得られるツールは無いかと探して見つけたのが先ほどのphcです。
このphcを利用して再々度変換を行ってみる事にしました。このツールを使うと先ほどのエントリでも書いたようにphpスクリプトの構文木をXMLとして出力する事ができます。このXMLを見ながらpythonのコードを出力すれば良いので、php => pythonの変換を直接するよりもxml => pythonの方が遥かに簡単に期待の動作を得られそうです。(出力されるXMLのスキーマはこれです)
すでに構文木が得られているので、今まで問題となっていた複雑すぎる状態変数等に悩まされる事無く、単純に構文木の各ノードを辿りながら対応するpythonコードを出力するだけです。phpコードを入力としてpythonコードを吐くメインクラスは以下のようになりました。各々のノードタイプにあわせたProcessorを用意して、ノードをプロセッサに食わせると対応するpythonコードが出力される仕組みです。メインメソッドはprocess_nodeとprocess_downで、計15行と結構すっきり書けたんじゃないかと思います。
class ProcessContext {
# メンバ変数省略
function __construct($file) {
# dispatcherに各ノードに対応したプロセッサを準備しておく
$this->dispatcher = array(
'AST:PHP_script' => new NodeProcessor(),
'AST:Statement_list' => new StatementListProcessor(),
'AST:Catch' => new CatchProcessor(),
# 途中省略
'AST:Throw' => new ThrowProcessor(),
);
$dom = new DomDocument();
$dom->preserveWhiteSpace = false;
$dom->load($file);
$this->dom = $dom;
}
public function start_process() {
$this->process_node($this->dom->firstChild);
}
# このメソッドがメインメソッド
function process_node($cur, $contextData=null) {
if (!$cur->nodeName) { return; }
$this->process_down($cur, $contextData);
$this->process_node($cur->nextSibling, $contextData);
}
function process_down($cur, $contextData=null) {
$processor = $this->dispatcher[$cur->nodeName];
if (!$processor) { $processor = $this->defaultProcessor; }
$processor->comment_process($cur, $this, $contextData);
$done = $processor->pre_process($cur, $this, $contextData);
if (!$done) {
$this->process_node($cur->firstChild, $contextData);
}
$processor->post_process($cur, $this, $contextData);
}
# 残りのメンバメソッド省略
}
まだ変換漏れも多く、バグも潜んでいると思いますが、手元ではこのスクリプトを使って以下のような変換結果となっています。元のphpコードよりは変換後のコードをベースにした方が楽に移植できそうな位には変換できてる気がします。
変換前
#!/usr/bin/php
< ?php
/**
* 変換を意識したFizzBuzzです。
* ただし変換後そのままでは動作しません
*/
class FizzBuzz {
// メインルーチン
function fizzbuzz() {
while ($i++ < 100) {
$out = "";
if ($i % 3 == 0) $out = 'Fizz';
if ($i % 5 == 0) {
$out .= 'Buzz';
}
if ($out) {
echo "$out" . "\n";
}
else {
print "$i\n";
}
}
}
}
$fb = new FizzBuzz();
$fb->fizzbuzz();
?>
変換後
#!/usr/bin/python
#-*- coding: utf-8 -*-
"""*
* 変換を意識したFizzBuzzです。
* ただし変換後そのままでは動作しません
"""
class FizzBuzz:
# メインルーチン
def fizzbuzz():
while i+=1<100:
out = ""
if i%3==0:
out = "Fizz"
if i%5==0:
out+="Buzz"
if out:
print(""+out+""+"\n")
else:
print(""+i+"\n")
fb = FizzBuzz()
fb.fizzbuzz()
まとめと課題
構文木にアクセスできれば、構文木に対する出力を制御できるので、なんちゃって言語変換プログラムを割と簡単に作る事が出来るという事が解りました。もっと他の言語の構文木を弄るのも面白そうなので時間があればやってみようと思います。一応現状のコードはコレになります。githubにリポジトリを作りました。
ただし変換後のコードの精度が必要な場合は、現状では全然不十分で使い物になりません。特に以下の2点をこれから気が向いた時に修正して行こうと思います。
- phpの構文木の変換フェーズを入れる
今のところphpの構文木から直接pythonのコードに変換していますが、その為に構文自体を弄るような処理がしづらく、処理抜けも多いです。そこでphpの構文木からpythonの構文木へと変換するフェーズを入れる事で、より見通しのよいプログラムが書ける気がします。また言語ごとに定義されている構文木をうまく抽象化できたら、もっと面白い事ができそうですが、先は遠いですね。 - APIの違いを吸収する
構文だけ変換しても、変換後のコードが動作するようにはならないので、APIの違いを吸収する必要があります。ただ、こちらは一番と違いどのようなアプローチが有効なのかイメージが掴めていないので試行錯誤が必要そうです。
あとソースを変換するのではなくて、バイトコードを変換するアプローチもあると思うので、そこらへんもちょっと調べてみようと思いますです。
1 thought on “phpからpythonへの変換を考える”