packages feed

indigo-0.4: src/Indigo/Compilation/Lambda.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

module Indigo.Compilation.Lambda
  ( compileLambdas
  ) where

import Prelude

import qualified Data.Map as M

import Indigo.Backend as B
import Indigo.Compilation.Sequential
import Indigo.Internal.Var

-- | Collects named lambdas that are used more than once and separates them into
-- a lambda creation and multiple lambda executions.
-- Leaves the remaining lambdas untouched, to be compiled inline.
compileLambdas :: (Block, RefId) -> (Block, RefId)
compileLambdas (block, nextRef) = (mkLambdas <> updatedBlock, newNextRef)
  where
    lambdaSet = collectNotInlinableLambdas block
    (lambdaRefDefs, newNextRef) = createLambdaRefs nextRef lambdaSet
    mkLambdas = createAllLambdas lambdaRefDefs
    updatedBlock = updateBlock block . M.fromList $ map (first ldName) lambdaRefDefs

-- | Collect all used lambdas in a computation that are called at least twice.
-- Only the outer lambdas will be gathered, for example, if we call lambda "func1"
-- from "func0", only "func0" will be considered.
collectNotInlinableLambdas :: Block -> Set Lambda1Def
collectNotInlinableLambdas = M.keysSet . M.filter (> 1) . executingState mempty . lookForLambdas

-- | Associates each given 'Lambda1Def' to a new 'RefId', starting from the given
-- one. Also returns the first unused 'RefId'
createLambdaRefs :: RefId -> Set Lambda1Def -> ([(Lambda1Def, RefId)], RefId)
createLambdaRefs nextRef =
  foldr (\lm (lst, ref) -> ((lm, ref) : lst, ref + 1)) ([], nextRef)

-- | Generates an 'Instruction' for each given tuple, to generate a lambda
-- (assigned to the respective variable) and leave it on the stack.
createAllLambdas :: [(Lambda1Def, RefId)] -> Block
createAllLambdas = map $ \(Lambda1Def {..}, lamRef) ->
  CreateLambda1 ldStack ldArgVar ldBody ldRet (Var lamRef)

