Skip to content

loopification non-sense around void args

Summary

Note [Void arguments in self-recursive tail calls] looks extremely suspicious. I reproduce it here:

-- Note [Void arguments in self-recursive tail calls]
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--
-- State# tokens can get in the way of the loopification optimization as seen in
-- #11372. Consider this:
--
-- foo :: [a]
--     -> (a -> State# s -> (# State s, Bool #))
--     -> State# s
--     -> (# State# s, Maybe a #)
-- foo [] f s = (# s, Nothing #)
-- foo (x:xs) f s = case f x s of
--      (# s', b #) -> case b of
--          True -> (# s', Just x #)
--          False -> foo xs f s'
--
-- We would like to compile the call to foo as a local jump instead of a call
-- (see Note [Self-recursive tail calls]). However, the generated function has
-- an arity of 2 while we apply it to 3 arguments, one of them being of void
-- type. Thus, we mustn't count arguments of void type when checking whether
-- we can turn a call into a self-recursive jump.
--

Nowadays it's very clear that foo has a post-unarise arity of 3, and we call it with 3 arguments (one of which happens to be void). And indeed if foo tail-called itself with only two arguments (and no final void argument) it would be utterly wrong to just jump to its body. (Of course, this sort of call is only type-correct in the presence of strange recursive newtypes.)

Here's a short program that demonstrates that we can in fact generate such bogus self-jumps today:

module Main (main) where

import Data.IORef (newIORef, readIORef, writeIORef)
import Control.Exception (evaluate)
import GHC.Exts (noinline)

newtype Tricky = TrickyCon { unTrickyCon :: IO Tricky }

main :: IO ()
main = do
  ref <- newIORef False
  let
    tricky :: Tricky
    tricky = TrickyCon $ do
      putStrLn "tricky call"
      v <- readIORef ref
      case v of
        False -> writeIORef ref True >> evaluate (noinline tricky)
        True  -> putStrLn "this shouldn't be printed" >> pure tricky
  () <$ unTrickyCon tricky

(Why does this reproducer uses the special handling of evaluate/seq# instead of a direct function call? ...as a work-around for other bugs. Ticket to follow...)

Compile with optimizations and run. The expected output is a single line "tricky call"; the actual output is as follows:

tricky call
tricky call
this shouldn't be printed

Environment

  • GHC version used: 9.8.1
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information