Worker/wrapper messes up specialisation
Problem
Consider
module Foo1 where
{-# SPECIALISE foo :: (Int,Int) -> Bool -> Int #-}
foo :: Num a => (a,a) -> Bool -> a
foo (x,y) True = x+y
foo (x,y) False = f (y,x) True
module Foo where
import Foo1
bar = foo ((1,2)::(Int,Int)) True
Compile with -O -ddump-rule-firings
and you'll see
Rule fired: SPEC foo (Foo1)
The specialisation for foo
created by the SPECIALISE
pragma is used when compiling Foo
, as you would expect.
But now add {-# NOINLINE [2] foo #-}
in Foo1. (You might want to delay inlining foo
so that some other RULE can fire.)
But alas! The rule-firing goes away! And indeed bar
runs by calling a non-specialised version of foo
. Yikes!
How it shows up
This is a real problem. I tripped over it when working on #19790 (closed) and !6222 (closed), for GHC.Real.lcm:
{-# SPECIALISE lcm :: Int -> Int -> Int #-}
{-# SPECIALISE lcm :: Word -> Word -> Word #-}
{-# NOINLINE [2] lcm #-}
lcm _ 0 = 0
lcm 0 _ = 0
lcm x y = abs ((x `quot` (gcd x y)) * y)
{-# RULES
"lcm/Integer->Integer->Integer" lcm = integerLcm
"lcm/Natural->Natural->Natural" lcm = naturalLcm
#-}
Notice: (a) SPECIALISE pragma (b) RULEs for lcm
, and (c) NOINLINE to give those rules a chance to fire.
Diagnosis
Notice that foo
is strict in the pair argument, so we'll do worker/wrapper even of the un-specialised version. And recall that the Simplifier works in phases: InitialPhase, Phase 2, Phase 1, Phase 0...
Without the NOINLINE
pragma:
- SPECIALISE generates a RULE that is always active
- Worker/wrapper generates a wrapper that is active from phase 2, precisely to give the SPECIALISE rule a chance to fire. See
Note [Wrapper activation]
in GHC.Core.Opt.WorkWrap.
But with the NOINLINE [2]
pragma
- SPECIALISE generates a rule that is active from phase 2 onwards. We don't want it to be active before phase 2, because the NOINLINE pragma is saying "don't mess with calls to the function before phase 2, because some other rules might be rewriting it". See
Note [Auto-specialisation and RULES]
in GHC.Core.Opt.Specialise - Worker wrapper still generates a wrapper that is active from phase 2
- So now, in phase 2, the wrapper inlining competes with the SPECIALISE rule. And inlining wins (it happens that the Simplifier tries inlining first). So GHC inlines the wrapper, and the SPECIALISE rule never gets a chance to fire.