Language.C を使ってC言語のソースコードを解析する (with Data.Generics)

Language.CHaskell用の、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

moveVarDecls.hs

使い方

コマンドライン引数に与えた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 _ = []