Skip to content

Combinators compose simple parsers into more complex ones. They handle alternation, error labeling, lookahead, and recursion.

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 -> 'a
let 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");
]
()

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 -> 'a
let 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.

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 -> 'a
let 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.

Parseff.optional tries the parser. Returns Some result on success, None on failure (without consuming input).

val optional : (unit -> 'a) -> unit -> 'a option
let 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 *)

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) -> 'a
let 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.

Parseff.expect runs a parser and relabels its failure with a clearer message.

val expect : string -> (unit -> 'a) -> 'a
let dot () =
Parseff.expect "a dot separator" (fun () -> Parseff.char '.')
let digit_val () = Parseff.expect "a digit (0-9)" Parseff.digit

Use 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.

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) -> 'a
let 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 elements

The ~max_depth parameter on Parseff.parse controls the limit (default: 128):

(* Reject inputs nested deeper than 64 levels *)
Parseff.parse ~max_depth:64 input json

When the limit is exceeded, parsing fails with "maximum nesting depth N exceeded" rather than a stack overflow.