axel-0.0.5: src/Axel/Macros.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
module Axel.Macros where
import Axel.AST
( Identifier
, MacroDefinition
, Statement(SDataDeclaration, SFunctionDefinition, SMacroDefinition,
SModuleDeclaration, SPragma, SQualifiedImport, SRestrictedImport,
STopLevel, STypeSignature, STypeSynonym, STypeclassInstance,
SUnrestrictedImport)
, ToHaskell(toHaskell)
, functionDefinition
, name
, statements
)
import Axel.Denormalize (denormalizeStatement)
import Axel.Error (Error(MacroError))
import Axel.Haskell.GHC (ghcInterpret)
import Axel.Haskell.Prettify (prettifyHaskell)
import Axel.Monad.FileSystem (MonadFileSystem)
import qualified Axel.Monad.FileSystem as FS
( MonadFileSystem(createDirectoryIfMissing, writeFile)
, withCurrentDirectory
, withTemporaryDirectory
)
import Axel.Monad.Process (MonadProcess)
import Axel.Monad.Resource (MonadResource, readResource)
import qualified Axel.Monad.Resource as Res
( astDefinition
, macroDefinitionAndEnvironmentFooter
, macroDefinitionAndEnvironmentHeader
, macroScaffold
)
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(bottomUpFmap, 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.Except (MonadError, catchError, runExceptT, throwError)
import Data.Function ((&))
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as NE (head, toList)
import Data.Semigroup ((<>))
import System.FilePath ((</>))
generateMacroProgram ::
(MonadError Error m, MonadFileSystem m, MonadResource m)
=> NonEmpty MacroDefinition
-> [Statement]
-> [Parse.Expression]
-> m (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 =
oldMacroName <>
if isOperator oldMacroName
then "%%%%%%%%%%"
else "_AXEL_AUTOGENERATED_MACRO_DEFINITION"
getMacroDefAndEnvHeader =
insertDefName <$> readResource Res.macroDefinitionAndEnvironmentHeader
getMacroDefAndEnvFooter = do
hygenicMacroDefs <-
traverse
(replaceName oldMacroName newMacroName . SMacroDefinition)
macroDefs
let source =
prettifyHaskell $ delimit Newlines $
map toHaskell (env <> NE.toList hygenicMacroDefs)
footer <-
insertDefName <$> readResource Res.macroDefinitionAndEnvironmentFooter
pure (unlines [source, footer])
getScaffold :: (Monad m, MonadFileSystem m, MonadResource m) => m String
getScaffold =
let insertApplicationArgs =
let applicationArgsPlaceholder = "%%%ARGUMENTS%%%"
in replace applicationArgsPlaceholder (show applicationArgs)
in prettifyHaskell . insertApplicationArgs . insertDefName <$>
readResource Res.macroScaffold
expansionPass ::
(MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
=> Parse.Expression
-> m 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 ::
(MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
=> Parse.Expression
-> m 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 (SRestrictedImport _) = True
isStatementNonconflicting (STopLevel _) = False
isStatementNonconflicting (STypeclassInstance _) = True
isStatementNonconflicting (STypeSignature _) = True
isStatementNonconflicting (STypeSynonym _) = True
isStatementNonconflicting (SUnrestrictedImport _) = True
expandMacros ::
(MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
=> [Parse.Expression]
-> m [Statement]
expandMacros topLevelExprs =
fst <$>
foldM
(\acc@(stmts, macroDefs) expr -> do
expandedExprs <- fullyExpandExpr stmts macroDefs expr
foldM
(\acc' expandedExpr -> do
stmt <- normalizeStatement expandedExpr
pure $ acc' &
case stmt of
SMacroDefinition macroDefinition ->
_2 %~ flip snoc macroDefinition
_ -> _1 %~ flip snoc stmt)
acc
expandedExprs)
([], [])
topLevelExprs
where
fullyExpandExpr ::
( MonadError Error m
, MonadFileSystem m
, MonadProcess m
, MonadResource m
)
=> [Statement]
-> [MacroDefinition]
-> Parse.Expression
-> m [Parse.Expression]
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.LiteralChar _ -> pure $ acc ++ [x]
Parse.LiteralInt _ -> pure $ acc ++ [x]
Parse.LiteralString _ -> pure $ acc ++ [x]
Parse.SExpression [] -> pure $ acc ++ [x]
Parse.SExpression (function:args) ->
case lookupMacroDefinitions function allMacroDefs of
Just macroDefs ->
(acc ++) <$>
expandMacroApplication
macroDefs
(filter isStatementNonconflicting stmts)
args
Nothing -> pure $ acc ++ [x]
Parse.Symbol _ -> pure $ acc ++ [x])
[]
xs
x -> pure x))
program
pure $ Parse.programToTopLevelExpressions expandedExpr
expandMacroApplication ::
(MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
=> NonEmpty MacroDefinition
-> [Statement]
-> [Parse.Expression]
-> m [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
stripMacroDefinitions :: Statement -> Statement
stripMacroDefinitions =
\case
STopLevel topLevel ->
STopLevel $
(statements %~ filter (not . isMacroDefinitionStatement)) topLevel
x -> x
isMacroDefinitionStatement :: Statement -> Bool
isMacroDefinitionStatement (SMacroDefinition _) = True
isMacroDefinitionStatement _ = False
replaceName ::
(MonadError Error m)
=> Identifier
-> Identifier
-> Statement
-> m Statement
replaceName oldName newName =
normalize . bottomUpFmap replaceSymbol . denormalizeStatement
where
normalize expr =
normalizeStatement expr `catchError` \_ ->
throwError (MacroError $ "Invalid macro name: `" <> oldName <> "`!")
replaceSymbol expr =
case expr of
Parse.Symbol identifier ->
Parse.Symbol $
if identifier == oldName
then newName
else identifier
_ -> expr
evalMacro ::
(MonadError Error m, MonadFileSystem m, MonadProcess m)
=> String
-> String
-> String
-> m 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
runExceptT (ghcInterpret scaffoldFileName) >>= \case
Left err ->
throwError $
MacroError
("Temporary directory: " <> directoryName <> "\n\n" <> "Error:\n" <>
err)
Right res -> pure res