Repository navigation
Description
This may be an XY problem, but I'm having an issue with sepEndBy not being greedy enough, and is hiding errors.
For context, I'm parsing a haskell-like language with the following syntax
module Main
import Prelude
let main = println "Hello world!"The code to do this is pretty simple:
module' :: Parser (Module Frontend)
module' = dbg "module'" $ do
header <- optional . try $ header
let _name = maybe (ModuleName ("Main" :| [])) fst header
skipNewlines
imports <- import' `sepEndBy` many newline
declarations <- declaration _name `sepEndBy` many newline
pure $
Module
{ _moduleName = _name
, _moduleExposing = maybe ExposingAll snd header
, _moduleImports = imports
, _moduleDeclarations = declarations
}
header :: Parser (ModuleName, Exposing MaybeQualified)
header = do
-- module Name exposing (..)
symbol "module"
moduleName' <- lexeme Parse.moduleName
exposing' <- exposing
pure (moduleName', exposing')Using module' <* eof as the "entrypoint" parser.
The problem is, if declaration fails for some reason, the error thrown doesn't propagate up. Instead, sepEndBy just returns an empty list, and the parser succeeds. This results in some very opaque error messages. For example, if I omitted the body for the let declaration:
let main =
I'd expect to see an error saying something akin to Unexpected end of input, expecting expression. Instead, the module' succeeds with an empty list of declarations, and the eof causes an unintuitive and vague error message:
unexpected 'l'
expecting "import", end of input, or newline
(I'm not actually sure why it says this and not something like Unexpected let ..., expecting eof, but that's not the main problem here)
I have dbg'd and declaration throws the expected error, the problem is with sepEndBy.
My question is, is there an alternative to sepEndBy that will "greedily" parse the declarations? I still want to allow empty modules so sepEndBy1 won't do, but if a let is seen it should keep parsing and fail if necessary, rather than backtracking and returning an empty list.
Thanks for any help!
Activity
bristermitten commentedon Feb 17, 2023
I wrote my own combinator
and it seems to work, but not sure if this is an optimal way
mrkkrp commentedon Feb 18, 2023
Can you show
declaration? I think the issue here is that whendeclarationfails it completely backtracks (I don't know why, do you wrap the whole thing with atryby chance?) What you want is to "commit" early enough to parsing a declaration, that is, you need yourdeclarationparser to consume input so that it cannot backtrack anymore. This waysepEndBywon't be able to succeed in the way you are observing and you should get a more helpful and localized error message. Your custom combinatorsepEndBy'forces at least one occurrence ofp(in that it is similar tosepEndBy1) and so it appears to solve the problem for the first declaration in your input, however you may find that the second and later declarations still suffer from the same problem.bristermitten commentedon Feb 19, 2023
That explanation makes sense, thanks for your response. How could I make it commit?
declarationcurrently looks like this:To allow the syntax
def <name> : <type>andlet <name> = <body>with some messy indentation handling ;)Let me know if you need to see any more code. Thanks!
mitchellwrosen commentedon Feb 20, 2023
@knightzmc Check out the
headed-megaparsecpackage for some inspiration. The technique used there, which I quite like and often copy around to various projects, is to have a parser either return a value as normal (with the usual alternative semantics) or return another parser (which effectively "commits" to the current branch, and returns the parser to run on the remainder of the input).However, I do wonder whether another package is even necessary. Could
megaparsecitself just expose acommit/cutcombinator which throws away the failure continuations of a parser? (Or perhaps there's a different way of committing that's already present?)bristermitten commentedon Feb 20, 2023
@mitchellwrosen I had a look at that package, seems to solve my problem! But I agree that there should be an easier way of doing this which doesn't require external libraries (and a lot of boilerplate converting to and from
ParsecandHeadedParseceverywhere)mrkkrp commentedon Feb 20, 2023
From what I see in your code it should work as it is, but it is still incomplete, so I cannot be sure. @knightzmc Can you provide a repository with complete source code and an example input? I could then give it a try and it would perhaps be clearer what is going on there.
BlueNebulaDev commentedon Mar 18, 2023
I'm having a similar issue.
I'd like to parse either a list of key-value pairs (separated by
:), or a single value. For instance:a:x,b:yshould be parsed as list of two key-value pairs[("a","x"), ("b","y")].xshould be parsed as the single value"x".Here is a very minimal, simplified grammar. It doesn't support white spaces and both keys and values can only be identifiers. But it's enough to show the issue.
This thing handles correctly the two examples I showed above, and reports the correct error for invalid inputs like
a:x,b:err ororerr or.However the error it reports for an input like
a:x,b:y,cora:x,b:y,err or,c:zis not what I would like to see:When the input is
a:x,b:y,cora:x,b:y,err or,c:z, I would like to see an error after the last,that parsed correctly.Is there any way to get Megaparsec to report the error I wish to see?
mrkkrp commentedon Mar 19, 2023
@BlueNebulaDev with your definition of
keyValit cannot backtrack, since once it consumes at least one letter ofidentit is already committed. Therefore somewhere after that final,it indeed fails, like you expect, but because you also havetryaround(KeyValList <$> keyVal `sepEndBy1` ",")that whole part backtracks to the very beginning of input. Next,identgets a chance to run and fails with the error that you observe. You need to removetryfrom your definition ofprogand instead think when it makes sense forkeyValto commit. It looks like as soon as there is:we can be sure that it should be a key-value pair so:Try this and it should behave as you want in all four cases.
Still waiting a complete example from @knightzmc which should be fixable without resorting to any third-party libraries, but I'm happy to be proven wrong.
BlueNebulaDev commentedon Mar 21, 2023
Thanks! After following your suggestion, the errors my parser is reporting are much better. I'm not sure I understand why
a:x,b:err orgave the expected error even without this change though. I'll try to play withdbgto understand exactly what happens.mrkkrp commentedon Mar 21, 2023
"a:x,b:err or"resulted in a better error message because thesepEndBy1combinator did not match the separator","after fully consuming the second key-value pair. This waysepEndBy1as a whole succeeded and then you probably haveeofsomewhere, which is actually what produces the error that you see.bristermitten commentedon Apr 6, 2023
Hey apologies for the delay. This code is pretty outdated now but if you're still happy to take a look I'd appreciate it!
https://github.com/ElaraLang/elara/blob/3de6a66f82a86a45726dc3b7aa1286bd7aaa6209/src/Elara/Parse/Declaration.hs
mrkkrp commentedon Nov 13, 2026
@bristermitten Sorry for taking so long with this. I finally looked at the code you linked, and the culprit is
optionallyIndentedinElara/Parse/Indents.hs:The second
trywraps the entireletdeclaration: the keyword, the name, the patterns,=, and the body. When the body fails, thattryrewinds the input back to before let, soletDec(and hencedeclaration) fails without consuming input. That is exactly the situation in whichsepEndByis allowed to choose itspure []branch, so the original error is discarded andeofthen fails at thel.The fix is to remove the try from the last alternative. Nothing is tried after it, so it can only hide errors:
I checked this on a stripped-down copy of your parser. With let main = as input the original definition gives:
and without the try:
Errors in the second and later declarations, and inside indented bodies, get localized in the same way. With this change the plain
sepEndByworks, so you don't needsepEndBy'and empty modules are allowed again.The general rule:
tryshould only wrap the shortest prefix needed to decide which branch to take. Once that prefix has been consumed, the parser is committed. This is the "commit"/"cut" behavior that was discussed above: in Megaparsec, consuming input already commits, and try is what undoes that. The othertrys in that code (insepEndBy',sepBy1',element,withIndentOrNormal) are worth reviewing with this in mind.I see that Elara has since moved to a separate lexer, so this is probably moot for you now. I'm going to close the issue, but feel free to reopen if you hit something similar.