Simon Peyton Jones pushed to branch wip/T26903 at Glasgow Haskell Compiler / GHC
Commits:
-
99d8c146
by Simon Peyton Jones at 2026-02-12T17:36:59+00:00
8 changed files:
- compiler/GHC/Core/Opt/Simplify/Iteration.hs
- compiler/GHC/Core/Opt/WorkWrap.hs
- compiler/GHC/Driver/Config/Core/Lint.hs
- compiler/GHC/Types/Id/Info.hs
- + testsuite/tests/simplCore/should_compile/T26903.hs
- + testsuite/tests/simplCore/should_compile/T26903.stderr
- testsuite/tests/simplCore/should_compile/T8331.stderr
- testsuite/tests/simplCore/should_compile/all.T
Changes:
| ... | ... | @@ -474,14 +474,14 @@ leaving a simpler job for demand-analysis worker/wrapper. See #19874. |
| 474 | 474 | |
| 475 | 475 | Wrinkles
|
| 476 | 476 | |
| 477 | -1. We must /not/ do cast w/w on
|
|
| 477 | +(CWW1) We must /not/ do cast w/w on
|
|
| 478 | 478 | f = g |> co
|
| 479 | 479 | otherwise it'll just keep repeating forever! You might think this
|
| 480 | 480 | is avoided because the call to tryCastWorkerWrapper is guarded by
|
| 481 | - preInlineUnconditinally, but I'm worried that a loop-breaker or an
|
|
| 482 | - exported Id might say False to preInlineUnonditionally.
|
|
| 481 | + preInlineUnconditionally, but I'm worried that a loop-breaker or an
|
|
| 482 | + exported Id might say False to preInlineUnconditionally.
|
|
| 483 | 483 | |
| 484 | -2. We need to be careful with inline/noinline pragmas:
|
|
| 484 | +(CWW2) We need to be careful with inline/noinline pragmas:
|
|
| 485 | 485 | rec { {-# NOINLINE f #-}
|
| 486 | 486 | f = (...g...) |> co
|
| 487 | 487 | ; g = ...f... }
|
| ... | ... | @@ -496,15 +496,15 @@ Wrinkles |
| 496 | 496 | f = $wf |> co
|
| 497 | 497 | ; g = ...f... }
|
| 498 | 498 | and that is bad: the whole point is that we want to inline that
|
| 499 | - cast! We want to transfer the pagma to $wf:
|
|
| 499 | + cast! We want to transfer the pragma to $wf:
|
|
| 500 | 500 | rec { {-# NOINLINE $wf #-}
|
| 501 | 501 | $wf = ...g...
|
| 502 | 502 | ; f = $wf |> co
|
| 503 | 503 | ; g = ...f... }
|
| 504 | 504 | c.f. Note [Worker/wrapper for NOINLINE functions] in GHC.Core.Opt.WorkWrap.
|
| 505 | 505 | |
| 506 | -3. We should still do cast w/w even if `f` is INLINEABLE. E.g.
|
|
| 507 | - {- f: Stable unfolding = <stable-big> -}
|
|
| 506 | +(CWW3) We should still do cast w/w even if `f` is INLINEABLE. E.g.
|
|
| 507 | + {- f: Stable unfolding (arity 2) = <stable-big> -}
|
|
| 508 | 508 | f = (\xy. <big-body>) |> co
|
| 509 | 509 | Then we want to w/w to
|
| 510 | 510 | {- $wf: Stable unfolding = <stable-big> |> sym co -}
|
| ... | ... | @@ -513,15 +513,43 @@ Wrinkles |
| 513 | 513 | Notice that the stable unfolding moves to the worker! Now demand analysis
|
| 514 | 514 | will work fine on $wf, whereas it has trouble with the original f.
|
| 515 | 515 | c.f. Note [Worker/wrapper for INLINABLE functions] in GHC.Core.Opt.WorkWrap.
|
| 516 | - This point also applies to strong loopbreakers with INLINE pragmas, see
|
|
| 517 | - wrinkle (4).
|
|
| 518 | 516 | |
| 519 | -4. We should /not/ do cast w/w for non-loop-breaker INLINE functions (hence
|
|
| 520 | - hasInlineUnfolding in tryCastWorkerWrapper, which responds False to
|
|
| 521 | - loop-breakers) because they'll definitely be inlined anyway, cast and
|
|
| 522 | - all. And if we do cast w/w for an INLINE function with arity zero, we get
|
|
| 517 | +(CWW4) We should /not/ do cast w/w for INLINE functions (hence `hasInlineUnfolding`
|
|
| 518 | + in `tryCastWorkerWrapper`) because they'll definitely be inlined anyway, cast
|
|
| 519 | + and all.
|
|
| 520 | + |
|
| 521 | + Moreover, if we do cast w/w for an INLINE function with arity zero, we get
|
|
| 523 | 522 | something really silly: we inline that "worker" right back into the wrapper!
|
| 524 | - Worse than a no-op, because we have then lost the stable unfolding.
|
|
| 523 | + In fact it is Much Worse than a no-op, because we have then lost the stable
|
|
| 524 | + unfolding --- aargh (see #26903). E.g. similar example to (CWW3)
|
|
| 525 | + {- g: Stable unfolding (arity 0) = <stable-big> -} NB arity 0!
|
|
| 526 | + g = (\xy. <big-body>) |> co
|
|
| 527 | + If we w/w to this:
|
|
| 528 | + {- $wg: Stable unfolding (arity 0) = <stable-big> |> sym co -}
|
|
| 529 | + $wg = \xy. <big-body>
|
|
| 530 | + g = $wg |> co
|
|
| 531 | + then we'll inline $wg at the call site in `g` giving
|
|
| 532 | + {- $wg: Stable unfolding (arity 0) = <stable-big> |> sym co -}
|
|
| 533 | + $wg = \xy. <big-body>
|
|
| 534 | + g = (<stable-big> |> sym co) |> co
|
|
| 535 | + and now we'll drop `$wg` as dead and we have lost the unfolding on `g`.
|
|
| 536 | + (We could /also/ give the binding `g = $wf |> co` a stable unfolding. Then
|
|
| 537 | + things would work right; but there is also no point in doing the cast
|
|
| 538 | + worker/wrapper in the first place.)
|
|
| 539 | + |
|
| 540 | + NB: you might wonder about a loop-breaker with an INLINE pragma; after all, a
|
|
| 541 | + loop breaker won't "definitely be inlined anyway", so arguably we should not
|
|
| 542 | + disable cast w/w/ for it. But a Rec group can /look/ recursive at an early
|
|
| 543 | + stage, and subsequently /become/ non-recursive after some simplification.
|
|
| 544 | + (This is common in instance decls; see Note [Checking for INLINE loop breakers]
|
|
| 545 | + in GHC.Core.Lint.) So the danger is that we'll permanently lose that stable
|
|
| 546 | + unfolding that we specifically wanted (#26903). Simple solution: disable cast
|
|
| 547 | + w/w for /any/ INLINE function. See the defn
|
|
| 548 | + of `GHC.Types.Id.Info.hasInlineUnfolding`.
|
|
| 549 | + |
|
| 550 | + The danger is that an INLINE pragma on a genuninely-recursive function
|
|
| 551 | + will kill worker-wrapper. Well, so be it. They are pretty suspicious anyway;
|
|
| 552 | + see Note [Checking for INLINE loop breakers].
|
|
| 525 | 553 | |
| 526 | 554 | All these wrinkles are exactly like worker/wrapper for strictness analysis:
|
| 527 | 555 | f is the wrapper and must inline like crazy
|
| ... | ... | @@ -586,11 +614,11 @@ tryCastWorkerWrapper env bind_cxt old_bndr bndr (Cast rhs co) |
| 586 | 614 | | BC_Let top_lvl is_rec <- bind_cxt -- Not join points
|
| 587 | 615 | , not (isDFunId bndr) -- nor DFuns; cast w/w is no help, and we can't transform
|
| 588 | 616 | -- a DFunUnfolding in mk_worker_unfolding
|
| 589 | - , not (exprIsTrivial rhs) -- Not x = y |> co; Wrinkle 1
|
|
| 590 | - , not (hasInlineUnfolding info) -- Not INLINE things: Wrinkle 4
|
|
| 591 | - , typeHasFixedRuntimeRep work_ty -- Don't peel off a cast if doing so would
|
|
| 592 | - -- lose the underlying runtime representation.
|
|
| 593 | - -- See Note [Preserve RuntimeRep info in cast w/w]
|
|
| 617 | + , not (exprIsTrivial rhs) -- Not x = y |> co; see (CWW1)
|
|
| 618 | + , not (hasInlineUnfolding info) -- Not INLINE things: see (CWW4)
|
|
| 619 | + , typeHasFixedRuntimeRep work_ty -- Don't peel off a cast if doing so would
|
|
| 620 | + -- lose the underlying runtime representation.
|
|
| 621 | + -- See Note [Preserve RuntimeRep info in cast w/w]
|
|
| 594 | 622 | , not (isOpaquePragma (idInlinePragma old_bndr)) -- Not for OPAQUE bindings
|
| 595 | 623 | -- See Note [OPAQUE pragma]
|
| 596 | 624 | = do { uniq <- getUniqueM
|
| ... | ... | @@ -637,13 +665,13 @@ tryCastWorkerWrapper env bind_cxt old_bndr bndr (Cast rhs co) |
| 637 | 665 | `setArityInfo` work_arity
|
| 638 | 666 | -- We do /not/ want to transfer OccInfo, Rules
|
| 639 | 667 | -- Note [Preserve strictness in cast w/w]
|
| 640 | - -- and Wrinkle 2 of Note [Cast worker/wrapper]
|
|
| 668 | + -- and (CWW2) of Note [Cast worker/wrapper]
|
|
| 641 | 669 | |
| 642 | 670 | ----------- Worker unfolding -----------
|
| 643 | 671 | -- Stable case: if there is a stable unfolding we have to compose with (Sym co);
|
| 644 | 672 | -- the next round of simplification will do the job
|
| 645 | 673 | -- Non-stable case: use work_rhs
|
| 646 | - -- Wrinkle 3 of Note [Cast worker/wrapper]
|
|
| 674 | + -- See (CWW4) of Note [Cast worker/wrapper]
|
|
| 647 | 675 | mk_worker_unfolding top_lvl work_id work_rhs
|
| 648 | 676 | = case realUnfoldingInfo info of -- NB: the real one, even for loop-breakers
|
| 649 | 677 | unf@(CoreUnfolding { uf_tmpl = unf_rhs, uf_src = src })
|
| ... | ... | @@ -179,8 +179,9 @@ several liked-named Ids bouncing around at the same time---absolute |
| 179 | 179 | mischief.)
|
| 180 | 180 | |
| 181 | 181 | Notice that we refrain from w/w'ing an INLINE function even if it is
|
| 182 | -in a recursive group. It might not be the loop breaker. (We could
|
|
| 183 | -test for loop-breaker-hood, but I'm not sure that ever matters.)
|
|
| 182 | +in a recursive group. It might not be the loop breaker. (We used to
|
|
| 183 | +test for loop-breaker-hood, but see (CWW4) in Note [Cast worker/wrapper]
|
|
| 184 | +in GHC.Core.Opt.Simplify.Iteration.)
|
|
| 184 | 185 | |
| 185 | 186 | Note [Worker/wrapper for INLINABLE functions]
|
| 186 | 187 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| ... | ... | @@ -132,8 +132,6 @@ perPassFlags dflags pass |
| 132 | 132 | -- there may be some INLINE knots still tied, which is tiresomely noisy
|
| 133 | 133 | CoreDoSimplify cfg
|
| 134 | 134 | | SimplPhase InitialPhase <- sm_phase (so_mode cfg)
|
| 135 | - -> True
|
|
| 136 | - | otherwise
|
|
| 137 | 135 | -> False
|
| 138 | 136 | _ -> True
|
| 139 | 137 |
| ... | ... | @@ -610,7 +610,12 @@ hasInlineUnfolding :: IdInfo -> Bool |
| 610 | 610 | -- ^ True of a /non-loop-breaker/ Id that has a /stable/ unfolding that is
|
| 611 | 611 | -- (a) always inlined; that is, with an `UnfWhen` guidance, or
|
| 612 | 612 | -- (b) a DFunUnfolding which never needs to be inlined
|
| 613 | -hasInlineUnfolding info = isInlineUnfolding (unfoldingInfo info)
|
|
| 613 | +--
|
|
| 614 | +-- Very important that this work with `realUnfoldingInfo` and so returns
|
|
| 615 | +-- True even for a loop-breaker that has an INLINE pragma.
|
|
| 616 | +-- See (CWW4) in Note [Cast worker/wrapper] in GHC.Core.Opt.Simplify.Iteration
|
|
| 617 | +-- for discussion, and #26903 for the dire consequences of getting this wrong.
|
|
| 618 | +hasInlineUnfolding info = isInlineUnfolding (realUnfoldingInfo info)
|
|
| 614 | 619 | |
| 615 | 620 | setArityInfo :: IdInfo -> ArityInfo -> IdInfo
|
| 616 | 621 | setArityInfo info ar =
|
| 1 | +{-# LANGUAGE DefaultSignatures #-}
|
|
| 2 | +module T26903 where
|
|
| 3 | + |
|
| 4 | +newtype T a = MkT [a]
|
|
| 5 | + |
|
| 6 | +class C a where
|
|
| 7 | + op :: [a] -> [a] -> T a
|
|
| 8 | + |
|
| 9 | + -- This default method
|
|
| 10 | + -- * Has an INLINE pragma
|
|
| 11 | + -- * Is too big to inline without a pragma
|
|
| 12 | + -- * Has arity zero
|
|
| 13 | + {-# INLINE[1] op #-}
|
|
| 14 | + default op :: Ord a => [a] -> [a] -> T a
|
|
| 15 | + op = \xs ys -> MkT $ if xs>ys then reverse (reverse (reverse (reverse xs)))
|
|
| 16 | + else reverse (reverse (reverse (reverse (xs ++ ys))))
|
|
| 17 | + |
|
| 18 | +instance C Int where {}
|
|
| 19 | + |
|
| 20 | +test :: [Int] -> T Int
|
|
| 21 | +test xs = op [] xs
|
|
| 22 | + -- We expect to see `op` inlined into the RHS of `test`
|
|
| 23 | + |
| 1 | + |
|
| 2 | +==================== Tidy Core ====================
|
|
| 3 | +Result size of Tidy Core
|
|
| 4 | + = {terms: 127, types: 130, coercions: 48, joins: 0/0}
|
|
| 5 | + |
|
| 6 | +$dmop
|
|
| 7 | + = (\ @a _ $dOrd xs ys ->
|
|
| 8 | + case $fOrdList_$ccompare $dOrd xs ys of {
|
|
| 9 | + __DEFAULT ->
|
|
| 10 | + reverse1 (reverse1 (reverse1 (reverse1 (++ xs ys) []) []) []) [];
|
|
| 11 | + GT -> reverse1 (reverse1 (reverse1 (reverse xs) []) []) []
|
|
| 12 | + })
|
|
| 13 | + `cast` <Co:20> :: ...
|
|
| 14 | + |
|
| 15 | +$fCInt_$cop
|
|
| 16 | + = (\ xs ys ->
|
|
| 17 | + case $fOrdList_$s$ccompare xs ys of {
|
|
| 18 | + __DEFAULT ->
|
|
| 19 | + reverse1 (reverse1 (reverse1 (reverse1 (++ xs ys) []) []) []) [];
|
|
| 20 | + GT -> reverse1 (reverse1 (reverse1 (reverse xs) []) []) []
|
|
| 21 | + })
|
|
| 22 | + `cast` <Co:11> :: ...
|
|
| 23 | + |
|
| 24 | +$fCInt1
|
|
| 25 | + = \ xs ys ->
|
|
| 26 | + case $fOrdList_$s$ccompare xs ys of {
|
|
| 27 | + __DEFAULT ->
|
|
| 28 | + reverse1 (reverse1 (reverse1 (reverse1 (++ xs ys) []) []) []) [];
|
|
| 29 | + GT -> reverse1 (reverse1 (reverse1 (reverse xs) []) []) []
|
|
| 30 | + }
|
|
| 31 | + |
|
| 32 | +$fCInt = C:C ($fCInt1 `cast` <Co:11> :: ...)
|
|
| 33 | + |
|
| 34 | +test4 = reverse1 [] []
|
|
| 35 | + |
|
| 36 | +test3 = reverse1 test4 []
|
|
| 37 | + |
|
| 38 | +test2 = reverse1 test3 []
|
|
| 39 | + |
|
| 40 | +test1 = reverse1 test2 []
|
|
| 41 | + |
|
| 42 | +test
|
|
| 43 | + = \ xs ->
|
|
| 44 | + case $fOrdList_$s$ccompare [] xs of {
|
|
| 45 | + __DEFAULT ->
|
|
| 46 | + (reverse1 (reverse1 (reverse1 (reverse1 (++ [] xs) []) []) []) [])
|
|
| 47 | + `cast` <Co:3> :: ...;
|
|
| 48 | + GT -> test1 `cast` <Co:3> :: ...
|
|
| 49 | + }
|
|
| 50 | + |
|
| 51 | + |
|
| 52 | + |
| ... | ... | @@ -56,11 +56,11 @@ |
| 56 | 56 | ReaderT r (ST s) (a -> b) -> ReaderT r (ST s) a -> r -> ST s b))
|
| 57 | 57 | "SPEC $c>> @(ST s) @_"
|
| 58 | 58 | forall (@s) (@r) ($dMonad :: Monad (ST s)).
|
| 59 | - $fMonadReaderT1 @(ST s) @r $dMonad
|
|
| 59 | + $fMonadReaderT_$c>> @(ST s) @r $dMonad
|
|
| 60 | 60 | = $fMonadAbstractIOSTReaderT_$s$c>> @s @r
|
| 61 | 61 | "SPEC $c>>= @(ST s) @_"
|
| 62 | 62 | forall (@s) (@r) ($dMonad :: Monad (ST s)).
|
| 63 | - $fMonadReaderT2 @(ST s) @r $dMonad
|
|
| 63 | + $fMonadReaderT1 @(ST s) @r $dMonad
|
|
| 64 | 64 | = ($fMonadAbstractIOSTReaderT2 @s @r)
|
| 65 | 65 | `cast` (forall (a ::~ <*>_N) (b ::~ <*>_N).
|
| 66 | 66 | <ReaderT r (ST s) a>_R
|
| ... | ... | @@ -581,3 +581,4 @@ test('T26722', [grep_errmsg(r'SPEC')], compile, ['-O -dno-typeable-binds']) |
| 581 | 581 | |
| 582 | 582 | test('T26805', [grep_errmsg(r'fromInteger')], compile, ['-O -dno-typeable-binds -ddump-simpl -dsuppress-uniques'])
|
| 583 | 583 | test('T26826', normal, compile, ['-O'])
|
| 584 | +test('T26903', [grep_errmsg(r'reverse')], compile, ['-O -dno-typeable-binds -ddump-simpl -dsuppress-uniques -dsuppress-all']) |