0

First the Question - is the behavior below expected logically, or a bug to be reported for GHC?

The code below will leak memory (tested on ghc-8.8.4) because ghc seems to add join point and jumps to it at the end of the loop, building up the stack.

{-# OPTIONS_GHC -fno-full-laziness #-}

module Main where

import Control.Concurrent.Async (async,waitCatch)
import Data.IORef
import GHC.Conc

main :: IO ()
main = do
  val <- newIORef 0 :: IO (IORef Int)
  let loop1 = do
          cval <- readIORef val
          threadDelay 1
          writeIORef val (cval + 1)
          case cval > 100000000 of
            True -> error "done"
            False -> loop1
          loop1 -- Deliberately, add this to cause space leak, but this statement is never executed because of case branching above

  loop1Async <- async loop1
  res <- waitCatch loop1Async

  return ()

Compiling with -O2 -rtsopts -threaded and running with +RTS -s -hT -N will show space leak because of growing stack.

Looking at core output, it seems the leak is due to join (I guess it is a join point) and a jump to it at the end of the loop which grows the stack (if I have read the core correctly). Removing the last statement in loop1 fixes the leak.

ghc core output is here.

Update: Based on feedback in comments, it seems to be logical behavior, not a bug in ghc. So, an answer explaining stack increase would be good to have. This helps us understand what is going on here. ghc core output has been posted above.

Sal
  • 4,312
  • 1
  • 17
  • 26
  • 1
    This comment in your source is wrong: "this statement is never executed because of case branching". It is probably also the source of your confusion. If I had to guess about the source of this wrong belief: remember that `return` does not mean early-exit in Haskell -- at least not when it's in an `IO` block. – Daniel Wagner Aug 28 '21 at 03:42
  • Yep, mistake in the code - fixed now. Now, the statement is never executed – Sal Aug 28 '21 at 08:41
  • 2
    @Sal I don't think it's a bug in GHC. It's an optimization that would be *nice*, but I don't believe it is specified as reliable anywhere. The first call is *not* in tail position, because it's followed by another call; analysis of `loop1` could reveal that it never returns, and thus anything after the first call to `loop1` (in any function, not just this one) is dead code that can be optimized away. The compiler can never be guaranteed to notice and optimize every fact about your code, though. In this case the "bug" is clearly in the program, not the compiler: don't write dead code. – Ben Aug 28 '21 at 12:32

1 Answers1

1

I would certainly expect that line to cause the use of stack space. That extra call to loop1 is hardly irrelevant.

Change the test constant to 10 instead of a larger number, and replace the return () branch with print cval.

Compare the output you get with and without the "never executed" statement. You might find it's somewhat more executed than you think.

Carl
  • 26,500
  • 4
  • 65
  • 86
  • a slight mistake in the code I put up. I fixed it. Now, the extra call to `loop1` should be irrelevant. It looks like tail-recursive code but adds stack. – Sal Aug 28 '21 at 08:44
  • 1
    You are asking GHC to take on a pretty difficult task, in expecting it to prove that `loop1` never reaches its last line. It reaches the last line if and only if the inner call to `loop1` returns normally. So I think this is kinda the halting problem? "Optimize this out if you can prove that this program never terminates". Obviously that doesn't mean it's impossible - for some code, you can prove it never halts. But in general it is impossible, and GHC may choose not to spend effort trying to prove it. – amalloy Aug 28 '21 at 08:47
  • @amalloy, some mistakes in the example I put up - the code is now fixed so that last statement is never reached logically. – Sal Aug 28 '21 at 09:21
  • 3
    @Sal I believe amalloy's comment applies even to the latest code you have posted. – Daniel Wagner Aug 28 '21 at 14:49