hnix-0.13.0: src/Nix/Reduce.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
-- | This module provides a "reducing" expression evaluator, which reduces
-- away pure, non self-referential aspects of an expression tree, yielding a
-- new expression tree. It does not yet attempt to reduce everything
-- possible, and will always yield a tree with the same meaning as the
-- original. It should be seen as an opportunistic simplifier, but which
-- gives up easily if faced with any potential for ambiguity in the result.
module Nix.Reduce
( reduceExpr
, reducingEvalExpr
) where
import Control.Monad.Catch ( MonadCatch(catch) )
#if !MIN_VERSION_base(4,13,0)
import Prelude hiding ( fail )
import Control.Monad.Fail
#endif
import Control.Monad.Fix ( MonadFix )
import Data.Fix ( Fix(..)
, foldFix
, foldFixM
)
import qualified Data.HashMap.Lazy as M
import qualified Data.HashMap.Strict as MS
import qualified Data.List.NonEmpty as NE
import qualified Text.Show
import Nix.Atoms
import Nix.Effects.Basic ( pathToDefaultNixFile )
import Nix.Expr
import Nix.Frames
import Nix.Options ( Options
, reduceSets
, reduceLists
)
import Nix.Parser
import Nix.Scope
import Nix.Utils
import System.Directory
import System.FilePath
newtype Reducer m a = Reducer
{ runReducer ::
ReaderT
( Maybe FilePath
, Scopes (Reducer m) NExprLoc
)
( StateT
( HashMap FilePath NExprLoc
, MS.HashMap Text Text
)
m
)
a
}
deriving
( Functor, Applicative, Alternative
, Monad, MonadPlus, MonadFix, MonadIO, MonadFail
, MonadReader (Maybe FilePath, Scopes (Reducer m) NExprLoc)
, MonadState (HashMap FilePath NExprLoc, MS.HashMap Text Text)
)
staticImport
:: forall m
. ( MonadIO m
, Scoped NExprLoc m
, MonadFail m
, MonadReader (Maybe FilePath, Scopes m NExprLoc) m
, MonadState (HashMap FilePath NExprLoc, HashMap Text Text) m
)
=> SrcSpan
-> FilePath
-> m NExprLoc
staticImport pann path = do
mfile <- asks fst
path <- liftIO $ pathToDefaultNixFile path
path' <- liftIO $ pathToDefaultNixFile =<< canonicalizePath
(maybe id ((</>) . takeDirectory) mfile path)
imports <- gets fst
maybe
(go path')
pure
(M.lookup path' imports)
where
go path = do
liftIO $ putStrLn $ "Importing file " <> path
eres <- liftIO $ parseNixFileLoc path
either
(\ err -> fail $ "Parse failed: " <> show err)
(\ x -> do
let
pos = SourcePos "Reduce.hs" (mkPos 1) (mkPos 1)
span = SrcSpan pos pos
cur =
NamedVar
(StaticKey "__cur_file" :| mempty)
(Fix (NLiteralPath_ pann path))
pos
x' = Fix $ NLet_ span [cur] x
modify $ first $ M.insert path x'
local
(const (pure path, emptyScopes @m @NExprLoc)) $
do
x'' <- foldFix reduce x'
modify $ first $ M.insert path x''
pure x''
)
eres
-- gatherNames :: NExprLoc -> HashSet VarName
-- gatherNames = foldFix $ \case
-- NSym_ _ var -> S.singleton var
-- Compose (Ann _ x) -> fold x
reduceExpr
:: (MonadIO m, MonadFail m) => Maybe FilePath -> NExprLoc -> m NExprLoc
reduceExpr mpath expr =
(`evalStateT` (mempty, MS.empty))
. (`runReaderT` (mpath, emptyScopes))
. runReducer
$ foldFix reduce expr
reduce
:: forall m
. ( MonadIO m
, Scoped NExprLoc m
, MonadFail m
, MonadReader (Maybe FilePath, Scopes m NExprLoc) m
, MonadState (HashMap FilePath NExprLoc, MS.HashMap Text Text) m
)
=> NExprLocF (m NExprLoc)
-> m NExprLoc
-- | Reduce the variable to its value if defined.
-- Leave it as it is otherwise.
reduce (NSym_ ann var) =
fromMaybe (Fix (NSym_ ann var)) <$> lookupVar var
-- | Reduce binary and integer negation.
reduce (NUnary_ uann op arg) =
do
x <- arg
pure $ Fix $
case (op, x) of
(NNeg, Fix (NConstant_ cann (NInt n))) -> NConstant_ cann $ NInt $ negate n
(NNot, Fix (NConstant_ cann (NBool b))) -> NConstant_ cann $ NBool $ not b
_ -> NUnary_ uann op x
-- | Reduce function applications.
--
-- * Reduce an import to the actual imported expression.
--
-- * Reduce a lambda function by adding its name to the local
-- scope and recursively reducing its body.
reduce (NBinary_ bann NApp fun arg) = fun >>= \case
f@(Fix (NSym_ _ "import")) ->
(\case
-- Fix (NEnvPath_ pann origPath) -> staticImport pann origPath
Fix (NLiteralPath_ pann origPath) -> staticImport pann origPath
v -> pure $ Fix $ NBinary_ bann NApp f v
) =<< arg
Fix (NAbs_ _ (Param name) body) ->
do
x <- arg
pushScope
(M.singleton name x)
(foldFix reduce body)
f -> Fix . NBinary_ bann NApp f <$> arg
-- | Reduce an integer addition to its result.
reduce (NBinary_ bann op larg rarg) =
do
lval <- larg
rval <- rarg
pure $ Fix $
case (op, lval, rval) of
(NPlus, Fix (NConstant_ ann (NInt x)), Fix (NConstant_ _ (NInt y))) -> NConstant_ ann $ NInt $ x + y
_ -> NBinary_ bann op lval rval
-- | Reduce a select on a Set by substituting the set to the selected value.
--
-- Before applying this reduction, we need to ensure that:
--
-- 1. The selected expr is indeed a set.
-- 2. The selection AttrPath is a list of StaticKeys.
-- 3. The selected AttrPath exists in the set.
reduce base@(NSelect_ _ _ attrs _)
| sAttrPath $ NE.toList attrs = do
(NSelect_ _ aset attrs _) <- sequence base
inspectSet (unFix aset) attrs
| otherwise = sId
where
sId = Fix <$> sequence base
-- The selection AttrPath is composed of StaticKeys.
sAttrPath (StaticKey _ : xs) = sAttrPath xs
sAttrPath [] = True
sAttrPath _ = False
-- Find appropriate bind in set's binds.
findBind [] _ = Nothing
findBind (x : xs) attrs@(a :| _) = case x of
n@(NamedVar (a' :| _) _ _) | a' == a -> pure n
_ -> findBind xs attrs
-- Follow the attrpath recursively in sets.
inspectSet (NSet_ _ NNonRecursive binds) attrs = case findBind binds attrs of
Just (NamedVar _ e _) -> case NE.uncons attrs of
(_, Just attrs) -> inspectSet (unFix e) attrs
_ -> pure e
_ -> sId
inspectSet _ _ = sId
-- reduce (NHasAttr aset attr) =
-- | Reduce a set by inlining its binds outside of the set
-- if none of the binds inherit the super set.
reduce e@(NSet_ ann NNonRecursive binds) =
do
let
usesInherit =
any
(\case
Inherit{} -> True
_ -> False
)
binds
bool
(Fix <$> sequence e)
(clearScopes @NExprLoc $ Fix . NSet_ ann NNonRecursive <$> traverse sequence binds)
usesInherit
-- Encountering a 'rec set' construction eliminates any hope of inlining
-- definitions.
reduce (NSet_ ann NRecursive binds) =
clearScopes @NExprLoc $ Fix . NSet_ ann NRecursive <$> traverse sequence binds
-- Encountering a 'with' construction eliminates any hope of inlining
-- definitions.
reduce (NWith_ ann scope body) =
clearScopes @NExprLoc $ Fix <$> (NWith_ ann <$> scope <*> body)
-- | Reduce a let binds section by pushing lambdas,
-- constants and strings to the body scope.
reduce (NLet_ ann binds body) =
do
binds' <- traverse sequence binds
body' <-
(`pushScope` body) . M.fromList . catMaybes =<<
traverse
(\case
NamedVar (StaticKey name :| []) def _pos ->
let
defcase =
\case
d@(Fix NAbs_{} ) -> pure (name, d)
d@(Fix NConstant_{}) -> pure (name, d)
d@(Fix NStr_{} ) -> pure (name, d)
_ -> Nothing
in
defcase <$> def
_ -> pure Nothing
)
binds
-- let names = gatherNames body'
-- binds' <- traverse sequence binds <&> \b -> flip filter b $ \case
-- NamedVar (StaticKey name _ :| mempty) _ ->
-- name `S.member` names
-- _ -> True
pure $ Fix $ NLet_ ann binds' body'
-- where
-- go m [] = pure m
-- go m (x:xs) = case x of
-- NamedVar (StaticKey name _ :| mempty) def -> do
-- v <- pushScope m def
-- go (M.insert name v m) xs
-- _ -> go m xs
-- | Reduce an if to the relevant path if
-- the condition is a boolean constant.
reduce e@(NIf_ _ b t f) =
(\case
Fix (NConstant_ _ (NBool b')) -> if b' then t else f
_ -> Fix <$> sequence e
) =<< b
-- | Reduce an assert atom to its encapsulated
-- symbol if the assertion is a boolean constant.
reduce e@(NAssert_ _ b body) =
(\case
Fix (NConstant_ _ (NBool b')) | b' -> body
_ -> Fix <$> sequence e
) =<< b
reduce (NAbs_ ann params body) = do
params' <- sequence params
-- Make sure that variable definitions in scope do not override function
-- arguments.
let
args =
case params' of
Param name -> M.singleton name $ Fix $ NSym_ ann name
ParamSet pset _ _ ->
M.fromList $ (\(k, _) -> (k, Fix $ NSym_ ann k)) <$> pset
Fix . NAbs_ ann params' <$> pushScope args body
reduce v = Fix <$> sequence v
-- newtype FlaggedF f r = FlaggedF { flagged :: (IORef Bool, f r) }
newtype FlaggedF f r = FlaggedF (IORef Bool, f r)
deriving (Functor, Foldable, Traversable)
instance Show (f r) => Show (FlaggedF f r) where
show (FlaggedF (_, x)) = show x
type Flagged f = Fix (FlaggedF f)
flagExprLoc :: (MonadIO n, Traversable f) => Fix f -> n (Flagged f)
flagExprLoc = foldFixM $ \x -> do
flag <- liftIO $ newIORef False
pure $ Fix $ FlaggedF (flag, x)
-- stripFlags :: Functor f => Flagged f -> Fix f
-- stripFlags = foldFix $ Fix . snd . flagged
pruneTree :: MonadIO n => Options -> Flagged NExprLocF -> n (Maybe NExprLoc)
pruneTree opts =
foldFixM $
\(FlaggedF (b, Compose x)) ->
bool
Nothing
(Fix . Compose <$> traverse prune x)
<$> liftIO (readIORef b)
where
prune :: NExprF (Maybe NExprLoc) -> Maybe (NExprF NExprLoc)
prune = \case
NStr str -> pure $ NStr $ pruneString str
NHasAttr (Just aset) attr ->
pure $ NHasAttr aset $ NE.map pruneKeyName attr
NAbs params (Just body) -> pure $ NAbs (pruneParams params) body
NList l -> pure $ NList $
bool
(fromMaybe nNull <$>)
catMaybes
(reduceLists opts) -- Reduce list members that aren't used; breaks if elemAt is used
l
NSet recur binds -> pure $ NSet recur $
bool
(fromMaybe nNull <<$>>)
(mapMaybe sequence)
(reduceSets opts) -- Reduce set members that aren't used; breaks if hasAttr is used
binds
NLet binds (Just body@(Fix (Compose (Ann _ x)))) ->
pure $
list
x
(`NLet` body)
(mapMaybe pruneBinding binds)
NSelect (Just aset) attr alt ->
pure $ NSelect aset (NE.map pruneKeyName attr) (join alt)
-- These are the only short-circuiting binary operators
NBinary NAnd (Just (Fix (Compose (Ann _ larg)))) _ -> pure larg
NBinary NOr (Just (Fix (Compose (Ann _ larg)))) _ -> pure larg
-- If the function was never called, it means its argument was in a
-- thunk that was forced elsewhere.
NBinary NApp Nothing (Just _) -> Nothing
-- The idea behind emitted a binary operator where one side may be
-- invalid is that we're trying to emit what will reproduce whatever
-- fail the user encountered, which means providing all aspects of
-- the evaluation path they ultimately followed.
NBinary op Nothing (Just rarg) -> pure $ NBinary op nNull rarg
NBinary op (Just larg) Nothing -> pure $ NBinary op larg nNull
-- If the scope of a with was never referenced, it's not needed
NWith Nothing (Just (Fix (Compose (Ann _ body)))) -> pure body
NAssert Nothing _ ->
fail "How can an assert be used, but its condition not?"
NAssert _ (Just (Fix (Compose (Ann _ body)))) -> pure body
NAssert (Just cond) _ -> pure $ NAssert cond nNull
NIf Nothing _ _ -> fail "How can an if be used, but its condition not?"
NIf _ Nothing (Just (Fix (Compose (Ann _ f)))) -> pure f
NIf _ (Just (Fix (Compose (Ann _ t)))) Nothing -> pure t
x -> sequence x
pruneString :: NString (Maybe NExprLoc) -> NString NExprLoc
pruneString (DoubleQuoted xs) =
DoubleQuoted (mapMaybe pruneAntiquotedText xs)
pruneString (Indented n xs) = Indented n (mapMaybe pruneAntiquotedText xs)
pruneAntiquotedText
:: Antiquoted Text (Maybe NExprLoc) -> Maybe (Antiquoted Text NExprLoc)
pruneAntiquotedText (Plain v) = pure $ Plain v
pruneAntiquotedText EscapedNewline = pure EscapedNewline
pruneAntiquotedText (Antiquoted (Just k)) = pure $ Antiquoted k
pruneAntiquotedText (Antiquoted Nothing ) = Nothing
pruneAntiquoted
:: Antiquoted (NString (Maybe NExprLoc)) (Maybe NExprLoc)
-> Maybe (Antiquoted (NString NExprLoc) NExprLoc)
pruneAntiquoted (Plain v) = pure $ Plain $ pruneString v
pruneAntiquoted EscapedNewline = pure EscapedNewline
pruneAntiquoted (Antiquoted (Just k)) = pure $ Antiquoted k
pruneAntiquoted (Antiquoted Nothing ) = Nothing
pruneKeyName :: NKeyName (Maybe NExprLoc) -> NKeyName NExprLoc
pruneKeyName (StaticKey n) = StaticKey n
pruneKeyName (DynamicKey k) | Just k' <- pruneAntiquoted k = DynamicKey k'
| otherwise = StaticKey "<unused?>"
pruneParams :: Params (Maybe NExprLoc) -> Params NExprLoc
pruneParams (Param n) = Param n
pruneParams (ParamSet xs b n) =
ParamSet (reduceOrPassMode <$> xs) b n
where
reduceOrPassMode =
second $
bool
fmap
((pure .) . maybe nNull)
(reduceSets opts) -- Reduce set members that aren't used; breaks if hasAttr is used
(fromMaybe nNull)
pruneBinding :: Binding (Maybe NExprLoc) -> Maybe (Binding NExprLoc)
pruneBinding (NamedVar _ Nothing _) = Nothing
pruneBinding (NamedVar xs (Just x) pos) =
pure $ NamedVar (NE.map pruneKeyName xs) x pos
pruneBinding (Inherit _ [] _) = Nothing
pruneBinding (Inherit (join -> Nothing) _ _) = Nothing
pruneBinding (Inherit (join -> m) xs pos) =
pure $ Inherit m (pruneKeyName <$> xs) pos
reducingEvalExpr
:: (Framed e m, Has e Options, Exception r, MonadCatch m, MonadIO m)
=> (NExprLocF (m a) -> m a)
-> Maybe FilePath
-> NExprLoc
-> m (NExprLoc, Either r a)
reducingEvalExpr eval mpath expr =
do
expr' <- flagExprLoc =<< liftIO (reduceExpr mpath expr)
eres <- catch (pure <$> foldFix (addEvalFlags eval) expr') $ pure . Left
opts :: Options <- asks $ view hasLens
expr'' <- pruneTree opts expr'
pure (fromMaybe nNull expr'', eres)
where
addEvalFlags k (FlaggedF (b, x)) = liftIO (writeIORef b True) *> k x
instance Monad m => Scoped NExprLoc (Reducer m) where
currentScopes = currentScopesReader
clearScopes = clearScopesReader @(Reducer m) @NExprLoc
pushScopes = pushScopesReader
lookupVar = lookupVarReader