I think what you probably want is the CharStream.ReadFrom
method:
Returns a string with the chars between the index of the stateWhereStringBegins
(inclusive) and the current Index
of the stream (exclusive).
What you'd do is this:
let trackLocation (parser: Parser<'A, 'B>): Parser<SourceLocation * 'A, 'B> =
fun (stream : CharStream<'B>) ->
let oldState = stream.State
let parseResult = parser stream
if parseResult.Status = Ok then
let newState = stream.State
let matchedText = stream.ReadFrom (oldState, true)
// Or (oldState, false) if you DON'T want to normalize newlines
let location = { from = oldState.GetPosition stream
``to`` = newState.GetPosition stream
text = matchedText }
let result = (location, parseResult.Result)
Reply(result)
else
Reply(parseResult.Status, parseResult.Error)
Usage example (which also happens to be the test code that I wrote to confirm that it works):
let pThing = trackLocation pfloat
let test p str =
match run p str with
| Success((loc, result), _, _) -> printfn "Success: %A at location: %A" result loc; result
| Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg; 0.0
test pThing "3.5"
// Prints: Success: 3.5 at location: {from = (Ln: 1, Col: 1);
// to = (Ln: 1, Col: 4);
// text = "3.5";}
Edit: Stephan Tolksdorf (the author of FParsec) pointed out in a comment that the withSkippedString combinator exists. That one will probably be simpler, as you don't have to write the CharStream
-consuming function yourself. (The skipped
combinator would return the string that the parser matched, but without returning the parser's result, whereas withSkippedString
passes both the parser's result and the string skipped over into a function that you supply). By using the withSkippedString
combinator, you can use your original trackLocation
function with only minimal changes. The updated version of trackLocation
would look like this:
let trackLocation (parser: Parser<'A, 'B>): Parser<SourceLocation * 'A, 'B> =
let mkLocation ((start: Position, (text: string, data: 'A)), stop: Position) =
let location = { from = start; ``to`` = stop; text = text }
in (location, data)
getPosition .>>. (parser |> withSkippedString (fun a b -> a,b)) .>>. getPosition |>> mkLocation
(I'm not 100% happy with the arrangement of the tuples here, since it results in a tuple within a tuple within a tuple. A different combinator order might yield a nicer signature. But since it's an internal function not intended for public consumption, a nasty tuple-nesting in the function signature may not be a big deal, so I've left it as is. Up to you to rearrange it if you want a better function signature).
The same test code from my original answer runs fine with this updated version of the function, and prints the same result: start position (Line 1, Col 1), end position (Line 1, Col 4), and parsed text "3.5"
.
inputText
, then you would just needinputText.[start.Index .. stop.Index]
and you've got the matched text. Note that FParsec'sPosition.Index
property is an int64, so you might need to cast toint
if your input is 2^32 bytes or less. – FirstinputText.[start.Index .. stop.Index - 1]
: I haven't experimented withgetPosition
and I don't know if you'll have closed intervals or half-open intervals. Check for fencepost errors before you blindly apply my suggestion. – FirstsepBy
parser returns aParser<'a list, 'u>
and you don't have to deal with the original text. But I think theCharStream.BacktrackTo
method might be what you need. Give me a second and I'll write up a possible approach. – FirstCharStream.ReadFrom
is exactly what you're looking for. You have to pass it a CharStreamState, not a Position, but apart from that it's quite handy. – Firstskipped
andwithSkippedString
combinators: quanttec.com/fparsec/reference/… – HeidelbergCharStream.ReadFrom
since it allows staying at the "higher" combinator level of FParsec rather than dropping down to the "lower" level of CharStream and Reply objects. I've updated my answer to usewithSkippedString
. – First