Hyをネイティブコンパイルするツールを作った

# はじめに
[前回の投稿](http://qiita.com/koji-kojiro/items/fcacb858e4d19695e86d)でソースコードをまとめて実行アーカイブ化するツールの紹介をしました．今回ご紹介するのはHyをネイティブコンパイルし実行ファイルや共有ライブラリを作成するツールです．ついでにPythonやC言語にも変換できます．[HyCC](https://github.com/koji-kojiro/hylang-hycc)という名前で[PyPI](https://pypi.python.org/pypi/hycc)に登録しています．ただし**予期せぬバグ**の可能性がまだまだ有りますので使用は**自己責任**でおねがいします．ちなみに**全てHyで実装**しています．


- [Hyの公式ドキュメント](http://docs.hylang.org/en/stable/)
- [HyのGithubレポジトリ](http://github.com/hylang/hy)
- [Qiita: Pythonで書かれたイカしたLisp: Hy](http://qiita.com/t-sin/items/07ac1c7e413b43f8b6a8)
- [Qiita: Hyのソースコードから実行ファイルを作成するパッケージを作った](http://qiita.com/koji-kojiro/items/fcacb858e4d19695e86d)


# 使い方
インストールは`pip`からどうぞ．
**ただしWindows環境での動作は未確認です．**

```shell-session
$ pip install hycc
```
~~なお，PyPIの最新版のHy（v0.12.1）だといくつかのコードでHyのバグ起因のエラーが発生することがあるのでGithubから最新版をインストールしておいてください．~~
**2017/07/01追記：HyおよびHyCCのアップデートによりこの作業は不要になりました.**

準備は以上ですのでここから以下のコードを例にとって説明します．

```clj:hello.hy
(defn hello []
  (print "hello!"))

(defmain [&rest args]
  (hello))
```
## 実行ファイルを作成する

```shell-session
$ hycc hello.hy
```
これでカレントディレクトリに`hello`という実行バイナリが作成されます．

```shell-session
$ ./hello
hello!
```
## 共有ライブラリを作成する
`--shared`オプションつきでビルドします．

```shell-session
$ hycc hello.hy --shared
```
これでカレントディレクトリに`hello.so`という共有オブジェクトが作成されます．
Pythonは共有オブジェクトをモジュールとしてインポートする機能が有りますがHyももちろん同様です．
従ってこの`hello.so`は以下のように使用できます．

```clj:Hyからhello.soをインポート
(import hello)
(hello.hello)
; >hello!
```

```py:Pythonからhello.soをインポート
import hello
hello.hello()
# >hello!
```

ネイティブコンパイルされたモジュールや実行ファイルは**だいたい２〜８倍ぐらい高速化**されます．なので`hyc`を用いて`.pyc`ファイルにバイトコンパイルするよりも有用かと思います．またもちろん**Pythonよりも高速**です．なお内部では[Cython](https://github.com/cython/cython)を用いていますが速さのイメージとしてはだいたい以下のような感じです．

**C < Cython（型指定有り） << Cython(型指定なし) < HyCC < Python < Hy**

概ね**`hyc`の上位互換**ですが，**一点だけ注意が有ります**．内部の仕組みに関わるものですので以下より仕組みを説明しつつ解説します．

# 仕組み
大雑把にいうとHyのコードをPythonに変換しCython経由でCに変換してコンパイルしています．ただし，[前回の投稿](http://qiita.com/koji-kojiro/items/fcacb858e4d19695e86d)で触れたように，Hyに標準で付属している`hy2py`を用いて生成されるPythonのコードは**そのままでは動きません**.従って`hy2py`して`cythonize`すればよいというわけにはいきません．

## HyをPythonに変換
そもそも**なぜ`hy2py`の吐くコードが動かないのか**というとPythonでは無効な識別子が用いられているからです．Pythonにおける無効な識別子とは以下のとおりです．

1. *0-9の数字で始まる識別子*
2. *_, a-z, A-Z, 0-9以外の文字を含む識別子*

以下のコードを例にとります．

```clj:sample.hy
(reduce + [1 2 3])
; > 6
```
これを`hy2py`で変換すると以下のようになります．

```py:hy2pyが吐くコード
from hy.core.language import reduce
from hy.core.shadow import +
# +は無効な識別子
reduce(+, [1, 2, 3])
```
ここで`+`は先に挙げた２．に該当するのでダメというわけです．これを単純に別の有効な名前，例えば`add`に置換しても上手くいきません．

```py:hy2pyが吐くコードの+をaddに置換
from hy.core.language import reduce
from hy.core.shadow import add
# ImportError
reduce(add, [1, 2, 3])
```
当たり前のことですが`hy.core.shadow`モジュールには`add`という名前が定義されていないので`ImportError`となります．HyCCではこの問題を解決するためにHyのソースからまず以下のようなPythonコードを生成します．

```py:HyCCのやり方
import hy.core.language as _
reduce = _.getattr("reduce")
import hy.core.shadow as _
+ = _.getattr("+")
reduce(+, [1, 2, 3])
```
このコードは`hy2py`が吐くコードと等価ですが`+`を任意の名前で置換しても問題ありません．HyCCではASTレベルでのアクセスとソースコードレベルでのアクセスを上手く組み合わせることでエラーを回避しているわけです．同様にオブジェクトのメンバアクセスも`getattr`と`setattr`で書き換えています．

## HyCC使用時の注意(**重要**)
先ほど触れていたHyCC使用時の注意についてです．ここまでで説明したようにHyCCでは無効な識別子を工夫を交えつつ有効なものへと置換しています．この際，一点だけ副作用というか問題が発生してしまいます．

```clj:HyCCでダメなヤツ
(def hoge/fuga 0)
(print (get (globals) "hoge/fuga"))
; > 0
```

上記のコードはHyCCにより以下のようなPythonコードに変換されます．

```py:HyCCが生成するコード
from __future__ import print_function
import hy
hogex2Ffuga = 0L
print(globals()[u'hoge/fuga'])
```

いくつかインポートが追加されているのは無視していただいて構いません．もとのHyのコードでは`hoge/fuga`がPythonで無効な名前でした．従ってHyCCの生成するコードでは`hogex2Ffuga`と置換されています．

このコードを実行すると以下のようなエラーとなります．

```shell-session
  File "test.py", line 4, in <module>
    print(globals()[u'hoge/fuga'])
KeyError: u'hoge/fuga'
```

おわかりいただけたでしょうか．無効な識別子を有効なものに置換する弊害として`globals`や`locals`，`inspect`モジュールなどが場合によっては正しく動かなくなってしまいます．

この問題に対する対策はいくつか検討中ですが，そもそも**Hy自体が同様の問題をすでに抱えています**．Hyでは構文解析が走る段階で`hoge!`が`hoge_bang`に，`hoge?`が`is_hoge`にそれぞれ置き換えられます．従って以下のコードは正しく動きません．

```clj:Hyでダメなヤツ
(def hoge! 0)
(print (get (globals) "hoge!"))
; > KeyError!
```
従って，`globals`などの利用は**HyCC使用時の注意というよりはHy使用時の注意**ともいえます．

***2017/06/04追記***
**アップデートで対応しました！**具体的には`globals`や`locals`をdictっぽい謎クラスでラップすることでKeyエラー回避してます．相変わらず`inspect`モジュールは正しく動きませんがそもそも**Cython自体が`inspect`に対応していない**のでどうしようも無いというのが本音です．詳しくは[このコミット](https://github.com/koji-kojiro/hylang-hycc/commit/f3b502cf626aa13537098cf1b56eaaacf9f4c2a3)をご参照ください．

# オマケ（ちゃんと動くPythonに変換する）
HyCCには`hy2py`のようにHyからPythonに変換する機能もついています．

```shell-session
$ hycc hello.hy --python
```
これでカレントディレクトリに`hello.py`が出力されます．先述の注意に気を付けていれば**`hy2py`が出力するコードとは違いしっかり動作します**．意外とこの機能のほうが需要は高いかもしれません．また同様に`--clang`オプションでC言語にも変換できますが使い途があるかは微妙です．

# 既知のバグ(2017/06/13追記)
## 共有ライブラリのfromインポートが失敗する

[githubのissue](https://github.com/koji-kojiro/hylang-hycc/issues/)にも書きましたがPythonの仕様なのかモジュールオブジェクトに対する`gettatr`でサブモジュールを取得するときにサブモジュールが共有ライブラリを含んでいると`AttributeError`となるようです．対応を考え中です．

# おわりに
[Hyをネイティブコンパイルするツール](https://github.com/koji-kojiro/hylang-hycc)を紹介しました．開発はすべて[github](https://github.com/koji-kojiro/hylang-hycc)で行っていますので気軽に[プルリク](https://github.com/koji-kojiro/hylang-hycc/pulls)や[issue](https://github.com/koji-kojiro/hylang-hycc/issues)など投げていただいてかまいません．もちろんここでのコメントも大歓迎です．Hyはまだまだ発展途上のクソマイナー言語と言った感じですがユーザーが増えて議論が活発になることでより洗練されていくことを期待しています．
ここまで読んでいただきありがとうございました．