Optimizer hangs in `kill_edge_terminator!`: `stmt === nothing && continue` never advances `idx`
Certain functions make `adce_pass!` spin forever inside `kill_edge_terminator!`. The hang is at **compile time** — `code_typed` alone reproduces it, no execution needed — and there is no diagnostic of any kind; the process simply never returns.
## Reproducer
```julia
function f(c)
r = Ref(false)
while true
if c !== nothing
r[] = true
end
if r[]
continue
else
break
end
end
end
f(nothing) # never returns
```
`code_typed(f, (Nothing,))` and `precompile(f, (Nothing,))` hang identically, so this is purely an optimizer issue.
With `c::Nothing`, `c !== nothing` is statically false, so `r[]` is provably `false` and the loop should `break` on its first iteration — `f(nothing)` should return immediately.
## Affected versions
| Version | Result |
|---|---|
| 1.7.2 | ok |
| 1.10.12 | ok |
| 1.11.0 | **hangs** |
| 1.11.9 | **hangs** |
| 1.12.7 | **hangs** |
| 1.13.0-rc3 | **hangs** |
Reproduced on official macOS aarch64 binaries (1.10.12, 1.11.0, 1.11.9, 1.13.0-rc3).
```
Julia Version 1.13.0-rc3
Commit a861d5fe286 (2026-08-13 18:20 UTC)
Build Info:
Official https://julialang.org release
Platform Info:
OS: macOS (arm64-apple-darwin25.5.0)
CPU: 8 × Apple M1 Pro
WORD_SIZE: 64
LLVM: libLLVM-20.1.8 (ORCJIT, apple-m1)
GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 6 virtual cores)
```
## Backtrace (SIGTERM into the hung process, 1.13.0-rc3)
```
size at ./essentials.jl:10 [inlined]
length at ./essentials.jl:11 [inlined]
getindex at ./../usr/share/julia/Compiler/src/ssair/ir.jl:318 [inlined]
kill_edge_terminator! at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1440
kill_edge_terminator! at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1413
kill_edge_terminator! at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1413
process_node! at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1524
iterate_compact at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1964
iterate at ./../usr/share/julia/Compiler/src/ssair/ir.jl:1886 [inlined]
adce_pass! at ./../usr/share/julia/Compiler/src/ssair/passes.jl:2161
run_passes_ipo_safe at ./../usr/share/julia/Compiler/src/optimize.jl:1049
run_passes_ipo_safe at ./../usr/share/julia/Compiler/src/optimize.jl:1062 [inlined]
optimize at ./../usr/share/julia/Compiler/src/optimize.jl:1035
finish_nocycle at ./../usr/share/julia/Compiler/src/typeinfer.jl:231
```
## Root cause
`Compiler/src/ssair/ir.jl`, lines 1432–1449 on 1.13.0-rc3:
```julia
# Remove this edge from all phi nodes in `to` block
# NOTE: It is possible for `to` to contain only `nothing` statements,
# so we must be careful to stop at its last statement
if to <= active_bb
bb = result_bbs[bb_rename_succ[to]]
stmts = compacted_stmt_range(compact, bb, active_bb, to)
idx = first(stmts)
while idx <= last(stmts)
stmt = compact.result[idx][:stmt]
stmt === nothing && continue # <-- never advances `idx`
isa(stmt, PhiNode) || break
i = findfirst(x::Int32->x==bb_rename_pred[from], stmt.edges)
if i !== nothing
deleteat!(stmt.edges, i)
deleteat!(stmt.values, i)
end
idx += 1
end
```
Line 1441 re-enters the `while` without incrementing `idx`, so the first `nothing` statement in the block makes the loop spin on that index forever. Sampling the backtrace of the hung process repeatedly lands on lines 1439, 1440 and 1441 — the only three lines of this loop — which pins the spin here rather than in the recursion at 1413.
The comment immediately above the loop anticipates precisely the `nothing`-statement case that the guard mishandles.
The apparent fix is to advance the index when skipping:
```julia
if stmt === nothing
idx += 1
continue
end
```
## Notes
- The identical `stmt === nothing && continue` is present in 1.10.12 (`base/compiler/ssair/ir.jl:1234`) and 1.11 (`ir.jl:1322`), so the spin itself is a long-standing latent bug. What changed in 1.11 is that this path now gets reached with a leading `nothing` statement for the shape above; pinning down which 1.10→1.11 optimizer change routes here would need a bisect.
- #60697 widened the guard on this branch from `to < active_bb` to `to <= active_bb` for 1.13, so the loop is reachable in strictly more cases there.
- Variations, in case they help characterize the triggering IR shape:
- a plain (unboxed) local `Bool` flag instead of `Ref{Bool}` — no hang
- `Ref{Int}` with `if r[] != 0` — no hang
- `if r[] == true` instead of `if r[]` — no hang; branching on the loaded `Bool` directly appears to be required
- a `mutable struct` field of type `Bool` in place of `Ref` — still hangs, so this is not `Ref`-specific
- `-O0` still hangs. `--compile=min` avoids it.
0 条评论