bricks-parsec (empty) → 0.0.0.4
raw patch · 5 files changed
+1690/−0 lines, 5 filesdep +basedep +bricks-internaldep +bricks-internal-test
Dependencies added: base, bricks-internal, bricks-internal-test, bricks-parsec, bricks-rendering, bricks-syntax, containers, doctest, hedgehog, parsec, text
Files
- bricks-parsec.cabal +80/−0
- license.txt +13/−0
- src/Bricks/Parsec.hs +952/−0
- test/cases.hs +641/−0
- test/doctest.hs +4/−0
+ bricks-parsec.cabal view
@@ -0,0 +1,80 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 2fff74c1e98a9b2d2205089727ad3ad2828ae83f37e1a4b5a35ebcaa79c68835++name: bricks-parsec+version: 0.0.0.4+synopsis: ...++description: ...+category: Language+homepage: https://github.com/chris-martin/bricks#readme+bug-reports: https://github.com/chris-martin/bricks/issues+author: Chris Martin <ch.martin@gmail.com>+maintainer: Chris Martin <ch.martin@gmail.com>+license: Apache-2.0+license-file: license.txt+build-type: Simple+cabal-version: >= 1.10++source-repository head+ type: git+ location: https://github.com/chris-martin/bricks++library+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ base >=4.9 && <4.11+ , bricks-internal+ , bricks-syntax+ , containers >=0.5.7 && <0.6+ , parsec >=3.1.6 && <3.2+ , text >=1.2.2 && <1.3+ exposed-modules:+ Bricks.Parsec+ other-modules:+ Paths_bricks_parsec+ default-language: Haskell2010++test-suite cases+ type: exitcode-stdio-1.0+ main-is: cases.hs+ hs-source-dirs:+ test+ ghc-options: -Wall -threaded+ build-depends:+ base >=4.9 && <4.11+ , bricks-internal+ , bricks-internal-test+ , bricks-parsec+ , bricks-rendering+ , bricks-syntax+ , containers >=0.5.7 && <0.6+ , hedgehog >=0.5 && <0.6+ , parsec >=3.1.6 && <3.2+ , text >=1.2.2 && <1.3+ other-modules:+ Paths_bricks_parsec+ default-language: Haskell2010++test-suite doctest+ type: exitcode-stdio-1.0+ main-is: doctest.hs+ hs-source-dirs:+ test+ ghc-options: -Wall -threaded+ build-depends:+ base >=4.9 && <4.11+ , bricks-internal+ , bricks-syntax+ , containers >=0.5.7 && <0.6+ , doctest >=0.11 && <0.14+ , parsec >=3.1.6 && <3.2+ , text >=1.2.2 && <1.3+ other-modules:+ Paths_bricks_parsec+ default-language: Haskell2010
+ license.txt view
@@ -0,0 +1,13 @@+Copyright 2017 Chris Martin++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ src/Bricks/Parsec.hs view
@@ -0,0 +1,952 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++{- |++Parsec 'Parser's for the Bricks language.++Most parsers consume trailing whitespace, except ones that operate within quoted+string environments where whitespace is significant.++-}+module Bricks.Parsec+ (+ -- * Expressions+ parse'expression+ , parse'expression'paren+ , parse'expression'antiquote+ , parse'expression'dictKey++ -- * Expression lists+ , parse'expressionList+ , parse'expressionList'1+ , parse'expressionList'1'noDot++ -- * Variables+ , parse'var++ -- * Strings+ , parse'strUnquoted+ , parse'strStatic+ , parse'strStatic'quoted+ , parse'strStatic'unquoted+ , parse'str'dynamic+ , parse'str'within'normalQ+ , parse'str'escape'normalQ+ , parse'inStr+ , parse'inStr'1++ -- * Lists+ , parse'list++ -- * Dicts+ , parse'dict+ , parse'dict'rec+ , parse'dict'noRec+ , parse'dictBinding+ , parse'dictBinding'inherit+ , parse'dictBinding'eq++ -- * Dict lookup+ , parse'dot'rhs'chain++ -- * Lambdas+ , parse'lambda++ -- * Function parameters+ , parse'param+ , parse'param'var+ , parse'param'noVar+ , parse'dictPattern+ , parse'dictPattern'start++ -- * @let@+ , parse'let+ , parse'letBinding+ , parse'letBinding'eq+ , parse'letBinding'inherit++ -- * Comments and whitespace+ , parse'spaces+ , parse'comment+ , parse'comment'inline+ , parse'comment'block++ -- * Keywords+ , parse'keyword++ ) where++-- Bricks+import Bricks.Expression+import Bricks.Keyword+import Bricks.Source+import Bricks.UnquotedString++-- Bricks internal+import Bricks.Internal.Prelude+import Bricks.Internal.Seq (Seq, (|>))+import qualified Bricks.Internal.Seq as Seq+import Bricks.Internal.Text (Text)+import qualified Bricks.Internal.Text as Text++-- Parsec+import Text.Parsec ((<?>))+import qualified Text.Parsec as P+import Text.Parsec.Text (Parser)++-- Containers+import Data.Set (Set)+import qualified Data.Set as Set++-- Base+import Control.Monad (fail)+import Prelude (fromIntegral, succ)++-- $setup+--+-- ==== Doctest setup+--+-- >>> import Data.Foldable (length)+-- >>> import Text.Parsec (parseTest)+-- >>> import Prelude (putStrLn)++parse'position :: Parser SourcePosition+parse'position = do+ x <- P.getPosition+ pure SourcePosition+ { sourcePosition'line = fromIntegral $ P.sourceLine x+ , sourcePosition'column = fromIntegral $ P.sourceColumn x+ }++parse'spaces :: Parser ()+parse'spaces =+ void $ P.many (void (P.space <?> "") <|> parse'comment)++parse'comment :: Parser ()+parse'comment =+ parse'comment'inline <|> parse'comment'block++parse'comment'inline :: Parser ()+parse'comment'inline =+ void $ P.try (P.string "--" <?> "") *> P.manyTill P.anyChar (P.char '\n')++parse'comment'block :: Parser ()+parse'comment'block =+ start <* P.manyTill middle end+ where+ start = void $ P.try (P.string "{-" <?> "")+ middle = parse'comment'block <|> void P.anyChar+ end = P.try (P.string "-}")++{- | Backtracking parser for a particular keyword. -}++parse'keyword :: Keyword -> Parser ()+parse'keyword k =+ P.try (void p)+ where+ p =+ -- Consume the keyword+ P.string (keywordString k) *>++ -- Do /not/ consume any subsequent character that are allowed to be part+ -- of a valid identifier. For example, this prevents this parser from+ -- interpreting the beginning of an identifier named "letter" as the+ -- keyword "let".+ P.notFollowedBy (P.satisfy char'canBeUnquoted) *>++ -- As usual, consume trailing spaces.+ parse'spaces++{- | Parser for an unquoted string. Unquoted strings are restricted to a+conservative set of characters, and they may not be any of the keywords. See+'text'canBeUnquoted' for a complete description of the unquoted string rules. -}++-- | ==== Examples+--+-- >>> parseTest parse'strUnquoted "abc"+-- ("abc",1:1-1:4)+--+-- Here the parser consumes letters up to but not including @{@, because that+-- character does not satisfy 'char'canBeUnquoted':+--+-- >>> parseTest parse'strUnquoted "ab{c"+-- ("ab",1:1-1:3)+--+-- \"let\" does not parse as an unquoted string because @let@ is a keyword:+--+-- >>> parseTest parse'strUnquoted "let"+-- parse error at (line 1, column 4):+-- unexpected end of input+--+-- This parser does /not/ parse quoted strings:+--+-- >>> parseTest parse'strUnquoted "\"abc\""+-- parse error at (line 1, column 1):+-- unexpected "\""++parse'strUnquoted :: Parser (UnquotedString, SourceRange)+parse'strUnquoted = do++ pos'1 <- parse'position+ text <- P.many1 (P.satisfy char'canBeUnquoted) <&> Text.pack+ pos'2 <- parse'position++ -- Fail if what we just parsed isn't a valid unquoted string+ case unquotedString'try text of+ Nothing -> P.parserZero+ Just b -> do+ _ <- parse'spaces+ pure (b, SourceRange pos'1 pos'2)++parse'var :: Parser Var+parse'var = do+ (a, b) <- parse'strUnquoted+ pure $ Var a (Just b)++{- | Parser for a static string which may be either quoted or unquoted. -}++-- | ==== Examples+--+-- >>> parseTest parse'strStatic "\"hello\""+-- {- 1:1-1:8 -} "hello"+--+-- >>> parseTest parse'strStatic "hello"+-- {- 1:1-1:6 -} "hello"+--+-- >>> parseTest parse'strStatic "\"a b\""+-- {- 1:1-1:6 -} "a b"+--+-- >>> parseTest parse'strStatic "a b"+-- {- 1:1-1:2 -} "a"+--+-- By "static," we mean that the string may /not/ contain antiquotation:+--+-- >>> parseTest parse'strStatic "\"a${x}b\" xyz"+-- parse error at (line 1, column 5):+-- antiquotation is not allowed in this context++parse'strStatic :: Parser Str'Static+parse'strStatic =+ (parse'strStatic'quoted <|> parse'strStatic'unquoted) <?> "static string"++{- | Parser for a static string that is quoted. -}++parse'strStatic'quoted :: Parser Str'Static+parse'strStatic'quoted =+ do+ pos'1 <- parse'position+ _ <- P.char '"'+ Str'Static text _ <- parse'str'within'normalQ+ _ <- P.char '"' <|> anti+ pos'2 <- parse'position+ _ <- parse'spaces+ let src = Just (SourceRange pos'1 pos'2)+ pure $ Str'Static text src+ where+ anti = do+ _ <- P.string "${"+ fail "antiquotation is not allowed in this context"++{- | Parser for an unquoted static string. -}++parse'strStatic'unquoted :: Parser Str'Static+parse'strStatic'unquoted = do+ (a, b) <- parse'strUnquoted+ pure $ Str'Static (unquotedString'text a) (Just b)++{- | Parser for a dynamic string enclosed in quotes (@"@ ... @"@). -}++parse'str'dynamic :: Parser Str'Dynamic+parse'str'dynamic =+ do+ pos'1 <- parse'position+ _ <- P.char '"'+ xs <- go Seq.empty+ pos'2 <- parse'position+ _ <- parse'spaces+ let src = Just (SourceRange pos'1 pos'2)+ pure $ Str'Dynamic xs src+ where+ go :: Seq Str'1 -> Parser (Seq Str'1)+ go previousParts =+ asum+ [ do+ _ <- P.char '"'+ pure previousParts+ , do+ x <- lit <|> (parse'expression'antiquote <&> Str'1'Antiquote)+ go $ previousParts |> x+ ]++ -- Read some literal characters+ lit :: Parser Str'1+ lit = parse'str'within'normalQ <&> Str'1'Literal++{- | Parser for at least one normal character, within a normally-quoted string+context, up to but not including the end of the string or the start of an+antiquotation. -}++parse'str'within'normalQ :: Parser Str'Static+parse'str'within'normalQ =+ do+ pos'1 <- parse'position+ t <- text+ pos'2 <- parse'position+ let src = Just (SourceRange pos'1 pos'2)+ pure $ Str'Static t src++ where+ text :: Parser Text+ text = P.many1 (char <|> parse'str'escape'normalQ) <&> Text.concat++ char :: Parser Text+ char = asum+ [ P.satisfy (\x -> x /= '$' && x /= '"' && x /= '\\')+ , P.try $ P.char '$' <* P.notFollowedBy (P.char '{')+ ] <&> Text.singleton++parse'str'escape'normalQ :: Parser Text+parse'str'escape'normalQ =+ P.char '\\' *> esc+ where+ esc = asum+ [ P.char '\\' $> "\\"+ , P.char '"' $> "\""+ , P.char 'n' $> "\n"+ , P.char 'r' $> "\r"+ , P.char 't' $> "\t"+ , P.string "${" $> "${"+ ]++{- | Parser for a dynamic string enclosed in "indented string" format, delimited+by two single-quotes @''@ ... @''@.++This form of string does not have any escape sequences. Therefore the only way+to express @''@ or @${@ within an indented string is to antiquote them. -}++-- | ==== Examples+--+-- >>> x = "''${\"''\"} and ${\"\\${\"}''"+--+-- >>> putStrLn x+-- ''${"''"} and ${"\${"}''+--+-- >>> parseTest (inStr'discardSource <$> parse'inStr) x+-- str'indented [indent 0 [antiquote (str ["''"]), " and ", antiquote (str ["${"])] Nothing]+--+-- >>> parseTest parse'inStr x+-- {- 1:1-1:25 -} str'indented [indent {- 1:3-1:3 -} 0 [antiquote ({- 1:5-1:9 -} str [{- 1:6-1:8 -} "''"]), {- 1:10-1:15 -} " and ", antiquote ({- 1:17-1:22 -} str [{- 1:18-1:21 -} "${"])] Nothing]++parse'inStr :: Parser InStr+parse'inStr =+ do+ pos'1 <- parse'position+ _ <- P.string "''"+ lines <- go Seq.empty+ pos'2 <- parse'position+ _ <- parse'spaces+ let src = Just (SourceRange pos'1 pos'2)+ pure $ InStr lines src++ where+ go :: Seq InStr'1 -> Parser (Seq InStr'1)+ go previousLines =+ do+ x <- parse'inStr'1+ let newLines = previousLines |> x++ if isJust (inStr'1'lineBreak x)+ then go newLines+ else P.string "''" $> newLines++{- | Parser for a single line of an 'InStr'. -}++parse'inStr'1 :: Parser InStr'1+parse'inStr'1 =+ do+ pos'1 <- parse'position+ n <- parse'count (P.char ' ')+ pos'2 <- parse'position+ (str, break) <- go Seq.empty+ pure InStr'1+ { inStr'1'level = n+ , inStr'1'indentSource = Just (SourceRange pos'1 pos'2)+ , inStr'1'str = str+ , inStr'1'lineBreak = break+ }+ where+ go :: Seq Str'1 -> Parser (Seq Str'1, Maybe Str'Static)+ go previousParts =+ asum+ [ do+ break <- end+ pure (previousParts, break)+ , do+ x <- chars+ go (previousParts |> x)+ , do+ x <- parse'expression'antiquote+ go (previousParts |> Str'1'Antiquote x)+ ]++ end :: Parser (Maybe Str'Static)+ end = asum+ [ do+ end'pos'1 <- parse'position+ s <- P.char '\n'+ end'pos'2 <- parse'position+ pure $ Just $ Str'Static+ { str'static'text = Text.singleton s+ , str'static'source = Just (SourceRange end'pos'1 end'pos'2)+ }+ , do+ _ <- P.lookAhead . P.try $ P.string "''"+ pure Nothing+ ]++ chars :: Parser Str'1+ chars = do+ chars'pos'1 <- parse'position+ s <- P.many1 $ asum+ [ P.satisfy (\c -> c /= '$' && c /= '\'' && c /= '\n')+ , P.try $ P.char '$' <* P.notFollowedBy (P.char '{')+ , P.try $ P.char '\'' <* P.notFollowedBy (P.char '\'')+ ]+ chars'pos'2 <- parse'position+ pure $ Str'1'Literal $ Str'Static+ { str'static'text = Text.pack s+ , str'static'source = Just (SourceRange chars'pos'1 chars'pos'2)+ }++{- | Parser for a function parameter (the beginning of a 'Lambda'), including+the colon. This forms part of 'parse'expression', so it backtracks in places+where it has overlap with other types of expressions. -}++parse'param :: Parser Param+parse'param =+ parse'param'var <|> parse'param'noVar++{- | Parser for a parameter that starts with a variable. This could be a simple+param that consists only of /only/ the variable, or the variable may be followed+by a dict pattern. -}++parse'param'var :: Parser Param+parse'param'var = do+ -- This part backtracks because until we get to the : or @, we don't+ -- know whether the variable name we're reading is a lambda parameter+ -- or just the name by itself (and not part of a lambda).+ (a, b) <- P.try $ do+ a <- parse'var <* parse'spaces+ b <- ((P.char ':' $> False) <|> (P.char '@' $> True)) <* parse'spaces+ pure (a, b)+ if b+ -- If we read an @, then the next thing is a pattern.+ then parse'dictPattern <* P.char ':' <* parse'spaces <&> Param'Both a+ -- Otherwise it's just the variable and we're done.+ else pure $ Param'Name a++{- | Parser for a param that has no variable, only a a dict pattern. This parser+backtracks because the beginning of a dict pattern looks like the beginning of a+dict expression. -}++parse'param'noVar :: Parser Param+parse'param'noVar = Param'DictPattern <$> do+ -- First we look ahead to determine whether it looks like a lambda.+ _ <- P.try . P.lookAhead $ parse'dictPattern'start++ -- And if so, then we go on and parse the dict pattern with no+ -- further backtracking.+ parse'dictPattern <* P.char ':' <* parse'spaces++{- | Parser for a dict pattern (the type of lambda parameter that does dict+destructuring. This parser does not backtrack. -}++parse'dictPattern :: Parser DictPattern+parse'dictPattern =+ P.char '{' *> parse'spaces *> go Seq.empty Set.empty+ where++ -- We keep track of what we've parsed so far in two forms:+ go :: Seq DictPattern'1 -- 1. A sequence of items (which will be+ -- included directly in the result)+ -> Set Text -- 2. A set of the names of the items (which is+ -- used to test each new item so we can issue+ -- an error message if the list contains two+ -- items having the same name)+ -> Parser DictPattern++ go previousItems previousNames =+ asum+ [ end $> DictPattern previousItems False+ , ellipsis $> DictPattern previousItems True+ , more+ ]+ where+ more :: Parser DictPattern+ more = item >>= \newItem ->+ let+ newName = var'text (dictPattern'1'name newItem)+ newItems = previousItems |> newItem+ newNames = Set.insert newName previousNames++ in+ if newName `Set.member` previousNames+ then fail $ "Name " <> Text.unpack newName <>+ " appears twice in a dict pattern"+ else asum+ [ P.char ',' *> parse'spaces *> go newItems newNames+ , end $> DictPattern newItems False+ ]++ item :: Parser DictPattern'1+ item = DictPattern'1 <$> parse'var <*> P.optionMaybe def++ def :: Parser Expression+ def = P.char '?' *> parse'spaces *> parse'expression++ ellipsis :: Parser ()+ ellipsis = P.string "..." *> parse'spaces *> end++ end :: Parser ()+ end = P.char '}' *> parse'spaces++{- | This is used in a lookahead by 'parse'param' to determine whether we're+about to start parsing a 'DictPattern'. -}++parse'dictPattern'start :: Parser ()+parse'dictPattern'start =+ P.char '{' *> parse'spaces *> asum+ [ void $ P.string "..."+ , void $ P.char '}' *> parse'spaces *> P.char ':'+ , void $ parse'strUnquoted *> (P.char ',' <|> P.char '?' <|> P.char '}')+ ]++{- | Parser for a lambda expression (@x: y@). -}++-- | ==== Examples+--+-- >>> test = parseTest (lambda'discardSource <$> parse'lambda)+--+-- >>> test "x: [x x \"a\"]"+-- lambda (param "x") (list [var "x", var "x", str ["a"]])+--+-- >>> test "{a,b}:a"+-- lambda (pattern [dict'param "a", dict'param "b"]) (var "a")+--+-- >>> test "{ ... }: \"x\""+-- lambda (pattern [] <> ellipsis) (str ["x"])+--+-- >>> test "a@{ f, b ? g x, ... }: f b"+-- lambda (param "a" <> pattern [dict'param "f", dict'param "b" & def (apply (var "g") (var "x"))] <> ellipsis) (apply (var "f") (var "b"))+--+-- >>> test "a: b: \"x\""+-- lambda (param "a") (lambda (param "b") (str ["x"]))++parse'lambda :: Parser Lambda+parse'lambda = do+ pos'1 <- parse'position+ head <- parse'param+ body <- parse'expression+ let pos'2 = sourceRange'end <$> expression'source body+ let src = SourceRange pos'1 <$> pos'2+ pure $ Lambda head body src++{- | Parser for a list expression (@[ ... ]@). -}++-- | ==== Examples+--+-- >>> parseTest parse'list "[]"+-- {- 1:1-1:3 -} list []+--+-- >>> :{+-- >>> parseTest (list'discardSource <$> parse'list)+-- >>> "[x \"one\" (a: b) (c d)]"+-- >>> :}+-- list [var "x", str ["one"], lambda (param "a") (var "b"), apply (var "c") (var "d")]++parse'list :: Parser List+parse'list =+ do+ pos'1 <- parse'position+ _ <- P.char '['+ _ <- parse'spaces+ x <- parse'expressionList+ _ <- P.char ']'+ pos'2 <- parse'position+ _ <- parse'spaces+ let src = (Just (SourceRange pos'1 pos'2))+ pure $ List (Seq.fromList x) src++{- | Parser for a dict expression, either recursive (@rec@ keyword) or not. -}++-- | ==== Examples+--+-- >>> parseTest parse'dict "{}"+-- {- 1:1-1:3 -} dict []+--+-- >>> parseTest parse'dict "rec { }"+-- {- 1:1-1:8 -} rec'dict []+--+-- >>> :{+-- >>> parseTest (dict'discardSource <$> parse'dict)+-- >>> "{ a = b; inherit (x) y z \"s t\"; }"+-- >>> :}+-- dict [dict'eq (str ["a"]) (var "b"), dict'inherit'from (var "x") ["y", "z", "s t"]]++parse'dict :: Parser Dict+parse'dict =+ parse'dict'noRec <|> parse'dict'rec++{- | Parser for a recursive (@rec@ keyword) dict. -}++-- | ==== Examples+--+-- >>> parseTest parse'dict'rec "rec { }"+-- {- 1:1-1:8 -} rec'dict []+--+-- >>> :{+-- >>> parseTest (dict'discardSource <$> parse'dict'rec)+-- >>> "rec { a = \"1\"; b = \"${a}2\"; }"+-- >>> :}+-- rec'dict [dict'eq (str ["a"]) (str ["1"]), dict'eq (str ["b"]) (str [antiquote (var "a"), "2"])]++parse'dict'rec :: Parser Dict+parse'dict'rec =+ do+ pos'1 <- parse'position+ _ <- parse'keyword keyword'rec+ Dict _ xs src' <- parse'dict'noRec+ let src = src' <&> \s -> s{ sourceRange'start = pos'1 }+ pure $ Dict True xs src++{- | Parser for a non-recursive (no @rec@ keyword) dict. -}++-- | ==== Examples+--+-- >>> parseTest parse'dict'noRec "{ }"+-- {- 1:1-1:4 -} dict []+--+-- >>> :{+-- >>> parseTest (dict'discardSource <$> parse'dict'noRec)+-- >>> "{ a = \"1\"; b = \"${a}2\"; }"+-- >>> :}+-- dict [dict'eq (str ["a"]) (str ["1"]), dict'eq (str ["b"]) (str [antiquote (var "a"), "2"])]++parse'dict'noRec :: Parser Dict+parse'dict'noRec =+ do+ pos'1 <- parse'position+ _ <- P.char '{'+ _ <- parse'spaces+ xs <- go Seq.empty+ pos'2 <- parse'position+ _ <- parse'spaces+ let src = Just (SourceRange pos'1 pos'2)+ pure $ Dict False xs src+ where+ go :: Seq DictBinding -> Parser (Seq DictBinding)+ go previousBindings =+ asum+ [ do+ _ <- P.char '}'+ pure previousBindings+ , do+ a <- parse'dictBinding+ go (previousBindings |> a)+ ]++{- | Parser for a chain of dict lookups (like @.a.b.c@) on the right-hand side+of a 'Dot' expression. -}++-- | ==== Examples+--+-- >>> parseTest parse'dot'rhs'chain ""+-- []+--+-- >>> parseTest parse'dot'rhs'chain ".abc"+-- [{- 1:2-1:5 -} str [{- 1:2-1:5 -} "abc"]]+--+-- >>> :{+-- >>> parseTest (fmap expression'discardSource <$> parse'dot'rhs'chain)+-- >>> ".a.${b}.\"c\".\"d${e}\""+-- >>> :}+-- [str ["a"],var "b",str ["c"],str ["d", antiquote (var "e")]]++parse'dot'rhs'chain :: Parser [Expression]+parse'dot'rhs'chain =+ P.many dot+ where+ dot = do+ _ <- P.char '.'+ _ <- parse'spaces+ x <- parse'expression'dictKey+ _ <- parse'spaces+ pure x++parse'let :: Parser Let+parse'let =+ do+ pos'1 <- parse'position+ _ <- parse'keyword keyword'let+ (xs, e) <- go Seq.empty+ let+ src = do+ pos'2 <- sourceRange'end <$> expression'source e+ pure $ SourceRange pos'1 pos'2+ pure $ Let xs e src+ where+ go :: Seq LetBinding -> Parser (Seq LetBinding, Expression)+ go previousBindings =+ asum+ [ do+ _ <- parse'keyword keyword'in+ e <- parse'expression+ pure (previousBindings, e)+ , do+ a <- parse'letBinding+ go (previousBindings |> a)+ ]++parse'dictBinding :: Parser DictBinding+parse'dictBinding =+ parse'dictBinding'inherit <|> parse'dictBinding'eq++parse'dictBinding'inherit :: Parser DictBinding+parse'dictBinding'inherit =+ do+ _ <- parse'keyword keyword'inherit+ asum+ [ do+ a <- parse'expression'paren+ xs <- go'strs Seq.empty+ pure $ DictBinding'Inherit'Dict a xs+ , do+ xs <- go'vars Seq.empty+ pure $ DictBinding'Inherit'Var xs+ ]+ where+ go'strs :: Seq Str'Static -> Parser (Seq Str'Static)+ go'strs previousList =+ asum+ [ do+ _ <- P.char ';'+ _ <- parse'spaces+ pure previousList+ , do+ x <- parse'strStatic+ go'strs (previousList |> x)+ ]++ go'vars :: Seq Var -> Parser (Seq Var)+ go'vars previousList =+ asum+ [ do+ _ <- P.char ';'+ _ <- parse'spaces+ pure previousList+ , do+ x <- parse'var+ go'vars (previousList |> x)+ ]++parse'dictBinding'eq :: Parser DictBinding+parse'dictBinding'eq =+ do+ key <- parse'expression'dictKey+ _ <- parse'spaces+ _ <- P.char '='+ _ <- parse'spaces+ val <- parse'expression+ _ <- parse'spaces+ _ <- P.char ';'+ _ <- parse'spaces+ pure $ DictBinding'Eq key val++parse'letBinding :: Parser LetBinding+parse'letBinding =+ parse'letBinding'inherit <|> parse'letBinding'eq++parse'letBinding'eq :: Parser LetBinding+parse'letBinding'eq =+ do+ key <- parse'var+ _ <- parse'spaces+ _ <- P.char '='+ _ <- parse'spaces+ val <- parse'expression+ _ <- parse'spaces+ _ <- P.char ';'+ _ <- parse'spaces+ pure $ LetBinding'Eq key val++parse'letBinding'inherit :: Parser LetBinding+parse'letBinding'inherit =+ do+ _ <- parse'keyword keyword'inherit+ a <- parse'expression'paren+ xs <- go Seq.empty+ pure $ LetBinding'Inherit a xs+ where+ go :: Seq Var -> Parser (Seq Var)+ go previousList =+ asum+ [ do+ _ <- P.char ';'+ _ <- parse'spaces+ pure previousList+ , do+ x <- parse'var+ go (previousList |> x)+ ]++{- | The primary, top-level expression parser. This is what you use to parse a+@.nix@ file. -}++-- | ==== Examples+--+-- >>> parseTest parse'expression ""+-- parse error at (line 1, column 1):+-- unexpected end of input+-- expecting expression++parse'expression :: Parser Expression+parse'expression =+ p <?> "expression"+ where+ p = asum+ [ parse'let <&> Expr'Let+ , parse'lambda <&> Expr'Lambda+ , list+ ]+ list = parse'expressionList >>= \case+ [] -> P.parserZero+ f : args -> pure $ expression'applyArgs f args++{- | Parser for a list of expressions in a list literal (@[ x y z ]@) or in a+chain of function arguments (@f x y z@). -}++-- | ==== Examples+--+-- >>> parseTest parse'expressionList ""+-- []+--+-- >>> parseTest (length <$> parse'expressionList) "x \"one two\" (a: b) (c d)"+-- 4+--+-- >>> parseTest (length <$> parse'expressionList) "(x \"one two\" (a: b) (c d))"+-- 1++parse'expressionList :: Parser [Expression]+parse'expressionList =+ P.many parse'expressionList'1 <?> "expression list"++{- | Parser for a single item within an expression list ('expressionListP').+This expression is not a lambda, a function application, a @let@-@in@+expression, or a @with@ expression. -}++-- | ==== Examples+--+-- >>> parseTest parse'expressionList'1 "ab.xy"+-- {- 1:1-1:6 -} dot ({- 1:1-1:3 -} var "ab") ({- 1:4-1:6 -} str [{- 1:4-1:6 -} "xy"])+--+-- >>> :{+-- >>> parseTest (expression'discardSource <$> parse'expressionList'1)+-- >>> "(x: f x x) y z"+-- >>> :}+-- lambda (param "x") (apply (apply (var "f") (var "x")) (var "x"))+--+-- >>> :{+-- >>> parseTest (expression'discardSource <$> parse'expressionList'1)+-- >>> "{ a = b; }.a y"+-- >>> :}+-- dot (dict [dict'eq (str ["a"]) (var "b")]) (str ["a"])++parse'expressionList'1 :: Parser Expression+parse'expressionList'1 =+ expression'applyDots+ <$> parse'expressionList'1'noDot+ <*> parse'dot'rhs'chain+ <?> "expression list item"++{- | Like 'parse'expressionList'1', but with the further restriction that the+expression may not be a 'Dot'. -}++-- | ==== Examples+--+-- >>> parseTest parse'expressionList'1'noDot "ab.xy"+-- {- 1:1-1:3 -} var "ab"+--+-- >>> :{+-- >>> parseTest (expression'discardSource <$> parse'expressionList'1'noDot)+-- >>> "(x: f x x) y z"+-- >>> :}+-- lambda (param "x") (apply (apply (var "f") (var "x")) (var "x"))+--+-- >>> :{+-- >>> parseTest (expression'discardSource <$> parse'expressionList'1'noDot)+-- >>> "{ a = b; }.a y"+-- >>> :}+-- dict [dict'eq (str ["a"]) (var "b")]++parse'expressionList'1'noDot :: Parser Expression+parse'expressionList'1'noDot =+ asum+ [ parse'str'dynamic <&> Expr'Str+ , parse'inStr <&> Expr'Str'Indented+ , parse'list <&> Expr'List+ , parse'dict <&> Expr'Dict+ , parse'var <&> Expr'Var+ , parse'expression'paren+ ]+ <?> "expression list item without a dot"++{- | Parser for a parenthesized expression, from opening parenthesis to closing+parenthesis. -}++parse'expression'paren :: Parser Expression+parse'expression'paren =+ P.char '(' *> parse'spaces *> parse'expression <* P.char ')' <* parse'spaces++parse'expression'antiquote :: Parser Expression+parse'expression'antiquote =+ P.try (P.string "${") *> parse'spaces *> parse'expression <* P.char '}'++{- | Parser for an expression in a context that is expecting a dict key.++One of:++ - an unquoted string+ - a quoted dynamic string+ - an arbitrary expression wrapped in antiquotes (@${@ ... @}@) -}++parse'expression'dictKey :: Parser Expression+parse'expression'dictKey =+ quoted <|> antiquoted <|> unquoted++ where+ quoted :: Parser Expression+ quoted = parse'str'dynamic <&> Expr'Str++ antiquoted :: Parser Expression+ antiquoted = do+ _ <- P.string "${"+ _ <- parse'spaces+ e <- parse'expression+ _ <- P.char '}'+ _ <- parse'spaces+ pure e++ unquoted :: Parser Expression+ unquoted = do+ (x, src) <- parse'strUnquoted+ pure $ Expr'Str $ str'static'to'dynamic $+ Str'Static (unquotedString'text x) (Just src)++parse'count :: Parser a -> Parser Natural+parse'count p =+ go 0+ where+ go :: Natural -> Parser Natural+ go n = (p *> go (succ n)) <|> pure n
+ test/cases.hs view
@@ -0,0 +1,641 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- Bricks+import Bricks.Expression+import Bricks.Parsec+import Bricks.Rendering++-- Bricks internal+import Bricks.Internal.Prelude+import qualified Bricks.Internal.Seq as Seq+import Bricks.Internal.Text (Text)+import qualified Bricks.Internal.Text as Text++-- Bricks test+import Bricks.Test.Hedgehog+import Bricks.Test.QQ++-- Parsec+import qualified Text.Parsec as P+import Text.Parsec.Text (Parser)++-- Hedgehog+import Hedgehog (Property, property, withTests, (===))+import qualified Hedgehog++-- Base+import System.IO (IO)+import Text.Show (show)++main :: IO ()+main = runTests $$(Hedgehog.discover)++{- | We'll use the @parseTest@ function a lot to test parsers. It's a bit like+'P.parseTest' from the Parsec library, but it works on parsers of type 'Text'+rather than @'Show' a => a@. It also prints the unparsed input so we can verify+that our parser consumes the right amount of input, and it prints a message if+the parser fails and consumes input. -}+parseTest :: Parser Text -> Text -> Text+parseTest p input =+ Text.intercalate "\n" $+ catMaybes++ [ Just $ case P.parse p "" input of+ Left err ->+ "Parse error at " <>+ Text.replace "\n" "\n - " (Text.pack (show err))+ Right x -> x++ , let+ p' = do+ _ <- p+ r <- P.many1 P.anyChar+ pure $ "Remaining input: " <> Text.pack (show r)+ in+ case P.parse p' "" input of+ Left _ -> Nothing+ Right x -> Just x++ , case P.parse (void p <|> pure ()) "" input of+ Left _ -> Just "Parser failed and consumed input"+ Right _ -> Nothing++ ]++prop_parse_str_unquoted :: Property+prop_parse_str_unquoted = withTests 1 $ property $ do++ let test = parseTest $ fmap var'text $ parse'var++ test "-ab_c" === [text|-ab_c|]++ test "" === [text|┃Parse error at (line 1, column 1):+ ┃ - unexpected end of input|]++ test "a\"b" === [text|┃a+ ┃Remaining input: "\"b"|]++ test "a b" === [text|┃a+ ┃Remaining input: "b"|]++ -- The unquoted string parser doesn't backtrack.+ -- Note how in this example it fails and consumes input.+ test "rec { }" === [text|┃Parse error at (line 1, column 4):+ ┃ - unexpected " "+ ┃Parser failed and consumed input|]++prop_parse_expression_dictKey :: Property+prop_parse_expression_dictKey = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'expression renderContext'default) $+ parse'expression'dictKey++ test "a" === [text|"a"|]++ test "\"a\"" === [text|"a"|]++ test "a b" === [text|┃"a"+ ┃Remaining input: "b"|]++ test "${a.b}" === [text|a.b|]++prop_parse_strDynamic :: Property+prop_parse_strDynamic = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'strDynamic'quoted renderContext'default) $+ parse'str'dynamic++ test "\"a\"" === [text|"a"|]++ test "\"a\" x" === [text|┃"a"+ ┃Remaining input: "x"|]++ test "\"a ${b} c\"" === [text|"a ${b} c"|]++ test "\"a${ b }c\"" === [text|"a${b}c"|]++ test "\"$\"" === [text|"$"|]++ test "\"a$\"" === [text|"a$"|]++ test "\"\\${\"" === [text|"\${"|]++ test "\"a\\${\"" === [text|"a\${"|]++prop_parse_strIndented :: Property+prop_parse_strIndented = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $ fmap render $+ P.spaces *> parse'inStr++ render :: InStr -> Text+ render =+ Text.intercalateMap "\n" render'1 . inStr'toSeq++ render'1 :: InStr'1 -> Text+ render'1 x =+ Text.show (inStr'1'level x) <> " [" <>+ Text.intercalateMap ", "+ (\s ->+ render'strDynamic'quoted renderContext'default $+ Str'Dynamic (Seq.singleton s) Nothing+ )+ (inStr'1'str x) <>+ "] " <> Text.show (isJust (inStr'1'lineBreak x))++ test "''hello''x" === [text|┃0 ["hello"] False+ ┃Remaining input: "x"|]++ test "''hello'' x" === [text|┃0 ["hello"] False+ ┃Remaining input: "x"|]++ test [text|┃ ''+ ┃ one+ ┃ two+ ┃ ''x|] === [text|┃0 [] True+ ┃4 ["one"] True+ ┃4 ["two"] True+ ┃2 [] False+ ┃Remaining input: "x"|]++ test [text|┃ ''+ ┃ one+ ┃+ ┃ two+ ┃ ''x|] === [text|┃0 [] True+ ┃4 ["one"] True+ ┃0 [] True+ ┃4 ["two"] True+ ┃2 [] False+ ┃Remaining input: "x"|]++ test "'''' x" === [text|┃0 [] False+ ┃Remaining input: "x"|]++ test "''abc''" === [text|0 ["abc"] False|]++ test [text|┃''+ ┃''|]+ === [text|┃0 [] True+ ┃0 [] False|]++ test [text|┃'' abc+ ┃def''|]+ === [text|┃3 ["abc"] True+ ┃0 ["def"] False|]++prop_parse_dict_pattern_start :: Property+prop_parse_dict_pattern_start = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ (P.try parse'dictPattern'start $> "yes") <|> pure "no"++ test "{a, b}:" === [text|┃yes+ ┃Remaining input: " b}:"|]++ test "{a ? 4, b}:" === [text|┃yes+ ┃Remaining input: " 4, b}:"|]++ test "{ }: x" === [text|┃yes+ ┃Remaining input: " x"|]++ -- { } is not enough to determine whether we're parsing a dict param, because+ -- if it isn't followed by a colon, then it's actually an empty dict literal.+ test "{ } x" === [text|┃no+ ┃Remaining input: "{ } x"|]++ test "{ ... }:" === [text|┃yes+ ┃Remaining input: " }:"|]++prop_parse_dict_pattern :: Property+prop_parse_dict_pattern = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'dictPattern renderContext'default) $+ parse'dictPattern++ test "{}" === [text|{ }|]+ test "{...}" === [text|{ ... }|]+ test "{a}" === [text|{ a }|]+ test "{a , b}" === [text|{ a, b }|]+ test "{a , b ? c, ...}" === [text|{ a, b ? c, ... }|]++prop_parse_dot_rhs_chain :: Property+prop_parse_dot_rhs_chain = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (+ Text.intercalate "\n" .+ fmap (render'expression renderContext'default)+ ) $+ parse'dot'rhs'chain++ -- The dots parser /does/ match the empty string.+ test "" === [text||]++ -- The simplest nonempty dot list+ test ".a" === [text|"a"|]++ -- The dots parser consumes any trailing whitespace beyond the dot list.+ test ".a " === [text|"a"|]++ -- Dot attributes are usually unquoted strings, but they may also be quoted.+ test ".\"a\"" === [text|"a"|]+ test ". \"a\"" === [text|"a"|]++ -- Here we throw some extra whitespace into the middle, which makes no+ -- difference, and some extra stuff onto the end, which does not get consumed.+ test ".a . b c" === [text|┃"a"+ ┃"b"+ ┃Remaining input: "c"|]++ -- Another example of a quoted dot, this time following an unquoted dot+ test ".a.\"b\"" === [text|┃"a"+ ┃"b"|]++ -- If quotes or braces are involved, the stuff that follows a dot expression+ -- can directly abut it with no whitespace in between.+ test [text|.a."b"x|] === [text|┃"a"+ ┃"b"+ ┃Remaining input: "x"|]++ test [text|.a.b"x"|] === [text|┃"a"+ ┃"b"+ ┃Remaining input: "\"x\""|]++ test ".a.b(x)" === [text|┃"a"+ ┃"b"+ ┃Remaining input: "(x)"|]++ test ". a . b" === [text|┃"a"+ ┃"b"|]++ test ". \"a\".b" === [text|┃"a"+ ┃"b"|]++ test ". \"a\".${b}" === [text|┃"a"+ ┃b|]++prop_parse_expression :: Property+prop_parse_expression = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'expression renderContext'default) $+ parse'expression++ -- The empty string is /not/ a valid expression.+ test "" === [text|┃Parse error at (line 1, column 1):+ ┃ - unexpected end of input+ ┃ - expecting expression|]++ -- A very simple expression: a one-letter variable+ test "a" === [text|a|]++ -- Parsing an expression consumes any subsequent whitespace.+ test "a " === [text|a|]++ -- When there are multiple expressions, that is parsed as a function call.+ test "f x" === [text|f x|]++ -- Expressions can directly abut each other, so it's important that the+ -- expression parser is also able to read an expression even when another+ -- expression directly follows it.+ test "f[x y]" === [text|┃f [+ ┃ x+ ┃ y+ ┃]|]++ -- A simple example of parsing a dot expression+ test "a.b" === [text|a.b|]++ -- Dot parsing also consumes trailing whitespace.+ test "a.b " === [text|a.b|]++ -- It looks odd when a subsequent expression appears after a dot expression+ -- with no whitespace, but it is permitted.+ test "a.b\"c\"" === [text|a.b "c"|]++ -- A simple list example+ test "[ a b ]" === [text|┃[+ ┃ a+ ┃ b+ ┃]|]++ -- A list with trailing whitespace that get consumed+ test "[ a b ] " === [text|┃[+ ┃ a+ ┃ b+ ┃]|]++ -- A list that is in the left-hand side of a function call. This will fail at+ -- runtime if the call is evaluated, because a list is not a function, but it+ -- should /parse/ successfully.+ test "[ a b ] x" === [text|┃[+ ┃ a+ ┃ b+ ┃] x|]++ -- The same thing with other weird stuff on the left-hand side of a function+ -- call.+ test "{ a = b; } x" === [text|┃{+ ┃ a = b;+ ┃} x|]++ test "{ } x" === [text|{ } x|]++ -- Note that the case where an empty dict is on the left-hand side of a+ -- function call looks very similar to the case where a function expression+ -- using dict deconstruction with no bindings. The only difference is the+ -- colon.+ test "{ }: x" === [text|┃{ }:+ ┃x|]++ -- A list with a function call inside+ test "[ (f x) ]" === [text|┃[+ ┃ (f x)+ ┃]|]++ test "[ a (f x) ]" === [text|┃[+ ┃ a+ ┃ (f x)+ ┃]|]++ -- A minimal dict literal+ test "{ x = y; }" === [text|┃{+ ┃ x = y;+ ┃}|]++ -- The left-hand side of a dict binding is allowed to be any expression.+ test "{ \"a b\" = y; }" === [text|┃{+ ┃ "a b" = y;+ ┃}|]+ test "{ ${x} = y; }" === [text|┃{+ ┃ ${x} = y;+ ┃}|]++ -- It may even be the empty string.+ test "{ \"\" = y; }" === [text|┃{+ ┃ "" = y;+ ┃}|]++ -- None of the conventional whitespace within a dict literal is mandatory.+ test "{x=y;}" === [text|┃{+ ┃ x = y;+ ┃}|]++ -- A simple dict literal with two bindings+ test "{ x = y; a = b; }" === [text|┃{+ ┃ x = y;+ ┃ a = b;+ ┃}|]++ -- The same thing without any whitespace+ test "{x=y;a=b;}" === [text|┃{+ ┃ x = y;+ ┃ a = b;+ ┃}|]++ -- Dicts with 'inherit' bindings+ test "{ inherit a; }" === [text|┃{+ ┃ inherit a;+ ┃}|]+ test "{ inherit a b; }" === [text|┃{+ ┃ inherit a b;+ ┃}|]+ test "{ inherit (x) a b; }" === [text|┃{+ ┃ inherit (x) a b;+ ┃}|]++ -- An inherit binding can be empty, although it is weird.+ test "{ inherit; }" === [text|┃{+ ┃ inherit;+ ┃}|]++ -- A simple function+ test "x : y" === [text|┃x:+ ┃y|]++ -- Whitespace before the colon is unconventional, but allowed.+ test "x : y" === [text|┃x:+ ┃y|]++ -- The space after the colon is not mandatory. (In Nix, this example would be+ -- parsed as the string "x:y", but here we do not support URI literals.)+ test "x:y" === [text|┃x:+ ┃y|]++ -- A slightly bigger example where we're starting to nest more things+ test "[ \"abc\" f { x = y; } ]"+ === [text|┃[+ ┃ "abc"+ ┃ f+ ┃ {+ ┃ x = y;+ ┃ }+ ┃]|]++ test "[ \"abc\" (f { x = y; }) ]"+ === [text|┃[+ ┃ "abc"+ ┃ (f {+ ┃ x = y;+ ┃ })+ ┃]|]++ -- This is not valid a expression (though the first bit of it is).+ test "a b: c" === [text|┃a b+ ┃Remaining input: ": c"|]++ -- This is not a valid expression.+ test "(a b: c)" === [text|┃Parse error at (line 1, column 5):+ ┃ - unexpected ":"+ ┃ - expecting expression list item or ")"+ ┃Parser failed and consumed input|]++ -- Some functions that use dict deconstruction+ test "{ a, b, c ? x, ... }: g b (f a c)"+ === [text|┃{ a, b, c ? x, ... }:+ ┃g b (f a c)|]++ test "{ x, ... }: f x" === [text|┃{ x, ... }:+ ┃f x|]+ test "{ x?\"abc\" }: x" === [text|┃{ x ? "abc" }:+ ┃x|]+ test "{ ... }: x" === [text|┃{ ... }:+ ┃x|]+ test "a@{ x, ... }: f x" === [text|┃a@{ x, ... }:+ ┃f x|]+ test "a@{ x?\"abc\" }: x" === [text|┃a@{ x ? "abc" }:+ ┃x|]+ test "a@{ ... }: x" === [text|┃a@{ ... }:+ ┃x|]++ -- A let expression+ test "let f = x: plus one x; in f seven"+ === [text|┃let+ ┃ f = x:+ ┃ plus one x;+ ┃in+ ┃ f seven|]++ -- A let binding list may be empty, although it is silly.+ test "let in f x"+ === [text|┃let+ ┃in+ ┃ f x|]++ -- Indented strings do not support any escape sequences.+ test [text|┃''+ ┃ There \ is \n no \$ escape.+ ┃''|]+ === [text|┃''+ ┃ There \\ is \\n no \\$ escape.+ ┃''|]++ -- Therefore if you want to include something like '' in an indented string,+ -- you have to put it inside an antiquote.+ test [text|┃''+ ┃ Isn't it+ ┃ ${"''"}interesting+ ┃''|]+ === [text|┃''+ ┃ Isn't it+ ┃ ${"''"}interesting+ ┃''|]++ -- Comments+ test [text|┃let -- hi+ ┃ x {- ! -} = "a"; -- yep+ ┃ in -- lol+ ┃ f x+ ┃|]+ === [text|┃let+ ┃ x = "a";+ ┃in+ ┃ f x|]++ -- Nested block comments+ test [text|┃f{- a+ ┃ -- b+ ┃ {- c {- d+ ┃ -}-} e+ ┃ -}x|]+ === "f x"++prop_parse_expression_list :: Property+prop_parse_expression_list = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (+ Text.intercalate "\n" .+ fmap (render'expression renderContext'default)+ ) $+ parse'expressionList++ test "" === [text||]++ test "x y z" === [text|┃x+ ┃y+ ┃z|]+++ test "(a)b c(d)" === [text|┃a+ ┃b+ ┃c+ ┃d|]++ test "a.\"b\"c" === [text|┃a.b+ ┃c|]++ test "\"abc\" (f { x = y; })" === [text|┃"abc"+ ┃f {+ ┃ x = y;+ ┃}|]++ -- Parsing lists of variables that are similar to keywords+ test "r re reck" === [text|┃r+ ┃re+ ┃reck|]+ test "r re rec { } reck" === [text|┃r+ ┃re+ ┃rec { }+ ┃reck|]+ test "l le lets" === [text|┃l+ ┃le+ ┃lets|]+ test "i ins" === [text|┃i+ ┃ins|]+ test "inheri inherits" === [text|┃inheri+ ┃inherits|]++prop_parse_expression_list_item :: Property+prop_parse_expression_list_item = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'expression renderContext'default) $+ parse'expressionList'1++ test "abc def" === [text|┃abc+ ┃Remaining input: "def"|]+ test "a.b c" === [text|┃a.b+ ┃Remaining input: "c"|]+ test "a.\"b\"c" === [text|┃a.b+ ┃Remaining input: "c"|]+ test "(a.b)c" === [text|┃a.b+ ┃Remaining input: "c"|]+ test "a.b(c)" === [text|┃a.b+ ┃Remaining input: "(c)"|]+ test "[ a b ]c" === [text|┃[+ ┃ a+ ┃ b+ ┃]+ ┃Remaining input: "c"|]+ test "a[ b c ]" === [text|┃a+ ┃Remaining input: "[ b c ]"|]+ test "\"a\"b" === [text|┃"a"+ ┃Remaining input: "b"|]++prop_parse_expression_list_item_no_dot :: Property+prop_parse_expression_list_item_no_dot = withTests 1 $ property $ do++ let+ test :: Text -> Text+ test =+ parseTest $+ fmap (render'expression renderContext'default) $+ parse'expressionList'1'noDot++ test "a.b c" === [text|┃a+ ┃Remaining input: ".b c"|]
+ test/doctest.hs view
@@ -0,0 +1,4 @@+import Test.DocTest (doctest)++main :: IO ()+main = doctest ["src", "-XOverloadedStrings"]