packages feed

purekell-0.1.0.0: src/Purekell/Parser.hs

-- | Megaparsec-based parsers for the shared expression grammar.
--
-- This module provides the building blocks for parsing both Haskell and
-- PureScript expressions into the shared AST. The parsers are
-- language-neutral — divergent syntax (like record dot-access) is handled
-- by a postfix callback passed to 'mkExprParsers'.
--
-- Most users should use the codecs in "Purekell.Haskell" and
-- "Purekell.PureScript" rather than these parsers directly.
module Purekell.Parser
  ( -- * Expression parser construction
    ExprParsers (..)
  , mkExprParsers
    -- * Lexer utilities
  , sc
  , lexeme
  , symbol
  , keyword
    -- * Literal and name parsers
  , pLit
  , pLowerName
  , pUpperName
    -- * Operator parsers
  , pBacktickOp
  , pOperator
    -- * Field separator
  , pFieldSep
    -- * Pattern parsers
  , pAtomPat
  , pConPat
  , pPat
    -- * Type parser
  , pType
  ) where

import Data.Text (Text)
import qualified Data.Text as T
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L

import Purekell.AST

type Parser = Parsec Void Text

-- | Skip whitespace (spaces, tabs, newlines). No comment support.
sc :: Parser ()
sc = L.space space1 empty empty

-- | Wrap a parser to consume trailing whitespace.
lexeme :: Parser a -> Parser a
lexeme = L.lexeme sc

-- | Parse an exact symbol and consume trailing whitespace.
symbol :: Text -> Parser Text
symbol = L.symbol sc

-- | Parse a keyword (must not be followed by identifier characters).
keyword :: Text -> Parser ()
keyword w = lexeme (string w *> notFollowedBy (alphaNumChar <|> char '_' <|> char '\''))

-- Literal parsers

pIntLit :: Parser Lit
pIntLit = IntLit <$> lexeme L.decimal

pFloatLit :: Parser Lit
pFloatLit = FloatLit <$> lexeme (try L.float)

pCharLit :: Parser Lit
pCharLit = CharLit <$> lexeme (between (char '\'') (char '\'') L.charLiteral)

pStringLit :: Parser Lit
pStringLit = StringLit . T.pack <$> lexeme (char '"' *> manyTill L.charLiteral (char '"'))

-- | Parse a literal: character, string, float, or integer.
--
-- Float is tried before integer to handle @3.14@ correctly.
pLit :: Parser Lit
pLit = pCharLit <|> pStringLit <|> pFloatLit <|> pIntLit

-- Name parsers

-- | Parse a lowercase identifier (variable or function name).
--
-- Rejects reserved words: @case@, @of@, @let@, @in@, @where@, @do@,
-- @if@, @then@, @else@, @_@.
pLowerName :: Parser Name
pLowerName = lexeme $ try $ do
  c <- lowerChar <|> char '_'
  cs <- many (alphaNumChar <|> char '_' <|> char '\'')
  let n = T.pack (c : cs)
  if n `elem` reserved then fail ("reserved: " ++ T.unpack n) else pure (Name n)
  where
    reserved = ["case", "of", "let", "in", "where", "do", "if", "then", "else", "_"]

-- | Parse an uppercase identifier (constructor or module name).
pUpperName :: Parser Name
pUpperName = lexeme $ do
  c <- upperChar
  cs <- many (alphaNumChar <|> char '_' <|> char '\'')
  pure (Name (T.pack (c : cs)))

-- Raw name parsers (no trailing whitespace, for qualified name components)

pRawUpperIdent :: Parser Text
pRawUpperIdent = do
  c <- upperChar
  cs <- many (alphaNumChar <|> char '_' <|> char '\'')
  pure (T.pack (c : cs))

pRawLowerIdent :: Parser Text
pRawLowerIdent = try $ do
  c <- lowerChar <|> char '_'
  cs <- many (alphaNumChar <|> char '_' <|> char '\'')
  let n = T.pack (c : cs)
  if n `elem` reserved then fail ("reserved: " ++ T.unpack n) else pure n
  where
    reserved = ["case", "of", "let", "in", "where", "do", "if", "then", "else", "_"]

