CBE can eliminate the entry block, silently dropping the proc's info table (.cmm only)
```c
#include "Cmm.h"
INFO_TABLE_RET (zork, RET_SMALL)
return ()
{
bits64 x;
x = 1 :: bits64;
goto E;
B:
x = 1 :: bits64;
goto E;
E:
if (x > 0 :: bits64) { goto B; }
return ();
}
```
```console
$ ghc-9.14.1 -c -O -dcmm-lint -I$RTS_INCLUDE EntryLose.cmm # compiles silently
$ nm EntryLose.o
0000000000000000 T zork_ret # no zork_info!
$ cat use.c
extern char zork_info[];
int main(void){ return zork_info[0]; }
$ gcc use.c EntryLose.o -o use
/usr/bin/ld: ... undefined reference to `zork_info'
```
I (or rather Claude) stumbled across this while looking into #27368.
Analysis from Claude:
<details>
If a proc's entry block is identical to another block in the same
graph, common block elimination can merge the *entry* into the other
block. `replaceLabels` duly redirects `g_entry`, but nothing rekeys
`info_tbls`, which is keyed by the old entry label. The stale-keyed
info table survives until `removeUnreachableBlocksProc` at the end of
`cpsTop` prunes it together with the unreachable old entry block — so
the info table that the source declares is silently never emitted.
`-dcmm-lint` passes throughout.
### Why the entry loses
CBE's merge order makes the block with the larger label unique the
loser (`groupByInt` reverses `toBlockList` order, `groupByLabel`
reverses it back, and `mergeBlockList` keeps the first block of each
group as the winner). The entry block is in the losing position more
often than one might think: `emitProc` allocates the entry `BlockId`
*after* the body has been built (`GHC.StgToCmm.Monad`), so the entry's
unique is typically the largest in the proc. In the reproducer the
labels are `B = c4`, `E = c3`, entry `= c7`; CBE produces
`c7 :-> c4` and `g_entry` becomes `c4`, while `info_tbls` still says
`[(c7, zork_info)]`.
That `emitProc` ordering applies to Haskell-generated procs as well,
but Haskell-generated code cannot actually trigger the bug: every
closure entry emitted by StgToCmm carries the proc's single stack
check (see Note [Single stack check] in GHC.StgToCmm.Heap), whose
`CmmHighStackMark` is still symbolic when CBE runs (CBE precedes stack
layout) and occurs in no other block of the graph — so no sibling
block can ever be CBE-equal to the entry. The bug is thus limited to
hand-written Cmm (including the RTS's `.cmm` files), which gets no
implicit stack check.
### Trace of the silent loss
With `-ddump-cmm-verbose-by-proc`:
- Parsed Cmm: `info_tbls: [(c7, ...)]`, entry `c7`.
- Post CBE: entry is `c4`; `c7` unreachable (still present).
- After `setInfoTableStackMap`: info table still keyed `c7`, now
`rep: StackRep []` — the old entry got a stack map before becoming
unreachable, so the `setInfoTableStackMap` panic (cf. #27368) does
not fire.
- Post control-flow optimisations (2) / `removeUnreachableBlocksProc`:
`info_tbls: []` — pruned along with the unreachable block.
- NCG: no info table emitted.
### Possible fixes
- Rekey `info_tbls` when CBE renames labels, as the control-flow
optimiser already does (`upd_info` in `cmmCfgOptsProc`, which also
preserves `cit_lbl` for the entry since that label is externally
visible); or
- never let a block with an info table (or at least the entry block)
lose a merge — e.g. skip blocks in `mapKeysSet (info_tbls)` ∪
`{g_entry}` when building CBE's substitution domain.
</details>
It doesn't currently seem possible to reproduce in pure Haskell, as every closure-entry proc's entry block contains the proc's stack check
at CBE time, and no other block in
the graph can contain the same check.
So it doesn't seem easy to run into this bug by accident, but it might be worth fixing anyway.
issue