Note [Eta reduction soundness]
GHC's eta reduction transforms \x y. <fun> x y ---> <fun> For soundness, we obviously require that `x` and `y` to not occur free. But what /other/ restrictions are there for eta reduction to be sound? We discuss separately what it means for eta reduction to be /desirable/, in Note [Eta reduction makes sense]. Eta reduction is *not* a sound transformation in general, because it may change termination behavior if *value* lambdas are involved: `bot` /= `\x. bot x` (as can be observed by a simple `seq`) The past has shown that oversight of this fact can not only lead to endless loops or exceptions, but also straight out *segfaults*. Nevertheless, we can give the following criteria for when it is sound to perform eta reduction on an expression with n leading lambdas `\xs. e xs` (checked in 'is_eta_reduction_sound' in 'tryEtaReduce', which focuses on the case where `e` is trivial): (A) It is sound to eta-reduce n arguments as long as n does not exceed the `exprArity` of `e`. (Needs Arity analysis.) This criterion exploits information about how `e` is *defined*. Example: If `e = \x. bot` then we know it won't diverge until it is called with one argument. Hence it is safe to eta-reduce `\x. e x` to `e`. By contrast, it would be *unsound* to eta-reduce 2 args, `\x y. e x y` to `e`: `e 42` diverges when `(\x y. e x y) 42` does not. (S) It is sound to eta-reduce n arguments in an evaluation context in which all calls happen with at least n arguments. (Needs Strictness analysis.) NB: This treats evaluations like a call with 0 args. NB: This criterion exploits information about how `e` is *used*. Example: Given a function `g` like `g c = Just (c 1 2 + c 2 3)` it is safe to eta-reduce the arg in `g (\x y. e x y)` to `g e` without knowing *anything* about `e` (perhaps it's a parameter occ itself), simply because `g` always calls its parameter with 2 arguments. It is also safe to eta-reduce just one arg, e.g., `g (\x. e x)` to `g e`. By contrast, it would *unsound* to eta-reduce 3 args in a call site like `g (\x y z. e x y z)` to `g e`, because that diverges when `e = \x y. bot`. Could we relax to "*At least one call in the same trace* is with n args"? No. Consider what happens for ``g2 c = c True `seq` c False 42`` Here, `g2` will call `c` with 2 arguments (if there is a call at all). But it is unsound to eta-reduce the arg in `g2 (\x y. e x y)` to `g2 e` when `e = \x. if x then bot else id`, because the latter will diverge when the former would not. Fortunately, the strictness analyser will report "Not always called with two arguments" for `g2` and we won't eta-expand. See Note [Eta reduction based on evaluation context] for the implementation details. This criterion is tested extensively in T21261. (R) Note [Eta reduction in recursive RHSs] tells us that we should not eta-reduce `f` in its own RHS and describes our fix. There we have `f = \x. f x` and we should not eta-reduce to `f=f`. Which might change a terminating program (think @f `seq` e@) to a non-terminating one. (E) (See fun_arity in tryEtaReduce.) As a perhaps special case on the boundary of (A) and (S), when we know that a fun binder `f` is in WHNF, we simply assume it has arity 1 and apply (A). Example: g f = f `seq` \x. f x Here it's sound eta-reduce `\x. f x` to `f`, because `f` can't be bottom after the `seq`. This turned up in #7542. T. If the binders are all type arguments, it's always safe to eta-reduce, regardless of the arity of f. /\a b. f @a @b --> f 2. Type and dictionary abstraction. Regardless of whether 'f' is a value, it is always sound to reduce /type lambdas/, thus: (/\a -> f a) --> f Moreover, we always want to, because it makes RULEs apply more often: This RULE: `forall g. foldr (build (/\a -> g a))` should match `foldr (build (/\b -> ...something complex...))` and the simplest way to do so is eta-reduce `/\a -> g a` in the RULE to `g`. More debatably, we extend this to dictionary arguments too, because the type checker can insert these eta-expanded versions, with both type and dictionary lambdas; hence the slightly ad-hoc (all ok_lam bndrs). That is, we eta-reduce \(d::Num a). f d --> f regardless of f's arity. Its not clear whether or not this is important, and it is not in general sound. But that's the way it is right now. And here are a few more technical criteria for when it is *not* sound to eta-reduce that are specific to Core and GHC: (J) We may not undersaturate join points. See Note [Invariants on join points] in GHC.Core, and #20599. (B) We may not undersaturate functions with no binding. See Note [Eta expanding primops]. (W) We may not undersaturate StrictWorkerIds. See Note [CBV Function Ids] in GHC.Types.Id.Info. Here is a list of historic accidents surrounding unsound eta-reduction: * Consider f = \x.f x h y = case (case y of { True -> f `seq` True; False -> False }) of True -> ...; False -> ... If we (unsoundly) eta-reduce f to get f=f, the strictness analyser says f=bottom, and replaces the (f `seq` True) with just (f `cast` unsafe-co). [SG in 2022: I don't think worker/wrapper would do this today.] BUT, as things stand, 'f' got arity 1, and it *keeps* arity 1 (perhaps also wrongly). So CorePrep eta-expands the definition again, so that it does not terminate after all. Result: seg-fault because the boolean case actually gets a function value. See #1947. * Never *reduce* arity. For example f = \xy. g x y Then if h has arity 1 we don't want to eta-reduce because then f's arity would decrease, and that is bad [SG in 2022: I don't understand this point. There is no `h`, perhaps that should have been `g`. Even then, this proposed eta-reduction is invalid by criterion (A), which might actually be the point this anecdote is trying to make. Perhaps the "no arity decrease" idea is also related to Note [Arity robustness]?]
References 7
- Eta expanding primops GHC.Builtin.PrimOps
- Eta reduction based on evaluation context GHC.Core.Opt.Arity
- Eta reduction in recursive RHSs GHC.Core.Opt.Arity
- Eta reduction makes sense GHC.Core.Opt.Arity
- Arity robustness GHC.Core.Opt.Simplify.Env
- Invariants on join points GHC.Core
- CBV Function Ids GHC.Types.Id.Info
Referenced by 8
- GHC.Core.Opt.Arity call site ×4
- Eta reduction makes sense GHC.Core.Opt.Arity ×2
- Eta reduction based on evaluation context GHC.Core.Opt.Arity
- Case-to-let for strictly-used binders GHC.Core.Opt.Simplify.Iteration