Debugging program with infinite loop somewhere

I have a 20,000 line Haskell program which I’ve been hacking on (does some IO and pure computations), and suddenly it hangs. I don’t really know which of my recent changes is causing this. Unfortunately I didn’t keep very good track of any of the recent changes. In the future I’ll commit more often and maybe fix things like this by reverting a commit.

I put it into the debugger but it’s not enlightening. Setting different breakpoints just shows that some of the breakpoints are never reached, but I’m not sure how to triangulate to specific lines as due to laziness things are not run in sequential order.

Any suggestions how to find an infinite loop?

2 Likes

The first thing I’d do is add lots of Debug.Trace.trace calls to determine when things are actually being evaluated.

6 Likes

Litter your code with trace functions (see Debug.Trace ) and check that you’re not doing a recursive let binding somewhere that fails to return.

Something I tried in the past as well was to do a time profile of my program (and kill it) to see which function was taking the most time (by hanging indefinitely). See 3. How to enable collection of performance statistics (profiling) — Cabal 3.16.0.0 User's Guide

7 Likes

What those fine persons above said.

Also keep a close eye for “shadowing” and “unused variable” warnings, you could have a loop there.

1 Like

Normally this is as simple as accidentally introducing a self referential let, and you don’t find out until later on when your program finally hits that call path. I remember discussing a GHC proposal to add a warning about self-referential variables, but I can’t remember if it went anywhere. I’ll check.

Found it: Warn on recursive bindings (#14527) · Issues · Glasgow Haskell Compiler / GHC · GitLab

2 Likes

I found it. It was a function that was supposed to call another function but due to a mistake I typed its own name, so it was calling itself.

In this case it was “debugging by inspection.” I changed a bunch of stuff without committing, but fortunately I remembered changing this function.

7 Likes

Oh, I completely forgot my old technique for finding these self referential bombs. Enable profiling and find the binding with the most entries. Usually the thing at the top was the culprit.

9 Likes

As an experiment, I configured info table profiling, run a faulty hanging function in a separate thread, and after I delay threw an asynchronous exception to that thread. My hope was that the IPEBacktrace would contain an entry for the infinitely looping function. Alas, it seems that asynchronous exceptions don’t carry backtraces (?) I guess it makes sense, given that they come from “outside” of the killed thread.

1 Like

This has also been the causes of most of my hangs. The most recent one being something like:

let req =
      otherRequest {
        requestHeaders = (hContentType, "application/json") : requestHeaders req
      }

where the second req should have been otherRequest :upside_down_face:

Standard Chartered has a NoRecursion extension in their Mu Haskell compiler. I heard they wanted to add that to GHC proper, but I don’t think it went anywhere. Maybe it could help avoid these issues.

This has been discussed somewhat over at this ghc-proposal, which it would be great to pick up in some form:

The recursive-by-default let is a real footgun in Haskell, the botched mtl-2.1 is a famous instance of it: Control/Monad/State/Class.hs

So even in a tiny function like state folks introduce loops by accidentially forgetting a .

(I spent two full days shrinking the Agda codebase only to find the loop was not introduced by us but by a dependency update.)

I am accidentally introducing loops by getting a let wrong quite regularly, so that is a real UX problem of Haskell.

3 Likes