package parseff

  1. Overview
  2. Docs

Zero-copy API

The zero-copy API allows you to parse without allocating intermediate strings, significantly improving performance and reducing memory usage.

Span type

type span = { buf : string; off : int; len : int }

A Parseff.span is a zero-copy slice of the input string. It holds a reference to the original buffer with an offset and length, avoiding string allocation until you explicitly call Parseff.span_to_string.

val span_to_string : span -> string

Span-based operations

take_while_span

Parseff.take_while_span is like Parseff.take_while but returns a zero-copy Parseff.span instead of allocating a string. No memory allocation until you call Parseff.span_to_string.

val take_while_span : (char -> bool) -> span
(* no allocation until you need it *)
let digits_span =
  Parseff.take_while_span (fun c -> c >= '0' && c <= '9')
let n = int_of_span digits_span

Real-world usage:

(* Parse integers efficiently *)
let int_of_span (span : Parseff.span) =
  let rec loop i acc =
    if i >= span.len then acc
    else
      let c = span.buf.[span.off + i] in
      loop (i + 1) (acc * 10 + (Char.code c - Char.code '0'))
  in
  loop 0 0

let fast_integer () =
  let digits =
    Parseff.take_while_span (fun c -> c >= '0' && c <= '9')
  in
  int_of_span digits

sep_by_take_span

Parseff.sep_by_take_span is like Parseff.sep_by_take but returns zero-copy Parseff.spans. No String.sub allocations per element.

val sep_by_take_span : (char -> bool) -> char -> (char -> bool) -> span list
let int_of_span (span : Parseff.span) =
  let rec loop i acc =
    if i >= span.len then acc
    else
      let c = span.buf.[span.off + i] in
      loop (i + 1) (acc * 10 + (Char.code c - Char.code '0'))
  in
  loop 0 0

(* Parse CSV line with zero-copy *)
let csv_line () =
  let spans =
    Parseff.sep_by_take_span Parseff.is_whitespace ','
      (fun c -> c <> ',' && c <> '\n')
  in
  (* Only allocate strings when needed *)
  List.map Parseff.span_to_string spans

(* or process spans directly, avoiding string allocation entirely *)
let csv_to_ints () =
  let spans =
    Parseff.sep_by_take_span Parseff.is_whitespace ','
      (fun c -> c >= '0' && c <= '9')
  in
  List.map int_of_span spans

Fused operations

fused_sep_take

Parseff.fused_sep_take performs: skip whitespace, match separator, skip whitespace, take_while1, all in a single effect dispatch. Much more efficient than separate calls.

val fused_sep_take : (char -> bool) -> char -> (char -> bool) -> string
(* multiple effect dispatches *)
let parse_value_slow () =
  Parseff.skip_whitespace ();
  let _ = Parseff.char ',' in
  Parseff.skip_whitespace ();
  Parseff.take_while1 (fun c -> c >= '0' && c <= '9') ~label:"digit"

(* single effect dispatch *)
let parse_value_fast () =
  Parseff.fused_sep_take Parseff.is_whitespace ','
    (fun c -> c >= '0' && c <= '9')

skip_while_then_char

Parseff.skip_while_then_char skips characters matching predicate, then matches a character. Fused operation for efficiency.

val skip_while_then_char : (char -> bool) -> char -> unit
(* two effect dispatches *)
let skip_ws_then_comma_slow () =
  Parseff.skip_whitespace ();
  let _ = Parseff.char ',' in
  ()

(* single fused dispatch *)
let skip_ws_then_comma_fast () =
  Parseff.skip_while_then_char Parseff.is_whitespace ','

sep_by_take

Parseff.sep_by_take parses zero or more separated values entirely in the handler. Returns a list of matched strings. Zero intermediate effect dispatches.

val sep_by_take : (char -> bool) -> char -> (char -> bool) -> string list
(* Parse array values efficiently *)
let array_values () =
  let _ = Parseff.char '[' in
  let values =
    Parseff.sep_by_take Parseff.is_whitespace ','
      (fun c -> c >= '0' && c <= '9')
  in
  let _ = Parseff.char ']' in
  List.map int_of_string values

(* Matches "[1, 2, 3]" -> [1; 2; 3] *)

Performance comparison

String allocation:

(* allocates intermediate strings *)
let parse_numbers () =
  Parseff.sep_by
    (fun () ->
      Parseff.skip_whitespace ();
      let s =
        Parseff.take_while1 (fun c -> c >= '0' && c <= '9') ~label:"digit"
      in
      int_of_string s)
    (fun () -> Parseff.char ',')
    ()

(* no allocations until conversion *)
let parse_numbers_fast () =
  let spans =
    Parseff.sep_by_take_span Parseff.is_whitespace ','
      (fun c -> c >= '0' && c <= '9')
  in
  List.map int_of_span spans

Effect dispatches:

(* multiple dispatches *)
let value () =
  Parseff.skip_whitespace ();
  let _ = Parseff.char ',' in
  Parseff.skip_whitespace ();
  Parseff.take_while1 (fun c -> c >= '0' && c <= '9') ~label:"digit"

(* single dispatch *)
let value () =
  Parseff.fused_sep_take Parseff.is_whitespace ','
    (fun c -> c >= '0' && c <= '9')

Complete example: high-performance JSON array

let int_of_span (span : Parseff.span) =
  let rec loop i acc =
    if i >= span.len then acc
    else
      let c = span.buf.[span.off + i] in
      loop (i + 1) (acc * 10 + (Char.code c - Char.code '0'))
  in
  loop 0 0

let json_array () =
  let _ = Parseff.char '[' in
  let spans =
    Parseff.sep_by_take_span Parseff.is_whitespace ','
      (fun c -> c >= '0' && c <= '9')
  in
  let _ = Parseff.char ']' in
  Parseff.end_of_input ();
  List.map int_of_span spans

let () =
  match Parseff.parse "[1, 2, 3, 4, 5]" json_array with
  | Ok nums ->
      Printf.printf "Sum: %d\n" (List.fold_left ( + ) 0 nums)
  | Error { pos; error = `Expected expected } ->
      Printf.printf "Error at %d: %s\n" pos expected
  | Error _ -> print_endline "Parse error"

The zero-copy version is roughly 2.5x faster than the standard API and uses 3x less memory.

When to use zero-copy

Use zero-copy when:

  • Parsing large inputs
  • Performance is critical
  • You can process spans directly (e.g., custom number parsing)
  • Parsing hot paths (inner loops)

Skip zero-copy when:

  • Readability is more important than performance
  • You need to store parsed strings long-term (just use the regular API)
  • The performance difference doesn't matter for your use case