STG

頑張れば、GHCの吐くCも読めそうな気がしてきた。が、どっちかというと、C--コードを読んだ方がよい気がする。アセンブリもまあ、STGマシンのレジスタと実機のレジスタの対応関係が分かれば読めそうな気がする(対応関係とかは、include/Regs.hあたりみれば分かるかも)

{-# OPTIONS -fglasgow-exts #-}
x::Double = 2.0
y::Double = 3.1
test = (sin x)+y
main = return test

を、-ddump-stg付けてコンパイルすると、

x1_r1H0 = NO_CCS GHC.Float.D#! [2.0];
SRT(x1_r1H0): []
y1_r1H2 = NO_CCS GHC.Float.D#! [3.1];
SRT(y1_r1H2): []
test1_r1H4 =
    \u srt:(0,*bitmap*) []
        case GHC.Float.$fFloatingDouble of tpl_s1If {
          GHC.Float.:DFloating tpl1_s1Hs
                               tpl2_s1Ig
                               tpl3_s1Ih
                               tpl4_s1Ii
                               tpl5_s1Ij
                               tpl6_s1Ik
                               tpl7_s1Il
                               tpl8_s1Im
                               tpl9_s1In
                               tpl10_s1Io
                               tpl11_s1Ip
                               tpl12_s1Iq
                               tpl13_s1Ir
                               tpl14_s1Is
                               tpl15_s1It
                               tpl16_s1Iu
                               tpl17_s1Iv
                               tpl18_s1Iw
                               tpl19_s1Ix ->
              case tpl1_s1Hs of tpl20_s1Iy {
                GHC.Real.:DFractional tpl21_s1Hy tpl22_s1Iz tpl23_s1IA tpl24_s1I
B ->
                    case tpl21_s1Hy of tpl25_s1IC {
                      GHC.Num.:DNum tpl26_s1ID
                                    tpl27_s1IE
                                    tpl28_s1I6
                                    tpl29_s1IF
                                    tpl30_s1IG
                                    tpl31_s1IH
                                    tpl32_s1II
                                    tpl33_s1IJ
                                    tpl34_s1IK ->
                          let {
                            sat_s1I5 =
                                \u srt:(0,*bitmap*) []
                                    case GHC.Float.$fFloatingDouble of tpl35_s1I
L {
                                      GHC.Float.:DFloating tpl36_s1IM
                                                           tpl37_s1IN
                                                           tpl38_s1IO
                                                           tpl39_s1IP
                                                           tpl40_s1IQ
                                                           tpl41_s1IR
                                                           tpl42_s1IS
                                                           tpl43_s1I3
                                                           tpl44_s1IT
                                                           tpl45_s1IU
                                                           tpl46_s1IV
                                                           tpl47_s1IW
                                                           tpl48_s1IX
                                                           tpl49_s1IY
                                                           tpl50_s1IZ
                                                           tpl51_s1J0
                                                           tpl52_s1J1
                                                           tpl53_s1J2
                                                           tpl54_s1J3 ->
                                          tpl43_s1I3 x1_r1H0;
                                    };
                          } in  tpl28_s1I6 sat_s1I5 y1_r1H2;
                    };
              };
        };
SRT(test1_r1H4): [GHC.Float.$fFloatingDouble]

mainは省略。やたら長いけど、Core言語と一緒でやってることは、関数テーブルをたどってるだけだったりする。

最初の"GHC.Float.:DFloating tpl1_s1Hs..."の部分は、:info Floatingすると分かるけど、Floatingクラスの関数テーブルが、

DWORD *Floating_ftbl[]={Fractional_ftbl , pi_closure , exp_closure , sqrt_closure , ...}

みたいになってる(のだと思う)。で、Fractional_ftblをたぐると、こいつは、 tpl21_s1Hy tpl22_s1Iz tpl23_s1IA tpl24_s1Iという4つのメンバーを持つ。順に、Num_ftbl,(/),recip,fromRationalに対応。で、更に、Num_ftblをたどると、3つ目に(+)演算子がメンバーに見つかる。同様に、Floatingクラスの8番目のメンバーがsinなので、tpl43_s1I3はsinを表す。

というわけで、上のコードは簡単に書くと、Core言語の段階ではまだ

test = (sin x)+y

となっているのを

let sat_s1I5 = sin x1_r1H0
 in  sat_s1I5 + y1_r1H2

な感じにしてるんだと思う。CPSとかANFとかと似たような感じ?と思ってたら、shelarcyさんに教えていただいたリンクに、おもっきりA Normal Formとか書いてあるので、A正規化の結果だと思います、ええ

同じコードを-ddump-cmm付けてコンパイルすると、

section "data" {
    r1H0_closure:
	const GHCziFloat_Dzh_static_info;
	const 2.0 :: F64;
}
 
-------------------
 
section "data" {
    r1H2_closure:
	const GHCziFloat_Dzh_static_info;
	const 3.1 :: F64;
}
 
-------------------
 
section "relreadonly" {
    r1H4_srt:
	const GHCziFloat_zdfFloatingDouble_closure;
}

section "data" {
    r1H4_closure:
	const r1H4_info;
	const 0;
	const 0;
	const 0;
}

s1IL_ret() {
	s1IL_info {
		const 0;
		const 36;
	}
    c1Jq:
	I32[Sp + 0] = r1H0_closure; //=x=2.0
	R1 = I32[R1 + 32]; //sin関数
	Sp = Sp + (-4);
	jump stg_ap_p_info;
}

s1I5_entry() {
	s1I5_info {
		const r1H4_srt-s1I5_info;
		const 131072;
		const 65554;
	}
    c1Jt:
	if (Sp - 16 < SpLim) goto c1Jv;
	I32[Sp + (-8)] = stg_upd_frame_info;
	I32[Sp + (-4)] = R1;
	R1 = GHCziFloat_zdfFloatingDouble_closure;
	I32[Sp + (-12)] = s1IL_info;
	Sp = Sp + (-12);
	jump I32[R1];
    c1Jv: jump stg_gc_enter_1;
}

s1IC_ret() {
	s1IC_info {
		const r1H4_srt-s1IC_info;
		const 0;
		const 65572;
	}
    c1Jx:
	Hp = Hp + 12;
	if (Hp > HpLim) goto c1Jz;
	I32[Hp + (-8)] = s1I5_info;
	I32[Sp + 0] = r1H2_closure; //=y=3.1
	I32[Sp + (-4)] = Hp + (-8); //push sin(2.0)?
	R1 = I32[R1 + 12]; //(+)関数
	Sp = Sp + (-8);
	jump stg_ap_pp_info;
    c1Jz:
	HpAlloc = 12;
	jump stg_gc_enter_1;
}

s1Iy_ret() {
	s1Iy_info {
		const r1H4_srt-s1Iy_info;
		const 0;
		const 65572;
	}
    c1JB:
	R1 = I32[R1 + 4]; //Num
	I32[Sp + 0] = s1IC_info;
	jump I32[R1];
}

s1If_ret() {
	s1If_info {
		const r1H4_srt-s1If_info;
		const 0;
		const 65572;
	}
    c1JD:
	R1 = I32[R1 + 4]; //Fractional
	I32[Sp + 0] = s1Iy_info;
	jump I32[R1];
}

r1H4_entry() {
	r1H4_info {
		const r1H4_srt-r1H4_info;
		const 131072;
		const 65560;
	}
    c1JG:
	if (Sp - 20 < SpLim) goto c1JI;
	Hp = Hp + 12;
	if (Hp > HpLim) goto c1JI;
	I32[Hp + (-8)] = stg_CAF_BLACKHOLE_info;
	call "ccall" newCAF((R1, PtrHint));
	I32[R1 + 4] = Hp + (-8);
	I32[R1] = stg_IND_STATIC_info;
	I32[Sp + (-8)] = stg_upd_frame_info;
	I32[Sp + (-4)] = Hp + (-8);
	R1 = GHCziFloat_zdfFloatingDouble_closure;
	I32[Sp + (-12)] = s1If_info;
	Sp = Sp + (-12);
	jump I32[R1];
    c1JI:
	HpAlloc = 12;
	jump stg_gc_enter_1;
}
 

となる。はい、読めません!いやまあ、なんかSTGマシンが分かれば、これくらいはスラスラ書けるのかもしれない(書かなくていいですが)。上の関数テーブルたどって、関数呼び出してる処理をフラットにしただけだとか、そういう雰囲気くらいは掴めるよ。R1とかは、見ての通りレジスター。Spは多分スタックポインタ。131072とか謎のマジックナンバーは何だろう。Core言語->STG言語->STGマシンコードあたりの変換は、普通の関数型言語コンパイルで使う技術と似たようなもんなのかな。にしても、各型クラスの関数テーブルの先頭に、親クラスの関数テーブルへのポインタを持っているような感じっぽいけど、いちいちこれ辿るの無駄じゃないのかなー。

というわけで、最初は、どっから手を付けていいか分からなかったGHCだけど、何とか理解できそうな気がしてきた。というか、やってることは意外と普通っぽいな〜。あと、デバッグも普通にできるんじゃね?という勇気が