packages feed

hnix-0.15.0: src/Nix/TH.hs

{-# language QuasiQuotes #-}
{-# language TemplateHaskell #-}

{-# options_ghc -Wno-missing-fields #-}
{-# options_ghc -Wno-name-shadowing #-}

module Nix.TH where

import           Data.Fix                       ( Fix(unFix) )
import           Data.Generics.Aliases          ( extQ )
import qualified Data.Set                      as Set
import           Language.Haskell.TH
import qualified Language.Haskell.TH.Syntax    as TH
import           Language.Haskell.TH.Quote
import           Nix.Atoms
import           Nix.Expr.Types
import           Nix.Expr.Types.Annotated
import           Nix.Parser

quoteExprExp :: String -> ExpQ
quoteExprExp s =
  do
    expr <- parseExpr $ fromString s
    dataToExpQ
      (extQOnFreeVars metaExp expr `extQ` (pure . (TH.lift :: Text -> Q Exp)))
      expr

quoteExprPat :: String -> PatQ
quoteExprPat s =
  do
    expr <- parseExpr $ fromString s
    dataToPatQ
      (extQOnFreeVars metaPat expr)
      expr

-- | Helper function.
extQOnFreeVars
  :: ( Typeable b
    , Typeable loc
    )
  => ( Set VarName
    -> loc
    -> Maybe q
    )
  -> NExpr
  -> b
  -> Maybe q
extQOnFreeVars f = extQ (const Nothing) . f . freeVars

parseExpr :: (MonadFail m) => Text -> m NExpr
parseExpr =
  either
    (fail . show)
    pure
    . parseNixText

freeVars :: NExpr -> Set VarName
freeVars e = case unFix e of
  (NConstant    _               ) -> mempty
  (NStr         string          ) -> mapFreeVars string
  (NSym         var             ) -> one var
  (NList        list            ) -> mapFreeVars list
  (NSet   NonRecursive  bindings) -> bindFreeVars bindings
  (NSet   Recursive     bindings) -> diffBetween bindFreeVars bindDefs bindings
  (NLiteralPath _               ) -> mempty
  (NEnvPath     _               ) -> mempty
  (NUnary       _    expr       ) -> freeVars expr
  (NBinary      _    left right ) -> collectFreeVars left right
  (NSelect      orExpr expr path) ->
    Set.unions
      [ freeVars expr
      , pathFree path
      , freeVars `whenJust` orExpr
      ]
  (NHasAttr expr            path) -> freeVars expr <> pathFree path
  (NAbs     (Param varname) expr) -> Set.delete varname (freeVars expr)
  (NAbs (ParamSet varname _ pset) expr) ->
    -- Include all free variables from the expression and the default arguments
    freeVars expr <>
    -- But remove the argument name if existing, and all arguments in the parameter set
    Set.difference
      (Set.unions $ freeVars <$> mapMaybe snd pset)
      (Set.difference
        (one `whenJust` varname)
        (Set.fromList $ fst <$> pset)
      )
  (NLet         bindings expr   ) ->
    freeVars expr <>
    diffBetween bindFreeVars bindDefs bindings
  (NIf          cond th   el    ) -> Set.unions $ freeVars <$> [cond, th, el]
  -- Evaluation is needed to find out whether x is a "real" free variable in `with y; x`, we just include it
  -- This also makes sense because its value can be overridden by `x: with y; x`
  (NWith        set  expr       ) -> collectFreeVars set expr
  (NAssert      assertion expr  ) -> collectFreeVars assertion expr
  (NSynHole     _               ) -> mempty

 where

  diffBetween :: (a -> Set VarName) -> (a -> Set VarName) -> a -> Set VarName
  diffBetween g f b = Set.difference (g b) (f b)

  collectFreeVars :: NExpr -> NExpr -> Set VarName
  collectFreeVars = (<>) `on` freeVars

  bindDefs :: Foldable t => t (Binding NExpr) -> Set VarName
  bindDefs = foldMap bind1Def
   where
    bind1Def :: Binding r -> Set VarName
    bind1Def (Inherit   Nothing                  _    _) = mempty
    bind1Def (Inherit  (Just _                 ) keys _) = Set.fromList keys
    bind1Def (NamedVar (StaticKey  varname :| _) _    _) = one varname
    bind1Def (NamedVar (DynamicKey _       :| _) _    _) = mempty

  bindFreeVars :: Foldable t => t (Binding NExpr) -> Set VarName
  bindFreeVars = foldMap bind1Free
   where
    bind1Free :: Binding NExpr -> Set VarName
    bind1Free (Inherit  Nothing     keys _) = Set.fromList keys
    bind1Free (Inherit (Just scope) _    _) = freeVars scope
    bind1Free (NamedVar path        expr _) = pathFree path <> freeVars expr

  pathFree :: NAttrPath NExpr -> Set VarName
  pathFree = foldMap mapFreeVars

  mapFreeVars :: Foldable t => t NExpr -> Set VarName
  mapFreeVars = foldMap freeVars


class ToExpr a where
  toExpr :: a -> NExprLoc

instance ToExpr NExprLoc where
  toExpr = id

instance ToExpr VarName where
  toExpr = NSymAnn nullSpan

instance ToExpr Int where
  toExpr = NConstantAnn nullSpan . NInt . fromIntegral

instance ToExpr Integer where
  toExpr = NConstantAnn nullSpan . NInt

instance ToExpr Float where
  toExpr = NConstantAnn nullSpan . NFloat

metaExp :: Set VarName -> NExprLoc -> Maybe ExpQ
metaExp fvs (NSymAnn _ x) | x `Set.member` fvs =
  pure [| toExpr $(varE (mkName $ toString x)) |]
metaExp _ _ = Nothing

metaPat :: Set VarName -> NExprLoc -> Maybe PatQ
metaPat fvs (NSymAnn _ x) | x `Set.member` fvs =
  pure $ varP $ mkName $ toString x
metaPat _ _ = Nothing

-- Use of @QuasiQuoter@ requires @String@.
-- After @Text -> String@ migrations done, _maybe_ think to use @QuasiText@.
nix :: QuasiQuoter
nix = QuasiQuoter { quoteExp = quoteExprExp, quotePat = quoteExprPat }