I numeri di analisi possono essere piuttosto complicati se si desidera generare buoni messaggi di errore e verificare correttamente la presenza di overflow.
La seguente è una semplice implementazione FParsec del vostro numero di parser:
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"
Generazione di buoni messaggi di errore sul overflow complicherebbe questa implementazione un po ', come si farebbe in posizione ideale anche bisogno di fare marcia indietro dopo l'errore, in modo che la posizione dell'errore termina all'inizio del numero letterale (vedere i documenti numberLiteral per un esempio).
Un modo semplice per gestire con garbo possibile eccezione di overflow è quello di utilizzare un po 'di combinatore gestione delle eccezioni come la seguente:
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)
È quindi possibile scrivere
let number = mayThrow (choiceL [...] "number literal")
io non sono sicuro di quello che intendeva dire con "alterare il CharStream
di FParsec per rendere più semplice l'analisi condizionale", ma nell'esempio seguente viene illustrato come scrivere un'implementazione di basso livello che utilizza solo i metodi CharStream
direttamente.
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")
Mentre questa implementazione analizza esagono e numeri binari senza l'aiuto di metodi di sistema, alla fine delega il parsing di numeri decimali ai metodi Int32.TryParse e Double.TryParse.
Come ho detto: è disordinato.
+1, grazie per la rapida risposta, Stephan. Con "altera il CharStream di FParsec ...", intendevo una manipolazione di basso livello di 'CharStream'. Andrò per il primo approccio, semplice e comprensibile. A proposito, qual è il costo dell'uso dei combinatori con le etichette? È costato molto se uso le etichette ovunque nel parser? – pad
Ho appena aggiunto un commento su come si potrebbero gestire le eccezioni di overflow nella prima versione con maggiore eleganza. Per quanto riguarda le etichette: dipende. 'choiceL' è in realtà più veloce di' choice', perché non deve raccogliere i singoli messaggi di errore. In generale il sovraccarico di '>' e di combinatori simili dovrebbe essere difficilmente misurabile in applicazioni non banali. E se in effetti si riscontra un problema di prestazioni in un parser di FParsec, ci sono sempre dei modi per renderlo più veloce ... –
Grazie per la risposta dettagliata.Solo un punto minore, 'skipString' dovrebbe essere preferito a' pstring' in questo caso, giusto? – pad