-- | Updates a 'Block', it looks for lambda "Calls" (defined and used in place)
-- to replace them with lambda "Exec", provided there is a known variable for an
-- already created lambda.
updateBlock :: Block -> Map String RefId -> Block
updateBlock blk lambdaMap = updateBlock' blk
  where
    updateBlock' :: Block -> Block
    updateBlock' = map $ \case
      -- Instructions not concerned, will be kept the same
      LiftIndigoState sis -> LiftIndigoState sis
      Comment txt         -> Comment txt
      AssignVar vx ex -> AssignVar vx ex
      SetVar vx ex -> SetVar vx ex
      VarModification upd vx ey -> VarModification upd vx ey
      SetField vSt lName ex -> SetField vSt lName ex

      -- Lambda instructions to check for possible replacement
      lc@(LambdaCall1 lKind lName ex _var _block _ret retVars) ->
        case M.lookup lName lambdaMap of
          Nothing -> lc
          Just ref -> ExecLambda1 lKind Proxy ex (Var ref) retVars

      -- Lambda instructions not concerned, nothing to replace here
      c@(CreateLambda1{}) -> c
      e@(ExecLambda1{}) -> e

      -- Instructions with deeper code blocks to replace as well
      Scope block ret retVars ->
        Scope (updateBlock' block) ret retVars
      If ex blockA retA blockB retB retVars ->
        If ex (updateBlock' blockA) retA (updateBlock' blockB) retB retVars
      IfSome ex varX blockA retA blockB retB retVars ->
        IfSome ex varX (updateBlock' blockA) retA (updateBlock' blockB) retB retVars
      IfRight ex varR blockA retA varL blockB retB retVars ->
        IfRight ex varR (updateBlock' blockA) retA varL (updateBlock' blockB) retB retVars
      IfCons ex varX varLX blockA retA blockB retB retVars ->
        IfCons ex varX varLX (updateBlock' blockA) retA (updateBlock' blockB) retB retVars

      Case grd blockClauses retVars ->
        Case grd (updateClauses updateBlock' blockClauses) retVars
      EntryCase proxy grd blockClauses retVars ->
        EntryCase proxy grd (updateClauses updateBlock' blockClauses) retVars
      EntryCaseSimple grd blockClauses retVars ->
        EntryCaseSimple grd (updateClauses updateBlock' blockClauses) retVars

      While ex block ->
        While ex (updateBlock' block)
      WhileLeft ex varL block varR ->
        WhileLeft ex varL (updateBlock' block) varR
      ForEach varIop ex block ->
        ForEach varIop ex (updateBlock' block)

      ContractName tx block ->
        ContractName tx (updateBlock' block)
      DocGroup dg block ->
        DocGroup dg (updateBlock' block)
      ContractGeneral block ->
        ContractGeneral (updateBlock' block)
      FinalizeParamCallingDoc varCp block param ->
        FinalizeParamCallingDoc varCp (updateBlock' block) param

      -- Instructions not concerned, will be kept the same
      TransferTokens ex exm exc -> TransferTokens ex exm exc
      SetDelegate ex -> SetDelegate ex
      CreateContract varAddr ctrc exk exm exs -> CreateContract varAddr ctrc exk exm exs
      SelfCalling proxy varCR ep -> SelfCalling proxy varCR ep
      ContractCalling varMcr pCp epRef exAddr -> ContractCalling varMcr pCp epRef exAddr

      Fail failure -> Fail failure
      FailOver failure ex -> FailOver failure ex

-- Like 'collectLambdas', but uses 'State' to collect the 'Map' of all outer
-- lambdas encountered, including those used once.
lookForLambdas :: Block -> State (Map Lambda1Def Word) ()
lookForLambdas blk = forM_ blk match
  where
    -- pva701: it's crucial to have this function 'match' instead of code like
    -- @forM_ blk match $ \case@
    --        ... cases here ...
    -- because in the case of code above compilation of this function takes about 5-6 minutes
    -- it would be nice to figure out why (inspecting generated by GHC code)
    match :: Instruction -> State (Map Lambda1Def Word) ()
    match = \case
      -- Lambda instruction to collect
      LambdaCall1 lKind ldName _ex ldArgVar ldBody ldRet _retVars -> do
        let ldStack = initLambdaStackVars lKind ldArgVar
        withLambdaKind lKind $ addLambda $ Lambda1Def {..}

      -- Instructions with deeper code block to look into
      Scope block _ _ -> lookForLambdas block
      If _ blockA _ blockB _ _ ->
        lookForLambdas blockA >> lookForLambdas blockB
      IfSome _ _ blockA _ blockB _ _ ->
        lookForLambdas blockA >> lookForLambdas blockB
      IfRight _ _ blockA _ _ blockB _ _ ->
        lookForLambdas blockA >> lookForLambdas blockB
      IfCons _ _ _ blockA _ blockB _ _ ->
        lookForLambdas blockA >> lookForLambdas blockB
      Case _ blockClauses _ ->
        mapMClauses lookForLambdas blockClauses
      EntryCase _ _ blockClauses _ ->
        mapMClauses lookForLambdas blockClauses
      EntryCaseSimple _ blockClauses _ ->
        mapMClauses lookForLambdas blockClauses
      While _ block -> lookForLambdas block
      WhileLeft _ _ block _ -> lookForLambdas block
      ForEach _ _ block -> lookForLambdas block
      ContractName _ block -> lookForLambdas block
      DocGroup _ block -> lookForLambdas block
      ContractGeneral block -> lookForLambdas block
      FinalizeParamCallingDoc _ block _ -> lookForLambdas block

      -- We skip two types of instructions:
      -- 1) Instructions without deeper code block
      -- 2) Unnamed lambdas creation/usage (like CreateLambda1, ExecLambda1, etc)

      -- Instructions without deeper code block
      LiftIndigoState {} -> return ()
      Comment {}         -> return ()
      AssignVar {}       -> return ()
      SetVar {}          -> return ()
      VarModification {} -> return ()
      SetField {}        -> return ()

      TransferTokens {}  -> return ()
      SetDelegate {}     -> return ()
      CreateContract {}  -> return ()
      SelfCalling {}     -> return ()
      ContractCalling {} -> return ()
      Fail {}            -> return ()
      FailOver {}        -> return ()

      -- Nothing to collect in the case of already unnamed lambdas creation/usage
      CreateLambda1 {}   -> return ()
      ExecLambda1 {}     -> return ()

    addLambda :: Lambda1Def -> State (Map Lambda1Def Word) ()
    addLambda lDef = modify $ M.insertWith (+) lDef 1

-- | Contains all the data necessary for the generation of a single-argument
-- lambda. Is compared only on the base of it's 'ldName'.
data Lambda1Def where
  Lambda1Def
    :: (Typeable ret, CreateLambda1CGeneric extra arg ret)
    => { ldRet     :: ret
       , ldName    :: String
       , ldBody    :: Block
       , ldArgVar  :: Var arg
       , ldStack   :: StackVars (arg : extra)
       } -> Lambda1Def

instance Eq Lambda1Def where
  (==) l1 l2 = ldName l1 == ldName l2

instance Ord Lambda1Def where
  (<=) l1 l2 = ldName l1 <= ldName l2