Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cmd quasiquoter #75

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/Streamly/Internal/System.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
-- |
-- Module : Streamly.Internal.System
-- Copyright : (c) 2022 Composewell Technologies
-- License : Apache-2.0
-- Maintainer : [email protected]
-- Stability : experimental
-- Portability : GHC
--
{-# LANGUAGE TemplateHaskell #-}

module Streamly.Internal.System
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in the command module rather than creating a new module.

( cmd
, trim
)
where

import Control.Applicative (Alternative(..))
import Control.Exception (displayException)
import Data.Functor.Identity (runIdentity)
import Streamly.Internal.Data.Parser (Parser)

import Language.Haskell.TH
import Language.Haskell.TH.Quote

import qualified Streamly.Data.Fold as Fold
import qualified Streamly.Internal.Data.Parser as Parser
(some, many, takeWhile1)
import qualified Streamly.Data.Stream as Stream (fromList, parse)
import qualified Streamly.Internal.Unicode.Parser as Parser

data StrSegment
= StrText String
| StrVar String
deriving (Show, Eq)

formatSpace :: String -> String
formatSpace = foldr go ""
where
go x acc = x:if x == ' ' then dropWhile (' ' ==) acc else acc

-- | Replace a newline by a space and convert multiple spaces to single space
--
-- >>> :set -XQuasiQuotes
-- >>> import Streamly.Internal.System
-- >>> trim " abc \n bbb \n ccc "
-- " abc bbb ccc "
--
trim :: String -> String
trim = formatSpace <$> (unwords . fmap formatSpace . lines)
Comment on lines +36 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only need formatSpace and trim.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have idiomatic code to do this:

-- | Replace newlines followed by any number of spaces with a single space.
oneLine :: String -> String
oneLine = unwords . fmap (dropWhile isSpace) . lines


haskellIdentifier :: Monad m => Parser Char m String
haskellIdentifier =
let p = Parser.alphaNum <|> Parser.char '\'' <|> Parser.char '_'
in Parser.some p Fold.toList

strParser :: Monad m => Parser Char m [StrSegment]
strParser = Parser.many content Fold.toList

where

plainText = StrText . trim <$> Parser.takeWhile1 (/= '#') Fold.toList
escHash = StrText . (: []) <$> (Parser.char '#' *> Parser.char '#')
lineCont = StrText [] <$ (Parser.char '#' *> Parser.char '\n')
var = StrVar <$>
( Parser.char '#'
*> Parser.char '{'
*> haskellIdentifier
<* Parser.char '}'
)
plainHash = StrText . (: []) <$> Parser.char '#'

-- order is important
content = plainText <|> escHash <|> lineCont <|> var <|> plainHash

strSegmentExp :: StrSegment -> Q Exp
strSegmentExp (StrText text) = stringE text
strSegmentExp (StrVar name) = do
valueName <- lookupValueName name
case valueName of
Just vn -> varE vn
Nothing ->
fail
$ "cmd quote: Haskell symbol `" ++ name
++ "` is not in scope"

strExp :: [StrSegment] -> Q Exp
strExp xs = appE [| concat |] $ listE $ map strSegmentExp xs
Comment on lines +31 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need all this. This is replicated.


expandVars :: String -> Q Exp
expandVars ln =
case runIdentity $ Stream.parse strParser (Stream.fromList ln) of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can import strParser from Unicode.String. If not exported, you can export it internally.
After runIdentity, you can use trim and formatSpace

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's import as much as we can from streamly-core instead of writing it again.

Left e ->
fail $ "cmd QuasiQuoter parse error: " ++ displayException e
Right x ->
strExp x

-- | A QuasiQuoter that treats the input as a string literal:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to change the documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can point it to the str documentation and add the doc for the changes that you made.

--
-- >>> [cmd|x|]
-- "x"
--
-- Any @#{symbol}@ is replaced by the value of the Haskell symbol @symbol@
-- which is in scope:
--
-- >>> x = "hello"
-- >>> [cmd|#{x} world!|]
-- "hello world!"
--
-- @##@ means a literal @#@ without the special meaning for referencing
-- haskell symbols:
--
-- >>> [cmd|##{x} world!|]
-- "#{x} world!"
--
-- A @#@ at the end of line means the line continues to the next line without
-- introducing a newline character:
--
-- >>> :{
-- [cmd|hello#
-- world!|]
-- :}
-- "hello world!"
--
-- Bugs: because of a bug in parsers, a lone # at the end of input gets
-- removed.
--
cmd :: QuasiQuoter
cmd =
QuasiQuoter
{ quoteExp = expandVars
, quotePat = notSupported
, quoteType = notSupported
, quoteDec = notSupported
}

where

notSupported = error "cmd: Not supported."
4 changes: 4 additions & 0 deletions streamly-process.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ library
import: compile-options, optimization-options
hs-source-dirs: src
exposed-modules:

Streamly.System.Process
Streamly.System.Command
Streamly.Internal.System
Streamly.Internal.System.Process
Streamly.Internal.System.Command
if flag (use-native) && !os(windows)
Expand All @@ -96,6 +98,8 @@ library
-- Uses internal APIs
, streamly == 0.9.0.*
, streamly-core == 0.1.0
, template-haskell >= 2.14 && < 2.21

if !flag(use-native)
build-depends: process >= 1.0 && < 1.7
else
Expand Down