2012-02-06 18 views
6

Comencé a aprender FParsec. Tiene una forma muy flexible de analizar números; Puedo proporcionar un conjunto de formatos de número Quiero usar:Números de análisis en 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) 

Sin embargo, el lenguaje que trato de analizar es un poco extraño. Un número podría ser numeral (no negativo int), decimal (no negativo float), hexadecimal (con el prefijo #x) o binario (con el prefijo #b):

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

Ahora mismo tiene que hacer el análisis dos veces por sustituyendo # por 0 (si es necesario) para hacer uso de 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 

¿Qué es una mejor manera de analizar en este caso? ¿O cómo puedo alterar el CharStream de FParsec para poder hacer el análisis condicional más fácil?

Respuesta

9

Los números de análisis pueden ser bastante complicados si desea generar buenos mensajes de error y controlar adecuadamente los desbordamientos.

La siguiente es una sencilla aplicación FParsec de su número de analizador:

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" 

Generación de buenos mensajes de error en los desbordamientos complicaría esta aplicación un poco, ya que idealmente también tendría que dar marcha atrás después del error, de manera que la posición del error termina al comienzo del número literal (vea los documentos numberLiteral para ver un ejemplo).

Una forma sencilla de manejar con gracia posible excepción de desbordamiento es el uso de un poco de combinador manejo de excepciones como la siguiente:

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) 

A continuación, podría escribir

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

No estoy seguro de lo que significaba decir con "alterar elde FParsec para poder hacer el análisis condicional más fácil", pero el siguiente ejemplo demuestra cómo se puede escribir una implementación de bajo nivel que solo usa los métodos CharStream directamente.

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") 

Si bien esta aplicación analiza hexagonal y números binarios sin la ayuda de los métodos del sistema, con el tiempo los delegados el análisis de los números decimales a los métodos Int32.TryParse y Double.TryParse.

Como dije: es complicado.

+0

+1, gracias por la respuesta rápida, Stephan. Con "Alterar CharStream de FParsec ...", quise decir una manipulación de bajo nivel de 'CharStream'. Iré por el primer acercamiento, simple y comprensible. Por cierto, ¿cuál es el costo de usar combinators con etiquetas? ¿Cuesta mucho si uso etiquetas en todas partes en el analizador? – pad

+0

Acabo de añadir un comentario sobre cómo se podría tratar con excepciones de desbordamiento en la primera versión de forma más elegante. En cuanto a las etiquetas: depende. 'choiceL' es realmente más rápido que' choice', porque no tiene que recopilar los mensajes de error individuales. En general, la sobrecarga de '' y combinadores similares debería ser apenas medible en aplicaciones no triviales. Y si realmente detecta un problema de rendimiento en un analizador FParsec, siempre hay formas de hacerlo más rápido ... –

+0

Gracias por la respuesta detallada.Solo un punto menor, 'skipString' debe ser preferido a' pstring' en este caso, ¿verdad? – pad

Cuestiones relacionadas