packages feed

axel-0.0.13: src/Axel/Macros.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE LiberalTypeSynonyms #-}

module Axel.Macros where

import Axel.Prelude

import Axel (isPrelude, preludeMacros)
import Axel.AST
  ( Expression(EFunctionApplication, EIdentifier, ERawExpression)
  , FunctionApplication(FunctionApplication)
  , FunctionDefinition(FunctionDefinition)
  , Identifier
  , ImportSpecification(ImportAll)
  , MacroDefinition
  , QualifiedImport(QualifiedImport)
  , RestrictedImport(RestrictedImport)
  , SMStatement
  , Statement(SFunctionDefinition, SMacroDefinition, SMacroImport,
          SMacroImport, SModuleDeclaration, SQualifiedImport,
          SQualifiedImport, SRawStatement, SRestrictedImport, STypeSignature)
  , ToHaskell(toHaskell)
  , TypeSignature(TypeSignature)
  , _SMacroDefinition
  , _SModuleDeclaration
  , functionDefinition
  , imports
  , moduleName
  , name
  , statementsToProgram
  )
import Axel.Denormalize (denormalizeStatement)
import Axel.Eff ((:>>))
import qualified Axel.Eff as Effs
import Axel.Eff.Error (Error(MacroError, ParseError), fatal)
import qualified Axel.Eff.FileSystem as Effs (FileSystem)
import qualified Axel.Eff.FileSystem as FS
import qualified Axel.Eff.Ghci as Effs (Ghci)
import qualified Axel.Eff.Ghci as Ghci
import qualified Axel.Eff.Log as Effs (Log)
import Axel.Eff.Log (logStrLn)
import qualified Axel.Eff.Process as Effs (Process)
import qualified Axel.Eff.Restartable as Effs (Restartable)
import Axel.Eff.Restartable (restart, runRestartable)
import Axel.Haskell.Error (processStackOutputLine)
import Axel.Haskell.Macros (hygenisizeMacroName)
import Axel.Normalize
  ( normalizeExpression
  , normalizeStatement
  , unsafeNormalize
  , unsafeNormalize
  , withExprCtxt
  )
import Axel.Parse (parseMultiple)
import Axel.Parse.AST (_SExpression, bottomUpFmapSplicing, toAxel)
import qualified Axel.Parse.AST as Parse
import Axel.Sourcemap
  ( ModuleInfo
  , SourceMetadata
  , isCompoundExpressionWrapperHead
  , quoteSMExpression
  , renderSourcePosition
  , unwrapCompoundExpressions
  , wrapCompoundExpressions
  )
