Разбор чисел в FParsec

Я начал изучать FParsec. У него очень гибкий способ анализа чисел; Я могу предоставить набор числовых форматов, которые я хочу использовать:

type Number =
    | Numeral of int
    | Decimal of float
    | Hexadecimal of int
    | Binary of int

let numberFormat = NumberLiteralOptions.AllowFraction
                   ||| NumberLiteralOptions.AllowHexadecimal
                   ||| NumberLiteralOptions.AllowBinary

let pnumber = 
    numberLiteral numberFormat "number"
    |>> fun num -> if num.IsHexadecimal then Hexadecimal (int num.String)
                   elif num.IsBinary then Binary (int num.String)
                   elif num.IsInteger then Numeral (int num.String)
                   else Decimal (float num.String)

Однако язык, который я пытаюсь разобрать, немного странный. Число может быть числовым (неотрицательное int), десятичным (неотрицательное float), шестнадцатеричным (с префиксом #x) или двоичным (с префиксом #b):

numeral: 0, 2
decimal: 0.2, 2.0
hexadecimal: #xA04, #x611ff
binary: #b100, #b001

Прямо сейчас мне нужно дважды выполнить синтаксический анализ, заменив # на 0 (при необходимости), чтобы использовать pnumber:

let number: Parser<_, unit> =  
    let isDotOrDigit c = isDigit c || c = '.'
    let numOrDec = many1Satisfy2 isDigit isDotOrDigit 
    let hexOrBin = skipChar '#' >>. manyChars (letter <|> digit) |>> sprintf "0%s"
    let str = spaces >>. numOrDec <|> hexOrBin
    str |>> fun s -> match run pnumber s with
                     | Success(result, _, _)   -> result
                     | Failure(errorMsg, _, _) -> failwith errorMsg

Как лучше разобрать в этом случае? Или как я могу изменить CharStream FParsec, чтобы упростить условный анализ?


person pad    schedule 06.02.2012    source источник


Ответы (1)


Разбор чисел может быть довольно запутанным, если вы хотите генерировать хорошие сообщения об ошибках и должным образом проверять переполнения.

Ниже приведена простая реализация вашего парсера чисел на FParsec:

let numeralOrDecimal : Parser<_, unit> =
    // note: doesn't parse a float exponent suffix
    numberLiteral NumberLiteralOptions.AllowFraction "number" 
    |>> fun num -> 
            // raises an exception on overflow
            if num.IsInteger then Numeral(int num.String)
            else Decimal(float num.String)

let hexNumber =    
    pstring "#x" >>. many1SatisfyL isHex "hex digit"
    |>> fun hexStr -> 
            // raises an exception on overflow
            Hexadecimal(System.Convert.ToInt32(hexStr, 16)) 

let binaryNumber =    
    pstring "#b" >>. many1SatisfyL (fun c -> c = '0' || c = '1') "binary digit"
    |>> fun hexStr -> 
            // raises an exception on overflow
            Binary(System.Convert.ToInt32(hexStr, 2))


let number =
    choiceL [numeralOrDecimal
             hexNumber
             binaryNumber]
            "number literal"

Генерация хороших сообщений об ошибках при переполнении немного усложнила бы эту реализацию, так как в идеале вам также нужно было бы возвращаться после ошибки, чтобы позиция ошибки заканчивалась в начале числового литерала (см. numberLiteral в качестве примера).

Простой способ изящно обработать возможное исключение переполнения — использовать небольшой комбинатор обработки исключений, подобный следующему:

let mayThrow (p: Parser<'t,'u>) : Parser<'t,'u> =
    fun stream ->
        let state = stream.State        
        try 
            p stream
        with e -> // catching all exceptions is somewhat dangerous
            stream.BacktrackTo(state)
            Reply(FatalError, messageError e.Message)

Затем вы могли бы написать

let number = mayThrow (choiceL [...] "number literal")

Я не уверен, что вы имели в виду, говоря «изменить CharStream FParsec, чтобы упростить условный синтаксический анализ», но следующий пример демонстрирует, как вы можете написать низкоуровневую реализацию, которая напрямую использует только методы CharStream.

type NumberStyles = System.Globalization.NumberStyles
let invariantCulture = System.Globalization.CultureInfo.InvariantCulture

let number: Parser<Number, unit> =
  let expectedNumber = expected "number"
  let inline isBinary c = c = '0' || c = '1'
  let inline hex2int c = (int c &&& 15) + (int c >>> 6)*9

  let hexStringToInt (str: string) = // does no argument or overflow checking        
      let mutable n = 0
      for c in str do
          n <- n*16 + hex2int c
      n    

  let binStringToInt (str: string) = // does no argument or overflow checking
      let mutable n = 0
      for c in str do
          n <- n*2 + (int c - int '0')
      n

  let findIndexOfFirstNonNull (str: string) =
      let mutable i = 0
      while i < str.Length && str.[i] = '0' do
          i <- i + 1
      i

  let isHexFun = id isHex // tricks the compiler into caching the function object
  let isDigitFun = id isDigit
  let isBinaryFun = id isBinary

  fun stream ->
    let start = stream.IndexToken
    let cs = stream.Peek2()        
    match cs.Char0, cs.Char1 with
    | '#', 'x' ->
        stream.Skip(2)
        let str = stream.ReadCharsOrNewlinesWhile(isHexFun, false)
        if str.Length <> 0 then
            let i = findIndexOfFirstNonNull str
            let length = str.Length - i
            if length < 8 || (length = 8 && str.[i] <= '7') then
                Reply(Hexadecimal(hexStringToInt str))
            else
                stream.Seek(start)
                Reply(Error, messageError "hex number literal is too large for 32-bit int")
        else 
            Reply(Error, expected "hex digit")

    | '#', 'b' ->
        stream.Skip(2)
        let str = stream.ReadCharsOrNewlinesWhile(isBinaryFun, false)
        if str.Length <> 0 then
            let i = findIndexOfFirstNonNull str
            let length = str.Length - i
            if length < 32 then 
                Reply(Binary(binStringToInt str))
            else
                stream.Seek(start)
                Reply(Error, messageError "binary number literal is too large for 32-bit int")
        else 
            Reply(Error, expected "binary digit")

    | c, _ ->
        if not (isDigit c) then Reply(Error, expectedNumber)
        else
            stream.SkipCharsOrNewlinesWhile(isDigitFun) |> ignore
            if stream.Skip('.') then
                let n2 = stream.SkipCharsOrNewlinesWhile(isDigitFun)
                if n2 <> 0 then
                    // we don't parse any exponent, as in the other example
                    let mutable result = 0.
                    if System.Double.TryParse(stream.ReadFrom(start), 
                                              NumberStyles.AllowDecimalPoint,
                                              invariantCulture, 
                                              &result)
                    then Reply(Decimal(result))
                    else 
                        stream.Seek(start)
                        Reply(Error, messageError "decimal literal is larger than System.Double.MaxValue")                    
                else 
                    Reply(Error, expected "digit")
            else
               let decimalString = stream.ReadFrom(start)
               let mutable result = 0
               if System.Int32.TryParse(stream.ReadFrom(start),
                                        NumberStyles.None,
                                        invariantCulture,
                                        &result)
               then Reply(Numeral(result))
               else 
                   stream.Seek(start)
                   Reply(Error, messageError "decimal number literal is too large for 32-bit int")

Хотя эта реализация анализирует шестнадцатеричные и двоичные числа без помощи системных методов, в конечном итоге она делегирует анализ десятичных чисел методам Int32.TryParse и Double.TryParse.

Как я уже сказал: это грязно.

person Stephan Tolksdorf    schedule 06.02.2012
comment
+1, спасибо за быстрый ответ, Стефан. Под изменением CharStream FParsec... я имел в виду низкоуровневую манипуляцию с CharStream. Я пойду на первый подход, простой и понятный. Кстати, какова стоимость использования комбинаторов с метками? Много ли это будет стоить, если я буду использовать метки везде в парсере? - person pad; 06.02.2012
comment
Я только что добавил комментарий о том, как можно более изящно обрабатывать исключения переполнения в первой версии. Относительно этикеток: это зависит. choiceL на самом деле быстрее, чем choice, потому что ему не нужно собирать отдельные сообщения об ошибках. В целом накладные расходы <?> и подобных комбинаторов вряд ли можно измерить в нетривиальных приложениях. И если вы действительно обнаружите проблему с производительностью в парсере FParsec, всегда есть способы сделать это быстрее... - person Stephan Tolksdorf; 06.02.2012
comment
Спасибо за подробный ответ. Только одно незначительное замечание: в данном случае skipString следует предпочесть pstring, верно? - person pad; 06.02.2012
comment
Разницы в производительности нет, так как оба синтаксических анализатора не должны выполнять никакой работы по созданию результирующего значения (которое в обоих случаях является константой ссылочного типа). Следовательно, это только вопрос вкуса. - person Stephan Tolksdorf; 06.02.2012