Page
Library
Module
Module type
Parameter
Class
Class type
Source
ParseffSourceParser combinator library with an imperative-style API.
Parseff is a parser combinator library that uses algebraic effects to allow an imperative-style API for parsers and ensures
let number () =
let c = satisfy (fun c -> c >= '0' && c <= '9') ~label:"digit" in
Char.code c - Char.code '0'
let ip_address () =
let a = number () in
let _ = Parseff.consume "." in
let b = number () in
let _ = Parseff.consume "." in
let c = number () in
let _ = Parseff.consume "." in
let d = number () in
Parseff.end_of_input ();
(a, b, c, d)
match Parseff.parse "192.168.1.1" ip_address with
| Ok result -> Printf.printf "Parsed: %d.%d.%d.%d\n" (fst (fst (fst result))) ...
| Error { pos; error = `Expected msg } -> Printf.printf "Error at %d: %s\n" pos msg
| Error _ -> Printf.printf "Other error\n"A zero-copy slice of the input string. Use span_to_string to materialize when needed.
span_to_string s extracts the string from a span. Only call this when you actually need the string value.
A source location. line and col are 1-based. col counts bytes from the start of the line (not characters — relevant for UTF-8).
Parse result with support for custom error types.
The result type has two type parameters:
'a is the type of the parsed value'e is the type of errorsBuilt-in errors are polymorphic variants:
`Expected of string — a specific token or pattern was expected but the input contained something else`Failure of string — a user-initiated failure raised via fail`Unexpected_end_of_input — input ended before the parser could match`Depth_limit_exceeded of string — recursive nesting exceeded max_depth (see rec_)User errors raised via error are also returned as Error.
Non-fatal diagnostic emitted during parsing.
type ('e, 'd) error_with_diagnostics = {pos : int;error : 'e;diagnostics : 'd diagnostic list;}type ('a, 'e, 'd) result_with_diagnostics =
('a * 'd diagnostic list, ('e, 'd) error_with_diagnostics) resultParse outcome with diagnostics in both success and failure cases.
val parse :
?max_depth:int ->
string ->
(unit -> 'a) ->
('a,
[> `Expected of string
| `Failure of string
| `Unexpected_end_of_input
| `Depth_limit_exceeded of string ])
resultparse ?max_depth input parser runs parser on input string.
max_depth limits the nesting depth for parsers that use rec_ to mark recursive entry points. Defaults to 128. When exceeded, parsing fails with `Depth_limit_exceeded instead of risking a stack overflow.
This function does not require consuming the full input; call end_of_input in your parser when you want full-consumption behavior.
Returns Ok result on success, or Error { pos; error } on failure. Errors are:
`Expected msg — the input contained something unexpected`Failure msg — a user-initiated failure raised via fail`Unexpected_end_of_input — the input ended before the parser could match`Depth_limit_exceeded msg — recursive nesting exceeded max_deptherrorExample:
match parse "hello" (fun () -> consume "hello") with
| Ok s ->
Printf.printf "Matched %S\n" s
| Error { pos; error = `Expected msg } ->
Printf.printf "Expected at %d: %s\n" pos msg
| Error { pos; error = `Failure msg } ->
Printf.printf "Failed at %d: %s\n" pos msg
| Error { error = `Unexpected_end_of_input; _ } ->
Printf.printf "Input ended too early\n"
| Error _ ->
Printf.printf "Other error\n"val parse_until_end :
?max_depth:int ->
string ->
(unit -> 'a) ->
('a,
[> `Expected of string
| `Failure of string
| `Unexpected_end_of_input
| `Depth_limit_exceeded of string ],
'd)
result_with_diagnosticsconsume s matches the exact literal string s.
Example:
let parser () =
consume "hello";
consume " ";
consume "world"satisfy fun ~label matches the next character if fun returns true for it. If the character doesn't match or input is empty, fails with label in the error message.
Example:
let vowel () = satisfy (fun c -> String.contains "aeiou" c) ~label:"vowel"char c matches the exact character c.
Example:
let comma () = char ','match_regex re matches a regular expression. The regex must be compiled with Re.compile.
Example:
let identifier () =
let re = Re.compile (Re.Posix.re "[a-zA-Z_][a-zA-Z0-9_]*") in
match_regex retake_while fun reads characters one by one as long as fun returns true for each one. Returns the matched string (may be empty if the very first character doesn't match). Much faster than regex for simple character classes.
When ~at_least is specified, requires at least that many characters to match. Fails with ~label in the error message if fewer characters match.
Example:
let digits () = take_while (fun c -> c >= '0' && c <= '9')
let digits1 () =
take_while ~at_least:1 (fun c -> c >= '0' && c <= '9') ~label:"digit"skip_while fun advances past characters as long as fun returns true. Like take_while but doesn't build a string — use this when you only need to move past characters. Always succeeds (skips nothing if the first character doesn't match).
Example:
let skip_spaces () = skip_while (fun c -> c = ' ')skip_while_then_char fun c skips characters where fun returns true, then matches the exact character c. More efficient than calling skip_while followed by char separately.
sep_by_take is_whitespace separator is_value_char parses a list of values separated by separator. Whitespace (characters where is_whitespace returns true) is skipped around each separator. Each value consists of characters where is_value_char returns true. Returns a list of matched strings. Runs entirely in a single operation for maximum efficiency.
take_while_span fun like take_while but returns a zero-copy span instead of allocating a string. No memory allocation until you call span_to_string.
sep_by_take_span is_whitespace separator is_value_char like sep_by_take but returns zero-copy spans instead of strings. No String.sub allocations per element.
fused_sep_take is_whitespace separator is_value_char skips whitespace, matches separator, skips whitespace again, then reads one or more characters where is_value_char returns true. All steps run in a single operation. Returns the taken string. Much more efficient than calling each step separately when parsing separated values.
fail msg aborts parsing with a `Failure msg error. This is intended for user-initiated validation failures (e.g. a parsed value is out of range).
`Failure errors are not caught by backtracking combinators (or_, many, one_of, optional, look_ahead) or relabeling combinators (expect, one_of_labeled). Once raised, a failure propagates all the way to the runner.
For typed errors, see error. For errors that should participate in backtracking, the parser should signal failure through the parsing combinators themselves (e.g. consume, satisfy).
Example:
let validate_range n =
if n >= 0 && n <= 255 then
n
else
fail "number out of range"error e aborts parsing with a user-defined typed error value.
The error is returned as Error { pos; error = e }. Use polymorphic variants for rich error reporting:
let number () =
let n = parse_int () in
if n > 255 then error (`Out_of_range n)
else if n < 0 then error (`Negative n)
else n
match run input number with
| Ok n -> Printf.printf "Got %d\n" n
| Error { error = `Out_of_range n; _ } ->
Printf.printf "%d is too large\n" n
| Error { error = `Negative n; _ } ->
Printf.printf "%d is negative\n" n
| Error _ -> Printf.printf "Parse error\n"Note: User errors from error pass through expect and one_of_labeled without being caught or relabeled. However, backtracking combinators (or_, many, one_of, optional, look_ahead) will catch and absorb user errors just like any other parse failure.
For a simpler string-based alternative that also escapes backtracking, see fail.
warn diagnostic records a non-fatal diagnostic at the current position and continues parsing.
warn_at ~pos diagnostic records a non-fatal diagnostic at pos and continues parsing.
position () returns the current parser offset in bytes from the start of the input.
location () returns the current parser location with line and column. Zero cost if never called — the line index is built lazily on first use and extended incrementally on subsequent calls.
location_of_position input pos computes line and column for a byte position in input. Useful for converting error positions after parsing.
end_of_input () succeeds only if no input remains. Use this to ensure the entire input has been consumed.
Example:
let complete_parser () =
let result = some_parser () in
end_of_input ();
resultor_ is the alternation combinator. Tries the left parser; if it fails, backtracks and tries the right parser.
When both branches fail, error composition depends on position:
Expected errors, the messages are joined with " or ". If one is Expected and the other is Unexpected_end_of_input, the Expected error is preferred (it carries more information). Two Unexpected_end_of_input stay as-is.This means one_of (which chains or_) naturally produces composed messages like "expected \"if\" or expected \"else\"".
Example:
let bool_parser () =
or_
(fun () ->
consume "true";
true
)
(fun () ->
consume "false";
false
)
()look_ahead parser runs parser without consuming any input. If parser succeeds, the position stays where it was before — useful for peeking at what comes next. Fails if parser fails.
Example:
let check_next_is_digit () = look_ahead digit
(* position hasn't moved *)rec_ parser marks a recursive entry point for depth tracking. Wrap the body of recursive parsers with rec_ so that parse can enforce max_depth and fail cleanly instead of overflowing the stack.
Example:
let rec json () = Parseff.rec_ (fun () ->
Parseff.one_of [ array_parser; null_parser; ... ] ()
)
and array_parser () =
let _ = Parseff.consume "\[" in
let elements = ... json () ... in
...expect description parser runs parser and, if it fails with a parse error, replaces the error message with description. Reads naturally: "expect a dot separator".
Only parse errors (from consume, satisfy, etc.) are relabeled. Failures from fail propagate unchanged (use catch to intercept those). User errors raised via error propagate unchanged — this lets you use expect around parsers that perform validation without losing the structured error:
let octet () =
expect "an octet (0-255)" (fun () ->
let n = number () in
if n > 255 then
error (`Out_of_range n)
else
n
)
(* A non-digit input gives: "expected an octet (0-255)" *)
(* Input "300" gives: `Out_of_range 300 — not swallowed *)Example:
let dot () = expect "a dot separator" (fun () -> char '.')
let digit_val () = expect "a digit (0-9)" digitcatch parser handler runs parser. If parser raises a `Failure (via fail), calls handler msg instead of propagating the failure.
This is the escape hatch for when you want a fail to participate in backtracking or be recovered from. Without catch, failures always propagate through combinators like or_ and many.
Example:
(* Make a failure recoverable inside or_ *)
let lenient_byte () =
Parseff.or_
(fun () ->
Parseff.catch
(fun () ->
let n = number () in
if n > 255 then
Parseff.fail "out of range"
else
n
)
(fun _msg -> -1)
)
(fun () -> 0)
()one_of parsers tries each parser in order until one succeeds.
Example:
let keyword () =
one_of
[
(fun () -> consume "if");
(fun () -> consume "else");
(fun () -> consume "while");
]
()one_of_labeled labeled_parsers tries each parser in order. On failure, reports all labels in the error message.
Like expect, only parse errors are relabeled. User errors raised via error inside any branch propagate unchanged.
Example:
let literal () =
one_of_labeled
[
("number", number_parser);
("string", string_parser);
("boolean", bool_parser);
]
()
(* On failure: "expected one of: number, string, boolean" *)many parser applies parser repeatedly until it fails. Returns a list of all successful results. Always succeeds (returns [] if parser fails immediately).
When ~at_least is specified, requires at least that many successful matches. Fails if parser doesn't succeed enough times.
Example:
let digits () = many digit () (* parses "123" -> [1; 2; 3] *)
let non_empty_digits () = many ~at_least:1 digit ()sep_by element separator parses zero or more occurrences of element with separator between each pair. Returns a list of the parsed elements.
When ~at_least is specified, requires at least that many elements.
Example:
let csv_line () =
sep_by
(fun () -> match_regex (Re.compile (Re.Posix.re "[^,]+")))
(fun () -> char ',')
()
let csv_line_nonempty () =
sep_by ~at_least:1
(fun () -> match_regex (Re.compile (Re.Posix.re "[^,]+")))
(fun () -> char ',')
()between open_ close_ parser parses open_, then parser, then close_, and returns the value produced by parser.
end_by element separator parses zero or more elements, each followed by separator.
When ~at_least is specified, requires at least that many elements.
fold_left element op parses one or more element values separated by op, combining them left-associatively. Fails if there are zero element values.
When ~otherwise is specified, returns otherwise if there are zero element values instead of failing.
fold_right element op parses one or more element values separated by op, combining them right-associatively. Fails if there are zero element values.
When ~otherwise is specified, returns otherwise if there are zero element values instead of failing.
optional parser tries to apply parser. Returns Some result if it succeeds, or None if it fails (without consuming input).
Example:
let optional_sign () =
optional (fun () -> or_ (fun () -> char '-') (fun () -> char '+') ()) ()count n parser applies parser exactly n times. Fails if parser doesn't succeed n times.
Example:
let three_digits () = count 3 digit ()digit () parses a decimal digit (0-9) and returns its integer value.
Example:
let d = digit () in (* parses "7" -> 7 *)
...letter () parses an ASCII letter (a-z or A-Z).
is_whitespace c returns true for whitespace characters (space, tab, newline, CR).
whitespace () parses zero or more whitespace characters (space, tab, newline, carriage return). Uses fast character scanning (not regex).
When ~at_least is specified, requires at least that many whitespace characters. whitespace ~at_least:1 () fails if no whitespace is found.
skip_whitespace () skips zero or more whitespace characters (returns unit). More efficient than whitespace when you don't need the matched string.
alphanum () parses an alphanumeric character (letter or digit).
any_char () parses any character.
take n reads exactly n bytes and returns them as a string. Fails if fewer than n bytes remain or if n is negative.
Useful for length-prefixed binary fields and fixed-width text fields alike.
Example:
let payload () =
let len = BE.any_uint16 () in
take lenInput sources for incremental parsing. A source wraps a readable byte stream — a channel, file descriptor, or custom reader — behind a uniform interface. The parser pulls data on demand through the effect handler; existing parser code works unchanged.
val parse_source :
?max_depth:int ->
Source.t ->
(unit -> 'a) ->
('a,
[> `Expected of string
| `Failure of string
| `Unexpected_end_of_input
| `Depth_limit_exceeded of string ])
resultparse_source ?max_depth source parser runs parser pulling input from source on demand. Behaves identically to parse but the input does not need to be fully available up front.
The same parsers work with both parse and parse_source — no changes required.
Example:
let ic = open_in "data.json" in
let source = Source.of_channel ic in
let result = parse_source source json in
close_in ic;
resultval parse_source_until_end :
?max_depth:int ->
Source.t ->
(unit -> 'a) ->
('a,
[> `Expected of string
| `Failure of string
| `Unexpected_end_of_input
| `Depth_limit_exceeded of string ],
'd)
result_with_diagnosticsparse_source_until_end ?max_depth source parser is the streaming equivalent of parse_until_end. It enforces full consumption and returns diagnostics on both success and failure.
Example:
let ic = open_in "data.json" in
let source = Source.of_channel ic in
let outcome = parse_source_until_end source json in
close_in ic;
outcomeBig-endian and little-endian parsers for multi-byte integers, floats, and doubles. Pick the module matching your wire format's byte order.
Single-byte readers (BE.any_uint8, LE.any_uint8, etc.) are present in both modules for convenience — their results are identical since endianness does not affect single bytes.
Example:
let parse_png_chunk () =
let length = BE.any_int32 () in
let chunk_type = take 4 in
let data = take (Int32.to_int length) in
(chunk_type, data)
let parse_wav_size () =
let _ = consume "RIFF" in
let size = LE.any_int32 () in
let _ = consume "WAVE" in
sizeThe Utf8 module provides primitives that operate on Unicode code points (Uchar.t) instead of raw bytes (char). Input is still an OCaml string, but characters are decoded as UTF-8 sequences. Invalid UTF-8 raises a parse error.
Unicode character properties (letter, whitespace, etc.) use the uucp library for full Unicode support.
These primitives can be freely mixed with the byte-level primitives in the same parser. Position tracking remains in bytes.