Admin message

Due to a large amount of spam we do not allow new users to create repositories, they are "external" users. If you are a new user and want to create a repository, for example for forking GHC, open a new issue on ghc/ghc using the "get-verified" issue template

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