OCaml Module 15 – Web services, migration and the ecosystem
OCaml 5 migration, versions and the ecosystem
What changed from OCaml 4.14 to 5.5, why a 4.14 program stops compiling, how a Stream tokenizer moves to Seq, and the oldest compiler each library accepts.
What you will learn
- List what each release from OCaml 4.14 to 5.5 added, and what 5.0 removed
- Rewrite a Stream-based 4.14 program with Seq so that it builds on OCaml 5.5.1 and prints the same output
- Plan the migration of a 4.14 codebase, including global mutable state under domains and C stubs
- Choose libraries for a project: the standard library or Base, Eio or Lwt, and tools for tests, CLIs and the web
- Follow point releases and security advisories, and check the oldest compiler each dependency accepts
Before you start
What you’ll learn · Before you start
What you will learn
- List what each release from OCaml 4.14 to 5.5 added, and what 5.0 removed
- Rewrite a Stream-based 4.14 program with Seq so that it builds on OCaml 5.5.1 and prints the same output
- Plan the migration of a 4.14 codebase, including global mutable state under domains and C stubs
- Choose libraries for a project: the standard library or Base, Eio or Lwt, and tools for tests, CLIs and the web
- Follow point releases and security advisories, and check the oldest compiler each dependency accepts
Before you start
On this page
A tokenizer written for OCaml 4.14 stops on OCaml 5.5.1 at line 12 with Unbound module Stream. Six feature
releases separate the two compilers, and the first of them, 5.0, removed modules and functions that 4.14 had only
marked deprecated.
An OCaml 5 migration therefore starts with the names that are gone, then the mutable state that domains can now
share and the C stubs the new runtime treats differently. The ecosystem moved as well: every library has an oldest
compiler it installs on, and security fixes keep arriving in point releases. You will list each release’s
changes, port the tokenizer to Seq and map the rest of a move.
Six feature releases followed 4.14, and 5.0 removed what 4.14 deprecated
OCaml’s Changes file marks each entry that can break existing programs with an asterisk, a
breaking change: A change in a new release of a compiler or library after which some programs that worked with the previous release no longer build or no longer behave the same; OCaml's Changes file marks such entries with an asterisk.. In the 5.0 section, entries #10896 and #10867 remove Stream,
Genlex, Pervasives, the standalone bigarray library and dozens of long-deprecated values such as String.set and
Thread.kill. Most of them already carried a deprecation: A notice that a function or module is due to be removed in a later release, shown as an alert wherever code uses it; OCaml 4.14 deprecated Stream and Genlex before OCaml 5.0 removed them. alert in 4.14, whose
stdlib.mli tells Stream users to “Use the camlp-streams library instead.” Walk through the releases in order:
- 4.14
The last 4.x series prepares the way deprecations
New
In_channelandOut_channelmodules and many newSeqfunctions,Seq.unconsamong them.StreamandGenlexare marked deprecated ahead of their removal, and tail modulo cons arrives as an experiment. The 4.14 series still gets point releases. - 5.0
A new runtime with domains and effect handlers removals
Domainruns OCaml code in parallel on several cores andEffectadds effect handlers.Stream,Genlex,Pervasivesand the standalone bigarray library are removed, with dozens of long-deprecated values such asString.set, and a C pointer may no longer stand in for an OCaml value. - 5.1
Tail-recursive List.map, and fewer regressions
Tail-recursion modulo cons makes 14 standard library functions,
List.mapandList.appendamong them, safe on very long lists. Many performance regressions and memory leaks of the new runtime are fixed, andIn_channelgainsinput_lines. - 5.2
Compaction, Dynarray and ThreadSanitizer
Heap compaction returns,
Dynarrayadds an array that grows, raw identifiers such as\#matchlet a keyword serve as a name, and the compiler can be built with ThreadSanitizer, a data-race detector, on FreeBSD, Linux and macOS. - 5.3
Syntax for effect handlers
Effects are handled with effect cases inside an ordinary
match, source files may be UTF-8 encoded, and the MSVC port for Windows is back. - 5.4
Labelled tuples and immutable arrays
Tuple positions may carry labels, the
iarraytype adds immutable arrays, a record field can be marked[@atomic], andPair,Pqueue,ReprandIarrayjoin the standard library. - 5.5
Module-dependent functions
A function can take a module and name it in its own type, a parameter can be polymorphic, the installed compiler can be moved to another folder and still work, and about 60 functions join the standard library, such as
String.split_all.
The timeline has 7 events, from 4.14 to 5.5.
Compiling one line of each release shows which versions a codebase needs. The probe below holds files for every release, each using something that release added, and four files that use a name 5.0 removed:
from pathlib import Path
from session import files, run
# A feature probe recorded on OCaml 5.5.1: one small file per release from 4.14
# to 5.5, each using one thing that release added, and four files that use
# something 5.0 removed. probe.sh compiles each file on its own and prints the
# file's first comment with the result. No OCaml 4.14 compiler is run here: what
# 4.14 itself accepted comes from the Changes file the lesson cites.
files(*sorted(str(p) for p in Path("probe").iterdir()))
run(
"""
cat probe/p53_effect_match.ml
bash probe/probe.sh
""",
) Output
$ cat probe/p53_effect_match.ml (* 5.3: effect cases inside match *) type _ Effect.t += Ask : int Effect.t let run f = match f () with v -> v | effect Ask, k -> Effect.Deep.continue k 42 $ bash probe/probe.sh 4.14: In_channel reads a whole file compiles 5.0: Domain runs code in parallel compiles 5.0: Effect.perform and effect types compiles 5.1: In_channel.input_lines compiles 5.2: Dynarray grows as you append compiles 5.2: raw identifiers such as \#match compiles 5.3: effect cases inside match compiles 5.4: [@atomic] record fields compiles 5.4: immutable arrays, type int iarray compiles 5.4: labelled tuples compiles 5.5: module-dependent functions compiles 5.5: polymorphic parameters compiles removed in 5.0: Genlex Error: Unbound module Genlex removed in 5.0: Pervasives Error: Unbound module Pervasives removed in 5.0: Stream Error: Unbound module Stream removed in 5.0: String.set Error: Unbound value String.set
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 probe_session.py
All twelve feature files compile on 5.5.1, and the four removed names fail with Unbound module or Unbound value.
No 4.14 compiler ran here, so what the older versions lacked comes from the Changes file.
A 4.14 tokenizer stops at Stream; the Seq port prints the same tokens
The 4.14 tokenizer reads characters with Stream.peek, which looks at the next one, and Stream.junk, which consumes
it, so the stream itself moves forward. The port keeps the structure on a sequence: A value of type 'a Seq.t: a function that, called with (), returns Seq.Nil at the end or Seq.Cons with the next element and the rest, so each element is computed only when a consumer asks for it. More in “Sequences, laziness and memoization” of type
char Seq.t: Seq.uncons returns the next character with the rest, so take_while hands back the word and the rest,
and String.to_seq replaces Stream.of_string. Compare the two files:
tokens_stream.ml (4.14) · port/tokens_stream.ml
(* A tokenizer as it was written for OCaml 4.14: it reads characters from a
Stream, looking at the next one with Stream.peek and consuming it with
Stream.junk, so the stream itself moves forward. *)
type token = Ident of string | Int of int | Sym of char
let is_digit c = c >= '0' && c <= '9'
let is_letter c = (c >= 'a' && c <= 'z') || c = '_'
let take_while p s =
let b = Buffer.create 8 in
let rec go () =
match Stream.peek s with
| Some c when p c ->
Stream.junk s;
Buffer.add_char b c;
go ()
| _ -> Buffer.contents b
in
go ()
let rec tokens s =
match Stream.peek s with
| None -> []
| Some (' ' | '\t') ->
Stream.junk s;
tokens s
| Some c when is_digit c ->
let n = take_while is_digit s in
Int (int_of_string n) :: tokens s
| Some c when is_letter c ->
let w = take_while is_letter s in
Ident w :: tokens s
| Some c ->
Stream.junk s;
Sym c :: tokens s
let show = function
| Ident w -> "IDENT " ^ w
| Int n -> "INT " ^ string_of_int n
| Sym c -> "SYM " ^ String.make 1 c
let () =
Stream.of_string "total = price * qty + 40"
|> tokens
|> List.iter (fun t -> print_endline (show t)) tokens_seq.ml (Seq port) · port/tokens_seq.ml
(* The same tokenizer for OCaml 5: a char Seq.t replaces the Stream. Seq.uncons
returns the next character together with the rest of the sequence, so each
function hands the rest on instead of moving a stream forward. *)
type token = Ident of string | Int of int | Sym of char
let is_digit c = c >= '0' && c <= '9'
let is_letter c = (c >= 'a' && c <= 'z') || c = '_'
(* The word made of characters that satisfy p, and the sequence after it. *)
let take_while p seq =
let b = Buffer.create 8 in
let rec go seq =
match Seq.uncons seq with
| Some (c, rest) when p c ->
Buffer.add_char b c;
go rest
| _ -> (Buffer.contents b, seq)
in
go seq
let rec tokens seq =
match Seq.uncons seq with
| None -> []
| Some ((' ' | '\t'), rest) -> tokens rest
| Some (c, _) when is_digit c ->
let n, rest = take_while is_digit seq in
Int (int_of_string n) :: tokens rest
| Some (c, _) when is_letter c ->
let w, rest = take_while is_letter seq in
Ident w :: tokens rest
| Some (c, rest) -> Sym c :: tokens rest
let show = function
| Ident w -> "IDENT " ^ w
| Int n -> "INT " ^ string_of_int n
| Sym c -> "SYM " ^ String.make 1 c
let () =
String.to_seq "total = price * qty + 40"
|> tokens
|> List.iter (fun t -> print_endline (show t)) On OCaml 5.5.1, dune stops the old file at its first use of Stream and builds the port, whose seven tokens match the
list kept in expected_tokens.txt:
from session import files, run
# The free part's worked example. The tokenizer written for OCaml 4.14 stops at
# its first use of Stream on OCaml 5.5.1; the Seq rewrite builds, and its output
# is compared with the token list the project's test file keeps.
files(
"port/dune-project=dune-project",
"port/dune=dune",
"port/tokens_stream.ml=tokens_stream.ml",
"port/tokens_seq.ml=tokens_seq.ml",
"port/expected_tokens.txt=expected_tokens.txt",
)
run(
"""
ocamlopt -version
!dune build ./tokens_stream.exe
dune build ./tokens_seq.exe
dune exec ./tokens_seq.exe
dune exec ./tokens_seq.exe | diff - expected_tokens.txt && echo "same tokens as expected_tokens.txt"
""",
) Output
$ ocamlopt -version
5.5.1
$ dune build ./tokens_stream.exe
File "tokens_stream.ml", line 12, characters 10-16:
12 | match Stream.peek s with
^^^^^^
Error: Unbound module Stream
[exit status 1]
$ dune build ./tokens_seq.exe
$ dune exec ./tokens_seq.exe
IDENT total
SYM =
IDENT price
SYM *
IDENT qty
SYM +
INT 40
$ dune exec ./tokens_seq.exe | diff - expected_tokens.txt && echo "same tokens as expected_tokens.txt"
same tokens as expected_tokens.txt
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 port_session.py
A string's Seq does not move forward when you read it
A Stream passed to two functions was consumed by the first. The sequence String.to_seq builds starts from the
same place each time it is read, so a port that drops the rest reads the word twice; return the rest, as
take_while returns (word, rest).
Seq.uncons and String.to_seq are both documented in 4.14, so the port can land while the project still builds
there.
Checkpoint
Check: What each release added, and what 5.0 removed · Porting Stream code to Seq
An OCaml 5 migration has three parts, and the ecosystem sets a compiler floor
Removed names are the first part, and the compiler points at each one. The other two compile cleanly and can go
wrong only at run time. Global mutable state, such as a module-level ref or Hashtbl that was safe in one
domain: An OCaml 5 unit of parallel execution: Domain.spawn starts a function on a new domain, which runs OCaml code on another processor, and Domain.join waits for its result. More in “Parallelism with domains and atomics”, needs Atomic or Mutex once code runs on several.
C stub: A small C function that connects OCaml to a C routine: it reads its OCaml arguments with the runtime's macros, does the work in C, and returns an OCaml value, cooperating with the garbage collector when it allocates. More in “Interfacing with C” must stop handing OCaml a naked pointer: A pointer to memory outside the OCaml heap stored directly as an OCaml value, with no block around it. OCaml 4 allowed it in C stubs; OCaml 5 does not, so the pointer goes inside a block the runtime knows.; the C
pointer goes into a custom block instead. For code not ported yet, the camlp-streams package restores Stream and
Genlex unchanged.
The ecosystem sets the floor of an OCaml 5 migration, because every package’s opam file fixes the oldest compiler it installs on, directly or through its dependencies, and newer versions of a library often need more:
| Package | Oldest OCaml |
|---|---|
| camlp-streams 5.0.1 | 4.02.3 |
| Alcotest 1.9.1 | 4.08 |
| Lwt 6.1.2 | 4.14 |
| Base v0.16.3 | 4.14.0 |
| Base v0.17.3 | 5.1.0 |
| Eio 1.6 | 5.2.0 |
A project that must keep building on 4.14 during the move keeps Lwt and A library that replaces OCaml's standard library as a whole, with conventions of its own; Core is a larger library built on top of it. v0.16, and adopts Eio 1.6 once every build is on 5.2. The same floors guide the other choices: Base and Core or the standard library, the lwt_eio bridge that runs Lwt and Eio code together, and tools like Cmdliner and Dream. Security fixes arrive in a point release: A release that changes only the last number of a version, such as 5.5.1 after 5.5.0: it fixes bugs and security problems in one release series instead of adding features. such as 5.5.1, and ocaml.org/security describes how each security advisory: A public notice that some versions of a compiler or library have a security flaw, saying which versions are affected and which release or change fixes it. is published.
Key idea
Fix the names 5.0 removed first, then the shared state and C stubs that compile but can still go wrong, and keep only dependencies that accept every compiler your builds use.
Checkpoint
Check: The parts of a migration and compiler floors
Practise: releases, the Seq port and compiler floors
Each question uses the timeline, the probe, the tokenizer or the floor table above, and the idea you know least comes first.
Practice that adapts to you
Practise this lesson
12 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Porting a 4.14 codebase to OCaml 5, step by step Pro: opens with a Pro, Premium or Ultimate pass
A 4.14 codebase meets OCaml 5 in three places: names the standard library dropped, mutable state that domains can now share, and C stubs that gave the runtime raw pointers. Each one is reproduced on OCaml 5.5.1 with its fix, followed by the order to work in and the commands for each step.
In this part:
- Removed names: camlp-streams keeps old code building while you port it
- Shared mutable state: a measured race and two fixes that hold
- C stubs: a pointer from malloc goes into a custom block
- The order that keeps the build green, with the commands to run
4 runnable examples (Python, dune, main.ml, tally_stubs.c (5.x), tally_stubs_414.c (4.14)) · about 11 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Choosing libraries, and following releases and advisories Pro: opens with a Pro, Premium or Ultimate pass
Library choices decide which compilers a project can use, and compiler point releases carry security fixes that a pinned build does not pick up by itself. Both are shown with recorded runs on OCaml 5.5.1 and the numbers in each package’s opam file.
In this part:
- Stdlib or Base: one program written both ways
- Eio or Lwt, chosen from your builds, your libraries and your work
- Point releases carry the security fixes, and advisories say which
2 runnable examples (Python) · about 11 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
A warm-up for freshers and 2–5 years: which parts of the standard library did 5.0 drop, and what takes their place?
Stream and Genlex left the standard library and live on in the camlp-streams package; new code reads a Seq.t
with Seq.uncons instead. Pervasives, an old name for Stdlib, is gone, so Pervasives.print_endline becomes
print_endline. The standalone bigarray library went too, because Bigarray is part of the standard library. Values
deprecated for years, such as String.set, whose job Bytes.set does on bytes, were removed in the same release,
and 4.14’s alerts named most of them in advance.
Three harder questions follow; sketch an answer to each before you open it.
-
Mid to senior
A billing service runs on OCaml 4.14. Its config reader uses Stream, a module-level Hashtbl caches tariffs, two C stubs wrap a C library handle, and its web layer is Dream on Lwt. Plan its move to OCaml 5.5.1: what you do, in which order, and what has to keep building on 4.14 until when.
-
Mid to senior
After a move to OCaml 5.5.1, a service parses requests on four domains. A module-level Hashtbl memoises a slow tariff lookup: find_opt, then add on a miss. Under load the table ends with more bindings than distinct keys, and nothing failed at build time. Explain the cause, fix it and prove the fix.
-
Senior to staff
Your services build with OCaml 5.5.1, pinned in CI, and a security advisory is published against the compiler's demarshaling. One service unmarshals a cache file that other machines write. What do you check, what do you ship today, and what do you change for good?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What did OCaml 5.0 remove that 4.14 had marked deprecated?
Stream, Genlex, Pervasives, the standalone bigarray library and long-deprecated values such as String.set.
-
Why does a 4.14 tokenizer stop on OCaml 5.5.1 with Unbound module Stream?
OCaml 5.0 removed Stream. The Seq port, which returns the rest of the sequence instead of consuming it, printed the same seven tokens.
-
Why must a Seq port of a tokenizer return the rest of the input?
A Stream passed to two functions was consumed by the first, but the sequence String.to_seq builds starts from the same place each time it is read, so take_while returns (word, rest).
-
In what order does an OCaml 5 migration go?
The removed names first; then shared mutable state and C stubs, which compile cleanly but need Atomic or Mutex and custom blocks.
-
What is a library's compiler floor, and where do OCaml's security fixes arrive?
The oldest compiler it installs on: 4.14 for Lwt 6.1.2 and 5.2 for Eio 1.6. Security fixes arrive in point releases.
Key takeaways
- Six feature releases followed 4.14, and 5.0 removed
Stream,Genlex,Pervasives, the standalone bigarray library and long-deprecated values such asString.set, most of which 4.14 had marked deprecated. - On OCaml 5.5.1 the old tokenizer stopped at its first
Streamcall, and theSeqport, which returns the rest of the sequence instead of consuming it, printed the same seven tokens. - An OCaml 5 migration fixes the removed names first; shared mutable state and C stubs compile cleanly and need
AtomicorMutexand custom blocks. - Across the ecosystem, every dependency has an oldest compiler, from 4.14 for Lwt 6.1.2 to 5.2 for Eio 1.6, and security fixes arrive in point releases.
Terms in this lesson
- breaking change
- A change in a new release of a compiler or library after which some programs that worked with the previous release no longer build or no longer behave the same; OCaml's Changes file marks such entries with an asterisk.
- deprecation
- A notice that a function or module is due to be removed in a later release, shown as an alert wherever code uses it; OCaml 4.14 deprecated Stream and Genlex before OCaml 5.0 removed them.
- naked pointer
- A pointer to memory outside the OCaml heap stored directly as an OCaml value, with no block around it. OCaml 4 allowed it in C stubs; OCaml 5 does not, so the pointer goes inside a block the runtime knows.
- point release
- A release that changes only the last number of a version, such as 5.5.1 after 5.5.0: it fixes bugs and security problems in one release series instead of adding features.
- security advisory
- A public notice that some versions of a compiler or library have a security flaw, saying which versions are affected and which release or change fixes it.
References
- OCaml Changes, 5.5 branch (The OCaml developers)
- OCaml releases (OCaml.org)
- OCaml 5.5.1 release notes (OCaml.org)
- OCaml security (OCaml.org)
- OCaml Security Advisory Database (OCaml Security Response Team)
- Transitioning to Multicore with ThreadSanitizer (OCaml.org)
- The OCaml Manual: Interfacing C with OCaml (Inria and the OCaml developers)
- The OCaml Manual: module Seq (Inria and the OCaml developers)
- The OCaml Manual: module Marshal (Inria and the OCaml developers)
- The OCaml Manual: module Mutex (Inria and the OCaml developers)
- The OCaml Manual: module Atomic (Inria and the OCaml developers)
- The OCaml Manual: module Hashtbl (Inria and the OCaml developers)
- The OCaml Manual: module Domain.DLS (Inria and the OCaml developers)
- OCaml 4.14 stdlib.mli, with its deprecation messages (The OCaml developers)
- OCaml 4.14 string.mli, where String.set points to Bytes.set (The OCaml developers)
- camlp-streams (The OCaml developers)
- ocaml-option-nnpchecker opam file (opam-repository)
- Base (Jane Street)
- Core v0.17.2 opam file (opam-repository)
- Base v0.17.3 opam file (opam-repository)
- Base v0.16.3 opam file (opam-repository)
- Eio: effects-based parallel IO for OCaml (OCaml Multicore)
- Eio 1.6 opam file (opam-repository)
- Lwt (Ocsigen)
- Lwt 6.1.2 opam file (opam-repository)
- lwt_eio (OCaml Multicore)
- Dream 1.0.0 opam file (opam-repository)
- cohttp-eio 6.3.0 opam file (opam-repository)
- opam-repository: the package definitions opam installs (opam-repository)
- Introducing OxCaml (Jane Street)
Report a problem with this lesson
Kept only in this browser. Your Learn progress