-- |
-- Module      : Fusion
-- License     : BSD-3-Clause
-- Copyright   : (c) 2026 Olivier Chéron
--
-- Infrastructure to decrease intermediate allocations and prefer in-place
-- mutation when possible
--
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilyDependencies #-}
module Fusion
    ( Fusion(..), MapF(..)
    , Context, runContext, newContext, thawContext, mapContext, modifyContext
    , foldContext, seqContext
    ) where

import Control.Monad ( forM_, (>=>) )
import Control.Monad.ST

-- class of values that can be mutated in the ST monad
class Fusion a where
    type Mut a s = mut | mut -> a
    newF :: ST s (Mut a s)
    thawF :: a -> ST s (Mut a s)
    unsafeFreezeF :: Mut a s -> ST s a

-- a transformation step in the fusion pipeline, with two implementations
-- provided: one that operates on an existing mutation context, and one that
-- initiates a new context from the input
data MapF a b = MapF
    { mapUpdate :: forall s. Mut a s -> ST s (Mut b s)
    , mapInit :: forall s. a -> ST s (Mut b s)
    }

-- MapF is almost a category except for the 'Fusion' constraint on objects
--
-- idMapF :: Fusion a => MapF a a
-- idMapF = MapF { mapUpdate = pure, mapInit = thawF }

composeMapF :: MapF b c -> MapF a b -> MapF a c
composeMapF m2 m1 = MapF
    { mapUpdate = mapUpdate m1 >=> mapUpdate m2
    , mapInit = mapInit m1 >=> mapUpdate m2
    }

-- fusion context
newtype Context a = Context (forall s. ST s (Mut a s))

newContext :: Fusion a => Context a
newContext = Context newF

thawContext :: Fusion a => a -> Context a
thawContext a = Context $ thawF a
{-# INLINE [0] thawContext #-}

modifyContext :: (forall s. Mut a s -> ST s ()) -> Context a -> Context a
modifyContext f = bindContext $ \ma -> f ma >> return ma

mapContext :: MapF a b -> Context a -> Context b
mapContext m = bindContext (mapUpdate m)
{-# INLINE [0] mapContext #-}

initContext :: MapF a b -> a -> Context b
initContext m a = Context $ mapInit m a

bindContext :: (forall s. Mut a s -> ST s (Mut b s)) -> Context a -> Context b
bindContext f (Context ctx) = Context $ ctx >>= f

foldContext :: Foldable t => (forall s. b -> Mut a s -> ST s ()) -> Context a -> t b -> Context a
foldContext f c bs = modifyContext (\ma -> forM_ bs $ \b -> f b ma) c

runContext :: Fusion a => Context a -> a
runContext (Context ctx) = runST (ctx >>= unsafeFreezeF)
{-# INLINE [0] runContext #-}

seqContext :: a -> Context b -> Context b
seqContext = seq
{-# INLINE [0] seqContext #-}


-- Fusion rules
--
-- "thawContext/runContext" is the canonical optimization that eliminates an
-- allocation + value copy.  Instead, it sequences two transformations on the
-- same mutation context.
--
-- "mapContext/seqContext" moves strictness annotations upstream so that they
-- do not prevent other rules from firing.
--
-- "mapContext/mapContext" is not strictly needed: the function is ultimately
-- inlined to the same code.  But we keep it so that simplifications fire early
-- and do not wait for the final phase.
--
-- "mapContext/thawContext" is the rule that invokes mapInit instead of copying
-- the input and calling mapUpdate.

{-# RULES
"thawContext/runContext" [~0] forall c. thawContext (runContext c) = c
"mapContext/seqContext" [~0] forall a m c. mapContext m (seqContext a c) = seqContext a (mapContext m c)
"mapContext/mapContext" [~0] forall m1 m2 c. mapContext m2 (mapContext m1 c) = mapContext (composeMapF m2 m1) c
"mapContext/thawContext" [1] forall m a. mapContext m (thawContext a) = initContext m a
  #-}
