Your program has a Main.main :: IO ()
. The compiler would wrap it with a “top handler” that handles exceptions uncaught in user code to form :Main.main :: IO ()
, after Z-encoding it’s a static heap object ZCMain_main_closure
.
Now, at run-time, ZCMain_main_closure
is evaluated at here. rts_evalLazyIO
is one of the RTS API functions that takes a closure (heap object) with type IO r
and executes the side effects, returning r
without forcing it. If you visit its definition, it calls createIOThread
, which creates the main Haskell thread that’s used for executing this IO r
thing.
All Haskell threads are first created with createThread
that pushes a stop frame on the bottom of the stack. Now, createIOThread
will first push a stg_ap_v
stack frame, then on top of that, a stg_enter
frame with the ZCMain_main_closure
as its payload. The stg_enter
frame “enters” a closure, which means it will evaluate its payload to WHNF. Given ZCMain_main_closure
is a thunk and needs to be evaluated first, this makes sense.
After it’s forced to a function closure, stg_ap_v
will actually invoke the function by passing nothing to it. So this is where OP’s question really gets answered. The ap
stands for “apply” and v
stands for void, which is State# RealWorld
in the Haskell land, not backed by anything at runtime. IO r
in Haskell is State# RealWorld -> (# State# RealWorld, r #)
, and at Cmm level it’s a function closure that doesn’t really take any argument and only returns the r
closure. It’s a zero-argument function, and the crucial difference between IO r
and r
is you can invoke the IO r
many times and the side effects will be executed each time, but with r
it will be executed only once and then the result is memoized.