2012-02-06 11 views
6

私は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)、(接頭辞#b付き)またはバイナリ(接頭#x付き)進:

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

は今、私は2回構文解析を行う必要がありますpnumberを利用するように0によって#(必要な場合)を代入:

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 

この場合の解析の良い方法は何ですか?または、FParsecのCharStreamを変更して、条件付き解析を容易にする方法はありますか?

答えて

9

エラーメッセージを生成し、オーバーフローを適切にチェックしたい場合は、解析番号をかなり乱雑にすることができます。

次はあなたの番号パーサの簡単な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") 

を書くことができ何を「FParsecのCharStreamを変更することで条件付き解析を容易にする」と言う意味ですが、次のサンプルでは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方法に小数の解析。

私が言ったように:それは面倒です。

+0

+1、高速応答のおかげで、ステファン。 "FParsecのCharStreamを...変更する"とは、CharStreamの低レベル操作を意味していました。私は最初のアプローチ、簡単で理解できるように行きます。ところで、ラベルでコンビネータを使用するコストはいくらですか?パーサ内のどこにでもラベルを使用すれば、コストがかかりますか? – pad

+0

私は、最初のバージョンのオーバーフロー例外をどのように扱うことができるかについてコメントを追加しました。ラベルに関して:それは依存しています。 'choiceL'は実際には' choice'よりも高速です。個々のエラーメッセージを収集する必要がないからです。一般に、 ''と類似のコンビネータのオーバーヘッドは、自明でないアプリケーションではほとんど測定できません。 FParsecパーサーで実際にパフォーマンスの問題を発見した場合は、常に高速化する方法があります。 –

+0

詳細な回答ありがとうございます。この場合、 'skstring'を' pstring'よりも優先すべきですか? – pad

関連する問題