如何避免使用 FParsec 回溯解析 SQL?

How to avoid backtracking parsing SQL with FParsec?

提问人:lamg 提问时间:9/23/2023 最后编辑:lamg 更新时间:9/25/2023 访问量:61

问:

我正在使用 FParsec 解析 SQL (Sqlite),并且在解析像 .由于用于识别标识符的解析器也识别关键字,因此我必须确保首先尝试解析关键字,并且只有在失败时才继续使用标识符。为此,我正在使用解析器组合器,但似乎我仍然无法逃避回溯。START_KEYWORD element_0, …, element_N END_KEYWORDmany1Till

下面是我当前如何解析子句的示例:ORDER BY

let sep1CommaEnd p endP =
    let commaOrEnd = symbol S.Comma <|> followedBy endP
    many1Till (p .>> commaOrEnd) (followedBy endP)

let orderBy =
    parse {
        do! keyword K.Order >>. keyword K.By
        let! xs = sep1CommaEnd ident (keyword K.Asc <|> keyword K.Desc)
        let! asc = (keyword K.Asc >>. preturn true <|> (keyword K.Desc >>. preturn false) <|> preturn true )
        return { columns = xs; asc = asc }
    }

此实现存在一些问题:

  • 它在 中使用两次,这可能会导致回溯。followedBysep1CommaEnd
  • 它重复解析用于结束构造的关键字,这是低效的。

为了解决这些问题,我想出了创建一个解析器组合器的想法,它允许我:sepBy1Cont

  • 在检测到用于结束的关键字后立即结束。
  • 累积所有结果,包括用于结束的关键字的解析器返回的结果。 这是我的实现:sepBy1Cont
type Either<'a, 'b> =
    | Left of 'a
    | Right of 'b

let sepBy1Cont (p: Parser<'a, unit>) (sepEnd: Parser<Either<'b, 'c>, unit>) =
    let rec loop (acc: 'a list) =
        parse {
            let! x = p
            let! d = sepEnd

            match d with
            | Left _ -> return! loop (x :: acc)
            | Right next ->
                let xs = List.rev (x :: acc)
                return (xs, next)
        }

    loop []

我的问题是:

  • 有没有更好的方法来解决我缺少的 FParsec 中的这个问题?
  • 如果是一个好的解决方案,有没有更好的方法来实施它?我应该了解 FParsec 的内部结构以改进它吗?sepBy1Cont
fparsec sql-parser

评论


答:

1赞 RonaldSchlenker 9/24/2023 #1

回答您的第一个问题(一般而言):FParsec 中有许多内置字符串解析器可以/应该用于清晰度和性能。

给定一个应该解析的伪语法:

START_KEYWORD element_0, …, element_N END_KEYWORD

这里有一个想法:

let sigStart = "START"
let sigEnd = "END"

let pDemo : Parser<_,unit> = 
    let field = many1Chars2 letter (letter <|> digit <|> pchar '_')
    let separator = pchar ',' .>> spaces
    let fields = sepBy1 field separator
    between (pstring sigStart .>> spaces1) (spaces1 >>. pstring sigEnd) fields

let run p s =
    match runParserOnString p () "" s with
    | Success (result, _, _) -> printfn "Success: %A" result
    | Failure (msg, _, _) -> printfn "Failure: %s" msg

$"{sigStart} field_0, field_1, field_N {sigEnd}"
|> run pDemo

// Success: ["field_0"; "field_1"; "field_N"]

评论

0赞 lamg 9/24/2023
谢谢。我认为在那种特定情况下使用是个好主意。但是,对于更一般的 do 意味着结果不会确切地告诉我它是否以 or 结尾。因此,为了在 的结果之后继续解析,我将不得不再次解析。这意味着我最终会得到我试图解决的相同问题。betweenSELECT col0, col1, … (FROM | ORDER BY)between (keyword "SELECT") (keyword "FROM" <|> (keyword "ORDER" >>. keyword "BY")) columnParserFROMORDER BYbetweenFROM | ORDER BY