Search code examples
haskellmegaparsec

Errorbundles after parsing with megaparsec


I currently have a working parser in megaparsec, where I build an AST for my program. I now want to do some weeding operations on my AST, while being able to use the same kind of pretty errors as the parser. While this stage is after parsing, I'm wondering if there are general practices for megaparsec in doing so. Is there a way for me to extract every line and comment (used in the bundle) and add it to each item in my AST? Is there any other way that people tackle this problem?

Apologies in advance if this sounds open ended, but I'm mainly wondering is there are some better ideas than getting the line numbers and creating bundles myself. I'm still new to haskell so I haven't been able to navigate properly through all the source code.


Solution

  • This was answered by the megaparsec developer here.

    To summarize, parsers have a getOffset function that returns the current char index. You can use that along with an initial PosState to create an error bundle which you can later pretty print.

    I have a sample project within the github thread, and pasted again here:

    module TestParser where
    
    import           Data.List.NonEmpty as NonEmpty
    import qualified Data.Maybe         as Maybe
    import qualified Data.Set           as Set
    import           Data.Void
    import           Parser
    import           Text.Megaparsec
    
    data Sample
      = Test Int
             String
      | TestBlock [Sample]
      | TestBlank
      deriving (Show, Eq)
    
    sampleParser :: Parser Sample
    sampleParser = do
      l <- many testParser
      return $ f l
      where
        f []  = TestBlank
        f [s] = s
        f p   = TestBlock p
    
    testParser :: Parser Sample
    testParser = do
      offset <- getOffset
      test <- symbol "test"
      return $ Test offset test
    
    fullTestParser :: Parser Sample
    fullTestParser = baseParser testParser
    
    testParse :: String -> Maybe (ParseErrorBundle String Void)
    testParse input =
      case parse (baseParser sampleParser) "" input of
        Left e -> Just e
        Right x -> do
          (offset, msg) <- testVerify x
          let initialState =
                PosState
                  { pstateInput = input
                  , pstateOffset = 0
                  , pstateSourcePos = initialPos ""
                  , pstateTabWidth = defaultTabWidth
                  , pstateLinePrefix = ""
                  }
          let errorBundle =
                ParseErrorBundle
                  { bundleErrors = NonEmpty.fromList [TrivialError offset Nothing Set.empty]
                                -- ^ A collection of 'ParseError's that is sorted by parse error offsets
                  , bundlePosState = initialState
                                -- ^ State that is used for line\/column calculation
                  }
          return errorBundle
    
    -- Sample verify; throw an error on the second test key
    testVerify :: Sample -> Maybe (Int, String)
    testVerify tree =
      case tree of
        TestBlock [_, Test a _, _] -> Just (a, "Bad")
        _                          -> Nothing
    
    testMain :: IO ()
    testMain = do
      testExample "test test test"
      putStrLn "Done"
    
    testExample :: String -> IO ()
    testExample input =
      case testParse input of
        Just error -> putStrLn (errorBundlePretty error)
        Nothing    -> putStrLn "pass"
    

    Some parts are from other files, but the important parts are in the code.