packages feed

pgrep-0.1.0.0: src/Scrappy/Grep/DSL/Parser.hs

module Scrappy.Grep.DSL.Parser
  ( parseExpr
  , ParseError
  ) where

import Scrappy.Grep.DSL

import Text.Parsec hiding ((<|>), many)
import Text.Parsec.String (Parser)
import Control.Applicative ((<|>), many)
import Control.Monad (void)

-- | Parse a DSL string into an AST
parseExpr :: String -> Either ParseError ParserExpr
parseExpr = parse (spaces *> exprParser <* eof) "parsec-grep DSL"

-- | Main expression parser - handles combinators with proper precedence
-- Precedence (lowest to highest): <|>, >> / <+>, modifiers, atoms
exprParser :: Parser ParserExpr
exprParser = altExpr

-- | Alternative: p1 <|> p2
altExpr :: Parser ParserExpr
altExpr = chainl1 seqExpr altOp
  where
    altOp = PAlt <$ try (spaces *> string "<|>" <* spaces)

-- | Sequence: p1 >> p2 or p1 <+> p2
seqExpr :: Parser ParserExpr
seqExpr = chainl1 termExpr seqOp
  where
    seqOp = try (PSeqConcat <$ try (spaces *> string "<+>" <* spaces))
        <|> (PSeq <$ try (spaces *> string ">>" <* spaces))

-- | Term: modified expression or atom
termExpr :: Parser ParserExpr
termExpr = modifiedExpr <|> atomExpr

-- | Modifiers: many, some, optional, try
modifiedExpr :: Parser ParserExpr
modifiedExpr = do
  modifier <- try $ do
    m <- choice
      [ PMany <$ try (string "many")
      , PSome <$ try (string "some")
      , POptional <$ try (string "optional")
      , PTry <$ try (string "try")
      ]
    spaces1
    pure m
  inner <- termExpr
  pure $ modifier inner

-- | At least one space
spaces1 :: Parser ()
spaces1 = void $ many1 (oneOf " \t\n")

-- | Atom: primitive, reference, or parenthesized expression
atomExpr :: Parser ParserExpr
atomExpr = choice
  [ parenExpr
  , primitiveExpr
  , refExpr
  ]

-- | Parenthesized expression
parenExpr :: Parser ParserExpr
parenExpr = between (char '(' <* spaces) (spaces *> char ')') exprParser

-- | Reference to named parser: ref "name"
refExpr :: Parser ParserExpr
refExpr = do
  try $ string "ref"
  spaces
  PRef <$> stringLiteral

-- | Primitive parsers
primitiveExpr :: Parser ParserExpr
primitiveExpr = choice
  [ try manyTillExpr
  , try betweenExpr
  , try countExpr
  , try charExpr
  , try stringExpr
  , try oneOfExpr
  , try noneOfExpr
  , try $ PAlphaNum <$ string "alphaNum"  -- must come before 'alpha' prefix match
  , try $ PAnyChar <$ string "anyChar"
  , try $ PDigit <$ string "digit"
  , try $ PLetter <$ string "letter"
  , try $ PSpaces <$ string "spaces"      -- must come before 'space'
  , try $ PSpace <$ string "space"
  , try $ PNewline <$ string "newline"
  ]

-- | char 'x'
charExpr :: Parser ParserExpr
charExpr = do
  string "char"
  spaces
  PChar <$> charLiteral

-- | string "abc"
stringExpr :: Parser ParserExpr
stringExpr = do
  string "string"
  spaces
  PString <$> stringLiteral

-- | oneOf "abc"
oneOfExpr :: Parser ParserExpr
oneOfExpr = do
  string "oneOf"
  spaces
  POneOf <$> stringLiteral

-- | noneOf "abc"
noneOfExpr :: Parser ParserExpr
noneOfExpr = do
  string "noneOf"
  spaces
  PNoneOf <$> stringLiteral

-- | between '(' ')' expr
betweenExpr :: Parser ParserExpr
betweenExpr = do
  string "between"
  spaces
  open <- charLiteral
  spaces
  close <- charLiteral
  spaces
  inner <- termExpr
  pure $ PBetween open close inner

-- | count 3 expr
countExpr :: Parser ParserExpr
countExpr = do
  _ <- string "count"
  spaces
  n <- read <$> many1 digit
  spaces
  inner <- termExpr
  pure $ PCount n inner

-- | manyTill p end - match p until end, non-greedy
manyTillExpr :: Parser ParserExpr
manyTillExpr = do
  _ <- string "manyTill"
  spaces
  p <- termExpr
  spaces
  end <- termExpr
  pure $ PManyTill p end

-- | Character literal: 'x' or '\n' etc
charLiteral :: Parser Char
charLiteral = between (char '\'') (char '\'') charContent
  where
    charContent = escapedChar <|> noneOf "'\\"

-- | String literal: "abc" with escape support
stringLiteral :: Parser String
stringLiteral = between (char '"') (char '"') (many stringChar)
  where
    stringChar = escapedChar <|> noneOf "\"\\"

-- | Escape sequences
escapedChar :: Parser Char
escapedChar = char '\\' >> choice
  [ '\n' <$ char 'n'
  , '\t' <$ char 't'
  , '\r' <$ char 'r'
  , '\\' <$ char '\\'
  , '\'' <$ char '\''
  , '"' <$ char '"'
  ]