Header menu logo XParsec

XParsec

XParsec is a modern parser combinator library for F#. It allows you to build powerful, type-safe, and efficient parsers by composing small, reusable functions. It's designed from the ground up to provide excellent performance, great error messages, and a developer-friendly API that works seamlessly in both .NET and Fable projects.

Getting Started

Installation

You can add XParsec to your project via NuGet:

dotnet add package XParsec

Your First Parser

The best way to learn XParsec is to build something. We'll write a parser for a simple configuration file format. The file looks like this:

# config.txt
name = "XParsec"
version = 1.2
is_beta = true
# Comments should be ignored

Our goal is to parse this into a list of key-value pairs, ignoring comments and whitespace.

1. Define the Data Types

First, let's define the F# types that will represent our successfully parsed data. This is a great practice as it makes the goal of our parser clear.

open System

// A discriminated union for the different value types we can parse
type ConfigValue =
    | String of string
    | Float of float
    | Bool of bool

// A record to hold a single key-value pair
type KeyValuePair = { Key: string; Value: ConfigValue }

2. Writing the Parsers

Now, we'll build up our parser piece by piece. We'll start with the smallest elements of our format and combine them into a parser for the whole file.

open XParsec
open XParsec.CharParsers

// -- Basic Building Blocks --

// A parser for a valid identifier (our keys), which must have at least one character.
let pIdentifier = many1Chars (satisfy (fun c -> Char.IsLetterOrDigit c || c = '_'))

// A parser for a string literal enclosed in double quotes.
// `between` runs three parsers in sequence and returns the result of the middle one.
let pQuotedString = between (pchar '"') (pchar '"') (manyChars (noneOf [ '"' ]))

// A parser for comments, starting with '#' and consuming to the end of the line.
let pComment = pchar '#' >>. skipMany (satisfy (fun c -> c <> '\n'))

// A helper to parse any whitespace or comments. We'll use this to clean up.
// `skipMany` repeatedly runs a parser, consuming input but returning nothing (unit).
// `<|>` is the "choice" operator: it tries the left parser, and if it fails, tries the right.
let pWhitespaceOrComment = skipMany (spaces1 <|> pComment)

// -- Value Parsers --

// Now we parse the specific values. We use `|>>` (the map operator)
// to transform the parsed result into our `ConfigValue` DU cases.
let pString = pQuotedString |>> ConfigValue.String

let pFloat =
    pfloat .>> notFollowedBy (satisfy (fun c -> Char.IsLetter c))
    |>> ConfigValue.Float

// For booleans, we can be more explicit.
// `pstring "true" >>% true` parses the literal string "true" and returns the boolean `true`.
// We use `<|>` again to choose between the "true" and "false" parsers.
let pBool =
    pstring "true" >>% true <|> (pstring "false" >>% false) |>> ConfigValue.Bool

// The `choice` combinator tries a list of parsers in order until one succeeds.
let pValue = choice [ pString; pFloat; pBool ]

// -- Combining Everything --

// Now we define a parser for a full key-value pair line using a computation expression.
let pKeyValuePair =
    parser {
        let! key = pIdentifier
        do! pWhitespaceOrComment >>. pchar '=' >>. pWhitespaceOrComment // Consume whitespace and the '='
        let! value = pValue
        return { Key = key; Value = value }
    }

// Finally, the parser for the entire file.
// `many` parses zero or more `pKeyValuePair`s, separated by newlines.
// We wrap it all in `pWhitespaceOrComment` to handle leading/trailing space or comments.
// `.>> eof` is a common way to assert the parser should consume all input.
let pConfigFile =
    between 
        pWhitespaceOrComment 
        pWhitespaceOrComment 
        (many (pKeyValuePair .>> skipNewline)) 
    .>> eof

3. Running the Parser

With our pConfigFile parser defined, we can run it on our input.

let configText = """
# My Awesome Config
name = "XParsec"
version = 1.2
is_beta = true
"""

let result = Reader.ofString configText () |> pConfigFile

// Handle the result using pattern matching
match result with
| Ok success ->
    printfn "Successfully parsed config:"
    for kvp in success.Parsed do
        printfn $"- {kvp.Key}: {kvp.Value}"
| Error err ->
    // This case is handled in the next section.
    printfn "An error occurred."

This will produce the output:

Successfully parsed config:
- name: String "XParsec"
- version: Float 1.2
- is_beta: Bool true

4. Handling Parse Errors

One of XParsec's key features is its ability to generate human-readable error messages. Let's see what happens with invalid input.

let invalidConfigText = """
name = "XParsec"
version = 1.2a  # Invalid float
is_beta = true
"""

let result = Reader.ofString invalidConfigText () |> pConfigFile

match result with
| Ok _ -> () // This won't be hit
| Error err ->
    // The ErrorFormatting module can create a nicely formatted report.
    let errorMsg = ErrorFormatting.formatStringError invalidConfigText err
    printfn "Error parsing config:\n%s" errorMsg

This example demonstrates the core philosophy of XParsec: start with small, simple building blocks, and combine them using powerful operators and functions to create a readable, robust, and type-safe parser for any format.

namespace System
module String from Microsoft.FSharp.Core
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val float: value: 'T -> float (requires member op_Explicit)

--------------------
type float = System.Double

--------------------
type float<'Measure> = float
type bool = System.Boolean
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>

Type something to start searching.