The Mojo 1.0 compiler hangs on a recursive expression transformer
- Dominant language
- Mojo
- Stars
- 1
- Forks
- 0
- PR merge metrics
- PR metrics pending
Description
Recorded here so nobody folds firepanda/sql/transform.mojo back into a recursion and loses a day to it.
What happens. A build of the package hangs. The compiler does its work, memory drops from around 196 MB back to a flat 34 MB, and then every thread parks in _dispatch_semaphore_wait_slow at 0 percent CPU and stays there. It never finishes and never fails. There is no crash dump because it does not crash.
Where it is. In the Mojo front end or in lowering, not in LLVM and not in the linker: --emit llvm and --no-optimization both hang the same way. It is not affected by -j, by @no_inline, by clearing the compiler cache, by MODULAR_THREAD_BUSY_WAIT_US, by MODULAR_ENABLE_AFFINITY=0 or by MODULAR_TELEMETRY_ENABLED=0. The compiler does hold an open socket to telemetry.modular.com while it is stuck, but turning telemetry off removes the socket and not the hang.
What triggers it. A cycle of mutually recursive functions, past some threshold of total control flow around the cycle rather than of size. Bisection:
- 200 bulk functions with loops, branches and raises, none of them recursive: 3.6 seconds.
- Twelve mutually recursive methods in a standalone file, each with a loop containing branches and raises: 1.4 seconds.
- The real transformer with five of its twenty forms live: compiles.
- The same file with a different five: compiles.
- Both sets together: hangs.
- Adding one if to a loop inside one form flips a 3.7 second build into a hang.
- Moving that loop into a non-recursive helper fixes it. Moving it into a recursive one does not.
- Splitting the file into two modules that import each other: still hangs, so the analysis is not per module.
- The whole file, all twenty-three dispatch cases intact, every recursive call replaced by a non-recursive stub: 2.1 seconds.
So the recursion cycle is the axis, and the amount of control flow reachable around that cycle is what pushes it over.
What we did about it. transform.mojo walks with an explicit stack and a results array instead of calling itself. That is a better shape for other reasons, since depth now costs heap, but it was not a free choice. The same file builds in 5 seconds.
Still to do: cut the reproducer down to something small enough to send to modular/modular, and check whether the nightly toolchain still has it. The package does not build on nightly yet for unrelated reasons, so that has to wait.
Contributor guide
Assessment
This issue has not been assessed yet.