2012-02-06 3 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 

은 지금은에 의해 두 번 구문 분석 할 필요가

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 

이 경우 분석의 더 나은 방법은 무엇입니까 : 0에 의해 # (필요한 경우)을 대체하는 것은 pnumber을 사용하려면? 또는 FParsec의 CharStream을 변경하여 조건부 구문 분석을 쉽게 수행 할 수 있습니까?

답변

9

좋은 오류 메시지를 생성하고 오버플로를 제대로 확인하려는 경우 구문 분석 번호가 꽤 복잡 할 수 있습니다. 당신은 또한 오류가 발생한 후 역 추적 할 필요가 이상적 것처럼 있도록,이 구현 조금 복잡 것이다 오버 플로우에 좋은 오류 메시지를 생성

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" 

:

다음

은 간단한 FParsec의 전화 번호 파서의 구현 오류 위치는 숫자 리터럴의 시작 부분에서 끝납니다 (예제는 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 빠른 응답, Stephan에게 감사드립니다. "FParsec의 CharStream을 ... 변경"함으로써, 나는 CharStream의 낮은 수준의 조작을 의미했습니다. 나는 첫 번째 접근법을 간단하고 이해하기 쉽도록 갈 것입니다. BTW, 레이블이있는 결합자를 사용하는 비용은 얼마입니까? 파서에서 어디서나 레이블을 사용하면 비용이 많이 듭니까? – pad

+0

방금 ​​첫 번째 버전에서 오버플로 예외를보다 효과적으로 처리 할 수있는 방법에 대한 의견을 추가했습니다. 라벨에 관한 내용은 다릅니다. 'choiceL'은 개별 오류 메시지를 수집 할 필요가 없기 때문에 실제로'choice'보다 빠릅니다. 일반적으로 ''과 유사한 연결자의 오버 헤드는 중요하지 않은 응용 프로그램에서는 거의 측정 할 수 없습니다. FParsec 파서에서 성능 문제를 실제로 발견하면 항상 더 빨리 수행 할 수있는 방법이 있습니다. –

+0

자세한 답변을 보내 주셔서 감사합니다.이 경우에는'skstring'을'pstring'보다 선호해야합니다. – pad