Skip to content

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.
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information