Language.C を使ってC言語のソースコードを解析する (with Data.Generics)
Language.CはHaskell用の、C言語のソースコードを構文解析するライブラリである。
Language.CはパーザとともにC言語の構文木に相当するデータ型を提供しており、C言語のソースコードをHaskellのデータとして操作可能である。このため、Language.CはC言語のコードを色々と解析・変換するのに便利である。さらに意味解析に役立ついくつかの補助関数も定義されている。
組込みソフトウェアに関する研究で必要になったため、Language.C を用いてC言語のソースコードを変換する簡単なプログラムを書いた。
動機
- JHC (Haskellのコンパイラ;ISO C互換のコードを吐く)は Haskellのソースから C99のコードを生成する
- ツール(ARMバイナリを生成するコンパイラ)の制約でgcc 2.95(19991024)しか手元にない。 gcc 2.95はC89しか受け付けない
- JHC が吐くコードを gcc 2.95 でコンパイルできるよう自動変換したい
そこで Haskell用の Cパーザである Language.C を使って、JHCが吐くコードを gcc 2.95でコンパイルできるように変換した。
- C89では 変数宣言はブロックの始まりにしか配置できない。そこで全ての変数宣言をブロックの頭に移動させるコードを書いた。
- Data.Generics という 木構造のトラバースに便利なライブラリを使った。scrap your boilerplate(SYB)と呼ばれる。
C言語のコードを解析したり、変換したりしたいという要望はどの業界でもありそうに思います。これを機に皆様がHaskellとLanguage.Cを使い始めることを願っています。
Language.C のインストール
Haskell処理系は Glasgow Haskell Compiler を元にした Haskell Platformが良い。
ツールCabalを使えば、Language.Cおよび必要な依存ライブラリを全て一度にインストールできる。
cabal install language-c
ソースdownload
使い方
コマンドライン引数に与えたC言語のソースを変換した結果を標準出力に出力する.
./moveVarDecls test.c
具体的には、以下の例のように、変数宣言をブロックの始めに移動する.
test.c (入力)
void f2() { } void f1() { { int y=0, z=y+1; f2(); int x=1; } f2(); int y=2; for(int i=0; i<100; i++) { } }
出力
void f2() { } void f1() { int y; { int y, z; int x; y = 0; z = y + 1; f2(); x = 1; } f2(); y = 2; for (int i = 0; i < 100; i++) { } }
ソース
module Main where import System import Data.Generics import Language.C import Language.C.System.GCC -- 第一引数のファイルを読み込み標準出力に出力 main = do (filename:[]) <- getArgs parseMyFile filename >>= (printMyAST . moveVarDecls) -- Language.C を使ってソースを parse parseMyFile :: FilePath -> IO CTranslUnit parseMyFile input_file = do parse_result <- parseCFile (newGCC "gcc") Nothing [] input_file -- プリプロセスに使うgcc (cpp?) のパスを設定しよう case parse_result of Left parse_err -> error (show parse_err) Right ast -> return ast -- 表示 printMyAST :: CTranslUnit -> IO () printMyAST ctu = (print . pretty) ctu -- Data.Generics (scrap your boilerplate) を使った、構文木のトラバース moveVarDecls :: CTranslUnit -> CTranslUnit moveVarDecls = everywhere (mkT moveVarDecls_) -- ブロックの内容を宣言と代入文に分ける. moveVarDecls_ :: [CBlockItem] -> [CBlockItem] moveVarDecls_ bs = concat decls++concat stmts -- 宣言の後にブロックが来る where (decls,stmts) = unzip (map splitVarDecls bs) -- 1つの変数宣言を宣言と代入文に分割する splitVarDecls :: CBlockItem -> ([CBlockItem],[CBlockItem]) splitVarDecls (CBlockDecl (CDecl sp assign ninfo)) = ([mkDecl sp assign ninfo], mkStmts assign ninfo) splitVarDecls x = ([],[x]) -- 宣言文から代入文を除去する. -- sp 型、記憶子、修飾子のリスト -- assign 変数名および代入文 -- ninfo ノード情報 mkDecl :: [CDeclSpec] -> [(Maybe CDeclr, Maybe CInit, Maybe CExpr)] -> NodeInfo -> CBlockItem mkDecl sp assign ninfo = CBlockDecl (CDecl sp (map mkAssign assign) ninfo) where -- declr 変数名と、constやポインタ等の修飾子 (int *x; における *xの部分) -- init 代入文の右辺 -- expr 構造体のフィールド宣言におけるビット長(ここではNothing) mkAssign (a@(_, Nothing, _)) = a mkAssign (declr, Just init, expr) = (declr, Nothing, expr) -- 代入文を除去 -- 宣言文から代入文を抜き出す. -- assign 変数名および代入文 -- ninfo ノード情報 mkStmts :: [(Maybe CDeclr, Maybe CInit, Maybe CExpr)] -> NodeInfo -> [CBlockItem] mkStmts assign ninfo = concatMap mkExpr assign where mkExpr (Just (CDeclr (Just name) _ _ _ v_ninfo), Just (CInitExpr expr i_ninfo), _) = [CBlockStmt (CExpr (Just (CAssign CAssignOp (CVar name v_ninfo) expr i_ninfo)) ninfo)] -- 代入文を生成 mkExpr _ = []