Search code examples
f#fparsec

FParsec Parsing list with optional content, fails on continuing with next parser


I'm trying to parse a list of asset types where each asset type potentially has a name. After the list is done I'd like to continue parsing a list of attributes for the asset types, one list for all asset types.

The string I'm trying to parse looks like: converter named a023, signaltower, powerunit named 23 attributes power, temperature The parser signature looks like Parser<((Asset * AssetName option) list * Attribute liste),unit> I got parsing the assets name, and the attributes separately, the problem arise when I combine the two and list is done, it then fails on the attributes string stating Expecting: 'named'.

To me it seems that it is trying the opt assetname parser which fails on the attributes string, but I am not sure how to ignore that and move on when the list is "done" (after all the asset name part is optional)..

type AssetName = AssetName of string
let named = str "named" >>. spaces1 >>. word
let assetName = spaces1 >>. (named |>> AssetName)

type Asset = | Converter | Signaltower | Powerunit 
let assetType = ["converter"; "signaltower"; "powerunit";] |> Seq.map pstring |> choice
let findAsset = function
| "converter" -> Converter
| "signaltower" -> Signaltower
| "powerunit" -> Powerunit
| _ -> raise <| Exception "Invalid asset type"
let asset = (assetType |>> findAsset) .>>. opt assetName 

type Attribute = Attribute of string
let attribute = word |>> Attribute
let attributes = spaces1 >>. str "attributes" >>. spaces1 >>. sepBy attribute commaMaybeSpace

let p = sepBy asset (pchar ',' >>. spaces) .>>. attributes
let r input = run p3 input
r "converter named a023, signaltower, powerunit named 23 attributes power, temperature"

Solution

  • Edit As discussed in a similar answer and also hinted at in the documentation, an optional parser opt p fails if p fails after changing its state. Instead, backtracking (restoring the state) should be used, either with the attempt parser, or with one of the backtracking operators.

    Keeping in mind that opt (attempt (p >>. q)) is equivalent to opt (p >>? q), try

    let assetName = spaces1 >>? (named |>> AssetName)
    

    Not sure if it has some bearing on the issue, but the type of the parser in the example does not match the grammar you are describing. I think you want:

    asset-type : | Converter | Signaltower | Powerunit

    named-asset : | asset-type | asset-type named ident

    asset-list : named-asset,...,named-asset

    attributed-asset-list : asset-list attributes ident-list

    The following line does not repeat the parser for named-asset.

    let p = asset .>>. attributes
    // val p : Parser<((Asset * AssetName option) * Attribute list),unit>
    

    You can replace it with

    let p = sepBy asset (pchar ',' >>. spaces) .>>. attributes
    // val p : Parser<((Asset * AssetName option) list * Attribute list),unit>
    
    "converter named a023, signaltower, powerunit named 23 attributes power, temperature"
    |> run p |> printfn "%A" 
    // Success: ([(Converter, Some (AssetName "a023")); (Signaltower, null);
    //   (Powerunit, Some (AssetName "23"))],
    //  [Attribute "power"; Attribute "temperature"])