This is a simple "improvement" of the STG runtime which was implemented in GHC. I'll share what I have understood, but GHC experts can likely provide more useful and accurate information.
GHC compiles to an intermediate language called Core, after having done several optimizations. You can see it using ghc -ddump-simpl ...
Very roughly, in Core, an unevaluated binding (like let x = 1+y+x in f x
) creates a thunk. Some memory is allocated somewhere to represent the closure, and x
is made to point at it.
When (and if) x
is forced by f
, then the thunk is evaluated. Here's the improvement: before the evaluation starts, the thunk of x
is overwritten with a special value called BLACKHOLE
. After x
is evaluated (to WHNF) then the black hole is again overwritten with the actual value (so we don't recompute it if e.g. f x = x+x
).
If the black hole is ever forced, <<loop>>
is triggered. This is actually an IO exception (those can be raised in pure code, too, so this is fine).
Examples:
let x = 1+x in 2*x -- <<loop>>
let g x = g (x+1) in g 0 -- diverges
let h x = h (10-x) in h 0 -- diverges, even if h 0 -> h 10 -> h 0 -> ...
let g0 = g10 ; g10 = g0 in g0 -- <<loop>>
Note that each call of h 0
is considered a distinct thunk, hence no black hole is forced there.
The tricky part is that it's not completely trivial to understand which thunks are actually created in Core since GHC can perform several optimizations before emitting Core. Hence, we should regard <<loop>>
as a bonus, not as a given / hard guarantee by GHC. New optimizations in the future might replace some <<loop>>
s with actual non-termination.
If you want to google something, "GHC, blackhole, STG" should be good keywords.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…