We (@Shogan.ai) are excited to share an alpha preview of Introcaml, an extension of OCaml 5.5.0 that brings the power of toplevel-style introspection directly into user programs.
What is Introcaml?
The OCaml toplevel has long been able to print arbitrary values without requiring explicit printer functions. However, this capability was internal to the toplevel. Introcaml brings this machinery into the standard library, enabling polymorphic printing and structural introspection of values of any type.
By recovering structure from metadata embedded in compiled code, Introcaml allows you to inspect complex data structures, even those hidden behind abstraction barriers, without writing tedious pp functions.
How to Try It
Introcaml is available via opam. The recommended way to test the alpha version is to create a specific switch:
opam switch create 5.5.0+introcaml
Key Capabilities:
- Polymorphic printing: Print any value regardless of its type, including abstract types.
- Fully integrated with the toplevel, the debugger, the bytecode/native compilers, and their respective dynamic linkers.
Code Examples
open Introspect.Print
(* 1. Simple polymorphic printing *)
type config = { host : string; port : int; debug : bool }
print_any_endline { host = "localhost"; port = 8080; debug = true };;
(* Output: {host = "localhost"; port = 8080; debug = true} *)
(* 2. Breaking through abstraction *)
module M = Map.Make(Int)
print_any_endline (M.of_list [1, "one"; 2, "two"]);;
(* Output: Node {l = Empty; v = 1; d = "one"; r = Node {l = Empty; v = 2; d = "two"; r = Empty; h = 1}; h = 2} *)
(* 3. Quick 'n' Dirty printing with Introspect.P *)
open Introspect.P
let month = "August"
let year = 2026
let () = println ["Welcome to "; month; " "; year; "!"]
(* Output: Welcome to August 2026! *)
How it Works
Introcaml implements a probabilistic metadata recovery scheme designed for high performance:
- Reserved bits: It stores a “tag” in the reserved header bits of OCaml objects.
- Index: A side-database (
Introspect.Index.t) maps these tags to a descriptor (Introspect.Desc.t), which describes the syntactic representation of the value. - Zero overhead: The compilation scheme is designed so that overhead is negligible in bytecode and virtually nonexistent in native mode.
The Introspect API
The new Introspect module provides several layers of access:
Low-level (for tool authors):
Desc: The representation of structural descriptors.Index: The mapping from object headers to descriptors.Dyn: A dynamic view of OCaml objects guided by descriptors, allowing for programmatic traversal (ideal for custom debug tools).
High-level (for general use):
Print: AFormat-based polymorphic printing API.P: A convenience module for “quick and dirty” generic printing.
Integration with existing printers
Previously, printing an opaque type in the toplevel or the debugger would simply result in <abstr>. Now, these tools use type-directed printing by default but seamlessly switch to tag-based printing when encountering opaque constructions.
# let h = Hashtbl.create 3;;
val h : ('_weak3, '_weak4) Hashtbl.t =
<abstr>
{size = 0; data = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
seed = 0; initial_size = 16}
Limitations & Current Status (Alpha)
As this is an alpha release, there are several known limitations. Some are temporary, while others are inherent to the design:
-
Architecture & Compiler:
- Does not support 32-bit architectures. (Tested on x86_64 and arm64).
- Not compatible with Flambda.
js_of_ocamlis currently unsupported (though a fix is straightforward).
-
Metadata Constraints:
- Constants: Due to limited space for metadata, some constants cannot be printed (e.g.,
print_any Nonemay print0, butprintln [None]will likely succeed because the list wrapper provides metadata). - Poly-variants: These are approximated (e.g.,
print_any `Amay print65 or `A). - FFI: Values originating from the FFI are not tagged and will be printed as raw tuples/values unless wrapped in a tagged structure. (Note: FFI compatibility is entirely preserved).
- Constants: Due to limited space for metadata, some constants cannot be printed (e.g.,
-
Marshalling: Tags are not preserved during marshalling by default. To preserve them, you must opt-in using the
Reserved_bitsflag:let roundtrip flags x = Marshal.from_string (Marshal.to_string x flags) 0;; println [roundtrip [Reserved_bits] (ref 1)];; (* Output: {contents = 1} *) -
Object Size: By reserving 22 bits for metadata, the maximum length for arrays is ~4 billion elements and for strings is 32GB.
Acknowledgements:
This work is funded by the Ahrefs Grant Program for OCaml.
Kudos to Çağdaş Bozman et al. for the original work on ocp-memprof, which provided the idea and infrastructure for repurposing header bits, and many thanks to the maintainers who have preserved this capability.
Note: No robots were harmed during the design and implementation of this feature, though their help was solicited for testing and proof-reading.