User trap of the week: Haskell's "Off-side Rule" indentation

OK, this is going to be a total geek-out. If you're not comfortable with Haskell, you should just move on. Also, I can't figure out how to conceal the answer here, so I'll just tell you what happened… So I'm working on my Internet MiniChess Server, which is about 1000 lines of Haskell. It's kind of a mess, and I've refactored it a lot. Rather than show you the bug I induced directly, it's probably better to show a small example.

Consider this fairly simple Haskell program. When run, it outputs "hello" and then "world" on separate lines. (Don't even get me started on why the "flip" is needed here.)

    import Control.Monad.Reader

    main :: IO ()
    main = do
      flip runReaderT "world" $ do
        flip runReaderT "hello" $ do
          t <- ask
          liftIO $ putStrLn t
        s <- ask
        liftIO $ putStrLn s

So far, so good. But what if this is all buried in a million lines of code, and when trying to work on that code you instead end up with the following indentation?

    import Control.Monad.Reader

    main :: IO ()
    main = do
      flip runReaderT "world" $ do
      flip runReaderT "hello" $ do
      t <- ask
      liftIO $ putStrLn t
      s <- ask
      liftIO $ putStrLn s

Now you might foolishly think, as I did, that GHC would produce an error here, or at least a warning when all warnings are enabled. After all, there are two do bodies that are not indented at all.

Apparently GHC takes the "Off-side Rule" really seriously. As in, since the stuff after the first do is indented not-to-the-left, it must be indented to the right. Same with the second do.

So the program compiles and runs without error and prints "hello" "hello".

1.5 hours to find the corresponding bug in context. (B)