Combinators
Section titled “Combinators”Combinators compose simple parsers into more complex ones. They handle alternation, error labeling, lookahead, and recursion.
Alternation
Section titled “Alternation”Parseff.or_ tries the left parser. If it fails, backtracks (resets the cursor) and tries the right parser.
val or_ : (unit -> 'a) -> (unit -> 'a) -> unit -> 'alet bool_parser () = Parseff.or_ (fun () -> let _ = Parseff.consume "true" in true) (fun () -> let _ = Parseff.consume "false" in false) ()(* "true" -> Ok true *)(* "false" -> Ok false *)(* "maybe" -> Error { pos = 0; error = `Expected "false" } *)On "maybe", the left branch fails at position 0 (expected "true", got "m"), backtracks, and the right branch also fails (expected "false", got "m"). The error from the last branch attempted is reported.
or_ is ideal for two alternatives. When you have more, use Parseff.one_of, which takes a list and avoids nested or_ calls:
let keyword_with_or () = Parseff.or_ (fun () -> Parseff.consume "let") (fun () -> Parseff.or_ (fun () -> Parseff.consume "const") (fun () -> Parseff.consume "var") ()) ()
let keyword_with_one_of () = Parseff.one_of [ (fun () -> Parseff.consume "let"); (fun () -> Parseff.consume "const"); (fun () -> Parseff.consume "var"); ] ()one_of
Section titled “one_of”Parseff.one_of tries each parser in order until one succeeds. Equivalent to chaining or_, but cleaner for more than two alternatives.
val one_of : (unit -> 'a) list -> unit -> 'alet json_value () = Parseff.one_of [ null_parser; bool_parser; number_parser; string_parser; array_parser; object_parser; ] ()If all parsers fail, one_of fails with the error from the last parser attempted.
one_of_labeled
Section titled “one_of_labeled”Parseff.one_of_labeled is like Parseff.one_of, but each parser has a label. On failure, the error message reports all labels:
val one_of_labeled : (string * (unit -> 'a)) list -> unit -> 'alet literal () = Parseff.one_of_labeled [ ("number", fun () -> Number (Parseff.digit ())); ("string", string_parser); ("boolean", bool_parser); ] ()(* On failure: "expected one of: number, string, boolean" *)This gives users a clear picture of what was expected at a given position, without exposing internal parser details.
optional
Section titled “optional”Parseff.optional tries the parser. Returns Some result on success, None on failure (without consuming input).
val optional : (unit -> 'a) -> unit -> 'a optionlet signed_number () = let sign = Parseff.optional (fun () -> Parseff.char '-') () in let n = number () in match sign with | Some _ -> -n | None -> n(* "42" -> 42 *)(* "-42" -> -42 *)Lookahead
Section titled “Lookahead”look_ahead
Section titled “look_ahead”Parseff.look_ahead runs a parser without consuming input. The cursor stays where it was before the call. Fails if the parser fails.
val look_ahead : (unit -> 'a) -> 'alet peek_open_paren () = let _ = Parseff.look_ahead (fun () -> Parseff.char '(') in (* cursor hasn't moved, '(' is still the next character *) parse_parenthesized_expr ()Useful for context-sensitive decisions: peek at what’s coming, then choose which parser to run.
Error labeling
Section titled “Error labeling”expect
Section titled “expect”Parseff.expect runs a parser and relabels its failure with a clearer message.
val expect : string -> (unit -> 'a) -> 'alet dot () = Parseff.expect "a dot separator" (fun () -> Parseff.char '.')
let digit_val () = Parseff.expect "a digit (0-9)" Parseff.digitUse expect at grammar boundaries where user-facing wording is better than raw token names:
let ip_address () = let a = number () in let _ = Parseff.expect "a dot separator" (fun () -> Parseff.char '.') in let b = number () in let _ = Parseff.expect "a dot separator" (fun () -> Parseff.char '.') in let c = number () in let _ = Parseff.expect "a dot separator" (fun () -> Parseff.char '.') in let d = number () in Parseff.end_of_input (); (a, b, c, d)Without expect, a failed char '.' reports expected '.'. With expect, it reports expected a dot separator.
Recursion
Section titled “Recursion”Parseff.rec_ marks a recursive entry point for depth tracking. Wrap the body of recursive parsers with rec_ so that Parseff.parse can enforce max_depth and fail cleanly instead of overflowing the stack.
val rec_ : (unit -> 'a) -> 'alet rec json () = Parseff.rec_ (fun () -> Parseff.skip_whitespace (); Parseff.one_of [ array_parser; object_parser; null_parser; bool_parser; number_parser; string_parser; ] () )
and array_parser () = let _ = Parseff.consume "[" in (* ... calls json () recursively ... *) let _ = Parseff.consume "]" in Array elementsThe ~max_depth parameter on Parseff.parse controls the limit (default: 128):
(* Reject inputs nested deeper than 64 levels *)Parseff.parse ~max_depth:64 input jsonWhen the limit is exceeded, parsing fails with "maximum nesting depth N exceeded" rather than a stack overflow.