Brandon Chinn pushed to branch wip/interpolated-strings at Glasgow Haskell Compiler / GHC
Commits:
-
af06dc63
by Brandon Chinn at 2026-11-01T17:03:37-08:00
14 changed files:
- compiler/GHC/Hs/Expr.hs
- compiler/GHC/HsToCore/Ticks.hs
- compiler/GHC/Parser.y
- compiler/GHC/Parser/Lexer.x
- compiler/GHC/Parser/Lexer/String.x
- compiler/GHC/Parser/String.hs
- compiler/GHC/Rename/Expr.hs
- compiler/GHC/Rename/String.hs
- compiler/Language/Haskell/Syntax/Expr.hs
- compiler/Language/Haskell/Syntax/Extension.hs
- testsuite/tests/parser/should_run/StringInterpolation.hs
- testsuite/tests/printer/Makefile
- + testsuite/tests/printer/PprStringInterpolation.hs
- testsuite/tests/printer/all.T
Changes:
| ... | ... | @@ -27,7 +27,6 @@ import GHC.Hs.Pat |
| 27 | 27 | import GHC.Hs.Lit
|
| 28 | 28 | import Language.Haskell.Syntax.Extension
|
| 29 | 29 | import Language.Haskell.Syntax.Basic (FieldLabelString(..))
|
| 30 | -import Language.Haskell.Syntax.StringMeta (StringMeta (..))
|
|
| 31 | 30 | import Language.Haskell.Syntax.Text
|
| 32 | 31 | import Language.Haskell.Syntax.Module.Name
|
| 33 | 32 | import GHC.Hs.Extension
|
| ... | ... | @@ -239,12 +238,12 @@ type instance XQualLitE (GhcPass _) = NoExtField |
| 239 | 238 | type instance XLam (GhcPass _) = EpAnnLam
|
| 240 | 239 | type instance XApp (GhcPass _) = NoExtField
|
| 241 | 240 | |
| 242 | -type instance XInterString GhcPs = NoExtField
|
|
| 243 | -type instance XInterString GhcRn = NoExtField
|
|
| 241 | +type instance XInterString GhcPs = AnnInterString
|
|
| 242 | +type instance XInterString GhcRn = AnnInterString
|
|
| 244 | 243 | type instance XInterString GhcTc = DataConCantHappen
|
| 245 | --- | Note: does not contain any delimiters
|
|
| 246 | -type instance XInterStringRaw (GhcPass _) = SourceText
|
|
| 247 | -type instance XInterStringExpr (GhcPass _) = NoExtField
|
|
| 244 | +type instance XInterStringRaw (GhcPass _) = SourceText -- Note: does not contain any delimiters
|
|
| 245 | +type instance XInterStringExpr (GhcPass _) = NoExtField
|
|
| 246 | +type instance XXInterStringPart (GhcPass _) = DataConCantHappen
|
|
| 248 | 247 | |
| 249 | 248 | type instance XAppTypeE GhcPs = EpToken "@"
|
| 250 | 249 | type instance XAppTypeE GhcRn = NoExtField
|
| ... | ... | @@ -623,6 +622,18 @@ data AnnFunRhs |
| 623 | 622 | instance NoAnn AnnFunRhs where
|
| 624 | 623 | noAnn = AnnFunRhs noAnn noAnn noAnn
|
| 625 | 624 | |
| 625 | +data AnnInterString = AnnInterString
|
|
| 626 | + { ais_delim :: !AnnInterStringDelim
|
|
| 627 | + , ais_open :: !EpaLocation -- ^ @s"@ or @s"""@, including any @M.@ prefix
|
|
| 628 | + , ais_close :: !EpaLocation -- ^ @"@ or @"""@
|
|
| 629 | + }
|
|
| 630 | + deriving (Data, Show, Eq)
|
|
| 631 | + |
|
| 632 | +data AnnInterStringDelim
|
|
| 633 | + = AnnInterStringSingle -- ^ @s"..."@
|
|
| 634 | + | AnnInterStringMulti -- ^ @s"""..."""@
|
|
| 635 | + deriving (Data, Show, Eq)
|
|
| 636 | + |
|
| 626 | 637 | -- ---------------------------------------------------------------------
|
| 627 | 638 | |
| 628 | 639 | type instance XSCC (GhcPass _) = (AnnPragSCC, SourceText)
|
| ... | ... | @@ -821,21 +832,26 @@ ppr_expr (HsLit _ lit) = ppr lit |
| 821 | 832 | ppr_expr (HsOverLit _ lit) = ppr lit
|
| 822 | 833 | ppr_expr (HsQualLit _ lit) = ppr lit
|
| 823 | 834 | |
| 824 | -ppr_expr (HsInterString _ meta parts) =
|
|
| 835 | +ppr_expr (HsInterString x _ parts) =
|
|
| 825 | 836 | char 's' <> delim <> hcat (map pprInterPart parts) <> delim
|
| 826 | 837 | where
|
| 838 | + isMultiline =
|
|
| 839 | + case (ghcPass @p, x) of
|
|
| 840 | + (GhcPs, AnnInterString{ais_delim = AnnInterStringMulti}) -> True
|
|
| 841 | + (GhcRn, AnnInterString{ais_delim = AnnInterStringMulti}) -> True
|
|
| 842 | + _ -> False
|
|
| 827 | 843 | delim
|
| 828 | - | strMetaMultiline meta = text "\"\"\""
|
|
| 844 | + | isMultiline = text "\"\"\""
|
|
| 829 | 845 | | otherwise = char '"'
|
| 830 | 846 | pprInterPart = \case
|
| 831 | 847 | HsInterStringExpr _ expr -> text "${" <> ppr_lexpr expr <> text "}"
|
| 832 | 848 | HsInterStringRaw st s ->
|
| 833 | 849 | case st of
|
| 834 | 850 | SourceText src
|
| 835 | - | strMetaMultiline meta -> vcat $ map text $ split '\n' (unpackFS src)
|
|
| 851 | + | isMultiline -> vcat $ map text $ split '\n' (unpackFS src)
|
|
| 836 | 852 | | otherwise -> ftext src
|
| 837 | 853 | NoSourceText
|
| 838 | - | strMetaMultiline meta -> pprHsStringMulti' (unpackHText s)
|
|
| 854 | + | isMultiline -> pprHsStringMulti' (unpackHText s)
|
|
| 839 | 855 | | otherwise -> pprHsString' (unpackHText s)
|
| 840 | 856 | |
| 841 | 857 | ppr_expr (HsHole x) = case (ghcPass @p, x) of
|
| ... | ... | @@ -584,8 +584,8 @@ addTickHsExpr e@(HsLit {}) = return e |
| 584 | 584 | addTickHsExpr e@(HsQualLit {}) = return e
|
| 585 | 585 | addTickHsExpr (HsInterString x ty parts) = do
|
| 586 | 586 | parts' <- forM parts $ \case
|
| 587 | - part@(HsInterStringRaw {}) -> return part
|
|
| 588 | 587 | HsInterStringExpr x e -> HsInterStringExpr x <$> addTickLHsExpr e
|
| 588 | + p -> return p
|
|
| 589 | 589 | return $ HsInterString x ty parts'
|
| 590 | 590 | addTickHsExpr e@(HsEmbTy {}) = return e
|
| 591 | 591 | addTickHsExpr e@(HsStar {}) = return e
|
| ... | ... | @@ -4419,9 +4419,18 @@ processStringInter :: |
| 4419 | 4419 | processStringInter tokBegin parts tokEnd = do
|
| 4420 | 4420 | parts' <- mapM mkInterStringPartPV $ processRawLexedStrings parts
|
| 4421 | 4421 | ams1 (L (comb2 tokBegin tokEnd) ()) $
|
| 4422 | - HsInterString noExtField meta parts'
|
|
| 4422 | + HsInterString ann (strMetaQualified meta) parts'
|
|
| 4423 | 4423 | where
|
| 4424 | 4424 | L _ (ITstringInterBegin meta) = tokBegin
|
| 4425 | + ann =
|
|
| 4426 | + AnnInterString
|
|
| 4427 | + { ais_delim =
|
|
| 4428 | + if strMetaMultiline meta
|
|
| 4429 | + then AnnInterStringMulti
|
|
| 4430 | + else AnnInterStringSingle
|
|
| 4431 | + , ais_open = noAnn -- FIXME(bchinn)
|
|
| 4432 | + , ais_close = noAnn -- FIXME(bchinn)
|
|
| 4433 | + }
|
|
| 4425 | 4434 | |
| 4426 | 4435 | processRawLexedStrings ::
|
| 4427 | 4436 | [Either (SourceText, RawLexedString) ECP] ->
|
| ... | ... | @@ -2236,7 +2236,7 @@ tok_string_inter_begin meta span _ _ _ = do |
| 2236 | 2236 | |
| 2237 | 2237 | tok_string_inter_qual_begin :: StringMeta -> Action
|
| 2238 | 2238 | tok_string_inter_qual_begin meta span buf len buf2 = do
|
| 2239 | - let modName = ModuleName $ lexemeToFastString buf modNameLen
|
|
| 2239 | + let modName = HsModuleName $ lexemeToText buf modNameLen
|
|
| 2240 | 2240 | let meta' = meta{strMetaQualified = Just modName}
|
| 2241 | 2241 | tok_string_inter_begin meta' span buf len buf2
|
| 2242 | 2242 | where
|
| ... | ... | @@ -91,7 +91,6 @@ $hexit = [$decdigit A-F a-f] |
| 91 | 91 | |
| 92 | 92 | -- Note [Lexing interpolated strings]
|
| 93 | 93 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 94 | ---
|
|
| 95 | 94 | -- Lexing interpolated strings will do the following steps:
|
| 96 | 95 | --
|
| 97 | 96 | -- 1. Lex `s"` => push `string_inter_content` lex state
|
| ... | ... | @@ -32,6 +32,7 @@ import qualified Data.Foldable1 as Foldable1 |
| 32 | 32 | import Data.Functor.Identity (Identity (..))
|
| 33 | 33 | import qualified Data.List.NonEmpty as NonEmpty
|
| 34 | 34 | import Data.Maybe (listToMaybe, mapMaybe)
|
| 35 | +import GHC.Data.Maybe (rightToMaybe)
|
|
| 35 | 36 | import GHC.Data.OrdList (fromOL, nilOL, snocOL)
|
| 36 | 37 | import GHC.Data.StringBuffer (StringBuffer)
|
| 37 | 38 | import qualified GHC.Data.StringBuffer as StringBuffer
|
| ... | ... | @@ -51,7 +52,7 @@ data StringLexError = StringLexError LexErr BufPos |
| 51 | 52 | deriving (Show, Eq)
|
| 52 | 53 | |
| 53 | 54 | lexString :: Int -> StringBuffer -> Either StringLexError String
|
| 54 | -lexString = lexStringWith (fromRight . processCharsSingle)
|
|
| 55 | +lexString = lexStringWith (rightToMaybe . processCharsSingle)
|
|
| 55 | 56 | |
| 56 | 57 | -- -----------------------------------------------------------------------------
|
| 57 | 58 | -- Lexing interface
|
| ... | ... | @@ -194,7 +195,7 @@ resolveEscapes = go nilOL |
| 194 | 195 | c : cs -> go (acc `snocOL` c) cs
|
| 195 | 196 | |
| 196 | 197 | resolveEscapesMaybe :: HasChar c => [c] -> Maybe [c]
|
| 197 | -resolveEscapesMaybe = fromRight . resolveEscapes
|
|
| 198 | +resolveEscapesMaybe = rightToMaybe . resolveEscapes
|
|
| 198 | 199 | |
| 199 | 200 | -- -----------------------------------------------------------------------------
|
| 200 | 201 | -- Escape characters
|
| ... | ... | @@ -626,9 +627,3 @@ It's more precisely defined with the following algorithm: |
| 626 | 627 | * Lines with only whitespace characters
|
| 627 | 628 | 3. Calculate the longest prefix of whitespace shared by all lines in the remaining list
|
| 628 | 629 | -} |
| 629 | - |
|
| 630 | --- -----------------------------------------------------------------------------
|
|
| 631 | --- Other utilities
|
|
| 632 | - |
|
| 633 | -fromRight :: Either e a -> Maybe a
|
|
| 634 | -fromRight = either (const Nothing) Just |
| ... | ... | @@ -379,9 +379,9 @@ rnExpr (HsOverLit x lit) |
| 379 | 379 | return (HsApp noExtField (noLocA neg) (noLocA (HsOverLit x lit'))
|
| 380 | 380 | , fvs ) }
|
| 381 | 381 | |
| 382 | -rnExpr (HsInterString _ meta parts) = do
|
|
| 382 | +rnExpr (HsInterString _ mQual parts) = do
|
|
| 383 | 383 | (parts', fvs1) <- unzip <$> mapM rnInterStringPart parts
|
| 384 | - (expr, fvs2) <- rewriteInterString meta parts'
|
|
| 384 | + (expr, fvs2) <- rewriteInterString mQual parts'
|
|
| 385 | 385 | pure (expr, plusFNs fvs1 `plusFN` fvs2)
|
| 386 | 386 | where
|
| 387 | 387 | rnInterStringPart = \case
|
| ... | ... | @@ -14,14 +14,14 @@ import GHC.Rename.Env (lookupNameWithQualifier, lookupSyntaxExpr, lookupSyntaxNa |
| 14 | 14 | import GHC.Tc.Utils.Monad
|
| 15 | 15 | import GHC.Types.Name.Set (FreeNames, emptyFNs, plusFNs)
|
| 16 | 16 | import GHC.Types.SrcLoc (unLoc)
|
| 17 | -import Language.Haskell.Syntax.StringMeta (StringMeta (..))
|
|
| 17 | +import Language.Haskell.Syntax.Module.Name (HsModuleName)
|
|
| 18 | 18 | |
| 19 | 19 | {- Note [Desugaring interpolated strings]
|
| 20 | 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 21 | 21 | |
| 22 | 22 | Cross-references:
|
| 23 | -* Note [Parsing interpolated strings]
|
|
| 24 | -* Note [Doing XXExprGhcRn in the Renamer vs Typechecker]
|
|
| 23 | + * Note [Parsing interpolated strings]
|
|
| 24 | + * Note [Doing XXExprGhcRn in the Renamer vs Typechecker]
|
|
| 25 | 25 | |
| 26 | 26 | Interpolated strings are represented with the following HsExpr tree:
|
| 27 | 27 | |
| ... | ... | @@ -52,10 +52,10 @@ can be typechecked as usual, without any additional work. |
| 52 | 52 | -- have already been renamed, but handles renaming of overloaded strings, if
|
| 53 | 53 | -- necessary.
|
| 54 | 54 | rewriteInterString ::
|
| 55 | - StringMeta ->
|
|
| 55 | + Maybe HsModuleName ->
|
|
| 56 | 56 | [HsInterStringPart GhcRn] ->
|
| 57 | 57 | RnM (HsExpr GhcRn, FreeNames)
|
| 58 | -rewriteInterString meta parts = do
|
|
| 58 | +rewriteInterString mQual parts = do
|
|
| 59 | 59 | (mkOverloaded, fns1) <- get_mkOverloaded
|
| 60 | 60 | (rawName, fns2) <- lookupName KnownOccs.interpolateRawIdOcc
|
| 61 | 61 | (convertName, fns3) <- lookupName KnownOccs.interpolateValueIdOcc
|
| ... | ... | @@ -73,7 +73,7 @@ rewriteInterString meta parts = do |
| 73 | 73 | pure (expr, plusFNs [fns1, fns2, fns3, fns4, fns5, fns6])
|
| 74 | 74 | where
|
| 75 | 75 | lookupName =
|
| 76 | - case strMetaQualified meta of
|
|
| 76 | + case mQual of
|
|
| 77 | 77 | Just mod -> lookupNameWithQualifier mod
|
| 78 | 78 | Nothing -> lookupSyntaxName
|
| 79 | 79 | |
| ... | ... | @@ -84,7 +84,7 @@ rewriteInterString meta parts = do |
| 84 | 84 | -- Handle -XOverloadedStrings
|
| 85 | 85 | get_mkOverloaded = do
|
| 86 | 86 | overloaded <- xoptM LangExt.OverloadedStrings
|
| 87 | - if overloaded && isNothing (strMetaQualified meta)
|
|
| 87 | + if overloaded && isNothing mQual
|
|
| 88 | 88 | then do
|
| 89 | 89 | (fromStringExpr, fns) <- lookupSyntaxExpr KnownOccs.fromStringClassOpOcc
|
| 90 | 90 | pure (nlHsApp (noLocA fromStringExpr), fns)
|
| ... | ... | @@ -21,7 +21,6 @@ import Language.Haskell.Syntax.Text |
| 21 | 21 | import Language.Haskell.Syntax.Module.Name
|
| 22 | 22 | import Language.Haskell.Syntax.Type
|
| 23 | 23 | import Language.Haskell.Syntax.Binds
|
| 24 | -import Language.Haskell.Syntax.StringMeta (StringMeta)
|
|
| 25 | 24 | |
| 26 | 25 | -- libraries:
|
| 27 | 26 | import Data.Data hiding (Fixity(..))
|
| ... | ... | @@ -345,7 +344,7 @@ data HsExpr p |
| 345 | 344 | | -- | See Note [Parsing interpolated strings]
|
| 346 | 345 | HsInterString
|
| 347 | 346 | (XInterString p)
|
| 348 | - StringMeta
|
|
| 347 | + (Maybe HsModuleName)
|
|
| 349 | 348 | [HsInterStringPart p]
|
| 350 | 349 | |
| 351 | 350 | -- | Lambda, Lambda-case, and Lambda-cases
|
| ... | ... | @@ -591,6 +590,7 @@ data HsLamVariant |
| 591 | 590 | data HsInterStringPart p
|
| 592 | 591 | = HsInterStringRaw (XInterStringRaw p) HText
|
| 593 | 592 | | HsInterStringExpr (XInterStringExpr p) (LHsExpr p)
|
| 593 | + | XHsInterStringPart !(XXInterStringPart p)
|
|
| 594 | 594 | |
| 595 | 595 | {-
|
| 596 | 596 | Note [Parens in HsSyn]
|
| ... | ... | @@ -500,6 +500,7 @@ type family XQualLitE x |
| 500 | 500 | type family XInterString x
|
| 501 | 501 | type family XInterStringRaw x
|
| 502 | 502 | type family XInterStringExpr x
|
| 503 | +type family XXInterStringPart x
|
|
| 503 | 504 | type family XLam x
|
| 504 | 505 | type family XLamCase x
|
| 505 | 506 | type family XApp x
|
| ... | ... | @@ -46,6 +46,14 @@ allTests = |
| 46 | 46 | , expression =
|
| 47 | 47 | let s = "world" in s ++ s" hello ${s}"
|
| 48 | 48 | }
|
| 49 | + , TestCase -- dollars
|
|
| 50 | + { label =
|
|
| 51 | + """
|
|
| 52 | + "$foo \$foo \${foo}"
|
|
| 53 | + """
|
|
| 54 | + , expression =
|
|
| 55 | + "$foo \$foo \${foo}"
|
|
| 56 | + }
|
|
| 49 | 57 | , TestCase -- curly brace Char
|
| 50 | 58 | { label =
|
| 51 | 59 | """
|
| ... | ... | @@ -54,6 +62,30 @@ allTests = |
| 54 | 62 | , expression =
|
| 55 | 63 | s"a ${'}'} b"
|
| 56 | 64 | }
|
| 65 | + , TestCase -- interpolated multiline expression
|
|
| 66 | + { label =
|
|
| 67 | + """
|
|
| 68 | + s"example ${fromMaybe
|
|
| 69 | + (0 :: Int)
|
|
| 70 | + (Just 10)} value"
|
|
| 71 | + """
|
|
| 72 | + , expression =
|
|
| 73 | + s"example ${fromMaybe
|
|
| 74 | + (0 :: Int)
|
|
| 75 | + (Just 10)} value"
|
|
| 76 | + }
|
|
| 77 | + , TestCase -- interpolated expression blocks
|
|
| 78 | + { label =
|
|
| 79 | + """
|
|
| 80 | + let a = s"${case True of True -> 1; False -> 2 :: Int}"
|
|
| 81 | + b = s"${do True}"
|
|
| 82 | + in a ++ b
|
|
| 83 | + """
|
|
| 84 | + , expression =
|
|
| 85 | + let a = s"${case True of True -> 1; False -> 2 :: Int}"
|
|
| 86 | + b = s"${do True}"
|
|
| 87 | + in a ++ b
|
|
| 88 | + }
|
|
| 57 | 89 | , TestCase -- interpolated expression with comment
|
| 58 | 90 | { label =
|
| 59 | 91 | """
|
| ... | ... | @@ -146,7 +178,24 @@ allTests = |
| 146 | 178 | , expression =
|
| 147 | 179 | let x = "test" in s"a “${x}” b"
|
| 148 | 180 | }
|
| 149 | - -- TODO(bchinn): interpolated multiline strings
|
|
| 181 | + , TestCase -- interpolated multiline strings
|
|
| 182 | + { label =
|
|
| 183 | + """
|
|
| 184 | + let x = "test" in
|
|
| 185 | + s\"""
|
|
| 186 | + line one
|
|
| 187 | + ${x}
|
|
| 188 | + a "quoted value"
|
|
| 189 | + \"""
|
|
| 190 | + """
|
|
| 191 | + , expression =
|
|
| 192 | + let x = "test" in
|
|
| 193 | + s"""
|
|
| 194 | + line one
|
|
| 195 | + ${x}
|
|
| 196 | + a "quoted value"
|
|
| 197 | + """
|
|
| 198 | + }
|
|
| 150 | 199 | ]
|
| 151 | 200 | |
| 152 | 201 | {----- Types for tests -----}
|
| ... | ... | @@ -933,6 +933,11 @@ PprQualifiedStrings: |
| 933 | 933 | $(CHECK_PPR) $(LIBDIR) PprQualifiedStrings.hs
|
| 934 | 934 | $(CHECK_EXACT) $(LIBDIR) PprQualifiedStrings.hs
|
| 935 | 935 | |
| 936 | +.PHONY: PprStringInterpolation
|
|
| 937 | +PprStringInterpolation:
|
|
| 938 | + $(CHECK_PPR) $(LIBDIR) PprStringInterpolation.hs
|
|
| 939 | + $(CHECK_EXACT) $(LIBDIR) PprStringInterpolation.hs
|
|
| 940 | + |
|
| 936 | 941 | .PHONY: Haddock1
|
| 937 | 942 | Haddock1:
|
| 938 | 943 | # $(CHECK_PPR) $(LIBDIR) Haddock1.hs
|
| 1 | +{-# LANGUAGE MultilineStrings #-}
|
|
| 2 | +{-# LANGUAGE QualifiedStrings #-}
|
|
| 3 | +{-# LANGUAGE StringInterpolation #-}
|
|
| 4 | + |
|
| 5 | +module PprQualifiedStrings where
|
|
| 6 | + |
|
| 7 | +import qualified Example.Text as Text
|
|
| 8 | + |
|
| 9 | +exprs :: IO ()
|
|
| 10 | +exprs = do
|
|
| 11 | + let name = "Alice"
|
|
| 12 | + print s"My name is: ${name}. Nice to meet you!"
|
|
| 13 | + print s"""
|
|
| 14 | + My name is:
|
|
| 15 | + ${name}
|
|
| 16 | + Nice to meet you!
|
|
| 17 | + """
|
|
| 18 | + print Text.s"My name is: ${name}. Nice to meet you!"
|
|
| 19 | + print Text.s"""
|
|
| 20 | + My name is:
|
|
| 21 | + ${name}
|
|
| 22 | + Nice to meet you!
|
|
| 23 | + """ |
| ... | ... | @@ -226,3 +226,4 @@ test('PprQualifiedStrings', [ignore_stderr,req_ppr_deps], makefile_test, ['PprQu |
| 226 | 226 | test('Haddock1', [ignore_stderr,req_ppr_deps], makefile_test, ['Haddock1'])
|
| 227 | 227 | test('T27423c', [ignore_stderr,req_ppr_deps], makefile_test, ['T27423c'])
|
| 228 | 228 | test('PprStringNoSourceText', normal, compile, [''])
|
| 229 | +test('PprStringInterpolation', [ignore_stderr,req_ppr_deps], makefile_test, ['PprStringInterpolation']) |