mkAbsentFiller makes absentError fillers for unary-class dictionaries
<!-- ============================================================
METADATA FOR SIMON (delete before filing)
Suggested title:
mkAbsentFiller makes absentError fillers for unary-class
dictionaries (isTerminatingType is weaker than the removed
isDictTy guard)
Suggested labels: ~bug, ~"demand analysis" (or worker/wrapper if
that label exists), ~"compiler crash"? (no — it's a potential
runtime miscompilation, not a compiler crash)
Suggested milestone: %9.14.2 (the backport 843e78856ff5 is in the
ghc-9.14.2-rc1 tag; also present on master for 10.1)
Related: #24934, #25924, #26416, #27626, MR !16219
Commits: master 9b714c4c83 + 4a59b3eece;
ghc-9.14 1eb8f64b83bd + 843e78856ff5
Reproducer files + run.sh in this directory (f1/); sources are
also embedded in the body below, so the ticket is self-contained.
============================================================ -->
## Summary
MR !16219 (master commits 9b714c4c83 and 4a59b3eece, backported to `ghc-9.14` as 1eb8f64b83bd and 843e78856ff5, included in 9.14.2-rc1) fixes #25924 by no longer making absent fillers for dictionaries. Its central change replaces the old guard in `mkAbsentFiller`
```haskell
not (isDictTy arg_ty) -- old condition on the absentError branch
```
with an early exit
```haskell
| isTerminatingType arg_ty = Nothing
```
But `isTerminatingType` is strictly weaker than `isDictTy`: it deliberately excludes **unary classes** (`GHC/Core/Type.hs`, `isTerminatingType ty = ... isClassTyCon tc && not (isUnaryClassTyCon tc)`, per (UCM3) in `Note [Unary class magic]` in GHC.Core.TyCon), whereas `isDictTy = isClassPred` accepts any class TyCon. So for an absent dictionary of a *unary* class — a class with exactly one method or one boxed superclass, e.g. `class Eq a => UD a where {}`, or any single-method class — `mkAbsentFiller` now falls through to the (AF1) branch and returns an **`absentError` thunk**, where GHC 9.14.1 produced a rubbish literal and where the removed (AF4) said "never use an error thunk for a dictionary" (the lesson of #24934).
This contradicts the patch's own documentation:
* the new note in `GHC.CoreToStg.Prep`: "Nowadays we never make a filler for a dictionary in the first place, so this can no longer happen";
* the changelog entry `fix-absent-dict-projection`: "We no longer make an absent filler (a rubbish literal or error thunk) for an absent dictionary or other terminating type."
Both are false for unary-class dictionaries.
## Demonstration
Compiling the three modules below with `ghc -O` using a post-patch compiler (master at d415f38a75, which contains 4a59b3eece) puts an `absentError` filler of dictionary type into `M`'s Tidy Core:
```
lvl1 = \ (@a) ->
discard
(case GHC.Internal.Prim.Panic.absentConstraintError @(UD a) lvl
of {})
```
The same program with GHC 9.14.1 (which has unary classes but not this patch) instead produces a rubbish-literal dictionary, as (AF4) prescribed:
```
lvl4 = \ (@a) ->
GHC.Internal.Classes.==
@a
(M.$p1UD @a (RUBBISH[c](GHC.Internal.Types.LiftedRep) @(UD a)))
(lvl1 @a) (lvl3 @a)
```
(Note, in the 9.14.1 output, the superclass selection `$p1UD` applied to the absent dictionary surviving all the way to Tidy Core — the projection-out-of-a-filler pattern that !16219 set out to eliminate.)
The mechanism: `discard`'s demand signature `<A>` makes the `x == y` thunk — and hence the `UD a` dictionary — absent in `g`, while `-fno-worker-wrapper` on `M0` keeps the call site from dropping the argument, so `g`'s worker still mentions the dictionary and worker/wrapper must bind it to a filler.
<details><summary>M0.hs</summary>
```haskell
{-# OPTIONS_GHC -fno-worker-wrapper #-}
module M0 where
{-# NOINLINE discard #-}
discard :: Bool -> Int
discard _ = 42
```
</details>
<details><summary>M.hs</summary>
```haskell
module M where
import M0
class Eq a => UD a where {}
instance UD Int
{-# NOINLINE g #-}
g :: UD a => a -> a -> Int
g x y = discard (x == y)
```
</details>
<details><summary>Main.hs</summary>
```haskell
module Main where
import M
main :: IO ()
main = print (g (1 :: Int) 2)
```
</details>
<details><summary>run.sh</summary>
```bash
#!/usr/bin/env bash
# Reproducer for: mkAbsentFiller makes absentError fillers for unary-class dictionaries.
# Usage: ./run.sh [GHC] (default: ghc in PATH; use a post-patch build, e.g.
# one containing master commit 4a59b3eece "Don't make absent fillers
# for terminating types")
set -euo pipefail
GHC=${1:-ghc}
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT
cp M0.hs M.hs Main.hs "$d"; cd "$d"
"$GHC" -O -fforce-recomp Main.hs -o repro -ddump-simpl -ddump-to-file
echo "== Filler for the unary-class dictionary in M's Tidy Core:"
grep -n -B3 -A3 'absentConstraintError\|RUBBISH' M.dump-simpl || echo "(no filler found)"
```
</details>
This small program runs fine — the filler thunk is never forced. I could not construct an end-to-end program that crashes at runtime; the danger is the same latent one as in #24934/#25924/#26416, which also needed real-world-sized programs (stale demand info) to manifest. The trigger scenario is below.
## Why the error thunk is dangerous
Speculation can force such a dictionary without ever "using" it legitimately, exactly as in #24934:
1. For `class Eq a => UD a`, the superclass selector `$p1UD :: UD a -> Eq a` has a *terminating* result type, so `mkDictSelId` marks its `ClassOpId` as having a terminating result (`GHC/Types/Id/Make.hs`, `terminating = isTerminatingType res_ty || ...` — computed from the result type only).
2. `exprOkForSpeculation`'s `ClassOpId` case (`GHC/Core/Utils.hs`, `app_ok`) therefore classifies `$p1UD d` as ok-for-speculation / ok-to-discard **without looking at `d` at all** — the very pattern that made the dictionary absent in #24934.
3. By (UCM1) in `Note [Unary class magic]`, CoreToStg rewrites `$p1UD d` to plain `d`. So speculatively evaluating the "selection" is precisely entering the `absentError` thunk: `Oops! Entered absent arg`, program abort.
The patch's belt-and-braces guard in `CoreToStg.Prep.decideFloatInfo` (`not (isAbsDmd dmd)`) only helps while the binding's demand info still says "absent"; the patch's own addition to `Note [Speculative evaluation]` concedes that demand info cannot be relied on after optimisation (see also #26416/#27626). Notably, the patch's own regression test contains `class (ABITypeable a, ABITypeable a) => YulCatObj a where -- crash stops without duplicate constraint` (testsuite/tests/core-to-stg/T25924/B.hs): the duplicated constraint makes the class non-unary; the unary variant of the class is exactly the case the new guard does not cover.
A related observation on step 2: since (UCM3) explicitly allows unary-class dictionaries to be bottom, `app_ok`'s judgement that `$p1UD d` is ok-for-speculation is unsound for a unary-class-typed `d` from *any* source, not just absent fillers — the selection *is* the dictionary, so speculating it forces a possibly-bottom value.
## Suggested fix
Have `mkAbsentFiller` refuse to make a filler for **any** class dictionary type (e.g. `isTerminatingType arg_ty || isDictTy arg_ty`), not just terminating ones, and align `mkLitRubbish`'s new INVARIANT 3 in `GHC.Types.Literal` accordingly. The only cost is that worker/wrapper keeps an absent unary-class dictionary argument — the same cost the patch already accepts for non-unary dictionaries. The documentation claims ("we never make a filler for a dictionary") would then become true.
## Provenance
Found by GlaHiCo, an LLM-generated review of the 9.14.2 backports ghc-9.14@1eb8f64b83bd and ghc-9.14@843e78856ff5 (master commits 9b714c4c83, 4a59b3eece; MR !16219): https://gitlab.haskell.org/sjakobi/GlaHiCo/-/snippets/6127. This report was reviewed by a human before filing.
issue