Skip to content

Backtrace info has vanished from GHC :-(

Today I got this from a stage-1 build of GHC

Panic: the impoossible happened
   ASSERT failed!

That's all. No call stack information whatsoever. Boo!

I talked to @bgamari. Turned out to be a consequence of

commit 36cddd2ce1a3bc62ea8a1307d8bc6006d54109cf
Author: Rodrigo Mesquita <rodrigo.m.mesquita@gmail.com>
Date:   Mon Sep 23 11:51:24 2024 +0100

    Remove redundant CallStack from exceptions
    
    Before the exception backtraces proposal was implemented, ErrorCall
    accumulated its own callstack via HasCallStack constraints, but
    ExceptionContext is now accumulated automatically.
    
    The original ErrorCall mechanism is now redundant and we get a duplicate
    CallStack
    
    Updates Cabal submodule to fix their usage of ErrorCallWithLocation to ErrorCall
    
    CLC proposal#285
    
    Fixes #25283

in particular this change in GHC.Utils.Panic.Plain

 assertPanic' :: HasCallStack => a
-assertPanic' =
-  let doc = unlines $ fmap ("  "++) $ lines (prettyCallStack callStack)
-  in
-  Exception.throw (Exception.AssertionFailed
-           ("ASSERT failed!\n"
-            ++ withFrozenCallStack doc))
+assertPanic' = Exception.throw (Exception.AssertionFailed "ASSERT failed!")

Notice that the patch removed the call-stack information! The reasoning was that in the Glorious Future, and perhaps in the stage-2 compiler, Exception.throw captures and displays the call-stack info.

But it is crippling for a stage-1 compiler. If the stage-1 compiler crashes with an ASSERT error, I can't build the stage-2 compiler, even if the latter had better backtrace info.

So, I beg please can we put back the backtrace info into assertPanic and friends so that we always get call-stack info in the stage-1 compiler.

Other points

In discussing this with Ben we realised that the error infrastructure is a bit of a mess:

  • The main interface is GHC.Utils.Panic.
    • It has functions like assertPpr :: HasCallStack => Bool -> SDoc -> a -> a, and hence depends on GHC.Utils.Outputable.
    • It wrangles call-stack SDocs and ultimately calls Exception.throw
  • There is another module GHC.Utils.Plain.Panic which also exports assert-like functions.
    • This one does not depend on Outputable.
  • Sadly GHC.Utils.Panic imports GHC.Utils.Plain.Panic and re-exports all of it.
    • The former does not really depend on the latter; rather, GHC.Utils.Panic calls Exception.throw directly.
    • GHC.Utils.Panic does depend on a data type, PlainGhcException in GHC.Utils.Plain.Panic.
    • I thought that GHC.Utils.Panic.assert would be defined as GHC.Utils.Panic.assertPpr empty; i.e. same but with no user trace info. But no! It's just a re-export of GHC.Utils.Plain.Panic.assert, which really goes by a whole different route to Exception.throw`.

I propose that

  • We "bless" GHC.Utils.Panic as the primary interface, and import GHC.Utils.Plain.Panic only in the very few places where Outputable is not available.
  • Provide GHC.Utils.Panic.assert different from GHC.Utils.Plain.Panic.assert; indeed rename the latter to plainAssert perhaps.
  • Document and describe the structure, including where call-stack docs are built and how they are displayed.
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information