Language.C の意味解析 (1)

気をとりなおして。 Haskell用の C言語のパーザライブラリ Language.C の意味解析機能を使ってみる。

まずはグローバルスコープのシンボルテーブルを表示してみる。

こんなことができる

入力 (sample.c)
enum enum1 {x, y, z};

void fundec1();

struct st1 {
  int a, b;
};

typedef struct st2_ {
  int c, d;
} st2;

static int i;
static int j = 2;
int k, l;
int m = 3;

static void staticfunc(){
}

int main(int argc, char** argv) {
  func();
  exit(-1);
}
実行
./test1 sample.c

(本来なら gcc -E とかで プリプロセスしたファイルを食わせないといけない。 sample.c はそれ自身で閉じているためそのまま食わせた。gcc -Eの処理もLanguage.Cに任せたい場合はProcessMain.hsのコメント参照)

出力
Global Declarations
    enumerators
            x  ~>  <econst enum1> x  =  0
            y  ~>  <econst enum1> y  =  1
            z  ~>  <econst enum1> z  =  2
    declarations
            fundec1  ~>  declaration fundec1 | function/external | void ()
    objects i  ~>  object i | static/internal | int
            j  ~>  object j | static/internal | int = 2
            k  ~>  object k | static/external | int
            l  ~>  object l | static/external | int
            m  ~>  object m | static/external | int = 3
    functions
            main  ~>  function main | function/external | int (int argc,
                                                               char * * argv)
            staticfunc  ~>  function staticfunc | function/internal | void ()
    tags    st1  ~>  struct st1 {a :: int; b :: int;}
            st2_  ~>  struct st2_ {c :: int; d :: int;}
            enum1  ~>  enum enum1 {x  =  0; y  =  1; z  =  2;}
    typeDefs
            st2  ~>  typedef st2 as struct st2_
            __builtin_va_list  ~>  typedef __builtin_va_list as va_list

うまくできてますね。
関数内の解析はまだできない模様。

ソースおよびコンパイル

ProcessMain.hs
module ProcessMain where

import System
import Language.C
import Language.C.System.GCC
import Control.Monad


-- Language.C を使ってソースを parse
parseMyFile :: FilePath -> IO CTranslUnit
parseMyFile input_file =
  do 
     content <- readFile input_file
     let parse_result = parseC (inputStreamFromString content) (Position input_file 0 0) -- プリプロセスしない場合
     -- parse_result <- parseCFile (newGCC "/usr/bin/gcc") Nothing [] input_file -- プリプロセスする場合
     case parse_result of
       Left parse_err -> error (show parse_err)
       Right ast      -> return ast

-- 第一引数のファイルを読み込み 処理
processMain :: (CTranslUnit -> IO ()) -> IO ()
processMain process = do
  [path] <- getArgs 
  ast <- parseMyFile path
  process ast
test1.hs
import System
import Language.C
import Language.C.Analysis
import ProcessMain
import Text.PrettyPrint

process unit = do
    putStrLn (fst $ either (error . show) id $ runTrav () trav)
  where
   trav = do
     decls <- analyseAST unit
     return (render $ pretty decls)

main = processMain process
コンパイル
ghc --make test1.hs

Language.C (cont.) - flexible array member(C99) を サイズ0の配列(GNU C互換)に変換

深追い日記.
jhcが生成した手元のコードを gcc 2.95でコンパイルするにはもうひとつ障害があった.

JHCはサンクを以下の構造体で管理するようだ:

typedef struct node {
        fptr_t head;
        sptr_t rest[];
} A_MAYALIAS node_t;

typedef struct dnode {
        what_t what;
        sptr_t rest[];
} A_MAYALIAS dnode_t;

ここで、 rest[] は flexible array member とよばれ、そのままだと(おおむね)サイズ0の配列として扱われる.この構造体を使うには、malloc(sizeof(node_t)+ほげほげ)のようにして、rest分の領域を確保する.

これのp.103, 6.7.2.1節の16に載ってます:
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

しかし gcc 2.95 は flexible array memberをサポートしてない.代わりに、GNU Cでサポートされている長さ0の配列を使うように変換したい.

5.12 Arrays of Length Zero - Using the GNU Compiler Collection (GCC)

flexible array memberを使っているのは現状ここだけなので、もうsedで書き換えても良い(もしくはjhcのコードを書き換えるか…)くらいなのだけど、あえて Language.Cでこれを実装した.

リファレンスとLanguage.C.Syntax.ASTの型名とにらめっこすれば大抵のことは分かるので気合いで書けます

converting flexible array member to an array of size 0

可読性が超悪くてごめんなさい

convFlexibleArrayMember :: CTranslUnit -> CTranslUnit
convFlexibleArrayMember = everywhere (mkT conv)
  where 
   conv :: CStructUnion -> CStructUnion
   conv (CStruct CStructTag ident (Just members@(_:_)) attrs ninfo) = CStruct CStructTag ident (Just $ init members ++ [conv_ $ last members]) attrs ninfo
   conv x = x
   conv_ (CDecl specs vars@(_:_) ninfo) = CDecl specs (init vars ++ [conv__ $ last vars]) ninfo
   conv_ x = x
   conv__ (Just (CDeclr ident vars@(_:_) lit attrs ninfo), cinit, expr) = (Just $ CDeclr ident (init vars++[convArrDeclr $ last vars]) lit attrs ninfo,cinit,expr)
   conv__ x = x
   convArrDeclr :: CDerivedDeclr -> CDerivedDeclr
   convArrDeclr (CArrDeclr quals (CNoArrSize False) ninfo) =  (CArrDeclr quals (CArrSize False (CConst (CIntConst (fromRight (readCInteger DecRepr "0")) (OnlyPos (Position (posFile $ posOfNode ninfo) 0 0))))) ninfo)
   convArrDeclr x = x
   fromRight (Right i) = i
   fromRight _ = error "impossible"

usage

こんな感じで使ってます:

arm-linux-gcc -E hs.out_code.c |./MoveVarDecls |./ConvIncompleteArray >converted.c && arm-linux-gcc -mstructure-size-boundary=8 converted.c

-mstructure-size-boundary=4 は ARM専用のオプションでアラインメントを4の倍数にするために使ってます (現状、意図通りに動いているもよう)嘘でした。boundaryは8の倍数でないとダメです。
JHCはポインタの下位2ビットをガベコレ用フラグと遅延評価フラグに使ってるのでこれをやらないとまずいことになるっぽいですこれをやってもassert failureは起きます。なんでだろう。

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 _ = []