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.
+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
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 ... –
Gracias por la respuesta detallada.Solo un punto menor, 'skipString' debe ser preferido a' pstring' en este caso, ¿verdad? – pad