{-# LANGUAGE TypeFamilies #-}

-- | = Constructing Futhark ASTs
--
-- This module re-exports and defines a bunch of building blocks for
-- constructing fragments of Futhark ASTs.  More importantly, it also
-- contains a basic introduction on how to use them.
--
-- The "Futhark.IR.Syntax" module contains the core
-- AST definition.  One important invariant is that all bound names in
-- a Futhark program must be /globally/ unique.  In principle, you
-- could use the facilities from "Futhark.MonadFreshNames" (or your
-- own bespoke source of unique names) to manually construct
-- expressions, statements, and entire ASTs.  In practice, this would
-- be very tedious.  Instead, we have defined a collection of building
-- blocks (centered around the 'MonadBuilder' type class) that permits
-- a more abstract way of generating code.
--
-- Constructing ASTs with these building blocks requires you to ensure
-- that all free variables are in scope.  See
-- "Futhark.IR.Prop.Scope".
--
-- == 'MonadBuilder'
--
-- A monad that implements 'MonadBuilder' tracks the statements added
-- so far, the current names in scope, and allows you to add
-- additional statements with 'addStm'.  Any monad that implements
-- 'MonadBuilder' also implements the t'Rep' type family, which
-- indicates which rep it works with.  Inside a 'MonadBuilder' we can
-- use 'collectStms' to gather up the 'Stms' added with 'addStm' in
-- some nested computation.
--
-- The 'BuilderT' monad (and its convenient 'Builder' version) provides
-- the simplest implementation of 'MonadBuilder'.
--
-- == Higher-level building blocks
--
-- On top of the raw facilities provided by 'MonadBuilder', we have
-- more convenient facilities.  For example, 'letSubExp' lets us
-- conveniently create a 'Stm' for an 'Exp' that produces a /single/
-- value, and returns the (fresh) name for the resulting variable:
--
-- @
-- z <- letExp "z" $ BasicOp $ BinOp (Add Int32) (Var x) (Var y)
-- @
--
-- == Monadic expression builders
--
-- This module also contains "monadic expression" functions that let
-- us build nested expressions in a "direct" style, rather than using
-- 'letExp' and friends to bind every sub-part first.  See functions
-- such as 'eIf' and 'eBody' for example.  See also
-- "Futhark.Analysis.PrimExp" and the 'ToExp' type class.
--
-- == Examples
--
-- The "Futhark.Transform.FirstOrderTransform" module is a
-- (relatively) simple example of how to use these components.  As are
-- some of the high-level building blocks in this very module.
module Futhark.Construct
  ( -- * Basic building blocks
    module Futhark.Builder,
    letSubExp,
    letExp,
    letTupExp,
    letTupExp',
    letInPlace,

    -- * Monadic expression builders
    eSubExp,
    eParam,
    eMatch',
    eMatch,
    eIf,
    eIf',
    eBinOp,
    eUnOp,
    eCmpOp,
    eConvOp,
    eSignum,
    eCopy,
    eBody,
    eLambda,
    eBlank,
    eAll,
    eAny,
    eDimInBounds,
    eOutOfBounds,
    eIndex,
    eLast,

    -- * Other building blocks
    asIntZ,
    asIntS,
    resultBody,
    resultBodyM,
    insertStmsM,
    buildBody,
    buildBody_,
    mapResult,
    foldBinOp,
    binOpLambda,
    cmpOpLambda,
    mkLambda,
    sliceDim,
    fullSlice,
    fullSliceNum,
    isFullSlice,
    sliceAt,

    -- * Result types
    instantiateShapes,
    instantiateShapes',
    removeExistentials,

    -- * Convenience
    simpleMkLetNames,
    ToExp (..),
    toSubExp,
  )
where

import Control.Monad
import Control.Monad.Identity
import Control.Monad.State
import Data.List qualified as L
import Data.Map.Strict qualified as M
import Futhark.Builder
import Futhark.IR
import Futhark.Util (maybeNth)

-- | @letSubExp desc e@ binds the expression @e@, which must produce a
-- single value.  Returns a t'SubExp' corresponding to the resulting
-- value.  For expressions that produce multiple values, see
-- 'letTupExp'.
letSubExp ::
  (MonadBuilder m) =>
  String ->
  Exp (Rep m) ->
  m SubExp
letSubExp _ (BasicOp (SubExp se)) = pure se
letSubExp desc e = Var <$> letExp desc e

-- | Like 'letSubExp', but returns a name rather than a t'SubExp'.
letExp ::
  (MonadBuilder m) =>
  String ->
  Exp (Rep m) ->
  m VName
letExp _ (BasicOp (SubExp (Var v))) =
  pure v
letExp desc e = do
  n <- length <$> expExtType e
  vs <- replicateM n $ newVName desc
  letBindNames vs e
  case vs of
    [v] -> pure v
    _ -> error $ "letExp: tuple-typed expression given:\n" ++ prettyString e

-- | Like 'letExp', but the 'VName' and 'Slice' denote an array that
-- is 'Update'd with the result of the expression.  The name of the
-- updated array is returned.
letInPlace ::
  (MonadBuilder m) =>
  String ->
  VName ->
  Slice SubExp ->
  Exp (Rep m) ->
  m VName
letInPlace desc src slice e = do
  tmp <- letSubExp (desc ++ "_tmp") e
  letExp desc $ BasicOp $ Update Unsafe src slice tmp

-- | Like 'letExp', but the expression may return multiple values.
letTupExp ::
  (MonadBuilder m) =>
  String ->
  Exp (Rep m) ->
  m [VName]
letTupExp _ (BasicOp (SubExp (Var v))) =
  pure [v]
letTupExp name e = do
  e_t <- expExtType e
  names <- replicateM (length e_t) $ newVName name
  letBindNames names e
  pure names

-- | Like 'letTupExp', but returns t'SubExp's instead of 'VName's.
letTupExp' ::
  (MonadBuilder m) =>
  String ->
  Exp (Rep m) ->
  m [SubExp]
letTupExp' _ (BasicOp (SubExp se)) = pure [se]
letTupExp' name ses = map Var <$> letTupExp name ses

-- | Turn a subexpression into a monad expression.  Does not actually
-- lead to any code generation.  This is supposed to be used alongside
-- the other monadic expression functions, such as 'eIf'.
eSubExp ::
  (MonadBuilder m) =>
  SubExp ->
  m (Exp (Rep m))
eSubExp = pure . BasicOp . SubExp

-- | Treat a parameter as a monadic expression.
eParam ::
  (MonadBuilder m) =>
  Param t ->
  m (Exp (Rep m))
eParam = eSubExp . Var . paramName

removeRedundantScrutinees :: [SubExp] -> [Case b] -> ([SubExp], [Case b])
removeRedundantScrutinees ses cases =
  let (ses', vs) =
        unzip $ filter interesting $ zip ses $ L.transpose (map casePat cases)
   in (ses', zipWith Case (L.transpose vs) $ map caseBody cases)
  where
    interesting = any (/= Nothing) . snd

-- | As 'eMatch', but an 'MatchSort' can be given.
eMatch' ::
  (MonadBuilder m, BranchType (Rep m) ~ ExtType) =>
  [SubExp] ->
  [Case (m (Body (Rep m)))] ->
  m (Body (Rep m)) ->
  MatchSort ->
  m (Exp (Rep m))
eMatch' ses cases_m defbody_m sort = do
  cases <- mapM (traverse insertStmsM) cases_m
  defbody <- insertStmsM defbody_m
  ts <-
    L.foldl' generaliseExtTypes
      <$> bodyExtType defbody
      <*> mapM (bodyExtType . caseBody) cases
  cases' <- mapM (traverse $ addContextForBranch ts) cases
  defbody' <- addContextForBranch ts defbody
  let ts' = replicate (length (shapeContext ts)) (Prim int64) ++ ts
      (ses', cases'') = removeRedundantScrutinees ses cases'
  pure $ Match ses' cases'' defbody' $ MatchDec ts' sort
  where
    addContextForBranch ts (Body _ stms val_res) = do
      body_ts <- extendedScope (traverse subExpResType val_res) stmsscope
      let ctx_res =
            map snd $ L.sortOn fst $ M.toList $ shapeExtMapping ts body_ts
      mkBodyM stms $ subExpsRes ctx_res ++ val_res
      where
        stmsscope = scopeOf stms

-- | Construct a 'Match' expression.  The main convenience here is
-- that the existential context of the return type is automatically
-- deduced, and the necessary elements added to the branches.
eMatch ::
  (MonadBuilder m, BranchType (Rep m) ~ ExtType) =>
  [SubExp] ->
  [Case (m (Body (Rep m)))] ->
  m (Body (Rep m)) ->
  m (Exp (Rep m))
eMatch ses cases_m defbody_m = eMatch' ses cases_m defbody_m MatchNormal

-- | Construct a 'Match' modelling an if-expression from a monadic
-- condition and monadic branches.  'eBody' might be convenient for
-- constructing the branches.
eIf ::
  (MonadBuilder m, BranchType (Rep m) ~ ExtType) =>
  m (Exp (Rep m)) ->
  m (Body (Rep m)) ->
  m (Body (Rep m)) ->
  m (Exp (Rep m))
eIf ce te fe = eIf' ce te fe MatchNormal

-- | As 'eIf', but an 'MatchSort' can be given.
eIf' ::
  (MonadBuilder m, BranchType (Rep m) ~ ExtType) =>
  m (Exp (Rep m)) ->
  m (Body (Rep m)) ->
  m (Body (Rep m)) ->
  MatchSort ->
  m (Exp (Rep m))
eIf' ce te fe if_sort = do
  ce' <- letSubExp "cond" =<< ce
  eMatch' [ce'] [Case [Just $ BoolValue True] te] fe if_sort

-- The type of a body.  Watch out: this only works for the degenerate
-- case where the body does not already return its context.
bodyExtType :: (HasScope rep m, Monad m) => Body rep -> m [ExtType]
bodyExtType (Body _ stms res) =
  existentialiseExtTypes (M.keys stmsscope) . staticShapes
    <$> extendedScope (traverse subExpResType res) stmsscope
  where
    stmsscope = scopeOf stms

-- | Construct a v'BinOp' expression with the given operator.
eBinOp ::
  (MonadBuilder m) =>
  BinOp ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eBinOp op x y = do
  x' <- letSubExp "x" =<< x
  y' <- letSubExp "y" =<< y
  pure $ BasicOp $ BinOp op x' y'

-- | Construct a v'UnOp' expression with the given operator.
eUnOp ::
  (MonadBuilder m) =>
  UnOp ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eUnOp op x = BasicOp . UnOp op <$> (letSubExp "x" =<< x)

-- | Construct a v'CmpOp' expression with the given comparison.
eCmpOp ::
  (MonadBuilder m) =>
  CmpOp ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eCmpOp op x y = do
  x' <- letSubExp "x" =<< x
  y' <- letSubExp "y" =<< y
  pure $ BasicOp $ CmpOp op x' y'

-- | Construct a v'ConvOp' expression with the given conversion.
eConvOp ::
  (MonadBuilder m) =>
  ConvOp ->
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eConvOp op x = do
  x' <- letSubExp "x" =<< x
  pure $ BasicOp $ ConvOp op x'

-- | Construct a 'SSignum' expression.  Fails if the provided
-- expression is not of integer type.
eSignum ::
  (MonadBuilder m) =>
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eSignum em = do
  e <- em
  e' <- letSubExp "signum_arg" e
  t <- subExpType e'
  case t of
    Prim (IntType int_t) ->
      pure $ BasicOp $ UnOp (SSignum int_t) e'
    _ ->
      error $ "eSignum: operand " ++ prettyString e ++ " has invalid type."

-- | Copy a value.
eCopy ::
  (MonadBuilder m) =>
  m (Exp (Rep m)) ->
  m (Exp (Rep m))
eCopy e = BasicOp . Replicate mempty <$> (letSubExp "copy_arg" =<< e)

-- | Construct a body from expressions.  If multiple expressions are
-- provided, their results will be concatenated in order and returned
-- as the result.
--
-- /Beware/: this will not produce correct code if the type of the
-- body would be existential.  That is, the type of the results being
-- returned should be invariant to the body.
eBody ::
  (MonadBuilder m) =>
  [m (Exp (Rep m))] ->
  m (Body (Rep m))
eBody es = buildBody_ $ do
  es' <- sequence es
  xs <- mapM (letTupExp "x") es'
  pure $ varsRes $ concat xs

-- | Bind each lambda parameter to the result of an expression, then
-- bind the body of the lambda.  The expressions must produce only a
-- single value each.
eLambda ::
  (MonadBuilder m) =>
  Lambda (Rep m) ->
  [m (Exp (Rep m))] ->
  m [SubExpRes]
eLambda lam args = do
  zipWithM_ bindParam (lambdaParams lam) args
  bodyBind $ lambdaBody lam
  where
    bindParam param arg = letBindNames [paramName param] =<< arg

-- | @eInBoundsForDim w i@ produces @0 <= i < w@.
eDimInBounds :: (MonadBuilder m) => m (Exp (Rep m)) -> m (Exp (Rep m)) -> m (Exp (Rep m))
eDimInBounds w i =
  eBinOp
    LogAnd
    (eCmpOp (CmpSle Int64) (eSubExp (intConst Int64 0)) i)
    (eCmpOp (CmpSlt Int64) i w)

-- | Are these indexes out-of-bounds for the array?
eOutOfBounds ::
  (MonadBuilder m) =>
  VName ->
  [m (Exp (Rep m))] ->
  m (Exp (Rep m))
eOutOfBounds arr is = do
  arr_t <- lookupType arr
  let ws = arrayDims arr_t
  is' <- mapM (letSubExp "write_i") =<< sequence is
  let checkDim w i = do
        less_than_zero <-
          letSubExp "less_than_zero" $
            BasicOp $
              CmpOp (CmpSlt Int64) i (constant (0 :: Int64))
        greater_than_size <-
          letSubExp "greater_than_size" $
            BasicOp $
              CmpOp (CmpSle Int64) w i
        letSubExp "outside_bounds_dim" $
          BasicOp $
            BinOp LogOr less_than_zero greater_than_size
  foldBinOp LogOr (constant False) =<< zipWithM checkDim ws is'

-- | The array element at this index.  Returns array unmodified if
-- indexes are null (does not even need to be an array in that case).
eIndex :: (MonadBuilder m) => VName -> [m (Exp (Rep m))] -> m (Exp (Rep m))
eIndex arr [] = eSubExp $ Var arr
eIndex arr is = do
  is' <- mapM (letSubExp "i" =<<) is
  arr_t <- lookupType arr
  pure $ BasicOp $ Index arr $ fullSlice arr_t $ map DimFix is'

-- | The last element of the given array.
eLast :: (MonadBuilder m) => VName -> m (Exp (Rep m))
eLast arr = do
  n <- arraySize 0 <$> lookupType arr
  nm1 <-
    letSubExp "nm1" . BasicOp $
      BinOp (Sub Int64 OverflowUndef) n (intConst Int64 1)
  eIndex arr [eSubExp nm1]

-- | Construct an unspecified value of the given type.
eBlank :: (MonadBuilder m) => Type -> m (Exp (Rep m))
eBlank (Prim t) = pure $ BasicOp $ SubExp $ Constant $ blankPrimValue t
eBlank (Array t shape _) = pure $ BasicOp $ Scratch t $ shapeDims shape
eBlank Acc {} = error "eBlank: cannot create blank accumulator"
eBlank Mem {} = error "eBlank: cannot create blank memory"

-- | Sign-extend to the given integer type.
asIntS :: (MonadBuilder m) => IntType -> SubExp -> m SubExp
asIntS = asInt SExt

-- | Zero-extend to the given integer type.
asIntZ :: (MonadBuilder m) => IntType -> SubExp -> m SubExp
asIntZ = asInt ZExt

asInt ::
  (MonadBuilder m) =>
  (IntType -> IntType -> ConvOp) ->
  IntType ->
  SubExp ->
  m SubExp
asInt ext to_it e = do
  e_t <- subExpType e
  case e_t of
    Prim (IntType from_it)
      | to_it == from_it -> pure e
      | otherwise -> letSubExp s $ BasicOp $ ConvOp (ext from_it to_it) e
    _ -> error "asInt: wrong type"
  where
    s = case e of
      Var v -> baseString v
      _ -> "to_" ++ prettyString to_it

-- | Apply a binary operator to several subexpressions.  A left-fold.
foldBinOp ::
  (MonadBuilder m) =>
  BinOp ->
  SubExp ->
  [SubExp] ->
  m (Exp (Rep m))
foldBinOp _ ne [] =
  pure $ BasicOp $ SubExp ne
foldBinOp bop ne (e : es) =
  eBinOp bop (pure $ BasicOp $ SubExp e) (foldBinOp bop ne es)

-- | True if all operands are true.
eAll :: (MonadBuilder m) => [SubExp] -> m (Exp (Rep m))
eAll [] = pure $ BasicOp $ SubExp $ constant True
eAll [x] = eSubExp x
eAll (x : xs) = foldBinOp LogAnd x xs

-- | True if any operand is true.
eAny :: (MonadBuilder m) => [SubExp] -> m (Exp (Rep m))
eAny [] = pure $ BasicOp $ SubExp $ constant False
eAny [x] = eSubExp x
eAny (x : xs) = foldBinOp LogOr x xs

-- | Create a two-parameter lambda whose body applies the given binary
-- operation to its arguments.  It is assumed that both argument and
-- result types are the same.  (This assumption should be fixed at
-- some point.)
binOpLambda ::
  (MonadBuilder m, Buildable (Rep m)) =>
  BinOp ->
  PrimType ->
  m (Lambda (Rep m))
binOpLambda bop t = binLambda (BinOp bop) t t

-- | As 'binOpLambda', but for t'CmpOp's.
cmpOpLambda ::
  (MonadBuilder m, Buildable (Rep m)) =>
  CmpOp ->
  m (Lambda (Rep m))
cmpOpLambda cop = binLambda (CmpOp cop) (cmpOpType cop) Bool

binLambda ::
  (MonadBuilder m, Buildable (Rep m)) =>
  (SubExp -> SubExp -> BasicOp) ->
  PrimType ->
  PrimType ->
  m (Lambda (Rep m))
binLambda bop arg_t ret_t = do
  x <- newVName "x"
  y <- newVName "y"
  body <-
    buildBody_ . fmap (pure . subExpRes) $
      letSubExp "binlam_res" $
        BasicOp $
          bop (Var x) (Var y)
  pure
    Lambda
      { lambdaParams =
          [ Param mempty x (Prim arg_t),
            Param mempty y (Prim arg_t)
          ],
        lambdaReturnType = [Prim ret_t],
        lambdaBody = body
      }

-- | Easily construct a t'Lambda' within a 'MonadBuilder'.  See also
-- 'runLambdaBuilder'.
mkLambda ::
  (MonadBuilder m) =>
  [LParam (Rep m)] ->
  m Result ->
  m (Lambda (Rep m))
mkLambda params m = do
  (body, ret) <- buildBody . localScope (scopeOfLParams params) $ do
    res <- m
    ret <- mapM subExpResType res
    pure (res, ret)
  pure $ Lambda params ret body

-- | Slice a full dimension of the given size.
sliceDim :: SubExp -> DimIndex SubExp
sliceDim d = DimSlice (constant (0 :: Int64)) d (constant (1 :: Int64))

-- | @fullSlice t slice@ returns @slice@, but with 'DimSlice's of
-- entire dimensions appended to the full dimensionality of @t@.  This
-- function is used to turn incomplete indexing complete, as required
-- by 'Index'.
fullSlice :: Type -> [DimIndex SubExp] -> Slice SubExp
fullSlice t slice =
  Slice $ slice ++ map sliceDim (drop (length slice) $ arrayDims t)

-- | @ sliceAt t n slice@ returns @slice@ but with 'DimSlice's of the
-- outer @n@ dimensions prepended, and as many appended as to make it
-- a full slice.  This is a generalisation of 'fullSlice'.
sliceAt :: Type -> Int -> [DimIndex SubExp] -> Slice SubExp
sliceAt t n slice =
  fullSlice t $ map sliceDim (take n $ arrayDims t) ++ slice

-- | Like 'fullSlice', but the dimensions are simply numeric.
fullSliceNum :: (Num d) => [d] -> [DimIndex d] -> Slice d
fullSliceNum dims slice =
  Slice $ slice ++ map (\d -> DimSlice 0 d 1) (drop (length slice) dims)

-- | Does the slice describe the full size of the array?  The most
-- obvious such slice is one that 'DimSlice's the full span of every
-- dimension, but also one that fixes all unit dimensions.
isFullSlice :: Shape -> Slice SubExp -> Bool
isFullSlice shape slice = and $ zipWith allOfIt (shapeDims shape) (unSlice slice)
  where
    allOfIt (Constant v) DimFix {} = oneIsh v
    allOfIt d (DimSlice _ n _) = d == n
    allOfIt _ _ = False

-- | Conveniently construct a body that contains no bindings.
resultBody :: (Buildable rep) => [SubExp] -> Body rep
resultBody = mkBody mempty . subExpsRes

-- | Conveniently construct a body that contains no bindings - but
-- this time, monadically!
resultBodyM :: (MonadBuilder m) => [SubExp] -> m (Body (Rep m))
resultBodyM = mkBodyM mempty . subExpsRes

-- | Evaluate the action, producing a body, then wrap it in all the
-- bindings it created using 'addStm'.
insertStmsM ::
  (MonadBuilder m) =>
  m (Body (Rep m)) ->
  m (Body (Rep m))
insertStmsM m = do
  (Body _ stms res, otherstms) <- collectStms m
  mkBodyM (otherstms <> stms) res

-- | Evaluate an action that produces a 'Result' and an auxiliary
-- value, then return the body constructed from the 'Result' and any
-- statements added during the action, along the auxiliary value.
buildBody ::
  (MonadBuilder m) =>
  m (Result, a) ->
  m (Body (Rep m), a)
buildBody m = do
  ((res, v), stms) <- collectStms m
  body <- mkBodyM stms res
  pure (body, v)

-- | As 'buildBody', but there is no auxiliary value.
buildBody_ ::
  (MonadBuilder m) =>
  m Result ->
  m (Body (Rep m))
buildBody_ m = fst <$> buildBody ((,()) <$> m)

-- | Change that result where evaluation of the body would stop.  Also
-- change type annotations at branches.
mapResult ::
  (Buildable rep) =>
  (Result -> Body rep) ->
  Body rep ->
  Body rep
mapResult f (Body _ stms res) =
  let Body _ stms2 newres = f res
   in mkBody (stms <> stms2) newres

-- | Instantiate all existential parts dimensions of the given
-- type, using a monadic action to create the necessary t'SubExp's.
-- You should call this function within some monad that allows you to
-- collect the actions performed (say, 'State').
instantiateShapes ::
  (Monad m) =>
  (Int -> m SubExp) ->
  [TypeBase ExtShape u] ->
  m [TypeBase Shape u]
instantiateShapes f ts = evalStateT (mapM instantiate ts) M.empty
  where
    instantiate t = do
      shape <- mapM instantiate' $ shapeDims $ arrayShape t
      pure $ t `setArrayShape` Shape shape
    instantiate' (Ext x) = do
      m <- get
      case M.lookup x m of
        Just se -> pure se
        Nothing -> do
          se <- lift $ f x
          put $ M.insert x se m
          pure se
    instantiate' (Free se) = pure se

-- | Like 'instantiateShapes', but obtains names from the provided
-- list.  If an 'Ext' is out of bounds of this list, the function
-- fails with 'error'.
instantiateShapes' :: [VName] -> [TypeBase ExtShape u] -> [TypeBase Shape u]
instantiateShapes' names ts =
  -- Carefully ensure that the order of idents we produce corresponds
  -- to their existential index.
  runIdentity $ instantiateShapes instantiate ts
  where
    instantiate x =
      case maybeNth x names of
        Nothing -> error $ "instantiateShapes': " ++ prettyString names ++ ", " ++ show x
        Just name -> pure $ Var name

-- | Remove existentials by imposing sizes from another type where
-- needed.
removeExistentials :: ExtType -> Type -> Type
removeExistentials t1 t2 =
  t1
    `setArrayDims` zipWith
      nonExistential
      (shapeDims $ arrayShape t1)
      (arrayDims t2)
  where
    nonExistential (Ext _) dim = dim
    nonExistential (Free dim) _ = dim

-- | Can be used as the definition of 'mkLetNames' for a 'Buildable'
-- instance for simple representations.
simpleMkLetNames ::
  ( ExpDec rep ~ (),
    LetDec rep ~ Type,
    MonadFreshNames m,
    TypedOp (OpC rep),
    HasScope rep m
  ) =>
  [VName] ->
  Exp rep ->
  m (Stm rep)
simpleMkLetNames names e = do
  et <- expExtType e
  let ts = instantiateShapes' names et
  pure $ Let (Pat $ zipWith PatElem names ts) (defAux ()) e

-- | Instances of this class can be converted to Futhark expressions
-- within a 'MonadBuilder'.
class ToExp a where
  toExp :: (MonadBuilder m) => a -> m (Exp (Rep m))

instance ToExp SubExp where
  toExp = pure . BasicOp . SubExp

instance ToExp VName where
  toExp = pure . BasicOp . SubExp . Var

-- | A convenient composition of 'letSubExp' and 'toExp'.
toSubExp :: (MonadBuilder m, ToExp a) => String -> a -> m SubExp
toSubExp s e = letSubExp s =<< toExp e
