Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsec: backtracking not working

I am trying to parse F# type syntax. I started writing an [F]Parsec grammar and ran into problems, so I simplified the grammar down to this:

type ::= identifier | type -> type
identifier ::= [A-Za-z0-9.`]+

After running into problems with FParsec, I switched to Parsec, since I have a full chapter of a book dedicated to explaining it. My code for this grammar is

typeP = choice [identP, arrowP]
identP = do
   id <- many1 (digit <|> letter <|> char '.' <|> char '`')
   -- more complicated code here later
   return id
arrowP = do
  domain <- typeP
  string "->"
  range <- typeP
  return $ "("++domain++" -> "++range++")"
run = parse (do t <- typeP
                eof
                return t) "F# type syntax"

The problem is that Parsec doesn't backtrack by default, so

> run "int"
Right "int"
-- works! 
> run "int->int"
Left "F# type syntax"
unexpected "-"
expecting digit, letter, ".", "`" or end of input
-- doesn't work!

The first thing I tried was to reorder typeP:

typeP = choice [arrowP, identP]

But this just stack overflows because the grammar is left-recursive--typeP never gets to trying identP because it keeps trying arrowP over and over. Next I tried try in various places, for example:

typeP = choice [try identP, arrowP]

But nothing I do seems to change the basic behaviours of (1) stack overflow or (2) non-recognition of "->" following an identifier.

My mistake is probably obvious to anybody who has successfully written a Parsec grammar. Can somebody point it out?

like image 505
Nathan Shively-Sanders Avatar asked Jun 24 '26 22:06

Nathan Shively-Sanders


1 Answers

I think the problem is that, and I'm making an assumption for F# (because I don't know it), arrows are right associative. I'm not sure how precise the linked grammar is supposed to be, as I'm not well versed in different grammars. But if we can assume arrows are right associative that makes the problem easier.

So with that assumption we can trivially do:

identP = many1 (digit <|> letter <|> char '.' <|> char '`')

typeP = try arrowP <|> identP

arrowP = do
  i <- identP
  string "->"
  t <- typeP
  return $ "(" ++ i ++ " -> " ++ t ++ ")"

run = flip parse "F# type syntax" $ do
        t <- typeP
        eof
        return t

So:

Haskell> run "int"
Right "int"
Haskell> run "int->int"
Right "(int -> int)"
Haskell> run "int->int->int->int"
Right "(int -> (int -> (int -> int)))"

Expanding further, what might be confusing you is that in that grammar it says type -> type, which means you could have an arrow on the left side. That's fine, but it needs to be in parentheses. Which helps, maybe seeing the following in action is helpful. It helped me.

typeP = try arrowP <|> parens typeP <|> identP

arrowP = do
 i <- parens typeP <|> identP
 string "->"
 t <- typeP
 return $ "(" ++ i ++ " -> " ++ t ++ ")"

parens p  = between (char '(') (char ')') p

Now we can write arrows on the left or the right side of an arrow:

Haskell> run "int->int->int"
Right "(int -> (int -> int))"
Haskell> run "(int->int)->int"
Right "((int -> int) -> int)"
like image 134
Christopher Done Avatar answered Jun 27 '26 15:06

Christopher Done