-- Qualified name parsers

-- | Parse a possibly-qualified name starting with uppercase.
-- Returns 'Con', 'QCon', or 'QVar' depending on the structure.
pQualifiedOrCon :: Parser Expr
pQualifiedOrCon = lexeme $ do
  first <- pRawUpperIdent
  rest <- many (try (char '.' *> pRawUpperIdent))
  mLower <- optional (try (char '.' *> pRawLowerIdent))
  let allUpper = first : rest
  case mLower of
    Just low -> pure (QVar (map Name allUpper) (Name low))
    Nothing  -> case allUpper of
      [one] -> pure (Con (Name one))
      _     -> pure (QCon (map Name (init allUpper)) (Name (last allUpper)))

-- | Parse a possibly-qualified type constructor.
pQualifiedOrTyCon :: Parser Type
pQualifiedOrTyCon = lexeme $ do
  first <- pRawUpperIdent
  rest <- many (try (char '.' *> pRawUpperIdent))
  let allUpper = first : rest
  case allUpper of
    [one] -> pure (TyCon (Name one))
    _     -> pure (TyQCon (map Name (init allUpper)) (Name (last allUpper)))

-- | Parse a backtick-quoted operator: @\`div\`@, @\`elem\`@.
pBacktickOp :: Parser Name
pBacktickOp = lexeme $ do
  _ <- char '`'
  n <- pRawLowerIdent <|> pRawUpperIdent
  _ <- char '`'
  pure (Name n)

-- | Parse a symbolic operator: @+@, @>>=@, @<>@, etc.
--
-- Rejects reserved operators: @->@, @|@, @<-@, @=@, @::@.
pSymbolicOp :: Parser Name
pSymbolicOp = lexeme $ try $ do
  op <- some (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))
  let n = T.pack op
  if n `elem` ["->", "|", "<-", "=", "::"] then fail "reserved operator" else pure (Name n)

-- | Parse an operator — either backtick-quoted or symbolic.
pOperator :: Parser Name
pOperator = pBacktickOp <|> pSymbolicOp

-- Pattern parsers

pPatMinus :: Parser ()
pPatMinus = () <$ lexeme (try (char '-' <* notFollowedBy (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))))

pNumLit :: Parser Lit
pNumLit = pFloatLit <|> pIntLit

-- | Parse an atomic pattern (no infix operators, no constructor arguments).
--
-- Handles: negated literals, literals, wildcards, nullary constructors,
-- as-patterns, variable patterns, parenthesized\/tuple patterns, and
-- list patterns.
pAtomPat :: Parser Pat
pAtomPat = choice
  [ NegLitPat <$> (pPatMinus *> pNumLit)
  , LitPat <$> pLit
  , WildPat <$ lexeme (char '_' <* notFollowedBy (alphaNumChar <|> char '_' <|> char '\''))
  , ConPat <$> pUpperName <*> pure []
  , try (AsPat <$> pLowerName <*> (symbol "@" *> pAtomPat))
  , VarPat <$> pLowerName
  , pParenOrTuplePat
  , pListPat
  ]

pListPat :: Parser Pat
pListPat = ListPat <$> (symbol "[" *> pPat `sepBy` symbol "," <* symbol "]")

pParenOrTuplePat :: Parser Pat
pParenOrTuplePat = do
  _ <- symbol "("
  p <- pPat
  rest <- many (symbol "," *> pPat)
  _ <- symbol ")"
  pure $ case rest of
    [] -> p
    _  -> TuplePat (p : rest)

pRecordPatFields :: Parser [(Name, Pat)]
pRecordPatFields = symbol "{" *> fieldPatAssign `sepBy1` symbol "," <* symbol "}"

fieldPatAssign :: Parser (Name, Pat)
fieldPatAssign = (,) <$> pLowerName <*> (pFieldSep *> pPat)

