Language.C を使ってみる with Data.Generics

Language.CHaskell用の、C言語ソースコード構文解析するライブラリ。
構文木Haskellのデータとして操作可能で、これのおかげでC言語のコードを色々と操作できる。意味解析に役立ついくつかの補助関数も定義されているようだ(よく調べてない)。

やったこと、動機

  • JHC (Haskellコンパイラ;ISO C互換のコードを吐く)は C99のコードを吐くようだ
  • ツールの制約で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 のインストール

  • happyをインストール
  • alexをインストール
  • haddockをインストール(haddockにリファレンスを生成させたい場合)

この後

darcs get http://code.haskell.org/language-c
cd language-c
runhaskell Setup.hs configure 
runhaskell Setup.hs build
sudo runhaskell Setup.hs install
runhaskell Setup.hs haddock # リファレンスが欲しい場合

ソースdownload

moveVarDecls.hs

使い方

このように変数の宣言をブロックの頭にもってくる.これだけでは jhcの吐くコードは gcc 2.95 ではコンパイルできないようだがそれはまた

-bash-3.2$ cat 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++) {
		}
}
	

-bash-3.2$ ./moveVarDecls test.c 
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 _ = []