axel-0.0.6: src/Axel/Macros.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Axel.Macros where
import Axel.AST
( MacroDefinition
, Statement(SDataDeclaration, SFunctionDefinition, SMacroDefinition,
SModuleDeclaration, SPragma, SQualifiedImport, SRawStatement,
SRestrictedImport, STopLevel, STypeSignature, STypeSynonym,
STypeclassInstance, SUnrestrictedImport)
, ToHaskell(toHaskell)
, functionDefinition
, name
)
import Axel.Denormalize (denormalizeStatement)
import qualified Axel.Eff.FileSystem as Effs (FileSystem)
import qualified Axel.Eff.FileSystem as FS
( createDirectoryIfMissing
, withCurrentDirectory
, withTemporaryDirectory
, writeFile
)
import Axel.Eff.Process (StreamSpecification(CreateStreams))
import qualified Axel.Eff.Process as Effs (Process)
import Axel.Eff.Resource (readResource)
import qualified Axel.Eff.Resource as Effs (Resource)
import qualified Axel.Eff.Resource as Res
( astDefinition
, macroDefinitionAndEnvironmentFooter
, macroDefinitionAndEnvironmentHeader
, macroScaffold
)
import Axel.Error (Error(MacroError))
import Axel.Haskell.Prettify (prettifyHaskell)
import Axel.Haskell.Stack (interpretFile)
import Axel.Normalize (normalizeStatement)
import qualified Axel.Parse as Parse
( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
Symbol)
, parseMultiple
, programToTopLevelExpressions
, topLevelExpressionsToProgram
)
import Axel.Utils.Display (Delimiter(Newlines), delimit, isOperator)
import Axel.Utils.Function (uncurry3)
import Axel.Utils.Recursion (Recursive(bottomUpTraverse), exhaustM)
import Axel.Utils.String (replace)
import Control.Lens.Cons (snoc)
import Control.Lens.Operators ((%~), (^.))
import Control.Lens.Tuple (_1, _2)
import Control.Monad (foldM)
import Control.Monad.Freer (Eff, Members)
import Control.Monad.Freer.Error (throwError)
import qualified Control.Monad.Freer.Error as Effs (Error)
import Data.Function ((&))
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as NE (head, map, toList)
import Data.Semigroup ((<>))
import qualified Data.Text as T (isSuffixOf, pack)
import System.Exit (ExitCode(ExitFailure))
import System.FilePath ((</>))
hygenisizeMacroName :: String -> String
hygenisizeMacroName oldName =
let suffix =
if isOperator oldName
then "%%%%%%%%%%"
else "_AXEL_AUTOGENERATED_MACRO_DEFINITION"
in if T.pack suffix `T.isSuffixOf` T.pack oldName
then oldName
else oldName <> suffix
hygenisizeMacroDefinition :: MacroDefinition -> MacroDefinition
hygenisizeMacroDefinition macroDef =
macroDef & functionDefinition . name %~ hygenisizeMacroName
generateMacroProgram ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Resource] effs)
=> NonEmpty MacroDefinition
-> [Statement]
-> [Parse.Expression]
-> Eff effs (String, String, String)
generateMacroProgram macroDefs env applicationArgs = do
astDef <- readResource Res.astDefinition
scaffold <- getScaffold
macroDefAndEnv <- (<>) <$> getMacroDefAndEnvHeader <*> getMacroDefAndEnvFooter
pure (astDef, scaffold, macroDefAndEnv)
where
insertDefName =
let defNamePlaceholder = "%%%MACRO_NAME%%%"
in replace defNamePlaceholder newMacroName
oldMacroName = NE.head macroDefs ^. functionDefinition . name
newMacroName = hygenisizeMacroName oldMacroName
getMacroDefAndEnvHeader =
insertDefName <$> readResource Res.macroDefinitionAndEnvironmentHeader
getMacroDefAndEnvFooter = do
let hygenicMacroDefs = NE.map hygenisizeMacroDefinition macroDefs
let source =
prettifyHaskell $ delimit Newlines $
map
toHaskell
(env <> NE.toList (NE.map SMacroDefinition hygenicMacroDefs))
footer <-
insertDefName <$> readResource Res.macroDefinitionAndEnvironmentFooter
pure $ unlines [source, footer]
getScaffold =
let insertApplicationArgs =
let applicationArgsPlaceholder = "%%%ARGUMENTS%%%"
in replace applicationArgsPlaceholder (show applicationArgs)
in prettifyHaskell . insertApplicationArgs . insertDefName <$>
readResource Res.macroScaffold
expansionPass ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Process, Effs.Resource] effs)
=> Parse.Expression
-> Eff effs Parse.Expression
expansionPass programExpr =
Parse.topLevelExpressionsToProgram . map denormalizeStatement <$>
expandMacros (Parse.programToTopLevelExpressions programExpr)
programToTopLevelExpressions :: Parse.Expression -> [Parse.Expression]
programToTopLevelExpressions (Parse.SExpression (Parse.Symbol "begin":stmts)) =
stmts
programToTopLevelExpressions _ =
error "programToTopLevelExpressions must be passed a top-level program!"
topLevelExpressionsToProgram :: [Parse.Expression] -> Parse.Expression
topLevelExpressionsToProgram stmts =
Parse.SExpression (Parse.Symbol "begin" : stmts)
exhaustivelyExpandMacros ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Process, Effs.Resource] effs)
=> Parse.Expression
-> Eff effs Parse.Expression
exhaustivelyExpandMacros = exhaustM expansionPass
isStatementNonconflicting :: Statement -> Bool
isStatementNonconflicting (SDataDeclaration _) = True
isStatementNonconflicting (SFunctionDefinition _) = True
isStatementNonconflicting (SPragma _) = True
isStatementNonconflicting (SMacroDefinition _) = True
isStatementNonconflicting (SModuleDeclaration _) = False
isStatementNonconflicting (SQualifiedImport _) = True
isStatementNonconflicting (SRawStatement _) = True
isStatementNonconflicting (SRestrictedImport _) = True
isStatementNonconflicting (STopLevel _) = False
isStatementNonconflicting (STypeclassInstance _) = True
isStatementNonconflicting (STypeSignature _) = True
isStatementNonconflicting (STypeSynonym _) = True
isStatementNonconflicting (SUnrestrictedImport _) = True
expandMacros ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Process, Effs.Resource] effs)
=> [Parse.Expression]
-> Eff effs [Statement]
expandMacros topLevelExprs = do
(stmts, macroDefs) <-
foldM
(\acc@(stmts, macroDefs) expr -> do
expandedExprs <- fullyExpandExpr stmts macroDefs expr
foldM
(\acc' expandedExpr -> do
stmt <- normalizeStatement expandedExpr
pure $ acc' &
case stmt of
SMacroDefinition macroDef -> _2 %~ flip snoc macroDef
_ -> _1 %~ flip snoc stmt)
acc
expandedExprs)
([], [])
topLevelExprs
pure $ stmts <> map (SMacroDefinition . hygenisizeMacroDefinition) macroDefs
where
fullyExpandExpr stmts allMacroDefs expr = do
let program = Parse.topLevelExpressionsToProgram [expr]
expandedExpr <-
exhaustM
(bottomUpTraverse
(\case
Parse.SExpression xs ->
Parse.SExpression <$>
foldM
(\acc x ->
case x of
Parse.SExpression (function:args) ->
case lookupMacroDefinitions function allMacroDefs of
Just macroDefs ->
(acc <>) <$>
expandMacroApplication
macroDefs
(filter isStatementNonconflicting stmts)
args
Nothing -> pure $ snoc acc x
_ -> pure $ snoc acc x)
[]
xs
x -> pure x))
program
pure $ Parse.programToTopLevelExpressions expandedExpr
expandMacroApplication ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Process, Effs.Resource] effs)
=> NonEmpty MacroDefinition
-> [Statement]
-> [Parse.Expression]
-> Eff effs [Parse.Expression]
expandMacroApplication macroDefs auxEnv args = do
macroProgram <- generateMacroProgram macroDefs auxEnv args
newSource <- uncurry3 evalMacro macroProgram
Parse.parseMultiple newSource
lookupMacroDefinitions ::
Parse.Expression -> [MacroDefinition] -> Maybe (NonEmpty MacroDefinition)
lookupMacroDefinitions identifierExpr =
nonEmpty . filter (`isMacroBeingCalled` identifierExpr)
isMacroBeingCalled :: MacroDefinition -> Parse.Expression -> Bool
isMacroBeingCalled macroDef identifierExpr =
case identifierExpr of
Parse.LiteralChar _ -> False
Parse.LiteralInt _ -> False
Parse.LiteralString _ -> False
Parse.SExpression _ -> False
Parse.Symbol identifier ->
macroDef ^. functionDefinition . name == identifier
isMacroDefinitionStatement :: Statement -> Bool
isMacroDefinitionStatement (SMacroDefinition _) = True
isMacroDefinitionStatement _ = False
evalMacro ::
(Members '[ Effs.Error Error, Effs.FileSystem, Effs.Process] effs)
=> String
-> String
-> String
-> Eff effs String
evalMacro astDefinition scaffold macroDefinitionAndEnvironment =
FS.withTemporaryDirectory $ \directoryName ->
FS.withCurrentDirectory directoryName $ do
let astDirectoryPath = "Axel" </> "Parse"
let macroDefinitionAndEnvironmentFileName =
"MacroDefinitionAndEnvironment.hs"
let scaffoldFileName = "Scaffold.hs"
FS.createDirectoryIfMissing True astDirectoryPath
FS.writeFile (astDirectoryPath </> "AST.hs") astDefinition
FS.writeFile
macroDefinitionAndEnvironmentFileName
macroDefinitionAndEnvironment
FS.writeFile scaffoldFileName scaffold
interpretFile @'CreateStreams scaffoldFileName "" >>= \case
(ExitFailure _, _, stderr) ->
throwError $
MacroError
("Temporary directory: " <> directoryName <> "\n\n" <> "Error:\n" <>
stderr)
(_, stdout, _) -> pure stdout