-- | Parse a constructor pattern, possibly with record fields or arguments.
pConPat :: Parser Pat
pConPat = do
  name <- pUpperName
  (RecordPat name <$> try pRecordPatFields) <|> (ConPat name <$> many pAtomPat)

pConsOp :: Parser ()
pConsOp = lexeme $ try $ () <$ char ':' <* notFollowedBy (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))

-- | Parse a record field separator: @=@ (Haskell-style) or @:@ (PureScript-style).
--
-- Accepts both styles so the parser is language-neutral.
pFieldSep :: Parser ()
pFieldSep = () <$ symbol "="
  <|> () <$ lexeme (try (char ':' <* notFollowedBy (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))))

-- | Parse a full pattern, including cons patterns (@x : xs@).
pPat :: Parser Pat
pPat = do
  left <- pConPat <|> pAtomPat
  rest <- optional (pConsOp *> pPat)
  pure $ case rest of
    Nothing -> left
    Just r  -> ConsPat left r

-- Type parsers

-- | Parse a type expression.
--
-- Supports type constructors, type variables, type application,
-- function arrows (@->@), qualified type constructors, and
-- parenthesized types.
pType :: Parser Type
pType = pTyFun

pTyFun :: Parser Type
pTyFun = do
  t <- pTyApp
  rest <- optional (symbol "->" *> pTyFun)  -- right-associative
  pure $ case rest of
    Nothing -> t
    Just r  -> TyFun t r

pTyApp :: Parser Type
pTyApp = do
  f <- pTyAtom
  args <- many pTyAtom
  pure (foldl TyApp f args)

pTyAtom :: Parser Type
pTyAtom = choice
  [ pQualifiedOrTyCon
  , TyVar <$> pLowerName
  , symbol "(" *> pType <* symbol ")"
  ]

pDoubleColon :: Parser ()
pDoubleColon = () <$ lexeme (try (string "::" <* notFollowedBy (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))))

-- Expression parsers

-- | The expression parsers produced by 'mkExprParsers'.
data ExprParsers = ExprParsers
  { epExpr  :: Parser Expr   -- ^ Full expression parser
  , epGuard :: Parser Guard  -- ^ Guard parser (@| condition@)
  }

