summaryrefslogtreecommitdiff
path: root/source/Felix/Syntax/Token.hs
diff options
context:
space:
mode:
Diffstat (limited to 'source/Felix/Syntax/Token.hs')
-rw-r--r--source/Felix/Syntax/Token.hs633
1 files changed, 633 insertions, 0 deletions
diff --git a/source/Felix/Syntax/Token.hs b/source/Felix/Syntax/Token.hs
new file mode 100644
index 0000000..31d1d19
--- /dev/null
+++ b/source/Felix/Syntax/Token.hs
@@ -0,0 +1,633 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- |
+-- This module defines the lexer and its associated data types.
+-- The lexer takes `Text` as input and produces a stream of tokens
+-- annotated with positional information. This information is bundled
+-- together with the original raw input for producing error messages.
+--
+-- The lexer perfoms some normalizations to make describing the grammar easier.
+-- Words outside of math environments are case-folded. Some commands are analysed
+-- as variable tokens and are equivalent to their respective unicode variants
+-- (α, β, γ, ..., 𝔸, 𝔹, ℂ, ...). Similarly, @\\begin{...}@ and @\\end{...}@ commands
+-- are each parsed as single tokens.
+--
+module Felix.Syntax.Token
+ ( Token(..)
+ , VariableDisplay(..)
+ , VariableSuffix(..)
+ , displayVariable
+ , renderVariableText
+ , tokToString
+ , tokToText
+ , TokStream(..)
+ , Located(..)
+ , runLexer
+ , gatherImports
+ ) where
+
+
+import Base hiding (many)
+
+import Felix.Report.Location
+
+import Control.DeepSeq (NFData)
+import Control.Monad.Combinators
+import Control.Monad.State.Strict
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Text qualified as Text
+import Prettyprinter (Pretty(..))
+import Text.Megaparsec hiding (Token, Label, label)
+import Text.Megaparsec.Char qualified as Char
+import Text.Megaparsec.Char.Lexer qualified as Lexer
+import Tptp.UnsortedFirstOrder (isAsciiLetter, isAsciiAlphaNumOrUnderscore)
+
+
+runLexer :: FileId -> String -> Text -> Either (ParseErrorBundle Text Void) ([FilePath], [[Located Token]])
+runLexer fileId file raw = runParser (evalStateT document (initLexerState fileId)) file raw
+
+
+type Lexer = StateT LexerState (Parsec Void Text)
+
+
+data LexerState = LexerState
+ { frames :: !(NonEmpty Frame)
+ , currentFileId :: !FileId
+ } deriving (Show, Eq)
+
+data Frame
+ = TopText
+ | MathFrame !Text
+ | TextFrame !Int
+ deriving (Show, Eq)
+
+initLexerState :: FileId -> LexerState
+initLexerState fileId = LexerState (TopText :| []) fileId
+
+topFrameOf :: LexerState -> Frame
+topFrameOf LexerState{frames = frame :| _} = frame
+
+topFrame :: Lexer Frame
+topFrame = gets topFrameOf
+
+pushFrame :: Frame -> LexerState -> LexerState
+pushFrame frame st@LexerState{frames = top :| rest} =
+ st{frames = frame :| (top : rest)}
+
+popFrame :: LexerState -> LexerState
+popFrame st@LexerState{frames = _ :| []} = st
+popFrame st@LexerState{frames = _ :| (top : rest)} =
+ st{frames = top :| rest}
+
+modifyTopFrame :: (Frame -> Frame) -> LexerState -> LexerState
+modifyTopFrame f st@LexerState{frames = top :| rest} =
+ st{frames = f top :| rest}
+
+-- Token recognizers only emit tokens; this is the single place that changes
+-- lexical context.
+advance :: Token -> LexerState -> LexerState
+advance tok st =
+ case (topFrameOf st, tok) of
+ (TopText, BeginEnv "math") ->
+ pushFrame (MathFrame "math") st
+ (TextFrame{}, BeginEnv "math") ->
+ pushFrame (MathFrame "math") st
+ (TopText, BeginEnv "align*") ->
+ pushFrame (MathFrame "align*") st
+ (TextFrame{}, BeginEnv "align*") ->
+ pushFrame (MathFrame "align*") st
+ (MathFrame env, EndEnv env')
+ | env == env' ->
+ popFrame st
+ (MathFrame{}, BeginEnv "text") ->
+ pushFrame (TextFrame 1) st
+ (TextFrame n, InvisibleBraceL) ->
+ modifyTopFrame (const (TextFrame (n + 1))) st
+ (TextFrame n, InvisibleBraceR)
+ | n > 1 ->
+ modifyTopFrame (const (TextFrame (n - 1))) st
+ (TextFrame 1, EndEnv "text") ->
+ popFrame st
+ _ ->
+ st
+
+-- |
+-- A token stream as input stream for a parser. Contains the raw input
+-- before tokenization as 'Text' for showing error messages.
+--
+data TokStream = TokStream
+ { rawInput :: !Text
+ , unTokStream :: ![[Located Token]]
+ } deriving (Show, Eq)
+
+instance Semigroup TokStream where
+ TokStream raw1 toks1 <> TokStream raw2 toks2 = TokStream (raw1 <> raw2) (toks1 <> toks2)
+
+instance Monoid TokStream where
+ mempty = TokStream mempty mempty
+
+-- | A LaTeX token.
+-- Invisible delimiters 'InvisibleBraceL' and 'InvisibleBraceR' are
+-- unescaped braces used for grouping in TEX (@{@),
+-- visibles braces are escaped braces (@\\{@).
+data Token
+ = Word !Text
+ | Variable !Text
+ | Symbol !Text
+ | Integer !Int
+ | Command !Text
+ | Label Text -- ^ A /@\\label{...}@/ command (case-sensitive).
+ | Ref (NonEmpty Text) -- ^ A /@\\ref{...}@/ command (case-sensitive).
+ | BeginEnv !Text
+ | EndEnv !Text
+ | ParenL | ParenR
+ | BracketL | BracketR
+ | VisibleBraceL | VisibleBraceR
+ | InvisibleBraceL | InvisibleBraceR
+ deriving (Show, Eq, Ord, Generic, Hashable, NFData)
+
+instance IsString Token where
+ fromString w = Word (Text.pack w)
+
+data VariableDisplay = VariableDisplay
+ { variableBaseText :: !Text
+ , variableSuffix :: !(Maybe VariableSuffix)
+ } deriving (Show, Eq, Ord)
+
+data VariableSuffix
+ = VariableSubscript !Text
+ | VariableTicks !Int
+ deriving (Show, Eq, Ord)
+
+displayVariable :: Text -> VariableDisplay
+displayVariable rawName =
+ case splitVariableBase rawName of
+ Nothing ->
+ VariableDisplay rawName Nothing
+ Just (baseText, suffixText) ->
+ VariableDisplay baseText (displayVariableSuffix suffixText)
+
+renderVariableText :: Text -> Text
+renderVariableText rawName =
+ case displayVariable rawName of
+ VariableDisplay baseText Nothing ->
+ baseText
+ VariableDisplay baseText (Just (VariableTicks n)) ->
+ baseText <> Text.replicate n "'"
+ VariableDisplay baseText (Just (VariableSubscript subscriptText)) ->
+ baseText <> renderSubscriptText subscriptText
+
+splitVariableBase :: Text -> Maybe (Text, Text)
+splitVariableBase rawName =
+ matchBlackboardBase rawName <|> matchGreekBase rawName <|> matchSingleLetterBase rawName
+
+matchBlackboardBase :: Text -> Maybe (Text, Text)
+matchBlackboardBase rawName = do
+ suffixText <- Text.stripPrefix "bb" rawName
+ case Text.uncons suffixText of
+ Just (upper, rest)
+ | 'A' <= upper && upper <= 'Z' ->
+ Just ("bb" <> Text.singleton upper, rest)
+ _ ->
+ Nothing
+
+matchGreekBase :: Text -> Maybe (Text, Text)
+matchGreekBase rawName =
+ asum
+ [ (\suffixText -> (rendered, suffixText)) <$> Text.stripPrefix prefix rawName
+ | (prefix, rendered) <- greekVariables
+ ]
+
+matchSingleLetterBase :: Text -> Maybe (Text, Text)
+matchSingleLetterBase rawName = do
+ (baseChar, suffixText) <- Text.uncons rawName
+ pure (Text.singleton baseChar, suffixText)
+
+displayVariableSuffix :: Text -> Maybe VariableSuffix
+displayVariableSuffix suffixText
+ | Text.null suffixText =
+ Nothing
+ | Text.all (== '_') suffixText =
+ Just (VariableTicks (Text.length suffixText))
+ | otherwise =
+ Just (VariableSubscript (Text.replace "_" "'" suffixText))
+
+renderSubscriptText :: Text -> Text
+renderSubscriptText subscriptText
+ | Text.length subscriptText == 1 =
+ "_" <> subscriptText
+ | otherwise =
+ "_{" <> subscriptText <> "}"
+
+greekVariables :: [(Text, Text)]
+greekVariables =
+ [ ("alpha", "α"), ("beta", "β"), ("gamma", "γ"), ("delta", "δ")
+ , ("epsilon", "ε"), ("zeta", "ζ"), ("eta", "η"), ("theta", "θ")
+ , ("iota", "ι"), ("kappa", "κ"), ("lambda", "λ"), ("mu", "μ")
+ , ("nu", "ν"), ("xi", "ξ"), ("pi", "π"), ("rho", "ρ"), ("sigma", "σ")
+ , ("tau", "τ"), ("upsilon", "υ"), ("phi", "φ"), ("chi", "χ")
+ , ("psi", "ψ"), ("omega", "ω")
+ , ("Gamma", "Γ"), ("Delta", "Δ"), ("Theta", "Θ"), ("Lambda", "Λ")
+ , ("Xi", "Ξ"), ("Pi", "Π"), ("Sigma", "Σ"), ("Upsilon", "Υ")
+ , ("Phi", "Φ"), ("Psi", "Ψ"), ("Omega", "Ω")
+ ]
+
+tokToText :: Token -> Text
+tokToText = \case
+ Word w -> w
+ Variable v -> renderVariableText v
+ Symbol s -> s
+ Integer n -> Text.pack (show n)
+ Command cmd -> Text.cons '\\' cmd
+ Label m -> "\\label{" <> m <> "}"
+ Ref ms -> "\\ref{" <> Text.intercalate ", " (toList ms) <> "}"
+ BeginEnv "math" -> "$"
+ EndEnv "math" -> "$"
+ BeginEnv env -> "\\begin{" <> env <> "}"
+ EndEnv env -> "\\end{" <> env <> "}"
+ ParenL -> "("
+ ParenR -> ")"
+ BracketL -> "["
+ BracketR -> "]"
+ VisibleBraceL -> "\\{"
+ VisibleBraceR -> "\\}"
+ InvisibleBraceL -> "{"
+ InvisibleBraceR -> "}"
+
+tokToString :: Token -> String
+tokToString = Text.unpack . tokToText
+
+instance Pretty Token where
+ pretty = \case
+ Word w -> pretty w
+ Variable v -> pretty (renderVariableText v)
+ Symbol s -> pretty s
+ Integer n -> pretty n
+ Command cmd -> "\\" <> pretty cmd
+ Label m -> "\\label{" <> pretty m <> "}"
+ Ref m -> "\\ref{" <> pretty m <> "}"
+ BeginEnv env -> "\\begin{" <> pretty env <> "}"
+ EndEnv env -> "\\end{" <> pretty env <> "}"
+ ParenL -> "("
+ ParenR -> ")"
+ BracketL -> "["
+ BracketR -> "]"
+ VisibleBraceL -> "\\{"
+ VisibleBraceR -> "\\}"
+ InvisibleBraceL -> "{"
+ InvisibleBraceR -> "}"
+
+
+data Located a = Located
+ { startPos :: !Location
+ , unLocated :: !a
+ , postWhitespace :: Whitespace
+ } deriving (Show, Functor)
+
+data Whitespace = NoSpace | Space deriving (Show)
+
+collapseWhitespace :: [Whitespace] -> Whitespace
+collapseWhitespace = \case
+ Space : _ -> Space
+ NoSpace : ws -> collapseWhitespace ws
+ [] -> NoSpace
+
+instance Eq a => Eq (Located a) where (==) = (==) `on` unLocated
+instance Ord a => Ord (Located a) where compare = compare `on` unLocated
+
+
+document :: Lexer ([FilePath], [[Located Token]])
+document = do
+ is <- importBlock
+ es <- many environment
+ eof
+ return (unLocated <$> is, es)
+
+
+importBlock :: Lexer [Located FilePath]
+importBlock = do
+ void (skipManyTill skipChar importLineOrBeginEnvOrEof)
+ many importLine
+ where
+ -- When skipping to the import block, we first need to try parsing whitespace to properly handle comments and avoid picking up a commented import line at the start of the import block.
+ skipChar, importLineOrBeginEnvOrEof :: Lexer ()
+ skipChar = comment <|> void anySingle
+ importLineOrBeginEnvOrEof =
+ lookAhead
+ (void (Char.string "\\import{")
+ <|> void beginToplevelEnvironment)
+ <|> eof
+
+ importLine :: Lexer (Located FilePath) = lexeme do
+ Char.string "\\import{"
+ path <- some (satisfy isTheoryNameChar)
+ Char.char '}'
+ pure path
+
+ isTheoryNameChar :: Char -> Bool
+ isTheoryNameChar c =
+ c /= '}' && c /= '\n' && c /= '\r' && c /= '\0'
+
+-- | Scan only the leading import block. Source-graph construction uses this
+-- authority-free pass before parsing modules under their composed syntax.
+gatherImports
+ :: FileId
+ -> String
+ -> Text
+ -> Either (ParseErrorBundle Text Void) [Located FilePath]
+gatherImports fileId file =
+ runParser (evalStateT importBlock (initLexerState fileId)) file
+
+
+beginToplevelEnvironment :: Lexer (Located Text)
+beginToplevelEnvironment = lexeme do
+ Char.string "\\begin{"
+ env :: Text <- asum (Char.string <$> ["definition", "theorem", "lemma", "axiom", "proof", "corollary", "proposition", "claim", "abbreviation", "datatype", "inductive", "signature", "struct"])
+ Char.char '}'
+ pure env
+
+-- | Parses tokens, switching tokenizing frames when encountering math and text environments.
+environment :: Lexer [Located Token]
+environment = do
+ env <- skipManyTill (comment <|> void anySingle) beginToplevelEnvironment
+ lts <- go (unLocated env) id
+ pure ((BeginEnv <$> env) : lts)
+ where
+ go env f = do
+ frame <- topFrame
+ r <- optional (nextTokenFor frame)
+ case r of
+ Nothing ->
+ pure (f [])
+ Just t@Located{unLocated = EndEnv env'}
+ | frame == TopText && env == env' ->
+ pure (f [t])
+ Just t -> do
+ modify' (advance (unLocated t))
+ go env (f . (t:))
+{-# INLINE environment #-}
+
+nextTokenFor :: Frame -> Lexer (Located Token)
+nextTokenFor = \case
+ TopText -> normalToken
+ MathFrame{} -> mathToken
+ TextFrame n -> textToken n
+
+-- | Parses a single normal-mode token.
+normalToken :: Lexer (Located Token)
+normalToken =
+ word <|> symbol <|> beginMath <|> beginAlign <|> subEnvironment <|> opening <|> closing <|> label <|> ref <|> end <|> command
+
+-- | Parses a single math mode token.
+mathToken :: Lexer (Located Token)
+mathToken =
+ var <|> symbol <|> number <|> beginCases <|> endAlign <|> endCases <|> opening <|> closing <|> beginText <|> beginExplanation <|> endMath <|> command
+
+beginText :: Lexer (Located Token)
+beginText = lexeme do
+ Char.string "\\text{" <|> Char.string "\\textbox{"
+ pure (BeginEnv "text")
+
+-- | Same as text modulo spacing, so we treat it synonymously
+beginExplanation :: Lexer (Located Token)
+beginExplanation = lexeme do
+ Char.string "\\explanation{"
+ pure (BeginEnv "text")
+
+subEnvironment :: Lexer (Located Token)
+subEnvironment = beginOrEnd ["enumerate", "subproof", "byCase"]
+ where
+ beginOrEnd envs = asum [beginEnv env <|> endEnv env | env <- envs]
+ beginEnv env = lexeme do
+ Char.string ("\\begin{" <> env <> "}")
+ pure (BeginEnv env)
+ endEnv env = lexeme do
+ Char.string ("\\end{" <> env <> "}")
+ pure (EndEnv env)
+
+-- | Normal mode embedded into math mode via @\text{...}@.
+textToken :: Int -> Lexer (Located Token)
+textToken n = word <|> symbol <|> textEnd <|> beginMath <|> beginAlign <|> opening' <|> closing' <|> ref <|> command
+ where
+ textEnd = lexeme do
+ guard (n == 1)
+ Char.char '}'
+ pure (EndEnv "text")
+
+ opening' = lexeme (group <|> optional (Char.string "\\left") *> (brace <|> paren <|> bracket))
+ where
+ brace = VisibleBraceL <$ lexeme (Char.string "\\{")
+ group = InvisibleBraceL <$ lexeme (Char.char '{')
+ paren = ParenL <$ lexeme (Char.char '(')
+ bracket = BracketL <$ lexeme (Char.char '[')
+
+ closing' = lexeme (group <|> optional (Char.string "\\right") *> (brace <|> paren <|> bracket))
+ where
+ brace = VisibleBraceR <$ lexeme (Char.string "\\}")
+ group = InvisibleBraceR <$ lexeme (Char.char '}')
+ paren = ParenR <$ lexeme (Char.char ')')
+ bracket = BracketR <$ lexeme (Char.char ']')
+
+
+-- | Parses a single begin math token.
+beginMath :: Lexer (Located Token)
+beginMath = lexeme do
+ Char.string "\\(" <|> Char.string "\\[" <|> Char.string "$"
+ pure (BeginEnv "math")
+
+beginAlign :: Lexer (Located Token)
+beginAlign = lexeme do
+ Char.string "\\begin{align*}"
+ pure (BeginEnv "align*")
+
+beginCases :: Lexer (Located Token)
+beginCases = lexeme do
+ Char.string "\\begin{cases}"
+ pure (BeginEnv "cases")
+
+-- | Parses a single end math token.
+endMath :: Lexer (Located Token)
+endMath = lexeme do
+ Char.string "\\)" <|> Char.string "\\]" <|> Char.string "$"
+ pure (EndEnv "math")
+
+endAlign :: Lexer (Located Token)
+endAlign = lexeme do
+ Char.string "\\end{align*}"
+ pure (EndEnv "align*")
+
+endCases :: Lexer (Located Token)
+endCases = lexeme do
+ Char.string "\\end{cases}"
+ pure (EndEnv "cases")
+
+
+-- | Parses the end of an environment.
+-- Commits only after having seen "\end{".
+end :: Lexer (Located Token)
+end = lexeme do
+ notFollowedBy (Char.string "\\end{cases}")
+ Char.string "\\end{"
+ env <- some (Char.letterChar <|> Char.char '*')
+ Char.char '}'
+ pure (EndEnv (Text.pack env))
+
+
+
+-- | Parses a word. Words are returned casefolded, since we want to ignore their case later on.
+word :: Lexer (Located Token)
+word = lexeme do
+ w <- some (Char.letterChar <|> Char.char '\'' <|> Char.char '-')
+ let t = Word (Text.toCaseFold (Text.pack w))
+ pure t
+
+number :: Lexer (Located Token)
+number = lexeme $ Integer <$> Lexer.decimal
+
+
+var :: Lexer (Located Token)
+var = lexeme (fmap Variable var')
+ where
+ var' = do
+ alphabeticPart <- letter <|> bb <|> greek
+ variationPart <- subscript <|> ticked <|> pure ""
+ pure (alphabeticPart <> variationPart)
+
+ subscript :: Lexer Text
+ subscript = do
+ Char.char '_'
+ unbraced <|> braced <|> text
+ where
+ unbraced = Text.singleton <$> Char.alphaNumChar
+ braced = Text.pack <$> (Char.char '{' *> some (Char.alphaNumChar <|> tick) <* Char.char '}')
+ text = Char.string "\\text" *> braced -- for rendering the subscript in roman type
+
+ -- A bit of a hack to fit the TPTP format.
+ tick :: Lexer Char
+ tick = '_' <$ Char.char '\''
+
+ ticked :: Lexer Text
+ ticked = do
+ ticks <- some tick
+ pure (Text.pack ticks)
+
+ letter :: Lexer Text
+ letter = fmap Text.singleton Char.letterChar
+
+ greek :: Lexer Text
+ greek = try do
+ Char.char '\\'
+ l <- symbolParser greeks
+ notFollowedBy Char.letterChar
+ pure l
+
+ greeks :: [Text]
+ greeks =
+ [ "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"
+ , "iota", "kappa", "lambda", "mu", "nu", "xi", "pi", "rho", "sigma"
+ , "tau", "upsilon", "phi", "chi", "psi", "omega"
+ , "Gamma", "Delta", "Theta", "Lambda", "Xi", "Pi", "Sigma", "Upsilon"
+ , "Phi", "Psi", "Omega"
+ ]
+
+ bb :: Lexer Text
+ bb = do
+ Char.string "\\mathbb{"
+ l <- symbolParser bbs
+ Char.char '}'
+ pure $ "bb" <> l
+
+ bbs :: [Text]
+ bbs = Text.singleton <$> ['A'..'Z']
+
+
+ symbolParser :: [Text] -> Lexer Text
+ symbolParser symbols = asum (fmap Char.string symbols)
+
+
+symbol :: Lexer (Located Token)
+symbol = lexeme do
+ symb <- some (satisfy (`elem` symbols))
+ pure (Symbol (Text.pack symb))
+ where
+ symbols :: [Char]
+ symbols = ".,:;!?@=≠+-/|^><≤≥*&≈⊂⊃⊆⊇∈“”‘’"
+
+-- | Parses a TEX-style command.
+command :: Lexer (Located Token)
+command = lexeme do
+ Char.char '\\'
+ cmd <- some Char.letterChar
+ pure (Command (Text.pack cmd))
+
+-- | Parses a label command and extracts its marker.
+label :: Lexer (Located Token)
+label = lexeme do
+ Char.string "\\label{"
+ m <- marker
+ Char.char '}'
+ pure (Label m)
+
+-- | Parses a label command and extracts its marker.
+ref :: Lexer (Located Token)
+ref = lexeme do
+ -- @\\cref@ is from @cleveref@ and @\\hyperref@ is from @hyperref@
+ cmd <- Char.string "\\ref{" <|> Char.string "\\cref{" <|> Char.string "\\hyperref["
+ ms <- NonEmpty.fromList <$> marker `sepBy1` Char.char ','
+ case cmd of
+ "\\hyperref[" -> Char.string "]{" *> some (satisfy (/= '}')) *> Char.char '}' *> pure (Ref ms)
+ _ -> Char.char '}' *> pure (Ref ms)
+
+marker :: Lexer Text
+marker = do
+ c <- satisfy isAsciiLetter
+ cs <- takeWhileP Nothing isAsciiAlphaNumOrUnderscore
+ pure (Text.cons c cs)
+
+-- | Parses an opening delimiter.
+opening :: Lexer (Located Token)
+opening = lexeme (group <|> optional (Char.string "\\left") *> (paren <|> brace <|> bracket))
+ where
+ brace = VisibleBraceL <$ lexeme (Char.string "\\{")
+ group = InvisibleBraceL <$ lexeme (Char.char '{')
+ paren = ParenL <$ lexeme (Char.char '(')
+ bracket = BracketL <$ lexeme (Char.char '[')
+
+-- | Parses a closing delimiter.
+closing :: Lexer (Located Token)
+closing = lexeme (group <|> optional (Char.string "\\right") *> (paren <|> brace <|> bracket))
+ where
+ brace = VisibleBraceR <$ lexeme (Char.string "\\}")
+ group = InvisibleBraceR <$ lexeme (Char.char '}')
+ paren = ParenR <$ lexeme (Char.char ')')
+ bracket = BracketR <$ lexeme (Char.char ']')
+
+-- | Turns a Lexer into one that tracks the source position of the token
+-- and consumes trailing whitespace.
+lexeme :: Lexer a -> Lexer (Located a)
+lexeme p = do
+ fileId <- gets currentFileId
+ start <- getSourcePos
+ location <-
+ either
+ (fail . show)
+ pure
+ (fromSourcePosChecked fileId start)
+ t <- p
+ w <- whitespace
+ pure (Located location t w)
+
+space :: Lexer Whitespace
+space = Space <$ (Char.char ' ' <|> Char.char '\n' <|> Char.char '\r')
+ <|> Space <$ (Char.string "\\ " <|> Char.string "\\\\" <|> Char.string "\\!" <|> Char.string "\\," <|> Char.string "\\:" <|> Char.string "\\;" <|> Char.string "\\;")
+
+whitespace :: Lexer Whitespace
+whitespace = do
+ ws <- many (spaces <|> NoSpace <$ comment)
+ pure (collapseWhitespace ws)
+ where
+ spaces = collapseWhitespace <$> some space
+
+comment :: Lexer ()
+comment = Lexer.skipLineComment "%"