nix-lang-qq (empty) → 0.1.0.0
raw patch · 4 files changed
+575/−0 lines, 4 filesdep +basedep +containersdep +haskell-src-meta
Dependencies added: base, containers, haskell-src-meta, megaparsec, nix-lang, nix-lang-qq, tasty, tasty-hunit, template-haskell, text
Files
- LICENSE +11/−0
- nix-lang-qq.cabal +81/−0
- src/Nix/Lang/QQ.hs +364/−0
- test/Spec.hs +119/−0
+ LICENSE view
@@ -0,0 +1,11 @@+Copyright (c) 2022-2026 berberman++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ nix-lang-qq.cabal view
@@ -0,0 +1,81 @@+cabal-version: 3.0+name: nix-lang-qq+version: 0.1.0.0+synopsis:+ Quasiquoter for generated Nix expressions with antiquotation++description:+ nix-lang-qq provides a Template Haskell quasiquoter on top of nix-lang.+ It parses Nix snippets with antiquotation support and lowers them into the+ fresh Syn AST used by RFCPrint.++category: Nix+license: BSD-3-Clause+license-file: LICENSE+author: berberman+maintainer: berberman <berberman@yandex.com>+copyright: Copyright (c) berberman 2022-2026+tested-with: GHC ==9.10.3+build-type: Simple++common common-attrs+ build-depends: base ^>=4.20+ default-language: Haskell2010+ ghc-options:+ -Wall -Wcompat -Widentities -Wincomplete-uni-patterns+ -Wincomplete-record-updates++ if impl(ghc >=8.0)+ ghc-options: -Wredundant-constraints++ if impl(ghc >=8.2)+ ghc-options: -fhide-source-paths++ default-extensions:+ BangPatterns+ ConstraintKinds+ DataKinds+ DeriveAnyClass+ DeriveFunctor+ DeriveGeneric+ DerivingStrategies+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ FunctionalDependencies+ KindSignatures+ LambdaCase+ NoStarIsType+ OverloadedStrings+ RecordWildCards+ ScopedTypeVariables+ TupleSections+ TypeApplications+ TypeFamilies+ TypeOperators+ ViewPatterns++library+ import: common-attrs+ hs-source-dirs: src+ build-depends:+ , containers ^>=0.7+ , haskell-src-meta ^>=0.8+ , megaparsec ^>=9.7.0+ , nix-lang+ , template-haskell ^>=2.22.0+ , text ^>=2.1.3++ exposed-modules: Nix.Lang.QQ++test-suite nix-lang-qq-test+ import: common-attrs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends:+ , nix-lang+ , nix-lang-qq+ , tasty+ , tasty-hunit+ , text
+ src/Nix/Lang/QQ.hs view
@@ -0,0 +1,364 @@+{-# 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
+ test/Spec.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Main (main) where++import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Edit+import Nix.Lang.ExactPrint (renderExactText)+import Nix.Lang.QQ (nixQQ)+import Nix.Lang.QQ.Parsed (nixParsedQQ)+import Nix.Lang.RFCPrint (formatExpr)+import qualified Nix.Lang.Types.Ps as Ps+import Nix.Lang.Types.Syn+import Nix.Lang.Utils (unLoc)+import Test.Tasty+import Test.Tasty.HUnit++pkg :: Text -> Expr+pkg name = mkSelectAttrs (mkVar "pkgs") [name]++devShellExpr :: [Text] -> Expr+devShellExpr packageNames =+ mkApp+ (mkSelectAttrs (mkVar "pkgs") ["mkShell"])+ ( mkStaticSet+ [ ("packages", mkList (pkg <$> packageNames))+ ]+ )++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests =+ testGroup+ "nix-lang-qq"+ [ testCase "builds Syn expressions" $ do+ formatExpr ([nixQQ| let x = 1; in x |] :: Expr)+ @?= "let\n x = 1;\nin\nx",+ testCase "splices antiquoted Syn expressions" $ do+ let value = mkInt 233+ formatExpr ([nixQQ| { x = %%(value); } |] :: Expr)+ @?= "{\n x = 233;\n}",+ testCase "converts antiquoted integers automatically" $ do+ let value = (233 :: Integer)+ formatExpr ([nixQQ| { x = %%(value); } |] :: Expr)+ @?= "{\n x = 233;\n}",+ testCase "converts antiquoted text automatically" $ do+ let value = ("hello" :: Text)+ formatExpr ([nixQQ| %%(value) |] :: Expr)+ @?= "\"hello\"",+ testCase "converts antiquoted string automatically" $ do+ let value = ("hello" :: String)+ formatExpr ([nixQQ| %%(value) |] :: Expr)+ @?= "\"hello\"",+ testCase "converts antiquoted bool automatically" $ do+ let value = True+ formatExpr ([nixQQ| %%(value) |] :: Expr)+ @?= "true",+ testCase "supports multiple antiquotes" $ do+ formatExpr ([nixQQ| { a = %%(mkInt 1); b = %%(mkInt 2); } |] :: Expr)+ @?= "{\n a = 1;\n b = 2;\n}",+ testCase "keeps non-antiquote percent text untouched" $ do+ formatExpr ([nixQQ| "% %%" |] :: Expr)+ @?= "\"% %%\"",+ testCase "supports nested antiquote expressions" $ do+ formatExpr ([nixQQ| { value = %%(mkApp (mkVar "f") (mkInt 1)); } |] :: Expr)+ @?= "{\n value = f 1;\n}",+ testCase "captures quoted closing parens inside antiquote payload" $ do+ formatExpr ([nixQQ| %%(mkText "not ) the end") |] :: Expr)+ @?= "\"not ) the end\"",+ testCase "captures escaped quotes inside antiquote payload" $ do+ formatExpr ([nixQQ| %%(mkText "a\"b") |] :: Expr)+ @?= "\"a\\\"b\"",+ testCase "antiquote works inside string interpolation" $ do+ formatExpr ([nixQQ| "a${%%(mkVar "b")}c" |] :: Expr)+ @?= "\"a${b}c\"",+ testCase "supports devShell example" $ do+ formatExpr+ [nixQQ|+ let pkgs = import <nixpkgs> {}; in+ %%(devShellExpr ["ghc", "cabal-install"])+ |]+ @?= T.intercalate+ "\n"+ [ "let",+ " pkgs = import <nixpkgs> { };",+ "in",+ "pkgs.mkShell {",+ " packages = [",+ " pkgs.ghc",+ " pkgs.cabal-install",+ " ];",+ "}"+ ],+ testCase "rejects placeholder collision" $ do+ formatExpr+ [nixQQ|+ let __nix_lang_qq_antiquote_0 = 1; in+ [ __nix_lang_qq_antiquote_0 %%(mkInt 2) ]+ |]+ @?= "let\n __nix_lang_qq_antiquote_0 = 1;\nin\n[\n __nix_lang_qq_antiquote_0\n 2\n]",+ testCase "builds parsed located expressions" $ do+ renderExactText (unLoc ([nixParsedQQ| let x = 1; in x |] :: Ps.LExpr))+ @?= "let x = 1; in x",+ testCase "parsed quasiquoter works with editExpr" $ do+ let expr =+ [nixParsedQQ|+ {+ a = 1;+ b = 2;+ }+ |] ::+ Ps.LExpr+ case editExpr (bindingByKey "b" // bindingValue) (setIntLiteral 20) expr of+ Right x -> renderExactText (unLoc x) @?= "{\n a = 1;\n b = 20;\n}"+ Left err -> fail $ show err+ ]