Edited at

PythonでS式からC言語を生成する

More than 1 year has passed since last update.


はじめに

注意事項


  • お遊びの類とお考えください.


  • gaucheのアレの話ではありません.

  • 筆者はC言語にすごく詳しいわけではありません.おかしいところがあればご指摘ください.

ここ最近PythonでLispを作って遊んだりしていました.

これはこれで楽しかったのですが,使う使わないは別として実用性が皆無なのでいまいちモチベーションが上がらず,次はなにで遊ぼうかと悩んでいました.そんな折,ふとgaucheにはS式→C言語なトランスパイラが付属しているらしいといった記事をかつて何処かで読んだのを思い出し,面白そうだったので自作Lispのリーダ部分をまるっと流用してPythonで実装してみました.

なお,本家のものはCiSE(C in S-Expression)と言うそうですが,今回実装したものは全くの別物です.ちなみにどちらかというとCommon Lisp風味な仕上がりです.

CLOPという名前をつけて公開しているので興味がアレばどうぞ.ちなみにあんまり気に入っていない名前なのでいいのが思いつけば変えるかもしれません.CiSEはかっこよくていいですね.


仕組み

S式からC言語を生成する流れは概ね以下のような感じです.


  1. S式→文字列のリストに変換

  2. リストの先頭が予め定義されている関数だった場合残りを引数として呼び出す

  3. そうでない場合はCの関数として扱う

予め定義された関数と言うのは例えば以下のようなものです.

def aref(array, subscript):

return f"{array}[{subscript}]"

例えば,(aref hoge fuga)["aref", "hoge", "fuga"]に変換された後(1),"hoge[fuga]"を生成する(2)といった具合です.

この時リストの先頭の要素がルックアップされなかった場合はcの関数として処理されます.例えば(printf "hello")"printf(\"hello\")"を生成(3)します.

また+hoge+HOGEに,hoge-hogehoge_hogeに変換されます.

予め定義された関数という表現をしましたが要はマクロのようなものです.CLOPでは予め以下のようなマクロが定義されています.

* + - / 1+ 1- < > and aref ash break cast cond continue

decf declare defconstant defenum defstruct defsyntax
defun defunion defvar eq for if incf include let logand
logior logxor mod not or progn return setf when while

ほとんどCommon Lispから名前を執ってきているかCそのまんまなのでどれが何をするのかは大体想像がつくと思います.

多分見落としがなければC言語の機能は概ねすべて扱えるはずです.


実例


Hello, world!


hello.clop

(include stdio)

(defun int:main (void)
(printf "Hello, world!\n")
(return 0))


割りと見たまんまだと思います.型はunsigned:int:hogeのように指定子をコロンで区切って宣言します.以下のコマンドでC言語に変換できます.

$ clop translate hello.clop --out hello.c


hello.c

#include <stdio.h>


int main (void)
{
printf("Hello, world!\n");
return 0;
}


マクロ

一応マクロが使えます.ただCLOPは結局のところリスト→文字列変換器に過ぎないのでリスト操作の機能は皆無です.従ってCommon Lispのような強力なマクロは使用できませんがC言語のマクロ関数よりは柔軟な記述ができます.例を示します.

(defsyntax null (ptr)

(eq ,ptr +null+))

これは以下のように展開されます.

hoge == NULL // (null hoge)

クォートがないのにバッククォートがあるのがなんとも気持ち悪いですがバッククォートをつけておくとマクロ展開時に与えられた引数から値が挿入されます.これだけだとCPPのマクロと変わらないのでもう少し複雑な例を示します.

CLOPではCommon Lisp風のオプション引数,キーワード引数,可変長引数が使えます.


with-open-file.clop

(include stdio)

(defsyntax null (ptr)
(eq ,ptr +null+))

(defsyntax with-open-file ((stream filespec &optional (mode "w")) &body body)
(let ((FILE*:,stream (fopen ,filespec ,mode)))
(if (null ,stream)
(perror "Failed to open file")
(progn
,@body
(fclose ,stream)))))


,@hogeとすれば引数の中身がそのまま展開されます.(カッコが一つとれます)

マクロの定義は単一のソースコードに書いてもいいですがrequireで別ファイルから引っ張って来ることもできます.この時includeされているヘッダファイルも既にインクルードしているものでなければ同時に引っ張ってきます.なお,マクロ定義とインクルード以外は読み飛ばされます.

先のwith-open-file.clopmain.cloprequireする例を示します.


main.clop

(include stdio)

(require with-open-file)

(defun int:main (void)
(with-open-file (fd "hello.txt")
(fprintf fd "Hello, world!\n"))
(return 0))


$ clop translate main.clop --out main.c


main.c

#include <stdio.h>


int main (void)
{
({
FILE* fd = fopen("hello.txt", "w");
fd == NULL? (perror("Failed to open file")) : (({
fprintf(fd, "Hello, world!\n");
fclose(fd);
}));
});
return 0;
}

なんとも気色の悪いコードですがちゃんと動きます.CLOPでは複文の式化(({hoge; fuge; ...})みたいの)を多用しているので場合によっては可読性がひどいことになります.


余談


1. 利点

むりやり利点を考えてみます.


  • 可読性が上がる(人による)

  • 記述が短くなる(多分)

  • ちょっとだけ柔軟にマクロを使える

3つ目以外は微妙ですね.


2. 苦労した点

C言語の構文が複雑だと感じました.生のCを書いてるときは思わないのですがS式から生成する場合にはどこに括弧をつけるべきか,どこでセミコロンを打つべきかなどなどかなり悩まされました.そう考えるとS式の表現力はすごいと思います.


まとめ

オレオレCiSEをPythonで実装しました.非S式なものをS式で書きたいという欲求は普遍的に一定数存在しているように思いますが(例えばHy)C言語の場合はどうなんでしょうか.本家gaucheのCiSEも触ってみたいと思いますがあんまりまとまった情報がないのが残念です.(見つけられてないだけかもしれません.よいドキュメントがあればせひ教えてください)

追記

こんなのありました.https://github.com/burtonsamograd/sxc