packages feed

nix-lang-qq-0.1.0.0: src/Nix/Lang/QQ.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances #-}

-- | Quasiquoting Nix expressions as fresh 'Nix.Lang.Types.Syn.Expr' values.
--
-- This quasiquoter is for /construction/, not exact-source recovery.
-- It parses Nix syntax, lowers it to the annotation-free 'Syn.Expr' AST, and
-- supports Haskell antiquotation with @%%(...)@ in expression positions.
--
-- Basic usage:
--
-- @
-- {-# LANGUAGE QuasiQuotes #-}
--
-- import Nix.Lang.QQ (nixQQ)
-- import Nix.Lang.Types.Syn (Expr)
--
-- expr :: Expr
-- expr = [nixQQ| let x = 1; in x |]
-- @
--
-- Antiquotation can splice existing 'Syn.Expr' values:
--
-- @
-- import Nix.Lang.Types.Syn (Expr, mkInt)
--
-- expr :: Expr
-- expr =
--   let value = mkInt 233
--    in [nixQQ| { x = %%(value); } |]
-- @
--
-- Antiquotation also accepts a small set of literal-like Haskell values via
-- 'ToNixExpr':
--
-- @
-- import Data.Text (Text)
-- import Nix.Lang.Types.Syn (Expr)
--
-- stringExpr :: Expr
-- stringExpr =
--   let value = ("hello" :: Text)
--    in [nixQQ| %%(value) |]
--
-- intExpr :: Expr
-- intExpr = [nixQQ| %%(233 :: Integer) |]
-- @
--
-- Note that textual antiquotation becomes a /Nix string literal/, not a Nix
-- identifier. If you want a variable, keep it explicit with smart
-- constructors such as 'Nix.Lang.Types.Syn.mkVar'.
--
-- For parsed/editable syntax trees instead of fresh 'Syn.Expr' construction,
-- use 'Nix.Lang.QQ.Parsed.nixParsedQQ' from the core @nix-lang@ package.
module Nix.Lang.QQ (ToNixExpr (..), nixQQ) where

import Control.Monad (void, when)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.Text as T
import Data.Void (Void)
import Language.Haskell.Meta.Parse (parseExp)
import Language.Haskell.TH hiding (Lit, match)
import Language.Haskell.TH.Quote
import Nix.Lang.Parser (located, nixFile, runNixParser)
import Nix.Lang.Span (Located)
import Nix.Lang.Types
import qualified Nix.Lang.Types.Ps as Ps
import qualified Nix.Lang.Types.Syn as Syn
import Nix.Lang.Utils (stripCommonPrefix, unLoc)
import Text.Megaparsec

class ToNixExpr a where
  toNixExpr :: a -> Syn.Expr

instance ToNixExpr Syn.Expr where
  toNixExpr = id

instance ToNixExpr Integer where
  toNixExpr = Syn.mkInt

instance ToNixExpr Int where
  toNixExpr = Syn.mkInt . fromIntegral

instance ToNixExpr Float where
  toNixExpr = Syn.mkFloat

instance ToNixExpr Bool where
  toNixExpr = Syn.mkBool

instance ToNixExpr T.Text where
  toNixExpr = Syn.mkText

instance ToNixExpr String where
  toNixExpr = Syn.mkText . T.pack

nixQQ :: QuasiQuoter
nixQQ =
  QuasiQuoter
    { quoteExp = nixQuot,
      quotePat = const $ error "nixQQ: unsupported quotePat",
      quoteType = const $ error "nixQQ: unsupported quoteType",
      quoteDec = const $ error "nixQQ: unsupported quoteDec"
    }

nixQuot :: String -> ExpQ
nixQuot src = do
  case rewriteAntiquotes (T.pack (stripCommonPrefix src)) of
    Left err -> fail err
    Right (rewritten, antiquotes) ->
      case runNixParser (located nixFile) "<qq>" rewritten of
        (Left err, _) -> fail (errorBundlePretty err)
        (Right expr, _) -> lowerExpr antiquotes expr

rewriteAntiquotes :: T.Text -> Either String (T.Text, IntMap Exp)
rewriteAntiquotes input = do
  chunks <- parseAntiquoteChunks input
  go 0 [] IntMap.empty chunks
  where
    go _ acc antiMap [] = Right (T.concat (reverse acc), antiMap)
    go next acc antiMap (PlainChunk text : rest) =
      go next (text : acc) antiMap rest
    go next acc antiMap (AntiquoteChunk payload : rest) = do
      parsedExp <- parseAntiquoteExp payload
      let (placeholderIdx, name) = nextPlaceholder input next
      go (placeholderIdx + 1) (name : acc) (IntMap.insert placeholderIdx parsedExp antiMap) rest

parseAntiquoteExp :: T.Text -> Either String Exp
parseAntiquoteExp payload =
  case parseExp (T.unpack payload) of
    Left err -> Left ("failed to parse nixQQ antiquotation: " <> err)
    Right expr -> Right expr

data QQChunk
  = PlainChunk T.Text
  | AntiquoteChunk T.Text

type QQParser = Parsec Void T.Text

parseAntiquoteChunks :: T.Text -> Either String [QQChunk]
parseAntiquoteChunks input =
  case parse (many qqChunk <* eof) "<nixQQ antiquotation>" input of
    Left err -> Left (errorBundlePretty err)
    Right chunks -> Right chunks

qqChunk :: QQParser QQChunk
qqChunk = choice [try antiquoteChunk, plainChunk]

plainChunk :: QQParser QQChunk
plainChunk = PlainChunk . T.concat <$> some plainPiece
  where
    plainPiece =
      choice
        [ takeWhile1P (Just "plain nixQQ chunk") (/= '%'),
          T.singleton <$> (notFollowedBy antiquoteStart *> single '%')
        ]

antiquoteChunk :: QQParser QQChunk
antiquoteChunk = AntiquoteChunk <$> (antiquoteStart *> payloadUntilMatchingClose)

antiquoteStart :: QQParser T.Text
antiquoteStart = chunk "%%("

-- TODO: do syntax aware lexing properly...
payloadUntilMatchingClose :: QQParser T.Text
payloadUntilMatchingClose = T.concat <$> go (1 :: Int)
  where
    go depth = do
      atInputEnd <- atEnd
      when atInputEnd $ fail "unterminated nixQQ antiquotation"
      c <- anySingle
      case c of
        '(' -> (T.singleton c :) <$> go (depth + 1)
        ')' ->
          if depth == 1
            then pure []
            else (T.singleton c :) <$> go (depth - 1)
        '"' -> do
          rest <- quotedTail '"'
          (T.cons c rest :) <$> go depth
        '\'' -> do
          rest <- quotedTail '\''
          (T.cons c rest :) <$> go depth
        _ -> (T.singleton c :) <$> go depth

quotedTail :: Char -> QQParser T.Text
quotedTail delim = fmap fst . match $ manyTill quotedPiece quotedEnd
  where
    quotedPiece = choice [escapedPiece, ordinaryPiece]
    escapedPiece = do
      void (chunk "\\")
      escapedEnd <- atEnd
      when escapedEnd $ fail "unterminated escape in nixQQ antiquotation"
      escaped <- anySingle
      pure (T.pack ['\\', escaped])
    ordinaryPiece = do
      piece <- takeWhile1P (Just "quoted antiquotation payload") (\c -> c /= '\\' && c /= delim)
      pure piece
    quotedEnd = do
      ended <- atEnd
      when ended $ fail "unterminated string in nixQQ antiquotation"
      void (single delim)

placeholderPrefix :: T.Text
placeholderPrefix = "__nix_lang_qq_antiquote_"

mkPlaceholderName :: Int -> T.Text
mkPlaceholderName n = placeholderPrefix <> T.pack (show n)

nextPlaceholder :: T.Text -> Int -> (Int, T.Text)
nextPlaceholder input = go
  where
    go idx =
      let name = mkPlaceholderName idx
       in if T.isInfixOf name input then go (idx + 1) else (idx, name)

lookupPlaceholder :: IntMap Exp -> T.Text -> Maybe Exp
lookupPlaceholder antiMap name = placeholderIndex name >>= (`IntMap.lookup` antiMap)

placeholderIndex :: T.Text -> Maybe Int
placeholderIndex name = do
  suffix <- T.stripPrefix placeholderPrefix name
  case reads (T.unpack suffix) of
    [(n, "")] -> Just n
    _ -> Nothing

lowerExpr :: IntMap Exp -> Located Ps.Expr -> ExpQ
lowerExpr antiMap = lowerExpr' antiMap . unLoc

lowerExpr' :: IntMap Exp -> Ps.Expr -> ExpQ
lowerExpr' antiMap = \case
  NixVar _ name ->
    case lookupPlaceholder antiMap (unLoc name) of
      Just expr -> [|toNixExpr $(pure expr)|]
      _ -> [|Syn.mkVar $(liftText (unLoc name))|]
  NixLit _ lit -> [|Syn.mkLit $(lowerLit (unLoc lit))|]
  NixPar _ expr -> [|Syn.mkPar $(lowerExpr antiMap expr)|]
  NixString _ str -> [|Syn.mkString $(lowerString antiMap (unLoc str))|]
  NixPath _ path -> [|Syn.mkPath $(lowerPath antiMap (unLoc path))|]
  NixEnvPath _ path -> [|Syn.mkEnvPath $(liftText (unLoc path))|]
  NixLam _ pat body -> [|Syn.mkLam $(lowerFuncPat antiMap (unLoc pat)) $(lowerExpr antiMap body)|]
  NixApp _ f x -> [|Syn.mkApp $(lowerExpr antiMap f) $(lowerExpr antiMap x)|]
  NixBinApp _ op lhs rhs -> [|Syn.mkBinApp $(lowerBinaryOp op) $(lowerExpr antiMap lhs) $(lowerExpr antiMap rhs)|]
  NixNotApp _ expr -> [|Syn.mkNot $(lowerExpr antiMap expr)|]
  NixNegApp _ expr -> [|Syn.mkNeg $(lowerExpr antiMap expr)|]
  NixList _ xs -> [|Syn.mkList $(lowerLocatedList antiMap xs)|]
  NixSet _ kind bindings ->
    case kind of
      NixSetRecursive -> [|Syn.mkRecSet $(lowerBindingList antiMap (unLoc bindings))|]
      NixSetNonRecursive -> [|Syn.mkSet $(lowerBindingList antiMap (unLoc bindings))|]
  NixLet _ bindings expr -> [|Syn.mkLet $(lowerBindingList antiMap (unLoc bindings)) $(lowerExpr antiMap expr)|]
  NixHasAttr _ expr path -> [|Syn.mkHasAttr $(lowerExpr antiMap expr) $(lowerAttrPath antiMap (unLoc path))|]
  NixSelect _ expr path Nothing -> [|Syn.mkSelect $(lowerExpr antiMap expr) $(lowerAttrPath antiMap (unLoc path))|]
  NixSelect _ expr path (Just fallback) -> [|Syn.mkSelectOr $(lowerExpr antiMap expr) $(lowerAttrPath antiMap (unLoc path)) $(lowerExpr antiMap fallback)|]
  NixIf _ cond t e -> [|Syn.mkIf $(lowerExpr antiMap cond) $(lowerExpr antiMap t) $(lowerExpr antiMap e)|]
  NixWith _ scope expr -> [|Syn.mkWith $(lowerExpr antiMap scope) $(lowerExpr antiMap expr)|]
  NixAssert _ assertion expr -> [|Syn.mkAssert $(lowerExpr antiMap assertion) $(lowerExpr antiMap expr)|]

lowerLit :: Ps.Lit -> ExpQ
lowerLit = \case
  NixUri _ uri -> [|Syn.mkUriLit $(liftText uri)|]
  NixInteger _ int -> [|Syn.mkIntegerLit int|]
  NixFloat _ float -> [|Syn.mkFloatLit float|]
  NixBoolean _ bool -> [|Syn.mkBooleanLit bool|]
  NixNull _ -> [|Syn.mkNullLit|]

lowerString :: IntMap Exp -> Ps.NString -> ExpQ
lowerString antiMap = \case
  NixDoubleQuotesString _ parts -> [|Syn.mkDoubleQuotedString $(lowerStringPartList antiMap parts)|]
  NixDoubleSingleQuotesString _ parts -> [|Syn.mkIndentedNString $(lowerStringPartList antiMap parts)|]

lowerStringPart :: IntMap Exp -> NixStringPart Ps.Ps -> ExpQ
lowerStringPart antiMap = \case
  NixStringLiteral _ text -> [|Syn.mkStringLiteral $(liftText text)|]
  NixStringInterpol _ expr -> [|Syn.mkInterpol $(lowerExpr antiMap expr)|]

lowerPath :: IntMap Exp -> Ps.Path -> ExpQ
lowerPath antiMap = \case
  NixLiteralPath _ text -> [|Syn.mkLiteralPath $(liftText text)|]
  NixInterpolPath _ parts -> [|Syn.mkInterpolatedPath $(lowerStringPartList antiMap parts)|]

lowerAttrKey :: IntMap Exp -> Ps.AttrKey -> ExpQ
lowerAttrKey antiMap = \case
  NixStaticAttrKey _ name -> [|Syn.mkAttr $(liftText (unLoc name))|]
  NixDynamicStringAttrKey _ parts -> [|Syn.mkDynamicAttr $(lowerStringPartList antiMap parts)|]
  NixDynamicInterpolAttrKey _ expr -> [|Syn.mkInterpolatedAttr $(lowerExpr antiMap expr)|]

lowerAttrPath :: IntMap Exp -> Ps.AttrPath -> ExpQ
lowerAttrPath antiMap (NixAttrPath _ keys) = [|Syn.mkAttrPath $(lowerAttrKeyList antiMap keys)|]

lowerBinding :: IntMap Exp -> Ps.Binding -> ExpQ
lowerBinding antiMap = \case
  NixNormalBinding _ path expr -> [|Syn.mkNormalBinding $(lowerAttrPath antiMap (unLoc path)) $(lowerExpr antiMap expr)|]
  NixInheritBinding _ mScope keys ->
    lowerInheritBinding antiMap mScope keys

lowerFuncPat :: IntMap Exp -> Ps.FuncPat -> ExpQ
lowerFuncPat antiMap = \case
  NixVarPat _ name -> [|Syn.mkVarPat $(liftText (unLoc name))|]
  NixSetPat _ ellipses mAs bindings ->
    [|Syn.mkSetPat $(lowerSetPatEllipses ellipses) $(lowerMaybe (lowerSetPatAs antiMap . unLoc) mAs) $(lowerSetPatBindingList antiMap bindings)|]

lowerInheritBinding :: IntMap Exp -> Maybe (Located Ps.Expr) -> [Located Ps.AttrKey] -> ExpQ
lowerInheritBinding antiMap mScope keys =
  case mScope of
    Nothing -> [|Syn.mkInheritKeys $(lowerAttrKeyList antiMap keys)|]
    Just scope -> [|Syn.mkInheritFromKeys $(lowerExpr antiMap scope) $(lowerAttrKeyList antiMap keys)|]

lowerSetPatAs :: IntMap Exp -> Ps.SetPatAs -> ExpQ
lowerSetPatAs _ NixSetPatAs {nspaLocation = asLocation, nspaVar = asVar} = [|Syn.mkSetPatAs $(lowerSetPatAsLocation asLocation) $(liftText (unLoc asVar))|]

lowerSetPatBinding :: IntMap Exp -> Ps.SetPatBinding -> ExpQ
lowerSetPatBinding antiMap NixSetPatBinding {nspbVar = bindVar, nspbDefault = bindDefault} = [|Syn.mkSetPatBinding $(liftText (unLoc bindVar)) $(lowerMaybe (lowerExpr antiMap) bindDefault)|]

lowerMaybe :: (a -> ExpQ) -> Maybe a -> ExpQ
lowerMaybe f = \case
  Nothing -> [|Nothing|]
  Just x -> [|Just $(f x)|]

lowerLocatedList :: IntMap Exp -> [Located Ps.Expr] -> ExpQ
lowerLocatedList antiMap = listE . fmap (lowerExpr antiMap)

lowerBindingList :: IntMap Exp -> [Located Ps.Binding] -> ExpQ
lowerBindingList antiMap = listE . fmap (lowerBinding antiMap . unLoc)

lowerStringPartList :: IntMap Exp -> [Located (NixStringPart Ps.Ps)] -> ExpQ
lowerStringPartList antiMap = listE . fmap (lowerStringPart antiMap . unLoc)

lowerAttrKeyList :: IntMap Exp -> [Located Ps.AttrKey] -> ExpQ
lowerAttrKeyList antiMap = listE . fmap (lowerAttrKey antiMap . unLoc)

lowerSetPatBindingList :: IntMap Exp -> [Located Ps.SetPatBinding] -> ExpQ
lowerSetPatBindingList antiMap = listE . fmap (lowerSetPatBinding antiMap . unLoc)

lowerBinaryOp :: BinaryOp -> ExpQ
lowerBinaryOp = \case
  OpConcat -> conE 'OpConcat
  OpMul -> conE 'OpMul
  OpDiv -> conE 'OpDiv
  OpAdd -> conE 'OpAdd
  OpSub -> conE 'OpSub
  OpUpdate -> conE 'OpUpdate
  OpLT -> conE 'OpLT
  OpLE -> conE 'OpLE
  OpGT -> conE 'OpGT
  OpGE -> conE 'OpGE
  OpEq -> conE 'OpEq
  OpNEq -> conE 'OpNEq
  OpAnd -> conE 'OpAnd
  OpOr -> conE 'OpOr
  OpImpl -> conE 'OpImpl

lowerSetPatEllipses :: NixSetPatEllipses -> ExpQ
lowerSetPatEllipses = \case
  NixSetPatIsEllipses -> conE 'NixSetPatIsEllipses
  NixSetPatNotEllipses -> conE 'NixSetPatNotEllipses

lowerSetPatAsLocation :: NixSetPatAsLocation -> ExpQ
lowerSetPatAsLocation = \case
  NixSetPatAsLeading -> conE 'NixSetPatAsLeading
  NixSetPatAsTrailing -> conE 'NixSetPatAsTrailing

liftText :: T.Text -> ExpQ
liftText = litE . StringL . T.unpack