-- | Build expression parsers with a language-specific postfix callback.
--
-- The postfix callback adds extra postfix operations after each atom.
-- For Haskell, this is @'pure'@ (no postfix operations). For PureScript,
-- this chains dot-accessed field names (@.field1.field2@).
--
-- This is the key extension point that makes the parser language-neutral:
-- the entire expression grammar is shared, with only the postfix
-- callback differing between languages.
mkExprParsers :: (Expr -> Parser Expr) -> ExprParsers
mkExprParsers postfix = ExprParsers { epExpr = expr, epGuard = guard }
  where
    atom = choice
      [ Literal <$> pLit
      , pQualifiedOrCon
      , Var <$> pLowerName
      , pParenOrTupleOrSection
      , pList
      ]
    pList = ListLit <$> (symbol "[" *> expr `sepBy` symbol "," <* symbol "]")
    pNonMinusSymOp = lexeme $ try $ do
      op <- some (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))
      let n = T.pack op
      if n `elem` ["->", "|", "<-", "=", "-", "::"] then fail "reserved/excluded operator" else pure (Name n)
    pNonMinusOp = pBacktickOp <|> pNonMinusSymOp
    pParenOrTupleOrSection = do
      _ <- symbol "("
      choice
        [ -- Right section: (op expr) — exclude solo minus to avoid (-x) conflict
          try (RightSection <$> pNonMinusOp <*> expr <* symbol ")")
        , -- Parse prefix-level expr, then decide (for left sections + infix)
          do e <- prefixExpr
             choice
               [ -- Left section: expr op )
                 try (LeftSection e <$> pOperator <* symbol ")")
               , -- Infix continuation → then optional ann, then optional where, then tuple or grouping
                 do rest <- many ((,) <$> pOperator <*> prefixExpr)
                    let infE = foldl (\l (op, r) -> InfixApp l op r) e rest
                    annE <- pOptionalAnn infE
                    fullE <- pOptionalWhere annE
                    choice
                      [ Tuple . (fullE :) <$> some (symbol "," *> expr) <* symbol ")"
                      , fullE <$ symbol ")"
                      ]
               ]
        , -- Non-prefix expressions (lambda, if, case, let, do) inside parens
          do e <- lam <|> try ifE <|> try caseE <|> try letE <|> try doE
             e1 <- pOptionalAnn e
             e' <- pOptionalWhere e1
             choice
               [ Tuple . (e' :) <$> some (symbol "," *> expr) <* symbol ")"
               , e' <$ symbol ")"
               ]
        ]
    fieldAssign = (,) <$> pLowerName <*> (pFieldSep *> expr)
    postfixChain e = do
      e1 <- postfix e
      updates <- many (try (symbol "{" *> fieldAssign `sepBy1` symbol "," <* symbol "}"))
      case updates of
        [] -> pure e1
        _  -> postfixChain (foldl RecordUpdate e1 updates)
    atomWithPostfix = atom >>= postfixChain
    appExpr = do
      f <- atomWithPostfix
      args <- many atomWithPostfix
      pure (foldl App f args)
    pPrefixMinus = lexeme (try (char '-' <* notFollowedBy (oneOf ("!#$%&*+./<=>?@\\^|-~:" :: [Char]))))
    prefixExpr = (Neg <$> (pPrefixMinus *> appExpr)) <|> appExpr
    infixExpr = do
      first <- prefixExpr
      rest <- many ((,) <$> pOperator <*> prefixExpr)
      pure (foldl (\l (op, r) -> InfixApp l op r) first rest)
    pOptionalAnn e = do
      mty <- optional (pDoubleColon *> pType)
      pure $ case mty of
        Nothing -> e
        Just ty -> Ann e ty
    annExpr = infixExpr >>= pOptionalAnn
    guard = symbol "|" *> (Guard <$> infixExpr)
    lam = Lam <$> (symbol "\\" *> some pAtomPat) <*> (symbol "->" *> expr)
    ifE = If <$> (keyword "if" *> expr) <*> (keyword "then" *> expr) <*> (keyword "else" *> expr)
    caseAlt = CaseAlt <$> pPat <*> many guard <*> (symbol "->" *> expr)
    caseE = Case <$> (keyword "case" *> expr <* keyword "of" <* symbol "{")
                 <*> (caseAlt `sepBy1` symbol ";" <* symbol "}")
    binding = try funBinding <|> simpleBinding
    funBinding = do
      name <- pLowerName
      pats <- some pAtomPat
      _ <- symbol "="
      body <- expr
      pure (Binding (VarPat name) (Lam pats body))
    simpleBinding = Binding <$> pPat <*> (symbol "=" *> expr)
    letE = Let <$> (keyword "let" *> symbol "{" *> binding `sepBy1` symbol ";" <* symbol "}")
               <*> (keyword "in" *> expr)
    stmtBind = StmtBind <$> pPat <*> (symbol "<-" *> expr)
    stmtLet = keyword "let" *> symbol "{" *> (StmtLet <$> binding `sepBy1` symbol ";")
              <* symbol "}" <* notFollowedBy (keyword "in")
    stmtExpr = StmtExpr <$> expr
    stmt = try stmtBind <|> try stmtLet <|> stmtExpr
    doE = Do <$> (keyword "do" *> symbol "{" *> stmt `sepBy1` symbol ";" <* symbol "}")
    pOptionalWhere e = do
      mbs <- optional (keyword "where" *> symbol "{" *> binding `sepBy1` symbol ";" <* symbol "}")
      pure $ case mbs of
        Nothing -> e
        Just bs -> Where e bs
    whereExpr = annExpr >>= pOptionalWhere
    expr = lam <|> try ifE <|> try caseE <|> try letE <|> try doE <|> whereExpr