main関数について。

GHCのコードを眺めていてmain-isというオプションがあることに気づきました。これを使うとmainとして使用する関数を変える事ができるみたいです。

試しにC言語風のmainを書いてみました。

-- Cmain.hs
module Cmain() where

import Main (main)
import System (getArgs, getProgName)

cmain :: IO ()
cmain = do
    progname <- getProgName
    args     <- getArgs
    let argv = progname : args
        argc = length argv
     in argv `seq` argc `seq` main(argc, argv)

引数の第0要素にはCと同じく実行ファイル名*1を入れました。

これを使って、catプログラムを書いてみました。(見た目上手続き言語っぽく書いてみようと思いましたが、微妙でした。)

-- Main.hs
module Main where

import Control.Monad
import System.Exit

main :: (Int, [String]) -> IO ()
main(argc, argv) = do 
    {
        when (argc < 2)$do
        {
            putStrLn( usage(argv!!0) );
            exitWith( ExitFailure(1) );
        };

        putStr =<< readFile(argv!!1);
    };


usage :: String -> String
usage prog = "usage : " ++ prog ++ " <file name>"

コンパイルするときには

% ghc --make -main-is Cmain.cmain Cmain.hs -o cat   

みたいにCmain.hs内のcmainをmainに指定します。

実行例

% ./cat                                                                                                           
usage : cat <file name>
% ./cat Main.hs                                                                                                   
-- Main.hs
module Main where

import Control.Monad
import System.Exit

main :: (Int, [String]) -> IO ()
main(argc, argv) = do 
    {
  ...

これ自体はただのお遊びなのですが、どういう流れでmainがコンパイルされるかに興味がでてきたのでメモ書き程度ですが、調べた事を書きます。

GHCコンパイラ自体のmainは、compiler/main/Main.hsから始まっていて、

  • main
  • doMake
    • ソースファイルをメイク。
    • mapM (compileFile ... ) ... とか何かかなりわかりやすい

プリプロセッサとかアセンブラとかをパイプライン実行
compiler/main/DrivePipeline.hs

  • compileFile (省略)
  • runPipeline (省略)
  • pipeLoop
    • runPhaseという関数を呼び出し、ループ
    • runPhase内ではプリプロセッサを呼び出したりアセンブラを呼び出したりということをPhaseというデータに基づいて実行。今興味があるのはHscフェーズ

Hscフェーズ。hscCompileOneShotを呼び出しコンパイル開始。

  • hscCompileOneShot
  • hscFileFrontEnd
    • ソースをパース->リネーム->型検査->脱糖している。
    • 調べたところ型検査時にmainを検出している模様

型検査とリネーム。

  • compiler/typecheck/TcRnDriver.lhs
  • tcRnModule
    • import文の解析などをしている
  • tcRnSrcDecls
    • トップレベルで宣言された関数などの型検査とリネーム。mainはトップレベルで定義されるのでここ。
    • 一つ一つに分解して、tc_rn_src_declsに渡す
  • tc_rn_src_decls
    • 基本的に型検査とリネームを行うが、ここで現在のモジュールがMainであるかどうかをチェックしている。
  • checkMain
    • Mainモジュールの名前, main関数の名前を見つけて、check_mainに渡す
  • check_main
    • main関数があるかどうかを検索する。(Mainモジュールにはmain関数がなければいけないというチェックをここでしているようだ)
  • main関数を見つけたら、runMainIO関数にmain関数を引数として渡すという内容の関数呼び出しを作って、root_main_idという変数にバインドしている。

とりあえずここで終了。
これを逆にたどると、
root_main_idを呼ぶ -> runMainIO mainが呼ばれる -> mainが実行される
という感じですね。

GHCコンパイラをざっと読んでみて思いましたが、すごく読みやすいんじゃないかと思います。(他のコンパイラのコードと比較してという話)
Haskellは参照透明なので、いちいち変数がどこで更新されているかといった状態遷移を追わなくていいってところが大きいんじゃなかと思います。普段は参照透明であることのありがたみはあまり感じないですが、規模が大きくなってくると違うんではないでしょうか。

とりあえずroot_main_idというアセンブリにつながりそうなキーワードが見えて来たので、最終的にどんな風にアセンブリが生成されるのかを追ってみようかと思います。目指すはHaskellのコードから生成されるアセンブリが分かる人(笑)

*1:実際は実行ファイル名からディレクトリ部などを除いたものです