import qualified Axel.Sourcemap as SM
import Axel.Utils.FilePath ((<.>), (</>), replaceExtension, takeFileName)
import Axel.Utils.List (filterMap, filterMapOut, head')
import Axel.Utils.Recursion (bottomUpFmap, zipperTopDownTraverse)
import Axel.Utils.Zipper (unsafeLeft, unsafeUp)

import Control.Lens (_1, isn't, op, snoc)
import Control.Lens.Extras (is)
import Control.Lens.Operators ((%~), (^.), (^?))
import Control.Monad (guard, unless, when)
import Control.Monad.Extra (whenJust)

import Data.Function (on)
import Data.Generics.Uniplate.Zipper (Zipper, fromZipper, hole, replaceHole, up)
import Data.Hashable (hash)
import Data.List (intersperse, nub)
import Data.List.Extra (mconcatMap)
import Data.List.Split (split, whenElt)
import qualified Data.Map as M
import Data.Maybe (isNothing)
import qualified Data.Text as T

import qualified Language.Haskell.Ghcid as Ghcid

import Effectful ((:>))
import qualified Effectful as Eff
import qualified Effectful.Error.Static as Eff
import qualified Effectful.Reader.Static as Eff
import qualified Effectful.State.Static.Local as Eff

type FunctionApplicationExpanderArgs a = SM.Expression -> a

type FunctionApplicationExpander effs
   = Effs.Callback effs FunctionApplicationExpanderArgs (Maybe [SM.Expression])

type FileExpanderArgs a = FilePath -> a

type FileExpander effs = Effs.Callback effs FileExpanderArgs ()

-- | Fully expand a program, and add macro definition type signatures.
processProgram ::
     forall fileExpanderEffs funAppExpanderEffs effs innerEffs.
     ( innerEffs ~ (Eff.State [SMStatement] ': Effs.Restartable SM.Expression ': Eff.Reader FilePath ': effs)
     , '[ Eff.Error Error, Effs.Ghci, Eff.Reader Ghcid.Ghci, Eff.State ModuleInfo] :>> effs
     , fileExpanderEffs :>> innerEffs
     , funAppExpanderEffs :>> innerEffs
     )
  => FunctionApplicationExpander funAppExpanderEffs
  -> FileExpander fileExpanderEffs
  -> FilePath
  -> SM.Expression
  -> Eff.Eff effs [SMStatement]
processProgram expandFunApp expandFile filePath program = do
  newProgramExpr <-
    Eff.runReader filePath $
    expandProgramExpr
      @funAppExpanderEffs
      @fileExpanderEffs
      expandFunApp
      expandFile
      program
  newStmts <-
    mapM
      (Eff.runReader filePath . withExprCtxt . normalizeStatement)
      (unwrapCompoundExpressions newProgramExpr)
  withAstImports <-
    insertImports filePath (autogeneratedImports filePath) newStmts
  pure $ finalizeProgram withAstImports

finalizeProgram :: [SMStatement] -> [SMStatement]
finalizeProgram stmts =
  let expandQuotes =
        bottomUpFmapSplicing
          (\case
             Parse.SExpression _ (Parse.Symbol _ "quote":xs) ->
               map quoteSMExpression xs
             x -> [x])
      convertList =
        bottomUpFmap $ \case
          Parse.Symbol ann' "List" -> Parse.Symbol ann' "[]"
          x -> x
      convertUnit =
        bottomUpFmap $ \case
          Parse.Symbol ann' "Unit" -> Parse.Symbol ann' "()"
          Parse.Symbol ann' "unit" -> Parse.Symbol ann' "()"
          x -> x
      (nonMacroDefs, macroDefs) = filterMapOut (^? _SMacroDefinition) stmts
      hygenicMacroDefs = map hygenisizeMacroDefinition macroDefs
      macroTySigs = typeMacroDefinitions hygenicMacroDefs
      toTopLevelStmts =
        map (unsafeNormalize normalizeStatement) . unwrapCompoundExpressions
      toProgramExpr = wrapCompoundExpressions . map denormalizeStatement
   in toTopLevelStmts $ convertUnit $ convertList $ expandQuotes $ toProgramExpr $
      nonMacroDefs <>
      map SMacroDefinition hygenicMacroDefs <>
      macroTySigs

isMacroImported ::
     (Eff.State [SMStatement] :> effs) => Identifier -> Eff.Eff effs Bool
isMacroImported macroName = do
  let isFromPrelude = macroName `elem` preludeMacros
  isImportedDirectly <-
    any
      (\case
         SMacroImport macroImport -> macroName `elem` macroImport ^. imports
         _notSMacroImport -> False) <$>
    Eff.get
  pure $ isFromPrelude || isImportedDirectly

ensureCompiledDependency ::
     forall fileExpanderEffs effs.
     (Eff.State ModuleInfo :> effs, fileExpanderEffs :>> effs)
  => FileExpander fileExpanderEffs
  -> Identifier
  -> Eff.Eff effs ()
ensureCompiledDependency expandFile dependencyName = do
  moduleInfo <-
    Eff.gets (M.filter (\(moduleId', _) -> moduleId' == dependencyName))
  case head' $ M.toList moduleInfo of
    Just (dependencyFilePath, (_, transpiledOutput)) ->
      when (isNothing transpiledOutput) $ expandFile dependencyFilePath
    Nothing -> pure ()

isStatementFocused :: Zipper SM.Expression SM.Expression -> Bool
isStatementFocused zipper =
  let wholeProgramExpr = fromZipper zipper
      isCompoundExpr = Just wholeProgramExpr == (hole <$> up zipper)
      isCompoundExprWrapper =
        case hole zipper of
          Parse.Symbol _ "begin" -> True
          _notSymbolBegin -> False
   in isCompoundExpr && not isCompoundExprWrapper

-- | Fully expand a top-level expression.
--   Macro expansion is top-down: it proceeds top to bottom, outwards to inwards,
--   and left to right. Whenever a macro is successfully expanded to yield new
--   expressions in place of the macro call in question, the substitution is made
--   and macro expansion is repeated from the beginning. As new definitions, etc.
--   are found at the top level while the program tree is being traversed, they
--   will be added to the environment accessible to macros during expansion.
expandProgramExpr ::
     forall funAppExpanderEffs fileExpanderEffs effs innerEffs.
     ( innerEffs ~ (Eff.State [SMStatement] ': Effs.Restartable SM.Expression ': effs)
     , '[ Eff.Error Error, Eff.State ModuleInfo, Eff.Reader Ghcid.Ghci, Eff.Reader FilePath] :>> effs
     , funAppExpanderEffs :>> innerEffs
     , fileExpanderEffs :>> innerEffs
     )
  => FunctionApplicationExpander funAppExpanderEffs
  -> FileExpander fileExpanderEffs
  -> SM.Expression
  -> Eff.Eff effs SM.Expression
expandProgramExpr expandFunApp expandFile programExpr =
  runRestartable @SM.Expression programExpr $
  Eff.evalState ([] :: [SMStatement]) .
  zipperTopDownTraverse
    (\zipper -> do
       when (isStatementFocused zipper) $
         -- NOTE This algorithm will exclude the last statement, but we won't
         --      have any macros that rely on it (since macros can only access
         --      what is before them in the file). Thus, this omission is okay.
         let prevTopLevelExpr = hole $ unsafeLeft zipper
          in unless (isCompoundExpressionWrapperHead prevTopLevelExpr) $
             addStatementToMacroEnvironment
               @fileExpanderEffs
               expandFile
               prevTopLevelExpr
       let expr = hole zipper
       when (is _SExpression expr) $ do
         maybeNewExprs <- expandFunApp expr
         case maybeNewExprs of
           Just newExprs -> replaceExpr zipper newExprs >>= restart
           Nothing -> pure ()
       pure expr)

-- | Returns the full program expr (after the necessary substitution has been applied).
replaceExpr ::
     ('[ Eff.Error Error, Eff.Reader FilePath] :>> effs)
  => Zipper SM.Expression SM.Expression
  -> [SM.Expression]
  -> Eff.Eff effs SM.Expression
replaceExpr zipper newExprs =
  let programExpr = fromZipper zipper
      oldExpr = hole zipper
      -- NOTE Using `unsafeUp` is safe since `begin` cannot be the name of a macro,
      --      and thus `zipper` will never be focused on the whole program.
      oldParentExprZ = unsafeUp zipper
      newParentExpr =
        case hole oldParentExprZ of
          Parse.SExpression ann' xs ->
            let xs' = do
                  x <- xs
                  -- TODO What if there are multiple, equivalent copies of `oldExpr`?
                  --      If they are not top-level statements, then the macro in question
                  --      must already exist and thus we would have expanded it already.
                  --      If they are top-level statements, but e.g. were auto-generated, then
                  --      `==` will call them equal. If the macro in question did not exist
                  --      when the first statement was defined, but it does by the second
                  --      statement, then our result may be incorrect.
                  if x == oldExpr
                    then newExprs
                    else pure x
             in Parse.SExpression ann' xs'
          _notSExpression -> fatal "expandProgramExpr" "0001"
      newProgramExpr = fromZipper $ replaceHole newParentExpr oldParentExprZ
   in if newProgramExpr == programExpr
        then throwLoopError oldExpr newExprs
        else pure newProgramExpr

throwLoopError ::
     ('[ Eff.Error Error, Eff.Reader FilePath] :>> effs)
  => SM.Expression
  -> [SM.Expression]
  -> Eff.Eff effs a
throwLoopError oldExpr newExprs = do
  filePath <- Eff.ask
  Eff.throwError $
    MacroError
      filePath
      oldExpr
      ("Infinite loop detected during macro expansion!\nCheck that no macro calls expand (directly or indirectly) to themselves.\n" <>
       toAxel oldExpr <>
       " expanded into " <>
       T.unwords (map toAxel newExprs) <>
       ".")

addStatementToMacroEnvironment ::
     forall fileExpanderEffs effs.
     ( '[ Eff.Error Error, Eff.State ModuleInfo, Eff.Reader FilePath, Eff.State [SMStatement]] :>> effs
     , fileExpanderEffs :>> effs
     )
  => FileExpander fileExpanderEffs
  -> SM.Expression
  -> Eff.Eff effs ()
addStatementToMacroEnvironment expandFile newExpr = do
  filePath <- Eff.ask
  stmt <- Eff.runReader filePath $ withExprCtxt $ normalizeStatement newExpr
  let maybeDependencyName =
        case stmt of
          SRestrictedImport restrictedImport ->
            Just $ restrictedImport ^. moduleName
          SQualifiedImport qualifiedImport ->
            Just $ qualifiedImport ^. moduleName
          SMacroImport macroImport -> Just $ macroImport ^. moduleName
          _ -> Nothing
  whenJust maybeDependencyName $
    ensureCompiledDependency @fileExpanderEffs expandFile
  Eff.modify @[SMStatement] (`snoc` stmt)

-- | If a function application is a macro call, expand it.
handleFunctionApplication ::
     ('[ Eff.Error Error, Effs.FileSystem, Effs.Ghci, Effs.Log, Effs.Process, Eff.State ModuleInfo, Eff.Reader Ghcid.Ghci, Eff.Reader FilePath, Eff.State [SMStatement]] :>> effs)
  => SM.Expression
  -> Eff.Eff effs (Maybe [SM.Expression])
handleFunctionApplication fnApp@(Parse.SExpression ann (Parse.Symbol _ functionName:args)) = do
  shouldExpand <- isMacroCall $ T.pack functionName
  if shouldExpand
    then Just <$>
         withExpansionId
           fnApp
           (expandMacroApplication ann (T.pack functionName) args)
    else pure Nothing
handleFunctionApplication _ = pure Nothing

isMacroCall ::
     (Eff.State [SMStatement] :> effs) => Identifier -> Eff.Eff effs Bool
isMacroCall function = do
  localDefs <- lookupMacroDefinitions function
  let isDefinedLocally = not $ null localDefs
  isImported <- isMacroImported function
  pure $ isImported || isDefinedLocally

lookupMacroDefinitions ::
     (Eff.State [SMStatement] :> effs)
  => Identifier
  -> Eff.Eff effs [MacroDefinition (Maybe SM.Expression)]
lookupMacroDefinitions identifier =
  filterMap
    (\stmt -> do
       macroDef <- stmt ^? _SMacroDefinition
       guard $ identifier == (macroDef ^. functionDefinition . name)
       pure macroDef) <$>
  Eff.get

hygenisizeMacroDefinition :: MacroDefinition ann -> MacroDefinition ann
hygenisizeMacroDefinition = functionDefinition . name %~ hygenisizeMacroName

insertImports ::
     (Eff.Error Error :> effs)
  => FilePath
  -> [SMStatement]
  -> [SMStatement]
  -> Eff.Eff effs [SMStatement]
insertImports filePath newStmts program =
  case split (whenElt $ is _SModuleDeclaration) program of
    [preEnv, [moduleDecl@(SModuleDeclaration _ _)], postEnv] ->
      pure $ preEnv <> [moduleDecl] <> newStmts <> postEnv
    [preEnv, [moduleDecl@(SModuleDeclaration _ _)]] ->
      pure $ preEnv <> [moduleDecl] <> newStmts
    [[moduleDecl@(SModuleDeclaration _ _)], postEnv] ->
      pure $ [moduleDecl] <> newStmts <> postEnv
    [[moduleDecl@(SModuleDeclaration _ _)]] -> pure $ [moduleDecl] <> newStmts
    _ ->
      Eff.throwError $
      ParseError
        filePath
        "Axel files must contain exactly one module declaration!"

mkMacroTypeSignature :: Identifier -> SMStatement
mkMacroTypeSignature macroName =
  SRawStatement Nothing $
  SM.raw (toHaskell (EIdentifier (Nothing :: Maybe SM.Expression) macroName)) <>
  " :: [AxelRuntime_AST.Expression AxelRuntime_Sourcemap.SourceMetadata] -> AxelRuntime_GHCPrelude.IO [AxelRuntime_AST.Expression AxelRuntime_Sourcemap.SourceMetadata]"

newtype ExpansionId =
  ExpansionId Text

mkMacroDefAndEnvModuleName :: ExpansionId -> Identifier
mkMacroDefAndEnvModuleName (ExpansionId expansionId) =
  "AutogeneratedAxelMacroDefinitionAndEnvironment" <> expansionId

mkScaffoldModuleName :: ExpansionId -> Identifier
mkScaffoldModuleName (ExpansionId expansionId) =
  "AutogeneratedAxelScaffold" <> expansionId

autogeneratedImports :: FilePath -> [SMStatement]
autogeneratedImports filePath
  -- We can't import the Axel prelude if we're actually compiling it.
 =
  [ SRestrictedImport $ RestrictedImport Nothing "Axel" (ImportAll Nothing)
  | not $ isPrelude filePath
  ] <>
  [ SQualifiedImport $
    QualifiedImport
      Nothing
      "Prelude"
      "AxelRuntime_GHCPrelude"
      (ImportAll Nothing) -- (in case `-XNoImplicitPrelude` is enabled)
  , SQualifiedImport $
    QualifiedImport
      Nothing
      "Axel.Parse.AST"
      "AxelRuntime_AST"
      (ImportAll Nothing)
  , SQualifiedImport $
    QualifiedImport
      Nothing
      "Axel.Sourcemap"
      "AxelRuntime_Sourcemap"
      (ImportAll Nothing)
  ]

generateMacroProgram ::
     ('[ Eff.Error Error, Effs.FileSystem, Eff.Reader ExpansionId, Eff.State [SMStatement]] :>> effs)
  => FilePath
  -> Identifier
  -> [SM.Expression]
  -> Eff.Eff effs (SM.Output, SM.Output)
generateMacroProgram filePath' oldMacroName args = do
  macroDefAndEnvModuleName <- Eff.asks mkMacroDefAndEnvModuleName
  scaffoldModuleName <- Eff.asks mkScaffoldModuleName
  let newMacroName = hygenisizeMacroName oldMacroName
  let mainFnName = "main_AXEL_AUTOGENERATED_FUNCTION_DEFINITION"
  let footer =
        [ mkMacroTypeSignature mainFnName
        , SRawStatement Nothing $ mainFnName <> " = " <>
          SM.raw
            (toHaskell
               (EIdentifier (Nothing :: Maybe SM.Expression) newMacroName))
        ]
  let (header, scaffold) =
        let mkModuleDecl = SModuleDeclaration Nothing
            mkQualImport moduleName' alias =
              SQualifiedImport $
              QualifiedImport Nothing moduleName' alias (ImportAll Nothing)
            mkId = EIdentifier Nothing
            mkQualId moduleName' identifier =
              EIdentifier Nothing $ moduleName' <> "." <> identifier
            mkTySig fnName ty =
              STypeSignature $ TypeSignature Nothing fnName [] ty
            mkFnDef fnName args' body =
              SFunctionDefinition $
              FunctionDefinition Nothing fnName args' body []
            mkFnApp fn args' =
              EFunctionApplication $ FunctionApplication Nothing fn args'
            mkRawExpr = ERawExpression Nothing
            mkList xs =
              mkFnApp (mkId "[") $ intersperse (mkRawExpr ",") xs <> [mkId "]"] -- This is VERY hacky, but it'll work without too much effort for now.
         in ( autogeneratedImports filePath' <>
              [mkQualImport "Axel.Sourcemap" "AxelRuntime_Sourcemap"]
            , mkModuleDecl scaffoldModuleName : autogeneratedImports filePath' <>
              [ mkQualImport macroDefAndEnvModuleName macroDefAndEnvModuleName
              , mkQualImport "Axel.Sourcemap" "AxelRuntime_Sourcemap"
              , mkTySig "main" $
                mkFnApp (mkId "AxelRuntime_GHCPrelude.IO") [mkId "()"]
              , mkFnDef "main" [] $
                mkFnApp
                  (mkId "(AxelRuntime_GHCPrelude.>>=)")
                  [ mkFnApp
                      (mkQualId
                         macroDefAndEnvModuleName
                         "main_AXEL_AUTOGENERATED_FUNCTION_DEFINITION")
                      [ mkList
                          (map
                             (unsafeNormalize normalizeExpression .
                              quoteSMExpression)
                             args)
                      ]
                  , mkFnApp
                      (mkId "(AxelRuntime_GHCPrelude..)")
                      [ mkId "AxelRuntime_GHCPrelude.putStrLn"
                      , mkFnApp
                          (mkId "(AxelRuntime_GHCPrelude..)")
                          [ mkId "AxelRuntime_GHCPrelude.unlines"
                          , mkFnApp
                              (mkId "AxelRuntime_GHCPrelude.map")
                              [mkId "AxelRuntime_AST.toAxel'"]
                          ]
                      ]
                  ]
              ])
  prevStmts <- Eff.get @[SMStatement]
  -- Mitigate the `::`/`=`-ordering problem (see the description of issue #65).
  let auxEnv =
        reverse . dropWhile (isn't _SMacroDefinition) . reverse $ prevStmts
  -- TODO If the file being transpiled has pragmas but no explicit module declaration,
  --      they will be erroneously included *after* the module declaration.
  --      Should we just require Axel files to have module declarations, or is there a
  --      less intrusive alternate solution?
  macroDefAndEnv <-
    do let moduleDecl = SModuleDeclaration Nothing macroDefAndEnvModuleName
       programStmts <-
         insertImports filePath' header $ replaceModuleDecl moduleDecl $ auxEnv <>
         footer
       pure $ finalizeProgram programStmts
  pure $ ((,) `on` toHaskell . statementsToProgram) scaffold macroDefAndEnv
  where
    replaceModuleDecl newModuleDecl stmts =
      if any (is _SModuleDeclaration) stmts
        then map
               (\case
                  SModuleDeclaration _ _ -> newModuleDecl
                  x -> x)
               stmts
        else newModuleDecl : stmts

typeMacroDefinitions :: [MacroDefinition ann] -> [SMStatement]
typeMacroDefinitions = map mkMacroTypeSignature . getMacroNames
  where
    getMacroNames = nub . map (^. functionDefinition . name)

-- | Source metadata is lost.
--   Use only for logging and such where that doesn't matter.
losslyReconstructMacroCall :: Identifier -> [SM.Expression] -> SM.Expression
losslyReconstructMacroCall macroName args =
  Parse.SExpression
    Nothing
    (Parse.Symbol Nothing (T.unpack macroName) : map (Nothing <$) args)

withExpansionId ::
     SM.Expression
  -> Eff.Eff (Eff.Reader ExpansionId ': effs) a
  -> Eff.Eff effs a
withExpansionId originalCall x =
  let expansionId = showText $ abs $ hash originalCall -- We take the absolute value so that folder names don't start with dashes
                                                       -- (it looks weird, even though it's not technically wrong).
                                                       -- In theory, this allows for collisions, but the chances are negligibly small(?).
   in Eff.runReader (ExpansionId expansionId) x

expandMacroApplication ::
     ('[ Eff.Error Error, Effs.FileSystem, Effs.Ghci, Effs.Log, Effs.Process, Eff.Reader ExpansionId, Eff.Reader Ghcid.Ghci, Eff.Reader FilePath, Eff.State [SMStatement]] :>> effs)
  => SourceMetadata
  -> Identifier
  -> [SM.Expression]
  -> Eff.Eff effs [SM.Expression]
expandMacroApplication originalAnn macroName args = do
  logStrLn $ "Expanding: " <> toAxel (losslyReconstructMacroCall macroName args)
  filePath' <- Eff.ask @FilePath
  macroProgram <- generateMacroProgram filePath' macroName args
  (tempFilePath, newSource) <-
    uncurry (evalMacro originalAnn macroName args) macroProgram
  logStrLn $ "Result: " <> newSource <> "\n\n"
  parseMultiple (Just tempFilePath) newSource

isMacroDefinitionStatement :: Statement ann -> Bool
isMacroDefinitionStatement (SMacroDefinition _) = True
isMacroDefinitionStatement _ = False

evalMacro ::
     forall effs.
     ('[ Eff.Error Error, Effs.FileSystem, Effs.Ghci, Effs.Process, Eff.Reader Ghcid.Ghci, Eff.Reader FilePath, Eff.Reader ExpansionId] :>> effs)
  => SourceMetadata
  -> Identifier
  -> [SM.Expression]
  -> SM.Output
  -> SM.Output
  -> Eff.Eff effs (FilePath, Text)
evalMacro originalCallAnn macroName args scaffoldProgram macroDefAndEnvProgram = do
  macroDefAndEnvModuleName <- Eff.asks mkMacroDefAndEnvModuleName
  scaffoldModuleName <- Eff.asks mkScaffoldModuleName
  tempDir <- getTempDirectory
  let macroDefAndEnvFileName =
        tempDir </> FilePath macroDefAndEnvModuleName <.> "hs"
  let scaffoldFileName = tempDir </> FilePath scaffoldModuleName <.> "hs"
  let resultFile = tempDir </> FilePath "result.axel"
  FS.writeFile scaffoldFileName scaffold
  FS.writeFile macroDefAndEnvFileName macroDefAndEnv
  let moduleInfo =
        M.fromList $
        map
          (_1 %~ flip replaceExtension "axel")
          [ (scaffoldFileName, (scaffoldModuleName, Just scaffoldProgram))
          , ( macroDefAndEnvFileName
            , (macroDefAndEnvModuleName, Just macroDefAndEnvProgram))
          ]
  ghci <- Eff.ask @Ghcid.Ghci
  loadResult <- Ghci.addFiles ghci [scaffoldFileName, macroDefAndEnvFileName]
  if any ("Ok, " `T.isPrefixOf`) loadResult
    then do
      result <- T.unlines <$> Ghci.exec ghci (scaffoldModuleName <> ".main")
      FS.writeFile resultFile $
        generateExpansionRecord
          originalCallAnn
          macroName
          args
          result
          scaffoldFileName
          macroDefAndEnvFileName
      if any ("*** Exception:" `T.isPrefixOf`) (T.lines result)
        then throwMacroError result
        else pure (resultFile, result)
    else throwMacroError $ mconcat $
         mconcatMap (processStackOutputLine moduleInfo) loadResult
  where
    macroDefAndEnv = SM.raw macroDefAndEnvProgram
    scaffold = SM.raw scaffoldProgram
    getTempDirectory = do
      ExpansionId expansionId <- Eff.ask @ExpansionId
      let dirName = FilePath "axelTemp" </> FilePath expansionId
      FS.createDirectoryIfMissing True dirName
      pure dirName
    throwMacroError msg = do
      originalFilePath <- Eff.ask @FilePath
      Eff.throwError $
        MacroError
          originalFilePath
          (losslyReconstructMacroCall macroName args)
          msg

generateExpansionRecord ::
     SourceMetadata
  -> Identifier
  -> [SM.Expression]
  -> Text
  -> FilePath
  -> FilePath
  -> Text
generateExpansionRecord originalAnn macroName args result scaffoldFilePath macroDefAndEnvFilePath =
  T.unlines
    [ result
    , "-- This file is an autogenerated record of a macro call and expansion."
    , "-- It is (likely) not a valid Axel program, so you probably don't want to run it directly."
    , ""
    , "-- The beginning of this file contains the result of the macro invocation at " <>
      locationHint <>
      ":"
    , toAxel (losslyReconstructMacroCall macroName args)
    , ""
    , "-- The macro call itself is transpiled in " <>
      op FilePath (takeFileName scaffoldFilePath) <>
      "."
    , ""
    , "-- To see the (transpiled) modules, definitions, extensions, etc. visible during the expansion, check " <>
      op FilePath (takeFileName macroDefAndEnvFilePath) <>
      "."
    ]
  where
    locationHint = maybe "<unknown>" renderSourcePosition originalAnn