Commit b2950e03 authored by Sebastian Graf's avatar Sebastian Graf

Implement late lambda lift

Summary:
This implements a selective lambda-lifting pass late in the STG
pipeline.

Lambda lifting has the effect of avoiding closure allocation at the cost
of having to make former free vars available at call sites, possibly
enlarging closures surrounding call sites in turn.

We identify beneficial cases by means of an analysis that estimates
closure growth.

There's a Wiki page at
https://ghc.haskell.org/trac/ghc/wiki/LateLamLift.

Reviewers: simonpj, bgamari, simonmar

Reviewed By: simonpj

Subscribers: rwbarton, carter

GHC Trac Issues: #9476

Differential Revision: https://phabricator.haskell.org/D5224
parent 7856676b
......@@ -10,7 +10,7 @@
module Demand (
StrDmd, UseDmd(..), Count,
Demand, CleanDemand, getStrDmd, getUseDmd,
Demand, DmdShell, CleanDemand, getStrDmd, getUseDmd,
mkProdDmd, mkOnceUsedDmd, mkManyUsedDmd, mkHeadStrict, oneifyDmd,
toCleanDmd,
absDmd, topDmd, botDmd, seqDmd,
......@@ -48,9 +48,9 @@ module Demand (
deferAfterIO,
postProcessUnsat, postProcessDmdType,
splitProdDmd_maybe, peelCallDmd, mkCallDmd, mkWorkerDemand,
dmdTransformSig, dmdTransformDataConSig, dmdTransformDictSelSig,
argOneShots, argsOneShots, saturatedByOneShots,
splitProdDmd_maybe, peelCallDmd, peelManyCalls, mkCallDmd,
mkWorkerDemand, dmdTransformSig, dmdTransformDataConSig,
dmdTransformDictSelSig, argOneShots, argsOneShots, saturatedByOneShots,
trimToType, TypeShape(..),
useCount, isUsedOnce, reuseEnv,
......@@ -787,7 +787,7 @@ botDmd = JD { sd = strBot, ud = useBot }
seqDmd :: Demand
seqDmd = JD { sd = Str VanStr HeadStr, ud = Use One UHead }
oneifyDmd :: Demand -> Demand
oneifyDmd :: JointDmd s (Use u) -> JointDmd s (Use u)
oneifyDmd (JD { sd = s, ud = Use _ a }) = JD { sd = s, ud = Use One a }
oneifyDmd jd = jd
......@@ -796,7 +796,7 @@ isTopDmd :: Demand -> Bool
isTopDmd (JD {sd = Lazy, ud = Use Many Used}) = True
isTopDmd _ = False
isAbsDmd :: Demand -> Bool
isAbsDmd :: JointDmd (Str s) (Use u) -> Bool
isAbsDmd (JD {ud = Abs}) = True -- The strictness part can be HyperStr
isAbsDmd _ = False -- for a bottom demand
......@@ -804,7 +804,7 @@ isSeqDmd :: Demand -> Bool
isSeqDmd (JD {sd = Str VanStr HeadStr, ud = Use _ UHead}) = True
isSeqDmd _ = False
isUsedOnce :: Demand -> Bool
isUsedOnce :: JointDmd (Str s) (Use u) -> Bool
isUsedOnce (JD { ud = a }) = case useCount a of
One -> True
Many -> False
......@@ -817,7 +817,7 @@ seqDemandList :: [Demand] -> ()
seqDemandList [] = ()
seqDemandList (d:ds) = seqDemand d `seq` seqDemandList ds
isStrictDmd :: Demand -> Bool
isStrictDmd :: JointDmd (Str s) (Use u) -> Bool
-- See Note [Strict demands]
isStrictDmd (JD {ud = Abs}) = False
isStrictDmd (JD {sd = Lazy}) = False
......
......@@ -897,9 +897,10 @@ zapStableUnfolding id
{-
Note [transferPolyIdInfo]
~~~~~~~~~~~~~~~~~~~~~~~~~
This transfer is used in two places:
This transfer is used in three places:
FloatOut (long-distance let-floating)
SimplUtils.abstractFloats (short-distance let-floating)
StgLiftLams (selectively lambda-lift local functions to top-level)
Consider the short-distance let-floating:
......
......@@ -45,7 +45,7 @@ import Module
import Outputable
import Stream
import BasicTypes
import VarSet ( isEmptyVarSet )
import VarSet ( isEmptyDVarSet )
import OrdList
import MkGraph
......@@ -156,7 +156,7 @@ cgTopRhs dflags _rec bndr (StgRhsCon _cc con args)
-- see Note [Post-unarisation invariants] in UnariseStg
cgTopRhs dflags rec bndr (StgRhsClosure fvs cc upd_flag args body)
= ASSERT(isEmptyVarSet fvs) -- There should be no free variables
= ASSERT(isEmptyDVarSet fvs) -- There should be no free variables
cgTopRhsClosure dflags rec bndr cc upd_flag args body
......
......@@ -44,7 +44,7 @@ import Name
import Module
import ListSetOps
import Util
import UniqSet ( nonDetEltsUniqSet )
import VarSet
import BasicTypes
import Outputable
import FastString
......@@ -209,10 +209,7 @@ cgRhs id (StgRhsCon cc con args)
{- See Note [GC recovery] in compiler/codeGen/StgCmmClosure.hs -}
cgRhs id (StgRhsClosure fvs cc upd_flag args body)
= do dflags <- getDynFlags
mkRhsClosure dflags id cc (nonVoidIds (nonDetEltsUniqSet fvs)) upd_flag args body
-- It's OK to use nonDetEltsUniqSet here because we're not aiming for
-- bit-for-bit determinism.
-- See Note [Unique Determinism and code generation]
mkRhsClosure dflags id cc (nonVoidIds (dVarSetElems fvs)) upd_flag args body
------------------------------------------------------------------------
-- Non-constructor right hand sides
......
......@@ -81,8 +81,8 @@ cgExpr (StgTick t e) = cgTick t >> cgExpr e
cgExpr (StgLit lit) = do cmm_lit <- cgLit lit
emitReturn [CmmLit cmm_lit]
cgExpr (StgLet binds expr) = do { cgBind binds; cgExpr expr }
cgExpr (StgLetNoEscape binds expr) =
cgExpr (StgLet _ binds expr) = do { cgBind binds; cgExpr expr }
cgExpr (StgLetNoEscape _ binds expr) =
do { u <- newUnique
; let join_id = mkBlockId u
; cgLneBinds join_id binds
......
......@@ -433,6 +433,11 @@ Library
SimplStg
StgStats
StgCse
StgLiftLams
StgLiftLams.Analysis
StgLiftLams.LiftM
StgLiftLams.Transformation
StgSubst
UnariseStg
RepType
Rules
......
......@@ -465,6 +465,7 @@ data GeneralFlag
| Opt_StaticArgumentTransformation
| Opt_CSE
| Opt_StgCSE
| Opt_StgLiftLams
| Opt_LiberateCase
| Opt_SpecConstr
| Opt_SpecConstrKeen
......@@ -672,6 +673,7 @@ optimisationFlags = EnumSet.fromList
, Opt_StaticArgumentTransformation
, Opt_CSE
, Opt_StgCSE
, Opt_StgLiftLams
, Opt_LiberateCase
, Opt_SpecConstr
, Opt_SpecConstrKeen
......@@ -903,6 +905,13 @@ data DynFlags = DynFlags {
floatLamArgs :: Maybe Int, -- ^ Arg count for lambda floating
-- See CoreMonad.FloatOutSwitches
liftLamsRecArgs :: Maybe Int, -- ^ Maximum number of arguments after lambda lifting a
-- recursive function.
liftLamsNonRecArgs :: Maybe Int, -- ^ Maximum number of arguments after lambda lifting a
-- non-recursive function.
liftLamsKnown :: Bool, -- ^ Lambda lift even when this turns a known call
-- into an unknown call.
cmmProcAlignment :: Maybe Int, -- ^ Align Cmm functions at this boundary or use default.
historySize :: Int, -- ^ Simplification history size
......@@ -1865,6 +1874,9 @@ defaultDynFlags mySettings (myLlvmTargets, myLlvmPasses) =
specConstrRecursive = 3,
liberateCaseThreshold = Just 2000,
floatLamArgs = Just 0, -- Default: float only if no fvs
liftLamsRecArgs = Just 5, -- Default: the number of available argument hardware registers on x86_64
liftLamsNonRecArgs = Just 5, -- Default: the number of available argument hardware registers on x86_64
liftLamsKnown = False, -- Default: don't turn known calls into unknown ones
cmmProcAlignment = Nothing,
historySize = 20,
......@@ -3522,6 +3534,18 @@ dynamic_flags_deps = [
(intSuffix (\n d -> d { floatLamArgs = Just n }))
, make_ord_flag defFlag "ffloat-all-lams"
(noArg (\d -> d { floatLamArgs = Nothing }))
, make_ord_flag defFlag "fstg-lift-lams-rec-args"
(intSuffix (\n d -> d { liftLamsRecArgs = Just n }))
, make_ord_flag defFlag "fstg-lift-lams-rec-args-any"
(noArg (\d -> d { liftLamsRecArgs = Nothing }))
, make_ord_flag defFlag "fstg-lift-lams-non-rec-args"
(intSuffix (\n d -> d { liftLamsRecArgs = Just n }))
, make_ord_flag defFlag "fstg-lift-lams-non-rec-args-any"
(noArg (\d -> d { liftLamsRecArgs = Nothing }))
, make_ord_flag defFlag "fstg-lift-lams-known"
(noArg (\d -> d { liftLamsKnown = True }))
, make_ord_flag defFlag "fno-stg-lift-lams-known"
(noArg (\d -> d { liftLamsKnown = False }))
, make_ord_flag defFlag "fproc-alignment"
(intSuffix (\n d -> d { cmmProcAlignment = Just n }))
, make_ord_flag defFlag "fblock-layout-weights"
......@@ -4016,6 +4040,7 @@ fFlagsDeps = [
flagSpec "cmm-sink" Opt_CmmSink,
flagSpec "cse" Opt_CSE,
flagSpec "stg-cse" Opt_StgCSE,
flagSpec "stg-lift-lams" Opt_StgLiftLams,
flagSpec "cpr-anal" Opt_CprAnal,
flagSpec "defer-type-errors" Opt_DeferTypeErrors,
flagSpec "defer-typed-holes" Opt_DeferTypedHoles,
......@@ -4546,6 +4571,7 @@ optLevelFlags -- see Note [Documenting optimisation flags]
, ([1,2], Opt_CmmSink)
, ([1,2], Opt_CSE)
, ([1,2], Opt_StgCSE)
, ([2], Opt_StgLiftLams)
, ([1,2], Opt_EnableRewriteRules) -- Off for -O0; see Note [Scoping for Builtin rules]
-- in PrelRules
, ([1,2], Opt_FloatIn)
......
......@@ -5,6 +5,7 @@
-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module SimplStg ( stg2stg ) where
......@@ -18,12 +19,25 @@ import StgLint ( lintStgTopBindings )
import StgStats ( showStgStats )
import UnariseStg ( unarise )
import StgCse ( stgCse )
import StgLiftLams ( stgLiftLams )
import DynFlags
import ErrUtils
import UniqSupply ( mkSplitUniqSupply )
import UniqSupply
import Outputable
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.State.Strict
newtype StgM a = StgM { _unStgM :: StateT UniqSupply IO a }
deriving (Functor, Applicative, Monad, MonadIO)
instance MonadUnique StgM where
getUniqueSupplyM = StgM (state splitUniqSupply)
getUniqueM = StgM (state takeUniqFromSupply)
runStgM :: UniqSupply -> StgM a -> IO a
runStgM us (StgM m) = evalStateT m us
stg2stg :: DynFlags -- includes spec of what stg-to-stg passes to do
-> [StgTopBinding] -- input...
......@@ -33,46 +47,56 @@ stg2stg dflags binds
= do { showPass dflags "Stg2Stg"
; us <- mkSplitUniqSupply 'g'
-- Do the main business!
; dumpIfSet_dyn dflags Opt_D_dump_stg "Pre unarise:"
(pprStgTopBindings binds)
-- Do the main business!
; binds' <- runStgM us $
foldM do_stg_pass binds (getStgToDo dflags)
; stg_linter False "Pre-unarise" binds
; let un_binds = unarise us binds
; stg_linter True "Unarise" un_binds
-- Important that unarisation comes first
-- See Note [StgCse after unarisation] in StgCse
; dump_when Opt_D_dump_stg "STG syntax:" binds'
; dumpIfSet_dyn dflags Opt_D_dump_stg "STG syntax:"
(pprStgTopBindings un_binds)
; foldM do_stg_pass un_binds (getStgToDo dflags)
}
; return binds'
}
where
stg_linter unarised
| gopt Opt_DoStgLinting dflags = lintStgTopBindings dflags unarised
stg_linter what
| gopt Opt_DoStgLinting dflags = lintStgTopBindings dflags what
| otherwise = \ _whodunnit _binds -> return ()
-------------------------------------------
do_stg_pass :: [StgTopBinding] -> StgToDo -> StgM [StgTopBinding]
do_stg_pass binds to_do
= case to_do of
D_stg_stats ->
trace (showStgStats binds) (return binds)
StgDoNothing ->
return binds
StgStats ->
trace (showStgStats binds) (return binds)
StgCSE ->
{-# SCC "StgCse" #-}
let
binds' = stgCse binds
in
end_pass "StgCse" binds'
StgCSE -> do
let binds' = {-# SCC "StgCse" #-} stgCse binds
end_pass "StgCse" binds'
StgLiftLams -> do
us <- getUniqueSupplyM
let binds' = {-# SCC "StgLiftLams" #-} stgLiftLams dflags us binds
end_pass "StgLiftLams" binds'
StgUnarise -> do
dump_when Opt_D_dump_stg "Pre unarise:" binds
us <- getUniqueSupplyM
liftIO (stg_linter False "Pre-unarise" binds)
let binds' = unarise us binds
liftIO (stg_linter True "Unarise" binds')
return binds'
dump_when flag header binds
= liftIO (dumpIfSet_dyn dflags flag header (pprStgTopBindings binds))
end_pass what binds2
= do -- report verbosely, if required
dumpIfSet_dyn dflags Opt_D_verbose_stg2stg what
(pprStgTopBindings binds2)
stg_linter True what binds2
return binds2
= liftIO $ do -- report verbosely, if required
dumpIfSet_dyn dflags Opt_D_verbose_stg2stg what
(vcat (map ppr binds2))
stg_linter False what binds2
return binds2
-- -----------------------------------------------------------------------------
-- StgToDo: abstraction of stg-to-stg passes to run.
......@@ -80,12 +104,31 @@ stg2stg dflags binds
-- | Optional Stg-to-Stg passes.
data StgToDo
= StgCSE
| D_stg_stats
-- | Which optional Stg-to-Stg passes to run. Depends on flags, ways etc.
-- ^ Common subexpression elimination
| StgLiftLams
-- ^ Lambda lifting closure variables, trading stack/register allocation for
-- heap allocation
| StgStats
| StgUnarise
-- ^ Mandatory unarise pass, desugaring unboxed tuple and sum binders
| StgDoNothing
-- ^ Useful for building up 'getStgToDo'
deriving Eq
-- | Which Stg-to-Stg passes to run. Depends on flags, ways etc.
getStgToDo :: DynFlags -> [StgToDo]
getStgToDo dflags
= [ StgCSE | gopt Opt_StgCSE dflags] ++
[ D_stg_stats | stg_stats ]
where
stg_stats = gopt Opt_StgStats dflags
getStgToDo dflags =
filter (/= StgDoNothing)
[ mandatory StgUnarise
-- Important that unarisation comes first
-- See Note [StgCse after unarisation] in StgCse
, optional Opt_StgCSE StgCSE
, optional Opt_StgLiftLams StgLiftLams
, optional Opt_StgStats StgStats
] where
optional opt = runWhen (gopt opt dflags)
mandatory = id
runWhen :: Bool -> StgToDo -> StgToDo
runWhen True todo = todo
runWhen _ _ = StgDoNothing
......@@ -331,14 +331,14 @@ stgCseExpr env (StgConApp dataCon args tys)
-- The binding might be removed due to CSE (we do not want trivial bindings on
-- the STG level), so use the smart constructor `mkStgLet` to remove the binding
-- if empty.
stgCseExpr env (StgLet binds body)
stgCseExpr env (StgLet ext binds body)
= let (binds', env') = stgCseBind env binds
body' = stgCseExpr env' body
in mkStgLet StgLet binds' body'
stgCseExpr env (StgLetNoEscape binds body)
in mkStgLet (StgLet ext) binds' body'
stgCseExpr env (StgLetNoEscape ext binds body)
= let (binds', env') = stgCseBind env binds
body' = stgCseExpr env' body
in mkStgLet StgLetNoEscape binds' body'
in mkStgLet (StgLetNoEscape ext) binds' body'
-- Case alternatives
-- Extend the CSE environment
......
-- | Implements a selective lambda lifter, running late in the optimisation
-- pipeline.
--
-- The transformation itself is implemented in "StgLiftLams.Transformation".
-- If you are interested in the cost model that is employed to decide whether
-- to lift a binding or not, look at "StgLiftLams.Analysis".
-- "StgLiftLams.LiftM" contains the transformation monad that hides away some
-- plumbing of the transformation.
module StgLiftLams (
-- * Late lambda lifting in STG
-- $note
Transformation.stgLiftLams
) where
import qualified StgLiftLams.Transformation as Transformation
-- Note [Late lambda lifting in STG]
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- $note
-- See also the <https://ghc.haskell.org/trac/ghc/wiki/LateLamLift wiki page>
-- and Trac #9476.
--
-- The basic idea behind lambda lifting is to turn locally defined functions
-- into top-level functions. Free variables are then passed as additional
-- arguments at *call sites* instead of having a closure allocated for them at
-- *definition site*. Example:
--
-- @
-- let x = ...; y = ... in
-- let f = {x y} \a -> a + x + y in
-- let g = {f x} \b -> f b + x in
-- g 5
-- @
--
-- Lambda lifting @f@ would
--
-- 1. Turn @f@'s free variables into formal parameters
-- 2. Update @f@'s call site within @g@ to @f x y b@
-- 3. Update @g@'s closure: Add @y@ as an additional free variable, while
-- removing @f@, because @f@ no longer allocates and can be floated to
-- top-level.
-- 4. Actually float the binding of @f@ to top-level, eliminating the @let@
-- in the process.
--
-- This results in the following program (with free var annotations):
--
-- @
-- f x y a = a + x + y;
-- let x = ...; y = ... in
-- let g = {x y} \b -> f x y b + x in
-- g 5
-- @
--
-- This optimisation is all about lifting only when it is beneficial to do so.
-- The above seems like a worthwhile lift, judging from heap allocation:
-- We eliminate @f@'s closure, saving to allocate a closure with 2 words, while
-- not changing the size of @g@'s closure.
--
-- You can probably sense that there's some kind of cost model at play here.
-- And you are right! But we also employ a couple of other heuristics for the
-- lifting decision which are outlined in "StgLiftLams.Analysis#when".
--
-- The transformation is done in "StgLiftLams.Transformation", which calls out
-- to 'StgLiftLams.Analysis.goodToLift' for its lifting decision.
-- It relies on "StgLiftLams.LiftM", which abstracts some subtle STG invariants
-- into a monadic substrate.
--
-- Suffice to say: We trade heap allocation for stack allocation.
-- The additional arguments have to passed on the stack (or in registers,
-- depending on architecture) every time we call the function to save a single
-- heap allocation when entering the let binding. Nofib suggests a mean
-- improvement of about 1% for this pass, so it seems like a worthwhile thing to
-- do. Compile-times went up by 0.6%, so all in all a very modest change.
--
-- For a concrete example, look at @spectral/atom@. There's a call to 'zipWith'
-- that is ultimately compiled to something like this
-- (module desugaring/lowering to actual STG):
--
-- @
-- propagate dt = ...;
-- runExperiment ... =
-- let xs = ... in
-- let ys = ... in
-- let go = {dt go} \xs ys -> case (xs, ys) of
-- ([], []) -> []
-- (x:xs', y:ys') -> propagate dt x y : go xs' ys'
-- in go xs ys
-- @
--
-- This will lambda lift @go@ to top-level, speeding up the resulting program
-- by roughly one percent:
--
-- @
-- propagate dt = ...;
-- go dt xs ys = case (xs, ys) of
-- ([], []) -> []
-- (x:xs', y:ys') -> propagate dt x y : go dt xs' ys'
-- runExperiment ... =
-- let xs = ... in
-- let ys = ... in
-- in go dt xs ys
-- @
This diff is collapsed.
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
-- | Hides away distracting bookkeeping while lambda lifting into a 'LiftM'
-- monad.
module StgLiftLams.LiftM (
decomposeStgBinding, mkStgBinding,
Env (..),
-- * #floats# Handling floats
-- $floats
FloatLang (..), collectFloats, -- Exported just for the docs
-- * Transformation monad
LiftM, runLiftM, withCaffyness,
-- ** Adding bindings
startBindingGroup, endBindingGroup, addTopStringLit, addLiftedBinding,
-- ** Substitution and binders
withSubstBndr, withSubstBndrs, withLiftedBndr, withLiftedBndrs,
-- ** Occurrences
substOcc, isLifted, formerFreeVars, liftedIdsExpander
) where
#include "HsVersions.h"
import GhcPrelude
import BasicTypes
import CostCentre ( isCurrentCCS, dontCareCCS )
import DynFlags
import FastString
import Id
import IdInfo
import Name
import Outputable
import OrdList
import StgSubst
import StgSyn
import Type
import UniqSupply
import Util
import VarEnv
import VarSet
import Control.Arrow ( second )
import Control.Monad.Trans.Class
import Control.Monad.Trans.RWS.Strict ( RWST, runRWST )
import qualified Control.Monad.Trans.RWS.Strict as RWS
import Control.Monad.Trans.Cont ( ContT (..) )
import Data.ByteString ( ByteString )
import Data.List ( foldl' )
-- | @uncurry 'mkStgBinding' . 'decomposeStgBinding' = id@
decomposeStgBinding :: GenStgBinding pass -> (RecFlag, [(BinderP pass, GenStgRhs pass)])
decomposeStgBinding (StgRec pairs) = (Recursive, pairs)
decomposeStgBinding (StgNonRec bndr rhs) = (NonRecursive, [(bndr, rhs)])
mkStgBinding :: RecFlag -> [(BinderP pass, GenStgRhs pass)] -> GenStgBinding pass
mkStgBinding Recursive = StgRec
mkStgBinding NonRecursive = uncurry StgNonRec . head
-- | Environment threaded around in a scoped, @Reader@-like fashion.
data Env
= Env
{ e_dflags :: !DynFlags
-- ^ Read-only.
, e_subst :: !Subst
-- ^ We need to track the renamings of local 'InId's to their lifted 'OutId',
-- because shadowing might make a closure's free variables unavailable at its
-- call sites. Consider:
-- @
-- let f y = x + y in let x = 4 in f x
-- @
-- Here, @f@ can't be lifted to top-level, because its free variable @x@ isn't
-- available at its call site.
, e_expansions :: !(IdEnv DIdSet)
-- ^ Lifted 'Id's don't occur as free variables in any closure anymore, because
-- they are bound at the top-level. Every occurrence must supply the formerly
-- free variables of the lifted 'Id', so they in turn become free variables of
-- the call sites. This environment tracks this expansion from lifted 'Id's to
-- their free variables.
--
-- 'InId's to 'OutId's.
--
-- Invariant: 'Id's not present in this map won't be substituted.
, e_in_caffy_context :: !Bool
-- ^ Are we currently analysing within a caffy context (e.g. the containing
-- top-level binder's 'idCafInfo' is 'MayHaveCafRefs')? If not, we can safely
-- assume that functions we lift out aren't caffy either.
}
emptyEnv :: DynFlags -> Env
emptyEnv dflags = Env dflags emptySubst emptyVarEnv False
-- Note [Handling floats]
-- ~~~~~~~~~~~~~~~~~~~~~~
-- $floats
-- Consider the following expression:
--
-- @
-- f x =
-- let g y = ... f y ...
-- in g x
-- @
--
-- What happens when we want to lift @g@? Normally, we'd put the lifted @l_g@
-- binding above the binding for @f@:
--
-- @
-- g f y = ... f y ...
-- f x = g f x
-- @
--
-- But this very unnecessarily turns a known call to @f@ into an unknown one, in
-- addition to complicating matters for the analysis.
-- Instead, we'd really like to put both functions in the same recursive group,
-- thereby preserving the known call:
--
-- @
-- Rec {
-- g y = ... f y ...