···491491 Ocaml_utils.Local_store.with_store store (fun () ->
492492 Topdirs.dir_directory path;
493493494494+ (* Register predefined exceptions (Assert_failure, Match_failure, etc.)
495495+ in the global symbol table. Without this, phrases using [assert]
496496+ fail with "Reference to undefined predefined exception". *)
497497+ Symtable.init ();
498498+494499 Toploop.initialize_toplevel_env ();
495500496501 List.iter (fun f -> f ()) functions;
···2233node_directive_test.js: [INFO] init()
44Initializing findlib
55-Loaded findlib_index findlib_index.json: 10 META files, 0 universes
55+Loaded findlib_index findlib_index.json: 19 META files, 0 universes
66Parsed uri: ./lib/stdlib-shims/META
77Reading library: stdlib-shims
88Number of children: 0
99Parsed uri: ./lib/sexplib0/META
1010Reading library: sexplib0
1111Number of children: 0
1212-Parsed uri: ./lib/ppxlib/META
1313-Reading library: ppxlib
1414-Number of children: 11
1515-Found child: __private__
1616-Reading library: ppxlib.__private__
1212+Parsed uri: ./lib/sexp_type/META
1313+Reading library: sexp_type
1714Number of children: 1
1818-Found child: ppx_foo_deriver
1919-Reading library: ppxlib.__private__.ppx_foo_deriver
1515+Found child: grammar
1616+Reading library: sexp_type.grammar
1717+Number of children: 0
1818+Parsed uri: ./lib/ppxlib_jane/META
1919+Reading library: ppxlib_jane
2020Number of children: 0
2121+Parsed uri: ./lib/ppxlib_ast/META
2222+Reading library: ppxlib_ast
2323+Number of children: 4
2424+Found child: ast
2525+Reading library: ppxlib_ast.ast
2626+Number of children: 0
2727+Found child: astlib
2828+Reading library: ppxlib_ast.astlib
2929+Number of children: 0
3030+Found child: stdppx
3131+Reading library: ppxlib_ast.stdppx
3232+Number of children: 0
3333+Found child: traverse_builtins
3434+Reading library: ppxlib_ast.traverse_builtins
3535+Number of children: 0
3636+Parsed uri: ./lib/ppxlib/META
3737+Reading library: ppxlib
3838+Number of children: 10
2139Found child: ast
2240Reading library: ppxlib.ast
2341Number of children: 0
···4866Found child: traverse_builtins
4967Reading library: ppxlib.traverse_builtins
5068Number of children: 0
6969+Parsed uri: ./lib/ppx_sexp_conv/META
7070+Reading library: ppx_sexp_conv
7171+Number of children: 2
7272+Found child: expander
7373+Reading library: ppx_sexp_conv.expander
7474+Number of children: 0
7575+Found child: runtime-lib
7676+Reading library: ppx_sexp_conv.runtime-lib
7777+Number of children: 0
7878+Parsed uri: ./lib/ppx_hash/META
7979+Reading library: ppx_hash
8080+Number of children: 3
8181+Found child: base_internalhash_types
8282+Reading library: ppx_hash.base_internalhash_types
8383+Number of children: 0
8484+Found child: expander
8585+Reading library: ppx_hash.expander
8686+Number of children: 0
8787+Found child: runtime-lib
8888+Reading library: ppx_hash.runtime-lib
8989+Number of children: 0
9090+Parsed uri: ./lib/ppx_enumerate/META
9191+Reading library: ppx_enumerate
9292+Number of children: 1
9393+Found child: runtime-lib
9494+Reading library: ppx_enumerate.runtime-lib
9595+Number of children: 0
5196Parsed uri: ./lib/ppx_deriving/META
5297Reading library: ppx_deriving
5398Number of children: 12
···90135Parsed uri: ./lib/ppx_derivers/META
91136Reading library: ppx_derivers
92137Number of children: 0
138138+Parsed uri: ./lib/ppx_compare/META
139139+Reading library: ppx_compare
140140+Number of children: 2
141141+Found child: expander
142142+Reading library: ppx_compare.expander
143143+Number of children: 0
144144+Found child: runtime-lib
145145+Reading library: ppx_compare.runtime-lib
146146+Number of children: 0
93147Parsed uri: ./lib/ocaml_intrinsics_kernel/META
94148Reading library: ocaml_intrinsics_kernel
95149Number of children: 0
···132186Found child: toplevel
133187Reading library: ocaml-compiler-libs.toplevel
134188Number of children: 0
189189+Parsed uri: ./lib/capsule0/META
190190+Reading library: capsule0
191191+Number of children: 2
192192+Found child: blocking_sync
193193+Reading library: capsule0.blocking_sync
194194+Number of children: 0
195195+Found child: expert
196196+Reading library: capsule0.expert
197197+Number of children: 0
198198+Parsed uri: ./lib/basement/META
199199+Reading library: basement
200200+Number of children: 0
135201Parsed uri: ./lib/base/META
136202Reading library: base
137203Number of children: 3
138138-Found child: base_internalhash_types
139139-Reading library: base.base_internalhash_types
204204+Found child: composition_infix
205205+Reading library: base.composition_infix
140206Number of children: 0
141207Found child: md5
142208Reading library: base.md5
···145211Reading library: base.shadow_stdlib
146212Number of children: 0
147213node_directive_test.js: [INFO] Adding toplevel modules for dynamic cmis from lib/ocaml/
148148-node_directive_test.js: [INFO] toplevel modules: CamlinternalFormat, CamlinternalLazy, CamlinternalFormatBasics, CamlinternalMod, Std_exit, Stdlib, CamlinternalOO
214214+node_directive_test.js: [INFO] toplevel modules: Camlinternaleval, CamlinternalFormat, CamlinternalLazy, CamlinternalAtomic, CamlinternalFormatBasics, Compiler_owee, Opttopdirs, CamlinternalComprehension, Gc_timings, CamlinternalQuote, CamlinternalMod, Std_exit, Stdlib, CamlinternalOO, Topdirs
149215node_directive_test.js: [INFO] init() finished
150216node_directive_test.js: [INFO] setup() for env default...
151217node_directive_test.js: [INFO] Fetching stdlib__Format.cmi
···171237 module MyMod : sig type t = int val zero : int end
172238[PASS] show_exception: # #show My_error;;
173239 exception My_error of string
174174-[PASS] show_type_list: # #show_type list;;
175175- type 'a list = [] | (::) of 'a * 'a list
240240+[FAIL] show_type_list: # #show_type list;;
241241+ type ('a : value_or_null) list = [] | (::) of 'a * 'a list
176242node_directive_test.js: [INFO] Fetching stdlib__List.cmi
177243178244[PASS] show_val_list_map: # #show_val List.map;;
···180246[PASS] show_module_list: # #show_module List;;
181247 module List :
182248 sig
183183- type 'a t = 'a list = [] | (::) of 'a * 'a list
184184- val length : 'a list -> int
185185- val compare_lengths : 'a list -> 'b list -> int
186186- val compare_length_with : 'a list -> int -> int
187187- val is_empty : 'a list -> bool
188188- val cons : 'a -> 'a list -> 'a list
189189- val singleton : 'a -> 'a list
190190- val hd : 'a list -> 'a
191191- val tl : 'a list -> 'a list
192192- val nth : 'a list -> int -> 'a
193193- val nth_opt : 'a list -> int -> 'a option
194194- val rev : 'a list -> 'a list
195195- val init : int -> (int -> 'a) -> 'a list
196196- val append : 'a list -> 'a list -> 'a list
197197- val rev_append : 'a list -> 'a list -> 'a list
198198- val concat : 'a list list -> 'a list
199199- val flatten : 'a list list -> 'a list
200200- val equal : ('a -> 'a -> bool) -> 'a list -> 'a list -> bool
201201- val compare : ('a -> 'a -> int) -> 'a list -> 'a list -> int
202202- val iter : ('a -> unit) -> 'a list -> unit
203203- val iteri : (int -> 'a -> unit) -> 'a list -> unit
204204- val map : ('a -> 'b) -> 'a list -> 'b list
205205- val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list
206206- val rev_map : ('a -> 'b) -> 'a list -> 'b list
207207- val filter_map : ('a -> 'b option) -> 'a list -> 'b list
208208- val concat_map : ('a -> 'b list) -> 'a list -> 'b list
249249+ type ('a : value_or_null) t = 'a list = [] | (::) of 'a * 'a list
250250+ val length : 'a list -> int @@ portable
251251+ val compare_lengths : 'a list -> 'b list -> int @@ portable
252252+ val compare_length_with : 'a list -> int -> int @@ portable
253253+ val is_empty : 'a list -> bool @@ portable
254254+ val cons : 'a -> 'a list -> 'a list @@ portable
255255+ val hd : 'a list -> 'a @@ portable
256256+ val tl : 'a list -> 'a list @@ portable
257257+ val nth : 'a list -> int -> 'a @@ portable
258258+ val nth_opt : 'a list -> int -> 'a option @@ portable
259259+ val rev : 'a list -> 'a list @@ portable
260260+ val init : int -> (int -> 'a) -> 'a list @@ portable
261261+ val append : 'a list -> 'a list -> 'a list @@ portable
262262+ val rev_append : 'a list -> 'a list -> 'a list @@ portable
263263+ val concat : 'a list list -> 'a list @@ portable
264264+ val flatten : 'a list list -> 'a list @@ portable
265265+ val equal : ('a -> 'a -> bool) -> 'a list -> 'a list -> bool @@ portable
266266+ val compare : ('a -> 'a -> int) -> 'a list -> 'a list -> int @@ portable
267267+ val iter : ('a -> unit) -> 'a list -> unit @@ portable
268268+ val iteri : (int -> 'a -> unit) -> 'a list -> unit @@ portable
269269+ val map : ('a -> 'b) -> 'a list -> 'b list @@ portable
270270+ val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list @@ portable
271271+ val rev_map : ('a -> 'b) -> 'a list -> 'b list @@ portable
272272+ val filter_map : ('a -> 'b option) -> 'a list -> 'b list @@ portable
273273+ val concat_map : ('a -> 'b list) -> 'a list -> 'b list @@ portable
209274 val fold_left_map :
210210- ('acc -> 'a -> 'acc * 'b) -> 'acc -> 'a list -> 'acc * 'b list
211211- val fold_left : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc
212212- val fold_right : ('a -> 'acc -> 'acc) -> 'a list -> 'acc -> 'acc
213213- val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
214214- val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
215215- val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
275275+ ('acc -> 'a -> 'acc * 'b) -> 'acc -> 'a list -> 'acc * 'b list @@
276276+ portable
277277+ val fold_left : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc @@
278278+ portable
279279+ val fold_right : ('a -> 'acc -> 'acc) -> 'a list -> 'acc -> 'acc @@
280280+ portable
281281+ val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit @@ portable
282282+ val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list @@ portable
283283+ val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list @@
284284+ portable
216285 val fold_left2 :
217217- ('acc -> 'a -> 'b -> 'acc) -> 'acc -> 'a list -> 'b list -> 'acc
286286+ ('acc -> 'a -> 'b -> 'acc) -> 'acc -> 'a list -> 'b list -> 'acc @@
287287+ portable
218288 val fold_right2 :
219219- ('a -> 'b -> 'acc -> 'acc) -> 'a list -> 'b list -> 'acc -> 'acc
220220- val for_all : ('a -> bool) -> 'a list -> bool
221221- val exists : ('a -> bool) -> 'a list -> bool
222222- val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
223223- val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
224224- val mem : 'a -> 'a list -> bool
225225- val memq : 'a -> 'a list -> bool
226226- val find : ('a -> bool) -> 'a list -> 'a
227227- val find_opt : ('a -> bool) -> 'a list -> 'a option
228228- val find_index : ('a -> bool) -> 'a list -> int option
229229- val find_map : ('a -> 'b option) -> 'a list -> 'b option
230230- val find_mapi : (int -> 'a -> 'b option) -> 'a list -> 'b option
231231- val filter : ('a -> bool) -> 'a list -> 'a list
232232- val find_all : ('a -> bool) -> 'a list -> 'a list
233233- val filteri : (int -> 'a -> bool) -> 'a list -> 'a list
234234- val take : int -> 'a list -> 'a list
235235- val drop : int -> 'a list -> 'a list
236236- val take_while : ('a -> bool) -> 'a list -> 'a list
237237- val drop_while : ('a -> bool) -> 'a list -> 'a list
238238- val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
289289+ ('a -> 'b -> 'acc -> 'acc) -> 'a list -> 'b list -> 'acc -> 'acc @@
290290+ portable
291291+ val for_all : ('a -> bool) -> 'a list -> bool @@ portable
292292+ val exists : ('a -> bool) -> 'a list -> bool @@ portable
293293+ val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool @@
294294+ portable
295295+ val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool @@
296296+ portable
297297+ val mem : 'a @ local -> 'a list @ local -> bool @@ portable
298298+ val memq : 'a @ local -> 'a list @ local -> bool @@ portable
299299+ val find : ('a -> bool) -> 'a list -> 'a @@ portable
300300+ val find_opt : ('a -> bool) -> 'a list -> 'a option @@ portable
301301+ val find_index : ('a -> bool) -> 'a list -> int option @@ portable
302302+ val find_map : ('a -> 'b option) -> 'a list -> 'b option @@ portable
303303+ val find_mapi : (int -> 'a -> 'b option) -> 'a list -> 'b option @@
304304+ portable
305305+ val filter : ('a -> bool) -> 'a list -> 'a list @@ portable
306306+ val find_all : ('a -> bool) -> 'a list -> 'a list @@ portable
307307+ val filteri : (int -> 'a -> bool) -> 'a list -> 'a list @@ portable
308308+ val partition : ('a -> bool) -> 'a list -> 'a list * 'a list @@ portable
239309 val partition_map :
240240- ('a -> ('b, 'c) Either.t) -> 'a list -> 'b list * 'c list
241241- val assoc : 'a -> ('a * 'b) list -> 'b
242242- val assoc_opt : 'a -> ('a * 'b) list -> 'b option
243243- val assq : 'a -> ('a * 'b) list -> 'b
244244- val assq_opt : 'a -> ('a * 'b) list -> 'b option
245245- val mem_assoc : 'a -> ('a * 'b) list -> bool
246246- val mem_assq : 'a -> ('a * 'b) list -> bool
247247- val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
248248- val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list
249249- val split : ('a * 'b) list -> 'a list * 'b list
250250- val combine : 'a list -> 'b list -> ('a * 'b) list
251251- val sort : ('a -> 'a -> int) -> 'a list -> 'a list
252252- val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list
253253- val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list
254254- val sort_uniq : ('a -> 'a -> int) -> 'a list -> 'a list
255255- val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
256256- val to_seq : 'a list -> 'a Seq.t
257257- val of_seq : 'a Seq.t -> 'a list
310310+ ('a -> ('b, 'c) Either.t) -> 'a list -> 'b list * 'c list @@ portable
311311+ val assoc : 'a -> ('a * 'b) list -> 'b @@ portable
312312+ val assoc_opt : 'a -> ('a * 'b) list -> 'b option @@ portable
313313+ val assq : 'a -> ('a * 'b) list -> 'b @@ portable
314314+ val assq_opt : 'a -> ('a * 'b) list -> 'b option @@ portable
315315+ val mem_assoc : 'a -> ('a * 'b) list -> bool @@ portable
316316+ val mem_assq : 'a -> ('a * 'b) list -> bool @@ portable
317317+ val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list @@ portable
318318+ val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list @@ portable
319319+ val split : ('a * 'b) list -> 'a list * 'b list @@ portable
320320+ val combine : 'a list -> 'b list -> ('a * 'b) list @@ portable
321321+ val sort : ('a -> 'a -> int) -> 'a list -> 'a list @@ portable
322322+ val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list @@ portable
323323+ val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list @@ portable
324324+ val sort_uniq : ('a -> 'a -> int) -> 'a list -> 'a list @@ portable
325325+ val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list @@
326326+ portable
327327+ val to_seq : 'a list -> 'a Seq.t @@ portable
328328+ val of_seq : 'a Seq.t -> 'a list @@ portable
258329 end
259330[PASS] show_exception_not_found: # #show_exception Not_found;;
260331 exception Not_found
···290361--- Section 6: #rectypes ---
291362292363Line 1, characters 0-23:
293293-Error: The type abbreviation t is cyclic:
294294- 'a t = 'a t -> int,
295295- 'a t -> int contains 'a t
364364+Error: The type abbreviation "t" is cyclic:
365365+ "'a t" = "'a t -> int",
366366+ "'a t -> int" contains "'a t"
296367[FAIL] rectypes_before: # type 'a t = 'a t -> int;;
297368[PASS] rectypes_after: # type 'a u = 'a u -> int;;
298369 type 'a u = 'a u -> int
···324395 class counter :
325396 object val mutable n : int method get : int method incr : unit end
326397327327-=== Results: 29/31 tests passed ===
398398+=== Results: 28/31 tests passed ===
328399FAILURE: Some tests failed.
+84-18
js_top_worker/test/node/node_env_test.expected
···2233node_env_test.js: [INFO] init()
44Initializing findlib
55-Loaded findlib_index findlib_index.json: 10 META files, 0 universes
55+Loaded findlib_index findlib_index.json: 19 META files, 0 universes
66Parsed uri: ./lib/stdlib-shims/META
77Reading library: stdlib-shims
88Number of children: 0
99Parsed uri: ./lib/sexplib0/META
1010Reading library: sexplib0
1111+Number of children: 0
1212+Parsed uri: ./lib/sexp_type/META
1313+Reading library: sexp_type
1414+Number of children: 1
1515+Found child: grammar
1616+Reading library: sexp_type.grammar
1717+Number of children: 0
1818+Parsed uri: ./lib/ppxlib_jane/META
1919+Reading library: ppxlib_jane
2020+Number of children: 0
2121+Parsed uri: ./lib/ppxlib_ast/META
2222+Reading library: ppxlib_ast
2323+Number of children: 4
2424+Found child: ast
2525+Reading library: ppxlib_ast.ast
2626+Number of children: 0
2727+Found child: astlib
2828+Reading library: ppxlib_ast.astlib
2929+Number of children: 0
3030+Found child: stdppx
3131+Reading library: ppxlib_ast.stdppx
3232+Number of children: 0
3333+Found child: traverse_builtins
3434+Reading library: ppxlib_ast.traverse_builtins
1135Number of children: 0
1236Parsed uri: ./lib/ppxlib/META
1337Reading library: ppxlib
1414-Number of children: 11
1515-Found child: __private__
1616-Reading library: ppxlib.__private__
1717-Number of children: 1
1818-Found child: ppx_foo_deriver
1919-Reading library: ppxlib.__private__.ppx_foo_deriver
2020-Number of children: 0
3838+Number of children: 10
2139Found child: ast
2240Reading library: ppxlib.ast
2341Number of children: 0
···4866Found child: traverse_builtins
4967Reading library: ppxlib.traverse_builtins
5068Number of children: 0
6969+Parsed uri: ./lib/ppx_sexp_conv/META
7070+Reading library: ppx_sexp_conv
7171+Number of children: 2
7272+Found child: expander
7373+Reading library: ppx_sexp_conv.expander
7474+Number of children: 0
7575+Found child: runtime-lib
7676+Reading library: ppx_sexp_conv.runtime-lib
7777+Number of children: 0
7878+Parsed uri: ./lib/ppx_hash/META
7979+Reading library: ppx_hash
8080+Number of children: 3
8181+Found child: base_internalhash_types
8282+Reading library: ppx_hash.base_internalhash_types
8383+Number of children: 0
8484+Found child: expander
8585+Reading library: ppx_hash.expander
8686+Number of children: 0
8787+Found child: runtime-lib
8888+Reading library: ppx_hash.runtime-lib
8989+Number of children: 0
9090+Parsed uri: ./lib/ppx_enumerate/META
9191+Reading library: ppx_enumerate
9292+Number of children: 1
9393+Found child: runtime-lib
9494+Reading library: ppx_enumerate.runtime-lib
9595+Number of children: 0
5196Parsed uri: ./lib/ppx_deriving/META
5297Reading library: ppx_deriving
5398Number of children: 12
···90135Parsed uri: ./lib/ppx_derivers/META
91136Reading library: ppx_derivers
92137Number of children: 0
138138+Parsed uri: ./lib/ppx_compare/META
139139+Reading library: ppx_compare
140140+Number of children: 2
141141+Found child: expander
142142+Reading library: ppx_compare.expander
143143+Number of children: 0
144144+Found child: runtime-lib
145145+Reading library: ppx_compare.runtime-lib
146146+Number of children: 0
93147Parsed uri: ./lib/ocaml_intrinsics_kernel/META
94148Reading library: ocaml_intrinsics_kernel
95149Number of children: 0
···132186Found child: toplevel
133187Reading library: ocaml-compiler-libs.toplevel
134188Number of children: 0
189189+Parsed uri: ./lib/capsule0/META
190190+Reading library: capsule0
191191+Number of children: 2
192192+Found child: blocking_sync
193193+Reading library: capsule0.blocking_sync
194194+Number of children: 0
195195+Found child: expert
196196+Reading library: capsule0.expert
197197+Number of children: 0
198198+Parsed uri: ./lib/basement/META
199199+Reading library: basement
200200+Number of children: 0
135201Parsed uri: ./lib/base/META
136202Reading library: base
137203Number of children: 3
138138-Found child: base_internalhash_types
139139-Reading library: base.base_internalhash_types
204204+Found child: composition_infix
205205+Reading library: base.composition_infix
140206Number of children: 0
141207Found child: md5
142208Reading library: base.md5
···145211Reading library: base.shadow_stdlib
146212Number of children: 0
147213node_env_test.js: [INFO] Adding toplevel modules for dynamic cmis from lib/ocaml/
148148-node_env_test.js: [INFO] toplevel modules: CamlinternalFormat, CamlinternalLazy, CamlinternalFormatBasics, CamlinternalMod, Std_exit, Stdlib, CamlinternalOO
214214+node_env_test.js: [INFO] toplevel modules: Camlinternaleval, CamlinternalFormat, CamlinternalLazy, CamlinternalAtomic, CamlinternalFormatBasics, Compiler_owee, Opttopdirs, CamlinternalComprehension, Gc_timings, CamlinternalQuote, CamlinternalMod, Std_exit, Stdlib, CamlinternalOO, Topdirs
149215node_env_test.js: [INFO] init() finished
150216--- Section 1: Default Environment ---
151217node_env_test.js: [INFO] setup() for env default...
···175241176242--- Section 3: Environment Isolation ---
177243Line 1, characters 0-11:
178178-Error: Unbound value default_val
244244+Error: Unbound value "default_val"
179245[PASS] isolation_default_from_env1: No leakage: # default_val;;
180246181247Line 1, characters 0-8:
182182-Error: Unbound value env1_val
248248+Error: Unbound value "env1_val"
183249[PASS] isolation_env1_from_default: No leakage: # env1_val;;
184250[PASS] default_still_works: # default_val;;
185251 - : int = 42
···196262 val env2_val : int = 200
197263198264Line 1, characters 0-8:
199199-Error: Unbound value env1_val
200200-Hint: Did you mean env2_val?
265265+Error: Unbound value "env1_val"
266266+Hint: Did you mean "env2_val"?
201267[PASS] isolation_env1_from_env2: No leakage: # env1_val;;
202268203269Line 1, characters 0-8:
204204-Error: Unbound value env2_val
205205-Hint: Did you mean env1_val?
270270+Error: Unbound value "env2_val"
271271+Hint: Did you mean "env1_val"?
206272[PASS] isolation_env2_from_env1: No leakage: # env2_val;;
207273208274--- Section 5: List Environments ---
···228294node_env_test.js: [INFO] setup() finished for env env2
229295230296Line 1, characters 0-8:
231231-Error: Unbound value env2_val
297297+Error: Unbound value "env2_val"
232298[PASS] new_env2_clean: Old value gone: # env2_val;;
233299[PASS] new_env2_define: # let new_env2_val = 999;;
234300 val new_env2_val : int = 999
···11{0 Admonition Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This extension adds support for admonition blocks (callouts) in odoc
46documentation. Admonitions are used to highlight important information,
57warnings, tips, and other notable content.
+2
odoc-docsite/doc/index.mld
···11{0 Odoc Documentation-site Shell}
2233+@admonition.warning This plugin was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This plugin for odoc provides a more modern styling for odoc's output, including
46SPA-style navigation.
+2
odoc-dot-extension/index.mld
···11{0 Graphviz/DOT Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This extension adds support for {{:https://graphviz.org/}Graphviz} diagrams
46using the DOT language in odoc documentation. Graphviz is a powerful graph
57visualization tool that can render complex node-edge diagrams.
+1-1
odoc-interactive-extension/doc/focs_2020_q2.mld
···11{0 Simplified Mastermind}
2233-@x-ocaml.universe https://jon.ludl.am/ocaml/
33+@x-ocaml.universe ./universe
4455{e Cambridge Computer Science Tripos Part IA -- 2020 -- Paper 1, Question 2}
66
+1-1
odoc-interactive-extension/doc/focs_2024_q1.mld
···11{0 Statistical Analysis with Fold, Map, and Filter}
2233-@x-ocaml.universe https://jon.ludl.am/ocaml/
33+@x-ocaml.universe ./universe
4455{e Cambridge Computer Science Tripos Part IA -- 2024 -- Paper 1, Question 1}
66
+1-1
odoc-interactive-extension/doc/focs_2025_q2.mld
···11{0 Expression Evaluation and Polish Notation}
2233-@x-ocaml.universe https://jon.ludl.am/ocaml/
33+@x-ocaml.universe ./universe
4455{e Cambridge Computer Science Tripos Part IA -- 2025 -- Paper 1, Question 2}
66
+2
odoc-interactive-extension/doc/index.mld
···11{0 Interactive OCaml Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35See the various notebooks in the sidebar for examples of this extension.
46
+2
odoc-jons-plugins/doc/index.mld
···11{0 odoc-jons-plugins}
2233+@admonition.warning This plugin was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Custom odoc shell and extensions for {{:https://jon.recoil.org}jon.recoil.org}.
4657This plugin provides the HTML shell used to render the site, including
+2
odoc-mermaid-extension/index.mld
···11{0 Mermaid Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This extension adds support for {{:https://mermaid.js.org/}Mermaid} diagrams
46in odoc documentation. Mermaid is a JavaScript-based diagramming tool that
57renders Markdown-inspired text definitions into diagrams.
+2
odoc-msc-extension/index.mld
···11{0 MSC Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This extension adds support for {{:https://www.mcternan.me.uk/mscgen/}Message
46Sequence Charts} (MSC) in odoc documentation. MSC is a graphical and textual
57language for describing interactions between components.
+2
odoc-rfc-extension/index.mld
···11{0 RFC Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35This extension adds support for citing IETF RFCs (Request for Comments) in
46odoc documentation. It provides a convenient way to reference internet
57standards with automatic linking and formatting.
+2
odoc-scrollycode-extension/doc/index.mld
···11{0 Scrollycode Extension for odoc}
2233+@admonition.warning This extension was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Scroll-driven code tutorials for odoc. Authored as [.mld] files using
46[@@scrolly] custom tags — each step reveals new code alongside an
57explanation.
···11{0 Odoc Standalone Output}
2233+@admonition.warning This plugin was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35A self-contained HTML shell plugin for odoc. Inlines all CSS and JavaScript
46into each generated HTML page, producing documentation files that work
57without a separate support-files directory. Fonts are loaded from Google
+64-21
odoc/doc/extensions.mld
···11{0 Writing Extensions}
2233+@admonition.note The odoc extension API is early in development and
44+the interface is still changing frequently. Expect breaking changes
55+between releases.
66+37[odoc] supports a plugin system for custom tags and code blocks. Extensions
48are OCaml libraries loaded at doc-generation time that transform custom
59markup into HTML, LaTeX, or other output formats.
···1216There are two kinds of extension:
13171418{ul
1515-{- {b Tag extensions} handle custom tags like [@@note], [@@rfc], [@@scrolly].
1919+{- {b Tag extensions} handle custom tags like [@note], [@rfc], [@scrolly].
1620 They receive the tag's content as a list of block elements and return
1721 document content, resources, and assets.}
1822{- {b Code block extensions} handle fenced code blocks with a custom
1919- language, e.g., [{@@dot ...}] or [{@@ocaml ...}].
2323+ language, e.g., [{@dot ...}] or [{@ocaml ...}].
2024 They receive the code text plus any options and return the same output
2125 types.}}
2226···8791(e.g., injecting a [<meta>] tag). Never put your main runtime in an
8892inline script.
89939494+{1 JavaScript Lifecycle}
9595+9696+The following diagram shows when extension JavaScript runs during both
9797+direct page loads and SPA navigations:
9898+9999+{@mermaid[
100100+sequenceDiagram
101101+ participant Browser
102102+ participant Head as head scripts
103103+ participant Shell as Docsite Shell
104104+ participant Ext as Extension JS
105105+ participant DOM
106106+107107+ rect rgb(40, 60, 80)
108108+ Note over Browser,DOM: Direct page load
109109+ Browser->>Head: Parse head
110110+ Head->>Ext: Load via script src="ext.js"
111111+ Ext->>Ext: readyState === 'loading'
112112+ Browser->>DOM: DOMContentLoaded
113113+ Ext->>DOM: initAll() — find and init widgets
114114+ Ext->>DOM: Listen for 'odoc-spa-loaded'
115115+ end
116116+117117+ rect rgb(60, 40, 60)
118118+ Note over Browser,DOM: SPA navigation (sidebar click)
119119+ Browser->>Shell: click event on sidebar link
120120+ Shell->>Shell: fetch(newPage.html)
121121+ Shell->>DOM: Swap .odoc-content innerHTML
122122+ Shell->>DOM: Load new head scripts
123123+ Shell->>DOM: Dispatch 'odoc-spa-loaded' event
124124+ Note over Head: ext.js already loaded — NOT re-executed
125125+ Note over DOM: DOMContentLoaded does NOT fire
126126+ DOM->>Ext: 'odoc-spa-loaded' handler fires
127127+ Ext->>DOM: initAll() — find new widgets, skip already-init'd
128128+ end
129129+]}
130130+131131+The key insight: on SPA navigation, neither the script nor
132132+[DOMContentLoaded] re-fires. The shell dispatches a custom
133133+[odoc-spa-loaded] event after swapping content and loading head
134134+scripts — this is the recommended way for extensions to detect
135135+new content after navigation.
136136+90137{1:spa SPA Navigation: The Critical Pitfall}
9113892139The odoc {b docsite shell} (and similar shells) implement single-page app
···155202156203 // Run on initial page load.
157204 if (document.readyState === 'loading') {
158158- document.addEventListener('DOMContentLoaded', function() {
159159- initAll();
160160- observe();
161161- });
205205+ document.addEventListener('DOMContentLoaded', function() { initAll(); });
162206 } else {
163207 initAll();
164164- observe();
165208 }
166209167167- // Watch for new content injected by SPA navigation.
168168- function observe() {
169169- new MutationObserver(function() { initAll(); })
170170- .observe(document.body, { childList: true, subtree: true });
171171- }
210210+ // Re-initialise after SPA navigation swaps in new content.
211211+ document.addEventListener('odoc-spa-loaded', function() { initAll(); });
172212})();
173213]}
174214···176216177217{ul
178218{- {b Guard against double-init.} Use a [data-*] attribute to mark
179179- initialised elements. The [MutationObserver] fires on every DOM
180180- mutation, so [initAll] may be called many times.}
219219+ initialised elements. [initAll] may be called multiple times.}
181220{- {b Check [document.readyState].} The script is in [<head>], so
182221 [document.body] doesn't exist yet on the initial load. Wait for
183183- [DOMContentLoaded] before attaching the [MutationObserver].}
222222+ [DOMContentLoaded] before first [initAll].}
223223+{- {b Listen for [odoc-spa-loaded].} The docsite shell dispatches this
224224+ custom event after swapping content and loading head scripts. This
225225+ is the recommended way for extensions to detect new content.}
184226{- {b Don't rely on [DOMContentLoaded] alone.} After SPA navigation the
185227 [Js_url] script has already loaded and [DOMContentLoaded] already fired.
186186- The [MutationObserver] is what detects the new content.}}
228228+ The [odoc-spa-loaded] event is what triggers re-initialisation.}}
187229188230{2 Case study: Scrollycode}
189231···225267]}
226268}
227269{- Replace the [DOMContentLoaded] gate with [readyState] check +
228228- [MutationObserver] (as shown in the pattern above).}
270270+ [odoc-spa-loaded] listener (as shown in the pattern above).}
229271{- Add a [data-sc-init] guard on each [.sc-container] to prevent
230272 double-initialisation.}}
231273···263305{- {b No body scripts.} All JavaScript is delivered via [Js_url] (support
264306 files) or small [Js_inline] bootstraps in [resources]. Nothing is
265307 embedded in the HTML body via [Raw_markup].}
266266-{- {b No [DOMContentLoaded] dependency.} Use [document.readyState] check +
267267- [MutationObserver] instead.}
308308+{- {b No [DOMContentLoaded] dependency.} Use [document.readyState] check
309309+ for initial load, plus [odoc-spa-loaded] listener for navigations.}
268310{- {b Double-init guard.} Every element you initialise is marked (e.g.,
269311 with a [data-*] attribute) and skipped on subsequent [initAll] calls.}
270312{- {b SPA navigation tested.} Both direct-load and sidebar-navigation
271313 paths work.}
272272-{- {b [MutationObserver] set up after [document.body] exists.} If your
273273- script is in [<head>], [document.body] is [null] on initial parse.}}
314314+{- {b Listens for [odoc-spa-loaded].} The docsite shell dispatches this
315315+ event after content swap — this is your extension's cue to scan for
316316+ new elements.}}
+3
odoc/src/extension_api/odoc_extension_api.ml
···11(** Odoc Extension API
2233+ {b Note:} This API is early in development and the interface is still
44+ changing frequently. Expect breaking changes between releases.
55+36 This module provides the interface for odoc tag extensions.
47 Extensions are dynamically loaded plugins that handle custom tags
58 like [@note], [@rfc], [@example], etc.
+14-15
odoc/test/generators/html/Markup.html
···2929//]]>
30303131 </script>
3232- <script>
3333-3434-//<![CDATA[
3535-(function(){if(window.__xOcamlLoaded)return;window.__xOcamlLoaded=true;var s=document.createElement('script');s.src='./_x-ocaml/x-ocaml.js';s.setAttribute('src-worker','./_x-ocaml/worker.js');s.setAttribute('backend','builtin');document.head.appendChild(s)})();
3636-//]]>
3737-3838- </script>
3932 </head>
4033 <body class="odoc">
4134 <nav class="odoc-nav"><a href="index.html">Up</a> –
···174167 <h2 id="preformatted-text">
175168 <a href="#preformatted-text" class="anchor"></a>Preformatted text
176169 </h2><p>This is a code block:</p>
177177- <x-ocaml mode="interactive"> let foo = ()
178178- (** There are some nested comments in here, but an unpaired comment
179179- terminator would terminate the whole doc surrounding comment. It's
180180- best to keep code blocks no wider than 72 characters. *)
181181-182182- let bar =
183183- ignore foo</x-ocaml>
184184- <p>There are also verbatim blocks:</p>
170170+ <div>
171171+ <pre class="language-ocaml">
172172+ <code>
173173+ let foo = ()
174174+ (** There are some nested comments in here, but an unpaired comment
175175+ terminator would terminate the whole doc surrounding comment.
176176+ It's
177177+ best to keep code blocks no wider than 72 characters. *)
178178+179179+ let bar =
180180+ ignore foo
181181+ </code>
182182+ </pre>
183183+ </div><p>There are also verbatim blocks:</p>
185184 <pre>The main difference is these don't get syntax highlighting.</pre>
186185 <h2 id="lists"><a href="#lists" class="anchor"></a>Lists</h2>
187186 <ul><li>This is a</li><li>shorthand bulleted list,</li>
+9-1
odoc/test/generators/latex/Markup.tex
···4444This is a reference to \hyperref[Markup--val-foo]{\ocamlinlinecode{\ocamlinlinecode{foo}}[p\pageref*{Markup--val-foo}]}. References can have replacement text: \hyperref[Markup--val-foo]{\ocamlinlinecode{the value foo}[p\pageref*{Markup--val-foo}]}. Except for the special lookup support, references are pretty much just like links. The replacement text can have nested styles: \hyperref[Markup--val-foo]{\ocamlinlinecode{\bold{bold}}[p\pageref*{Markup--val-foo}]}, \hyperref[Markup--val-foo]{\ocamlinlinecode{\emph{italic}}[p\pageref*{Markup--val-foo}]}, \hyperref[Markup--val-foo]{\ocamlinlinecode{\emph{emphasis}}[p\pageref*{Markup--val-foo}]}, \hyperref[Markup--val-foo]{\ocamlinlinecode{super\textsuperscript{script}}[p\pageref*{Markup--val-foo}]}, \hyperref[Markup--val-foo]{\ocamlinlinecode{sub\textsubscript{script}}[p\pageref*{Markup--val-foo}]}, and \hyperref[Markup--val-foo]{\ocamlinlinecode{\ocamlinlinecode{code}}[p\pageref*{Markup--val-foo}]}. It's also possible to surround a reference in a style: \bold{\hyperref[Markup--val-foo]{\ocamlinlinecode{\ocamlinlinecode{foo}}[p\pageref*{Markup--val-foo}]}}. References can't be nested inside references, and links and references can't be nested inside each other.
45454646\subsection{Preformatted text\label{Markup--preformatted-text}}%
4747-This is a code block:
4747+This is a code block:\medbreak
4848+\begin{ocamlcodeblock}
4949+ let foo = ()
5050+ (** There are some nested comments in here, but an unpaired comment
5151+ terminator would terminate the whole doc surrounding comment. It's
5252+ best to keep code blocks no wider than 72 characters. *)
48535454+ let bar =
5555+ ignore foo
5656+\end{ocamlcodeblock}\medbreak
4957There are also verbatim blocks:
50585159\begin{verbatim}The main difference is these don't get syntax highlighting.\end{verbatim}%
+10
odoc/test/generators/man/Markup.3o
···148148.fi
149149This is a code block:
150150.sp
151151+.EX
152152+ let foo = ()
153153+ (** There are some nested comments in here, but an unpaired comment
154154+ terminator would terminate the whole doc surrounding comment\. It's
155155+ best to keep code blocks no wider than 72 characters\. *)
156156+157157+ let bar =
158158+ ignore foo
159159+.EE
160160+.sp
151161There are also verbatim blocks:
152162.sp
153163.EX
+4-2
odoc/test/generators/markdown/Markup.md
···66666767This is a code block:
68686969-<x-ocaml mode="interactive"> let foo = ()
6969+```ocaml
7070+ let foo = ()
7071 (** There are some nested comments in here, but an unpaired comment
7172 terminator would terminate the whole doc surrounding comment. It's
7273 best to keep code blocks no wider than 72 characters. *)
73747475 let bar =
7575- ignore foo</x-ocaml>
7676+ ignore foo
7777+```
7678There are also verbatim blocks:
77797880```
+7-6
odoc/test/integration/code_block_handlers.t/run.t
···1313 $ odoc html-generate -o html page-test_code_blocks.odocl
1414 odoc: internal error, uncaught exception:
1515 Sys_error("html/test/test_code_blocks.html: Permission denied")
1616- Raised by primitive operation at Stdlib.open_out_gen in file "stdlib.ml", line 331, characters 29-55
1717- Called from Stdlib.open_out in file "stdlib.ml" (inlined), line 336, characters 2-74
1818- Called from Odoc_utils.Io_utils.with_open_out in file "odoc/src/utils/odoc_utils.ml" (inlined), line 55, characters 19-35
1919- Called from Odoc_utils.Io_utils.with_formatter_out in file "odoc/src/utils/odoc_utils.ml", line 62, characters 4-74
1616+ Raised by primitive operation at Stdlib.open_out_gen in file "stdlib.ml" (inlined), line 346, characters 29-55
1717+ Called from Stdlib.open_out in file "stdlib.ml" (inlined), line 351, characters 2-74
1818+ Called from Odoc_utils.Io_utils.with_open_out in file "odoc/src/utils/odoc_utils.ml", line 55, characters 19-35
2019 Called from Odoc_document__Renderer.traverse.aux in file "odoc/src/document/renderer.ml", line 18, characters 4-44
2121- Called from Stdlib__List.iter in file "list.ml", line 114, characters 12-15
2020+ Called from Stdlib__List.iter in file "list.ml" (inlined), line 117, characters 12-15
2121+ Called from Odoc_document__Renderer.traverse in file "odoc/src/document/renderer.ml", line 21, characters 2-17
2222 Called from Odoc_odoc__Rendering.generate_odoc.(fun) in file "odoc/src/odoc/rendering.ml", line 82, characters 2-68
2323- Called from Stdlib__List.fold_left in file "list.ml", line 125, characters 24-34
2323+ Called from Stdlib__List.fold_left in file "list.ml" (inlined), line 128, characters 24-34
2424+ Called from Dune__exe__Main.Make_renderer.Generate.generate in file "odoc/src/odoc/bin/main.ml", lines 919-921, characters 6-131
2425 Called from Cmdliner_term.app.(fun) in file "cmdliner_term.ml", line 24, characters 19-24
2526 Called from Cmdliner_term.app.(fun) in file "cmdliner_term.ml", line 22, characters 12-19
2627 Called from Cmdliner_eval.run_parser in file "cmdliner_eval.ml", line 35, characters 37-44
···28282929 For higher order functions, it will be suffixed **U** if it takes uncurried callback.
30303131- <x-ocaml mode="interactive"> val forEach : 'a t -> ('a -> unit) -> unit
3232- val forEachU : 'a t -> ('a -> unit [\@u]) -> unit</x-ocaml>
3131+ ```ocaml
3232+ val forEach : 'a t -> ('a -> unit) -> unit
3333+ val forEachU : 'a t -> ('a -> unit [\@u]) -> unit
3434+ ```
3335 In general, uncurried version will be faster, but it may be less familiar to people who have a background in functional programming.
34363537 **A special encoding for collection safety**
···38403941 The original OCaml stdlib solved the problem using *functor* which creates a big closure at runtime and makes dead code elimination much harder. We use a phantom type to solve the problem:
40424141- <x-ocaml mode="interactive"> module Comparable1 = Belt.Id.MakeComparable (struct
4343+ ```ocaml
4444+ module Comparable1 = Belt.Id.MakeComparable (struct
4245 type t = int * int
4346 let cmp (a0, a1) (b0, b1) =
4447 match Pervasives.compare a0 b0 with
4545- | 0 -> Pervasives.compare a1 b1
4646- | c -> c
4848+ | 0 -> Pervasives.compare a1 b1
4949+ | c -> c
4750 end)
48514952 let mySet1 = Belt.Set.make ~id:(module Comparable1)
···5255 type t = int * int
5356 let cmp (a0, a1) (b0, b1) =
5457 match Pervasives.compare a0 b0 with
5555- | 0 -> Pervasives.compare a1 b1
5656- | c -> c
5858+ | 0 -> Pervasives.compare a1 b1
5959+ | c -> c
5760 end)
58615959- let mySet2 = Belt.Set.make ~id:(module Comparable2)</x-ocaml>
6262+ let mySet2 = Belt.Set.make ~id:(module Comparable2)
6363+ ```
6064 Here, the compiler would infer `mySet1` and `mySet2` having different type, so e.g. a \`merge\` operation that tries to merge these two sets will correctly fail.
61656262- <x-ocaml mode="interactive"> val mySet1 : (int * int, Comparable1.identity) t
6363- val mySet2 : (int * int, Comparable2.identity) t</x-ocaml>
6666+ ```ocaml
6767+ val mySet1 : (int * int, Comparable1.identity) t
6868+ val mySet2 : (int * int, Comparable2.identity) t
6969+ ```
6470 `Comparable1.identity` and `Comparable2.identity` are not the same using our encoding scheme.
65716672 **Collection Hierarchy**
67736874 In general, we provide a generic collection module, but also create specialized modules for commonly used data type. Take *Belt.Set* for example, we provide:
69757070- <x-ocaml mode="interactive"> Belt.Set
7676+ ```ocaml
7777+ Belt.Set
7178 Belt.Set.Int
7272- Belt.Set.String</x-ocaml>
7979+ Belt.Set.String
8080+ ```
7381 The specialized modules *Belt.Set.Int*, *Belt.Set.String* are in general more efficient.
74827583 Currently, both *Belt\_Set* and *Belt.Set* are accessible to users for some technical reasons, we **strongly recommend** users stick to qualified import, *Belt.Set*, we may hide the internal, *i.e*, *Belt\_Set* in the future
···8383 {"id":[{"kind":"Root","name":"Main"},{"kind":"Type","name":"tdzdz"},{"kind":"Constructor","name":"B"}],"doc":"Bliiiiiiiiiii","kind":{"kind":"Constructor","args":{"kind":"Tuple","vals":["int list","int"]},"res":"tdzdz"},"display":{"url":"page/Main/index.html#type-tdzdz.B","html":"<code class=\"entry-kind\">cons</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.tdzdz.</span><span class=\"entry-name\">B</span><code class=\"entry-rhs\"> : int list * int -> tdzdz</code></code><div class=\"entry-comment\"><div><p>Bliiiiiiiiiii</p></div></div>"}}
8484 {"id":[{"kind":"Root","name":"J"}],"doc":"a paragraph two","kind":{"kind":"Doc"},"display":{"url":"page/J/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"entry-name\">J</span></code><div class=\"entry-comment\"><div><p>a paragraph two</p></div></div>"}}
8585 {"id":[{"kind":"Root","name":"Main"}],"doc":"a paragraph two","kind":{"kind":"Doc"},"display":{"url":"page/Main/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"entry-name\">Main</span></code><div class=\"entry-comment\"><div><p>a paragraph two</p></div></div>"}}
8686- {"id":[{"kind":"Root","name":"Main"}],"doc":"a paragraph\nand another\nverbatim\nx + 1\nblibli","kind":{"kind":"Doc"},"display":{"url":"page/Main/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"entry-name\">Main</span></code><div class=\"entry-comment\"><div><p>a paragraph</p><p>and another</p><pre>verbatim</pre><p><code class=\"odoc-katex-math\">x + 1</code></p><x-ocaml mode=\"interactive\">blibli</x-ocaml></div></div>"}}
8686+ {"id":[{"kind":"Root","name":"Main"}],"doc":"a paragraph\nand another\nverbatim\nx + 1\nblibli","kind":{"kind":"Doc"},"display":{"url":"page/Main/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"entry-name\">Main</span></code><div class=\"entry-comment\"><div><p>a paragraph</p><p>and another</p><pre>verbatim</pre><p><code class=\"odoc-katex-math\">x + 1</code></p><div><pre class=\"language-ocaml\"><code>blibli</code></pre></div></div></div>"}}
8787 {"id":[{"kind":"Root","name":"Main"}],"doc":"this is a title\nand this is a paragraph","kind":{"kind":"Doc"},"display":{"url":"page/Main/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"entry-name\">Main</span></code><div class=\"entry-comment\"><div><p>this is a title</p><p>and this is a paragraph</p></div></div>"}}
8888- {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"I"}],"doc":"a paragraph\nand another\nverbatim\nx + 1\nblibli","kind":{"kind":"Doc"},"display":{"url":"page/Main/I/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">I</span></code><div class=\"entry-comment\"><div><p>a paragraph</p><p>and another</p><pre>verbatim</pre><p><code class=\"odoc-katex-math\">x + 1</code></p><x-ocaml mode=\"interactive\">blibli</x-ocaml></div></div>"}}
8888+ {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"I"}],"doc":"a paragraph\nand another\nverbatim\nx + 1\nblibli","kind":{"kind":"Doc"},"display":{"url":"page/Main/I/index.html","html":"<code class=\"entry-kind\">doc</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">I</span></code><div class=\"entry-comment\"><div><p>a paragraph</p><p>and another</p><pre>verbatim</pre><p><code class=\"odoc-katex-math\">x + 1</code></p><div><pre class=\"language-ocaml\"><code>blibli</code></pre></div></div></div>"}}
8989 {"id":[{"kind":"Root","name":"J"}],"doc":"a paragraph one","kind":{"kind":"Module"},"display":{"url":"page/J/index.html","html":"<code class=\"entry-kind\">mod</code><code class=\"entry-title\"><span class=\"entry-name\">J</span></code><div class=\"entry-comment\"><div><p>a paragraph one</p></div></div>"}}
9090 {"id":[{"kind":"Root","name":"Main"}],"doc":"","kind":{"kind":"Module"},"display":{"url":"page/Main/index.html","html":"<code class=\"entry-kind\">mod</code><code class=\"entry-title\"><span class=\"entry-name\">Main</span></code><div class=\"entry-comment\"><div></div></div>"}}
9191 {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"I"}],"doc":"","kind":{"kind":"Module"},"display":{"url":"page/Main/index.html#module-I","html":"<code class=\"entry-kind\">mod</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">I</span></code><div class=\"entry-comment\"><div></div></div>"}}
9292 {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"M"}],"doc":"","kind":{"kind":"Module"},"display":{"url":"page/Main/index.html#module-M","html":"<code class=\"entry-kind\">mod</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">M</span></code><div class=\"entry-comment\"><div></div></div>"}}
9393 {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"X"}],"doc":"","kind":{"kind":"Module"},"display":{"url":"page/Main/index.html#module-X","html":"<code class=\"entry-kind\">mod</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">X</span></code><div class=\"entry-comment\"><div></div></div>"}}
9494- {"id":[{"kind":"Page","name":"page"}],"doc":"A title\nA paragraph\nsome verbatim\nand code\na list of things bliblib","kind":{"kind":"Page"},"display":{"url":"page/index.html","html":"<code class=\"entry-kind\">page</code><code class=\"entry-title\"><span class=\"entry-name\">page</span></code><div class=\"entry-comment\"><div><p>A title</p><p>A paragraph</p><pre>some verbatim</pre><x-ocaml mode=\"interactive\">and code</x-ocaml><ul><li>a list <em>of</em> things</li><li>bliblib</li></ul></div></div>"}}
9494+ {"id":[{"kind":"Page","name":"page"}],"doc":"A title\nA paragraph\nsome verbatim\nand code\na list of things bliblib","kind":{"kind":"Page"},"display":{"url":"page/index.html","html":"<code class=\"entry-kind\">page</code><code class=\"entry-title\"><span class=\"entry-name\">page</span></code><div class=\"entry-comment\"><div><p>A title</p><p>A paragraph</p><pre>some verbatim</pre><div><pre class=\"language-ocaml\"><code>and code</code></pre></div><ul><li>a list <em>of</em> things</li><li>bliblib</li></ul></div></div>"}}
9595 {"id":[{"kind":"Root","name":"Main"},{"kind":"Type","name":"t"}],"doc":"A comment","kind":{"kind":"TypeDecl","private":false,"manifest":"int","constraints":[]},"display":{"url":"page/Main/index.html#type-t","html":"<code class=\"entry-kind\">type</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">t</span><code class=\"entry-rhs\"> = int</code></code><div class=\"entry-comment\"><div><p>A comment</p></div></div>"}}
9696 {"id":[{"kind":"Root","name":"Main"},{"kind":"Type","name":"tdzdz"}],"doc":"A comment aaaaaaaaaa","kind":{"kind":"TypeDecl","private":false,"manifest":null,"constraints":[]},"display":{"url":"page/Main/index.html#type-tdzdz","html":"<code class=\"entry-kind\">type</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">tdzdz</span><code class=\"entry-rhs\"> = A of int * int | B of int list * int</code></code><div class=\"entry-comment\"><div><p>A comment aaaaaaaaaa</p></div></div>"}}
9797 {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"M"},{"kind":"Type","name":"t"}],"doc":"dsdsd","kind":{"kind":"TypeDecl","private":false,"manifest":null,"constraints":[]},"display":{"url":"page/Main/M/index.html#type-t","html":"<code class=\"entry-kind\">type</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.M.</span><span class=\"entry-name\">t</span></code><div class=\"entry-comment\"><div><p>dsdsd</p></div></div>"}}
···101101 {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"lorem4"}],"doc":"lorem 4","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-lorem4","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">lorem4</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div><p>lorem 4</p></div></div>"}}
102102 {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"lorem"}],"doc":"lorem 1 and a link","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-lorem","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">lorem</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div><p>lorem 1 and a <span>link</span></p></div></div>"}}
103103 {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"uu"}],"doc":"","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-uu","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">uu</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div></div></div>"}}
104104- {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"v"}],"doc":"a reference , and some formatted content with code and\n code blocks","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-v","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">v</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div><p>a reference <span><code>t</code></span>, and some <em>formatted</em> <b>content</b> with <code>code</code> and</p><x-ocaml mode=\"interactive\"> code blocks</x-ocaml></div></div>"}}
104104+ {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"v"}],"doc":"a reference , and some formatted content with code and\n code blocks","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-v","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">v</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div><p>a reference <span><code>t</code></span>, and some <em>formatted</em> <b>content</b> with <code>code</code> and</p><div><pre class=\"language-ocaml\"><code> code blocks</code></pre></div></div></div>"}}
105105 {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"x"}],"doc":"","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-x","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">x</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div></div></div>"}}
106106 {"id":[{"kind":"Root","name":"Main"},{"kind":"Value","name":"y"}],"doc":"","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/index.html#val-y","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.</span><span class=\"entry-name\">y</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div></div></div>"}}
107107 {"id":[{"kind":"Root","name":"Main"},{"kind":"Module","name":"I"},{"kind":"Value","name":"x"}],"doc":"","kind":{"kind":"Value","type":"int"},"display":{"url":"page/Main/I/index.html#val-x","html":"<code class=\"entry-kind\">val</code><code class=\"entry-title\"><span class=\"prefix-name\">Main.I.</span><span class=\"entry-name\">x</span><code class=\"entry-rhs\"> : int</code></code><div class=\"entry-comment\"><div></div></div>"}}
+5-16
odoc/test/xref2/shadow3.t/run.t
···1717 (sig :
1818 module type {B}1/shadowed/(CCCC) = A.B
1919 include {B}1/shadowed/(CCCC)
2020- (sig : module {A}1/shadowed/(AAAA) = A.A end)
2121- include sig
2222- module A :
2323- sig
2424- include module type of struct include {A}1/shadowed/(AAAA) end
2525- (sig : type t end)
2626- type a
2727- endend (sig : module {A}2/shadowed/(CCCC) = A.A end)
2020+ (sig : module {A}1/shadowed/(AAAA) = A.{A}1/shadowed/(AAAA) end)
2121+ include sigend (sig : module {A}2/shadowed/(CCCC) = A.A end)
2822 end)
2923 include module type of struct include B end
3024 (sig :
3125 module type B = B.B
3232- include B (sig : module {A}1/shadowed/(BBBB) = B.A end)
3333- include sig
3434- module A :
3535- sig
3636- include module type of struct include {A}1/shadowed/(BBBB) end
3737- (sig : type t end)
3838- type b
3939- endend (sig : module {A}3/shadowed/(CCCC) = B.A end)
2626+ include B
2727+ (sig : module {A}1/shadowed/(BBBB) = B.{A}1/shadowed/(BBBB) end)
2828+ include sigend (sig : module {A}3/shadowed/(CCCC) = B.A end)
4029 end)
4130 module A :
4231 sig
···991010 $ cat a.mli
1111 (** Module A defines a signature with an inline include. *)
1212-1212+1313 module type S = sig
1414 type t
1515-1515+1616 include sig
1717 val x : t
1818 val y : t -> t
1919 end
2020 end
21212222+2323+2224 $ cat b.mli
2325 (** Module B includes A.S with a type substitution.
2424-2626+2527 The substitution flows through [fragmap] in tools.ml. When the inline
2628 include's decl has been stripped, [map_include_decl] needs the
2729 reconstructed decl to correctly wrap the substitution. Without
2830 reconstruction, the empty decl produces [With(subst, empty_sig)]
2931 which loses the vals from the include. *)
3030-3232+3133 module M : A.S with type t := int
3434+3535+32363337Compile and link:
3438···47514852 $ odoc html-generate b.odocl -o html --indent
4953 $ grep 'id="val-[xy]"' html/test/B/M/index.html
5050- <div class="spec value anchored" id="val-x">
5151- <div class="spec value anchored" id="val-y">
5454+ <div class="spec value anchored" id="val-x">
5555+ <div class="spec value anchored" id="val-y">
···11-{0 Review of the last few months}
22-33-It's time to take a step back and write a retrospective on the last few months of
44-vibecoding with Claude.
11+{0 A Review of some LLM-assisted OCaml Development}
5233+A few months ago I set out to build a literate programming website with
44+Claude's help. I ended up touching dune, odoc, js_top_worker, and
55+building several new libraries from scratch. This post reviews what I built; the {{!./page-retrospective}retrospective} is where I reflect on the experience.
66+67{image!./vibecoding.png}
7888-{1 What's been done}
99+{1 Initial goals}
9101010-Firstly let's see the various projects I've worked on and see what's been
1111-accomplished.
1111+I set out with dual goals: Firstly to evaluate the new programming
1212+paradigm of developing with LLMs, and secondly to set up a literate
1313+programming environment as an integral part of my website. Those of
1414+you that have read this blog for a while will know I've been tinkering
1515+with notebooks for a year or so now, but I really wanted to push this
1616+hard and see how adding Claude into the mix would work out. Having a
1717+concrete destination in mind helped keep me focussed on ensuring that
1818+when I finished I'd have something useful.
1919+2020+It's important to me personally that the work I was doing would
2121+eventually be generally useful to others as well, so I wanted to make
2222+sure that as well as just making myself a neat looking website, the
2323+technology I was building made sense in the broader context of the
2424+OCaml community that has supported me so well for so long. It's also
2525+vital to spend some time thinking about how to remain a good
2626+open-source citizen, as there's a real danger if we're not careful
2727+that LLMs could end up having a deeply damaging effect on some open
2828+source communities.
2929+3030+I made some design decisions up front that set the course I took. The
3131+first was that I would be using OxCaml both for the build environment
3232+and the runtime environment on the web. This would give me some really
3333+useful experience working in this variant of OCaml in both contexts.
3434+For the second decision I needed to have a notebook target in mind.
3535+This was easy to make: My group has been spending a lot of time
3636+recently on {{:https://geotessera.org/}TESSERA}, and this provided a
3737+really interesting use-case for the notebooks, as it heavily relies
3838+upon being able to use interactive maps to choose areas of interest,
3939+and to visualise the embeddings and picture the output as map
4040+overlays. This meant supporting various widgets in the browser-based
4141+programming environment, where I'd only previously been able to render
4242+static rich content.
4343+4444+I ended up building out quite a lot of experimental tooling of varying
4545+quality that has culminated in this website, which is using
4646+more-or-less everything I've been building. So let's review piece by
4747+piece the parts that I worked on.
12481349{2 Dune}
14501515-Last year I wrote up my experiences writing the {{!//blog/2025/12/page-"claude-and-dune"}dune rules for odoc} with Claude.
1616-The {{:https://github.com/ocaml/dune/pull/12995}pull request has been made}, representing a "feature complete" replacement for the current rules,
1717-in that it can completely replace what's in dune now, but doesn't extend the rules for the new features of
1818-odoc. Since then we've merged the first part of it -- though that's the bit that
1919-wasn't written by Claude but by my colleague {{:https://choum.net/panglesd/}Paul-Elliot}. Fortunately after a brief
2020-sabbatical working on {{:https://docs.slipshow.org/en/stable/}slipshow}, he's now back working with us and we'll be meeting soon to discuss the next steps to get the rest of it merged.
5151+Last year I wrote up my experiences writing the
5252+{{!//blog/2025/12/page-"claude-and-dune"}dune rules for odoc} with
5353+Claude. The {{:https://github.com/ocaml/dune/pull/12995}pull request
5454+has been made}, representing a "feature complete" replacement for the
5555+current rules, in that it can completely replace what's in dune now,
5656+but doesn't extend the rules for the new features of odoc. Since then
5757+we've merged the first part of it -- though that's the bit that wasn't
5858+written by Claude but by my colleague
5959+{{:https://choum.net/panglesd/}Paul-Elliot}. Fortunately after a brief
6060+sabbatical working on
6161+{{:https://docs.slipshow.org/en/stable/}slipshow}, he's now back
6262+working with us and we'll be meeting soon to discuss the next steps to
6363+get the rest of it merged.
6464+6565+To support my goals though, I've had to extend the rules by quite a
6666+lot. We've got
6767+{{:https://github.com/ocaml/dune/commit/0aa0170938b92342a609476abab67b960c356bd5}support
6868+for assets} so I can put images on my blog posts, we've got support
6969+for
7070+{{:https://github.com/ocaml/dune/commit/91b08307c118193fc46a707891c28721c1778916}source
7171+rendering} so that you can see the code I'm using. There's
7272+{{:https://github.com/ocaml/dune/commit/4cb2b33e98634eba8b9a267a6dd9bcfc959575a4}markdown
7373+output} for Claude to consume, and
7474+{{:https://github.com/ocaml/dune/commit/76f61319a21ba8b03feeb0c15e2a567ca2114489}sherlodoc
7575+native support} so you (or your agent) can run sherlodoc queries on
7676+the command line. The
7777+{{:https://github.com/ocaml/dune/commit/91b08307c118193fc46a707891c28721c1778916}prefix}
7878+for all output is configurable, so that I was able to put everything
7979+dune built under "/reference", and pass
8080+{{:https://github.com/ocaml/dune/commit/48a5cbb79b24606304dc25f965c22aa5e4cb898e}arbitrary
8181+options} to the various invocations of odoc so that I could specify
8282+global defaults like the Javascript toplevel worker and opam
8383+repository to use in my notebooks. We've also now got
8484+{{:https://github.com/ocaml/dune/commit/a49e98a88d7358074afffedfb0f6ee922208cdb7}smarter
8585+rules} that don't pull in as many dependencies as the current PR, so
8686+that I didn't have to install a bunch of extra packages I wasn't using
8787+just to build my own docs. All of this is being used as part of the
8888+process of generating this site, so it all works quite well together
8989+but it hasn't been extensively tested.
21902222-In the mean time though, I've extended the rules by quite a lot. We've now got {{:https://github.com/ocaml/dune/commit/a49e98a88d7358074afffedfb0f6ee922208cdb7}smarter rules} that don't
2323-pull in as many dependencies as the current PR, we've got {{:https://github.com/ocaml/dune/commit/0aa0170938b92342a609476abab67b960c356bd5}support for assets}, we've got support for {{:https://github.com/ocaml/dune/commit/91b08307c118193fc46a707891c28721c1778916}source
2424-rendering}, there's {{:https://github.com/ocaml/dune/commit/4cb2b33e98634eba8b9a267a6dd9bcfc959575a4}markdown output} and {{:https://github.com/ocaml/dune/commit/76f61319a21ba8b03feeb0c15e2a567ca2114489}sherlodoc native support} so you can run sherlodoc queries on the
2525-command line. You can configure the {{:https://github.com/ocaml/dune/commit/91b08307c118193fc46a707891c28721c1778916}prefix} for your doc output, and pass {{:https://github.com/ocaml/dune/commit/48a5cbb79b24606304dc25f965c22aa5e4cb898e}arbitrary options} to the
2626-various invocations of odoc. All of this is being used as part of the process of generating this site,
2727-but that's about all the testing its had. This will all have to be carefully reviewed then either
2828-tacked onto the end of the current PR or we'll make new PRs for these. Very likely the latter, as the
2929-current PR is hard enough to review as it is.
9191+These changes will all have to be carefully reviewed then either
9292+tacked onto the end of the current PR or we'll make new PRs for
9393+these. Very likely the latter, as the current PR is hard enough to
9494+review as it is.
30953196{2 Odoc}
32973333-OxCaml support for odoc was contributed by Luke Maurer early on after OxCaml was released.
3434-However, this only fixed the build of odoc, it didn't give it any mode or layouts, nor
3535-any of the other new features of OxCaml. I asked Claude to look through the way the
3636-toplevel prints these annotations and port them to odoc, and that's been implemented on
3737-this site. For example, see {!Base.Uniform_array.val-length}.
9898+OxCaml support for odoc was contributed by Luke Maurer early on after
9999+OxCaml was released. However, this only fixed the build of odoc, it
100100+didn't give it any mode or layouts, nor any of the other new features
101101+of OxCaml. I asked Claude to look through the way the toplevel prints
102102+these annotations and port them to odoc, and that's been implemented
103103+on this site. For example, see {!Base.Uniform_array.val-length} - you
104104+can see there the [portable], [local] and [contended] annotations on
105105+the type. If you click on the [source] link, you'll see I also added
106106+some improvements to the source rendering - there are many more links
107107+now and we've got the ability to link to source from doc comments and
108108+mlds.
381093939-One of the earlier things I did was to give Odoc a nice new plugin system that
4040-has been hugely enabling for building the new features below. I'm using dune's {{:https://dune.readthedocs.io/en/stable/sites.html#sites}site} feature for
4141-the plugins, which really "just worked". It was very easy to add the feature and creating the plugins
4242-has been equally easy. Building a whole variety of plugins has also been very useful in testing the
4343-shape of the plugin API, and I've made numerous changes to it as I've built various plugins and they expose
4444-problems. To use the plugins, you need a way to write text that the plugins will operate on, and
4545-there are a couple of obvious ways to do this. The first is to allow {{:https://ocaml.org/manual/5.4/ocamldoc.html#sss:ocamldoc-custom-tags}custom tags}, a feature of ocamldoc
4646-that odoc didn't support, and the second is to annotate source-code blocks with metadata that the
4747-plugins can recognise. I also added some improvements to the source rendering - there are many more links now
4848-and we've got the ability to link to source from doc comments.
110110+One of the earliest things I did was to give Odoc a nice new plugin
111111+system that has been hugely enabling for building the new
112112+features. I'm using dune's
113113+{{:https://dune.readthedocs.io/en/stable/sites.html#sites}site}
114114+feature for the plugins, which really "just worked". It was very easy
115115+both to add the feature to odoc and to create the plugins
116116+themselves. Building a whole variety of plugins has also been very
117117+useful in testing the shape of the plugin API, and I've made numerous
118118+changes to it as I've built them and they expose various problems. Of
119119+course, to use the plugins, you need a way to write text that the
120120+plugins will operate on, and there are a couple of obvious ways to do
121121+this. The first is to allow
122122+{{:https://ocaml.org/manual/5.4/ocamldoc.html#sss:ocamldoc-custom-tags}custom
123123+tags}, a feature of ocamldoc that odoc didn't previously support, and
124124+the second is to annotate source-code blocks with metadata that the
125125+plugins can recognise.
4912650127Let's take a brief look through the plugins I've made.
5112852129{3 Admonitions}
531305454-This is a feature we've wanted to add to odoc for a while - and we have a {{:https://hackmd.io/ETSOAmetTI-E3vrDk3Bfrw}design sketched out} for it.
131131+This is a feature we've wanted to add to odoc for a while - and we
132132+have a {{:https://hackmd.io/ETSOAmetTI-E3vrDk3Bfrw}design sketched
133133+out} for it.
5513456135@admonition.note This is a 'note' admonition.
571365858-This is more-or-less a throwaway plugin as we'll be doing this "properly" and won't need it. It made for
5959-a nice first test though.
137137+This is more-or-less a throwaway plugin as we'll be doing this
138138+"properly" and won't need it. It made for a nice first test
139139+though and the functionality is useful and quite important.
140140+I've been using it to mark the truly agent-coded libraries where I've
141141+not seen the code at all, as opposed to those where I've been far more
142142+involved in the changes and I'm slightly more confident in how they
143143+work.
6014461145{3 Diagrams}
621466363-I've got 3 diagramming plugins - {{!/odoc-mermaid-extension/page-index}odoc-mermaid-extension}, {{!/odoc-msc-extension/page-index}odoc-msc-extension}
6464-and {{!/odoc-dot-extension/page-index}odoc-dot-extension}.
147147+I've got 3 diagramming plugins -
148148+{{!/odoc-mermaid-extension/page-index}odoc-mermaid-extension},
149149+{{!/odoc-msc-extension/page-index}odoc-msc-extension} and
150150+{{!/odoc-dot-extension/page-index}odoc-dot-extension}. These were
151151+particularly useful in understanding the need to determine the
152152+lifecycle of any javascript glue that's required, especially when
153153+we're dynamically loading pages like ocaml.org does. And in fact
154154+I've put the Mermaid extension to use documenting the issues in
155155+the {{!/odoc/page-extensions}extensions documentation}.
651566666-{3 Interative pages (notebooks)}
157157+{3 Interactive pages (notebooks)}
158158+159159+This was a big one for me. My previous efforts at creating notebooks
160160+involved a separate pipeline to build them, but with this it became
161161+trivial to have them built as part of the normal 'dune build' process.
671626868-The {{!/odoc-interactive-extension/page-index}odoc-interactive-extension} uses {{:https://github.com/art-w}art-w}'s {{:https://github.com/art-w/x-ocaml}x-ocaml} to add interactivity to the mld files.
6969-Rather than using a fixed "execution engine" though, my fork is using {{:https://github.com/jonludlam/js_top_worker}js_top_worker} so we can
163163+The
164164+{{!/odoc-interactive-extension/page-index}odoc-interactive-extension}
165165+uses {{:https://github.com/art-w}art-w}'s
166166+{{:https://github.com/art-w/x-ocaml}x-ocaml} to add interactivity to
167167+the mld files. Rather than using a fixed "execution engine" though,
168168+my fork is using
169169+{{:https://github.com/jonludlam/js_top_worker}js_top_worker} so we can
70170use different OCaml/OxCaml versions and load libraries as '#required'.
171171+This is intended to work cross-site too, so anyone can use this
172172+extension and my hosted Javascript opam repository.
7117372174{3 Scrollycode}
731757474-The {{!/odoc-scrollycode-extension/page-index}odoc-scrollycode-extension} is based on {{:https://pomb.us}Rodrigo Pombo}'s work. You just add in
7575-a few custom tags and some {{:https://tangled.org/jon.recoil.org/odoc-scrollycode-extension/blob/main/doc/notebook_testing.mld#L3}special markup}
7676-in the source and you get lovely animated tutorials.
176176+The
177177+{{!/odoc-scrollycode-extension/page-index}odoc-scrollycode-extension}
178178+is based on {{:https://pomb.us}Rodrigo Pombo}'s work. You just add in
179179+a few custom tags and some
180180+{{:https://tangled.org/jon.recoil.org/odoc-scrollycode-extension/blob/main/doc/notebook_testing.mld#L3}special
181181+markup} in the source and you get lovely animated tutorials.
7718278183{3 HTML shells}
791848080-The "traditional" way that you embed odoc output into another webpage, like ocaml.org does, is to output
8181-JSON. This is a bit annoying though, as it means once dune has finished, you need something else to come
8282-in, pick up all the json files and write out your new site. The HTML shells extension is part of the
8383-odoc plugin system, and allows you to swap out the HTML renderer for another one. I have two plugins
8484-that use this system:
185185+The "traditional" way that you embed odoc output into another webpage,
186186+like ocaml.org does, is to output JSON. This is a bit annoying though,
187187+as it means once dune has finished, you need something else to come
188188+in, pick up all the json files and write out your new site. The HTML
189189+shells extension is part of the odoc plugin system, and allows you to
190190+swap out the HTML renderer for another one. I have two plugins that
191191+use this system:
851928686-- {{!/odoc-docsite/page-index}odoc-docsite} which produces a more modern SPA-style site
193193+- {{!/odoc-docsite/page-index}odoc-docsite} which produces a more
194194+ modern SPA-style site
87195- {{!/odoc-jons-plugins/page-index}jons-shell} which produces this website
88196197197+The advantage of using these is that it doesn't affect the flow of
198198+the documentation pipeline, so there are no changes required to the
199199+dune rules for it to produce a very different output.
200200+89201{2 Js_top_worker}
902029191-I fixed various things in js_top_worker. We've got [#require] working so that you can
9292-load libraries from the code blocks, it's better at figuring out which libraries to
9393-load and which are already present in the worker.
203203+I fixed various things in js_top_worker. We've got [#require] working
204204+so that you can load libraries from the code blocks, and this meant I
205205+had to improve how it figures out which libraries to load and which
206206+are already present in the worker.
942079595-One important change to js_top_worker is the ability to use interactive widgets. This
9696-requires coordination between the main javascript thread, or frontend, and the web-worker
9797-backend which is actually running the OCaml code. I've been using {{!/note/page-index}Note}
9898-as the FRP library to make this nice to work with. A nice demo of this is the
208208+One important change to js_top_worker is the ability to use
209209+interactive widgets. This requires coordination between the main
210210+javascript thread, or frontend, and the web-worker backend which is
211211+actually running the OCaml code. I've been using
212212+{{!/note/page-index}Note} as the FRP library to make this nice to work
213213+with. A nice demo of this is the
99214{{!/js_top_worker-widget-leaflet/page-index}Leaflet widget}.
100215101101-{2 day10}
102102-103103-Day10 is Mark's {{:https://github.com/mtelvers/day10}build tool} that allows fast building
104104-of opam packages in a similar way to
105105-{{:https://github.com/ocurrent/ocaml-docs-ci}ocaml-docs-ci}. I've had dune insert the rest
106106-of the docs-CI logic into it so that I can now use day10 to build docs for OCaml and for
107107-OxCaml.
216216+Another extension I made to this was inspired by my experiences
217217+running the {{:https://www.cl.cam.ac.uk/teaching/2526/OCaml/}1A Computer Science OCaml practicals}, which are assessed
218218+via {{:https://github.com/akabe/ocaml-jupyter}Jupyter notebooks} running {{:https://github.com/jupyter/nbgrader}Nbgrader}. I wanted to have
219219+exercises in the notebooks with associated tests, and I wanted the
220220+tests to be automatically run.
108221109222{2 TESSERA notebooks}
110110-One of the most exciting things to come out of our group recently has been {{:https://geotessera.org/}TESSERA},
111111-a pixel-wise Earth observation foundation model. The code and demos for this are mostly
112112-in Python, particularly using Jupyter notebooks. OCaml notebooks have obviously long been
113113-an interest of mine, so I ported the {{:https://github.com/ucam-eo/tessera-interactive-map}simple notebook} to OCaml, and more specifically
114114-to {{!/site/notebooks/page-"interactive_map"}run in the browser}. This was pretty straightforward (aside from needing a coordinate
115115-transform), but very slow to download the hundreds of megs of tiles required. So I then
116116-switched to using the zarr format to stream just the areas of interest, and this
117117-was much faster.
118223119119-{1 Retrospective}
224224+One of the most exciting things to come out of our group recently has
225225+been {{:https://geotessera.org/}TESSERA}, a pixel-wise Earth
226226+observation foundation model. The code and demos for this are mostly
227227+in Python, particularly using Jupyter notebooks. This provided me with
228228+an excellent stress test that tied together many of the strands of
229229+this work and validated that they could be used together to build
230230+something useful. OCaml notebooks have
231231+obviously long been an interest of mine, so I ported the
232232+{{:https://github.com/ucam-eo/tessera-interactive-map}simple notebook}
233233+to OCaml, and more specifically to
234234+{{!/site/notebooks/page-"interactive_map"}run in the browser}. This
235235+really demonstrated that the approaches I've made with the various
236236+different strands of work can all be knitted together in a very
237237+useful way, allowing us to bring the strong type system of OCaml
238238+together with the ubiquitous runtime of the browser, and using
239239+the power of WebGPU to run calculations that can help to change our
240240+world for the better.
120241121121-There's quite a variety of different projects that I've made progress on, and its given
122122-me a lot of experience working with LLMs for code generation. It's been an eye-opening
123123-experience and it's clear that the way we code is fundamentally changed. With a good
124124-few projects under my belt now it's time to take a deep breath and assess exactly how
125125-the landscape has changed.
126126-127127-{2 Attribution}
128128-129129-The first thing is the commits. My early purely-agentic commits were all authored by me
130130-and co-authored by Claude. This is a lie. I couldn't carry on like this so I've switched
131131-now to having the commits authored by "Jon's Agent". My plan is to rewrite the author
132132-when I've gone through it line by line, and even then, I feel that Claude ought to be marked
133133-as Author and I should just be adding my "Reviewed-by" line onto it. In either case, for
134134-a pull request to be made the human in the loop has the responsibility to justify the
135135-changes, and therefore has to be totally familiar with both the changes and the code
136136-being changed. I'm not at all fundamentally opposed to having LLMs involved in the
137137-process of making changes to open source code, but in my experience so far, there's a
138138-huge amount of effort that needs to go into it even after you've got working code and
139139-tests. I've tested the waters with a {{:https://github.com/ocaml/odoc/pull/1402}simple
140140-bugfix or two}, and even these one-lines needed careful thought and attention before
141141-I felt I could make a PR.
142142-143143-{2 Bug-discovery}
144144-145145-One thing that I've found tremendously useful is narrowing down bugs. Armed with a repro,
146146-setting Claude off to track down issues has been a wonderful time saver. Additionally,
147147-asking it to explain the issue in detail with links to the source is very handy indeed.
148148-It's not, however, able to discover {i all} bugs, even with a lot of time. When working
149149-on the fix for a {{:https://github.com/ocaml/odoc/pull/1400}particularly nasty bug}, I
150150-found that with the patch applied we'd get a different error somewhere deep in some of Jane Street's async
151151-ecosystem. I had a good suspicion of what the problem was give the changes that had been
152152-made already, as the code I had altered had an analogue elsewhere in the codebase that
153153-hadn't been fixed, so I thought this would be quite a good test for Claude. I gave it
154154-lots of hints, but it flailed at the problem for several hours, often giving up,
155155-sometimes blaming the OxCaml compiler, and sometimes upstream OCaml. In the end I gave
156156-up, implemented what I thought would be the fix, and indeed the problem went away.
157157-158158-{2 API boundaries}
159159-160160-I've found it very helpful to have API boundaries to help structure the code that
161161-Claude has been producing. Anil has long been enthusiastically pushing the idea that
162162-we should write the [mli] files first, which constrain what types and values are
163163-available between the modules. We can then write tests that target these interfaces,
164164-and then adjust them where the tests have shown them to be inelegant or downright
165165-unusable. We can then write the implementations and watch the tests start to pass.
166166-A particularly interesting example of this is the odoc plugin interface. The
167167-experience of writing several very different plugins that all extended odoc in
168168-different directions
169169-was very helpful, and I adjusted the interface quite a few times. I also
170170-adjusted the {i documentation}, where qualities about how the interface {i behaved}
171171-that weren't obvious from the types could be carefully noted, for example how
172172-scripts might be made to behave correctly when the odoc pages were in an SPA shell.
173173-174174-{2 Failure modes}
175175-176176-When I was working on the {{!//blog/2025/12/page-"claude-and-dune"}dune rules}, I made
177177-the mistake of going too long without giving Claude some architectural constraints, and
178178-I ended up with a Big Ball of Code that I then had to spend time unpicking and teasing
179179-apart into sensible looking modules. I had rather hopefully believed this to be an
180180-Opus < 4.5 problem, but when adding the new features to day10 I mentioned above I hit
181181-a very similar situation, when it just added vast amounts of code to the CLI to
182182-implement the new features, and it was all very unstructured and unsatisfactory. This
183183-was despite going through a design process where we went through the goals, the use
184184-cases and desired features, but crucially, not at the level of the code.
185185-186186-Another failure mode I observed was {i my} failure. It's very easy, and very tempting,
187187-to get your agent to do the next neat thing on the roadmap. Especially when you've
188188-just spent a while going through the design and planning for the previous feature and
189189-Claude has got started on it. The problem is that this can generate a large amount of
190190-code that kind-of-works but has a bunch of bugs, which can end up costing a lot more
191191-time and effort. The cost of starting the agent going
192192-is much smaller than the cost of wading through the results, and it's quite easy to
193193-end up drowning under a load of very interesting and partly cool half results. I'm very
194194-much reminded of Dr Ian Malcolm's words from Jurrasic Park: "your scientists were so
195195-preoccupied with whether or not they could, they didn't stop to think if they should."
196196-197197-{1 What's next}
198198-199199-The problem with everything I've done is that, as of right now, it's not usable,
200200-at least by anyone but me.
201201-While it's technically possible to add my opam-repo to your switch, and install my
202202-versions of odoc, dune, and my various plugins, nobody is actually going to do that.
203203-Worse than that, people might get their agents to just grab the source and mutate it
204204-further, just diluting the efforts going into it.
205205-206206-Fortunately I've been talking with {{:https://choum.net/panglesd/}Paul Elliot}, who
207207-has volunteered to shepherd the dune PR through to completion. I'll be working with
208208-him on this of course, but I'm hoping he'll be doing the lion's share of the work.
209209-210210-The OxCaml work will be taken on by {{:https://github.com/art-w}art-w} who's already
211211-done an excellent job getting Luke Maurer's patches into shape and PR'd to ocaml/odoc.
212212-213213-I think the odoc plugin experience was very educational, and I think the next step there
214214-is to carefully consider how this ought to be used, how it will interact with the
215215-dune rules, how it would affect documentation on ocaml.org. The experience of the
216216-last few weeks will be really important in framing the discussion to be had there.
217217-218218-219219-220220-242242+I'm quite happy with the results of this journey so far. My goals of
243243+an improved interactive website have been achieved, and the TESSERA
244244+notebook is demonstrating how to fit all of the parts together.
245245+Head on over now to the {{!./page-retrospective}retrospective} for
246246+some further thoughts on all of this work.
221247222248223249
···132132 let* (mat_full, h_full, w_full, geo_bounds) =
133133 Tessera_zarr.fetch_region ~progress ~year ~store bbox in
134134 progress (Printf.sprintf "Fetched %d×%d. Downsampling..." h_full w_full);
135135- let (mat, h, w) = downsample mat_full ~h:h_full ~w:w_full ~max_pixels:200_000 in
135135+ let (mat, h, w) = downsample mat_full ~h:h_full ~w:w_full ~max_pixels:1_000_000 in
136136 let bounds = Leaflet_map.{
137137 south = geo_bounds.Geotessera.min_lat;
138138 north = geo_bounds.Geotessera.max_lat;
+2
tessera-geotessera-jsoo/doc/index.mld
···11{0 GeoTessera Browser Backend}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Browser fetch backend for tessera-geotessera. Uses synchronous
46XMLHttpRequest to retrieve GeoTessera tiles, suitable for use in web
57workers where blocking I/O is acceptable.
+2
tessera-geotessera/doc/index.mld
···11{0 GeoTessera Tile Client}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Fetch, dequantize, and mosaic
46{{:https://github.com/instadeepai/geotessera}GeoTessera} embedding tiles.
57
+2
tessera-linalg/doc/index.mld
···11{0 PCA and kNN for Bigarrays}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35PCA dimensionality reduction and k-nearest-neighbours classification
46for float32 Bigarray data. Pure OCaml with no external dependencies.
57
+2
tessera-npy/doc/index.mld
···11{0 NumPy File Reader}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Read and write NumPy [.npy] files in OCaml. Supports int8, uint8,
46float32, and float64 dtypes with arbitrary shapes, using Bigarray for
57zero-copy data representation. Handles both npy format versions 1.0
+4
tessera-tfjs/lib/tfjs.mli
···11(** TensorFlow.js PCA via SVD.
2233+ {b Warning:} This library was vibe-coded with AI assistance and has not
44+ been thoroughly reviewed or tested. Use at your own risk and expect
55+ breaking changes.
66+37 Requires TensorFlow.js to be loaded in the JavaScript environment
48 (e.g., via [importScripts] in a web worker). *)
59
+2
tessera-viz-jsoo/doc/index.mld
···11{0 Embedding Visualization for the Browser}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Browser display for tessera-viz images. Encodes RGBA pixel arrays
46as [data:image/png;base64] URLs for rendering in OCaml notebook cells.
+2
tessera-viz/doc/index.mld
···11{0 Embedding Visualization}
2233+@admonition.warning This library was vibe-coded with AI assistance and has not been thoroughly reviewed or tested. Use at your own risk and expect breaking changes.
44+35Visualization utilities for GeoTessera embeddings. Converts PCA
46components and classification results to RGBA pixel arrays, with
57percentile-based contrast stretching and configurable colour palettes.
+7
tessera-zarr-jsoo/lib/tessera_zarr_jsoo.ml
···1010 | Some o, Some l ->
1111 xhr##setRequestHeader (Js.string "Range")
1212 (Js.string (Printf.sprintf "bytes=%d-%d" o (o + l - 1)))
1313+ | None, Some l ->
1414+ (* Suffix range: last l bytes *)
1515+ xhr##setRequestHeader (Js.string "Range")
1616+ (Js.string (Printf.sprintf "bytes=-%d" l))
1317 | _ -> ());
1418 let (p, resolver) = Lwt.wait () in
1519 xhr##.onload := Dom.handler (fun _ ->
···4145 | Some o, Some l ->
4246 xhr##setRequestHeader (Js.string "Range")
4347 (Js.string (Printf.sprintf "bytes=%d-%d" o (o + l - 1)))
4848+ | None, Some l ->
4949+ xhr##setRequestHeader (Js.string "Range")
5050+ (Js.string (Printf.sprintf "bytes=-%d" l))
4451 | _ -> ());
4552 xhr##send Js.null;
4653 match xhr##.status with
+4
tessera-zarr-jsoo/lib/tessera_zarr_jsoo.mli
···11(** Browser backend for tessera-zarr.
2233+ {b Warning:} This library was vibe-coded with AI assistance and has not
44+ been thoroughly reviewed or tested. Use at your own risk and expect
55+ breaking changes.
66+37 Provides async and sync HTTP fetch with byte-range support,
48 convenience wrappers for opening the GeoTessera Zarr store,
59 and an FRP bridge for reactive notebooks.
+4
tessera-zarr/lib/tessera_zarr.mli
···11(** GeoTessera Zarr v3 client.
2233+ {b Warning:} This library was vibe-coded with AI assistance and has not
44+ been thoroughly reviewed or tested. Use at your own risk and expect
55+ breaking changes.
66+37 Fetches GeoTessera embeddings from sharded Zarr v3 stores,
48 mapping WGS84 bounding boxes to UTM pixel ranges. Dequantizes
59 int8 embeddings using float32 scales and reprojects from the
+3
zarr-v3-unix/lib/zarr_v3_unix.ml
···22 let cmd = match off, len with
33 | Some o, Some l ->
44 Printf.sprintf "curl -sf -H 'Range: bytes=%d-%d' '%s'" o (o + l - 1) url
55+ | None, Some l ->
66+ (* Suffix range: last l bytes *)
77+ Printf.sprintf "curl -sf -H 'Range: bytes=-%d' '%s'" l url
58 | _ ->
69 Printf.sprintf "curl -sf '%s'" url
710 in
+123-66
zarr-v3/lib/store.ml
···172172 done;
173173 !idx
174174175175+(* Decompress an inner chunk and copy overlapping pixels to the output buffer *)
176176+let decode_inner codecs codec_names
177177+ data local_off nbytes chunk_pixel_start chunk_pixel_stop
178178+ ndim start shape elem_size inner_chunk_shape out_buf =
179179+ let compressed = String.sub data local_off nbytes in
180180+ let raw = apply_inner_codecs codecs codec_names compressed in
181181+ let stop = Array.init ndim (fun d -> start.(d) + shape.(d)) in
182182+ let copy_lo = Array.init ndim (fun d ->
183183+ max chunk_pixel_start.(d) start.(d)) in
184184+ let copy_hi = Array.init ndim (fun d ->
185185+ min chunk_pixel_stop.(d) stop.(d)) in
186186+ let idx = Array.make ndim 0 in
187187+ let rec copy dim =
188188+ if dim = ndim then begin
189189+ let src_off = ref 0 in
190190+ let src_stride = ref elem_size in
191191+ for d = ndim - 1 downto 0 do
192192+ src_off := !src_off +
193193+ (idx.(d) - chunk_pixel_start.(d)) * !src_stride;
194194+ src_stride := !src_stride * inner_chunk_shape.(d)
195195+ done;
196196+ let dst_off = ref 0 in
197197+ let dst_stride = ref elem_size in
198198+ for d = ndim - 1 downto 0 do
199199+ dst_off := !dst_off +
200200+ (idx.(d) - start.(d)) * !dst_stride;
201201+ dst_stride := !dst_stride * shape.(d)
202202+ done;
203203+ Bytes.blit_string raw !src_off out_buf !dst_off elem_size
204204+ end else begin
205205+ for i = copy_lo.(dim) to copy_hi.(dim) - 1 do
206206+ idx.(dim) <- i;
207207+ copy (dim + 1)
208208+ done
209209+ end
210210+ in
211211+ copy 0
212212+175213let read ?on_shard arr ~start ~shape =
176214 let open Lwt.Syntax in
177215 let meta = arr.meta in
···222260 arr.store.base_url arr.path shard_key in
223261224262 let task =
225225- (* Fetch the entire shard *)
226226- let* shard_data = arr.store.fetch shard_url () in
263263+ (* Phase 1: Fetch just the shard index via suffix byte-range
264264+ request (bytes=-N fetches last N bytes).
265265+ For non-sharded arrays, fetch the whole chunk. *)
266266+ let* index_data =
267267+ if meta.is_sharded then
268268+ arr.store.fetch shard_url ~len:index_size ()
269269+ else
270270+ arr.store.fetch shard_url ()
271271+ in
272272+273273+ (* The server may not support suffix ranges (returns full shard)
274274+ or may return slightly more data. Handle gracefully. *)
275275+ let shard_data_opt, index_data =
276276+ if String.length index_data > index_size then
277277+ (* Got the whole shard — use it directly for sub-chunk reads *)
278278+ let full = index_data in
279279+ let idx_off = String.length full - index_size in
280280+ (Some full, String.sub full idx_off index_size)
281281+ else
282282+ (None, index_data)
283283+ in
284284+227285 incr shards_done;
228286 (match on_shard with
229287 | Some f -> f !shards_done n_shards
230288 | None -> ());
231231- let shard_len = String.length shard_data in
232289233233- (* Read the shard index from the end *)
234234- let index_off = shard_len - index_size in
235235- let index_data = String.sub shard_data index_off index_size in
236236-237237- (* Iterate over inner chunks within this shard *)
238238- let rec iter_inner inner_idx dim =
290290+ (* Phase 2: Collect all overlapping inner chunks, then fetch
291291+ the byte range spanning all of them in a single request. *)
292292+ let needed_chunks = ref [] in
293293+ let rec collect_inner inner_idx dim =
239294 if dim = ndim then begin
240240- (* Pixel range this inner chunk covers *)
241295 let chunk_pixel_start = Array.init ndim (fun d ->
242296 shard_idx.(d) * chunk_shape.(d) +
243297 inner_idx.(d) * inner_chunk_shape.(d)) in
244298 let chunk_pixel_stop = Array.init ndim (fun d ->
245299 min (chunk_pixel_start.(d) + inner_chunk_shape.(d))
246300 meta.shape.(d)) in
247247-248248- (* Check overlap with requested region *)
249301 let overlaps = ref true in
250302 for d = 0 to ndim - 1 do
251303 if chunk_pixel_start.(d) >= stop.(d) ||
252304 chunk_pixel_stop.(d) <= start.(d) then
253305 overlaps := false
254306 done;
255255-256307 if !overlaps then begin
257257- (* Look up this chunk in the shard index *)
258308 let lin = linearize_inner_idx inner_idx inner_per_shard ndim in
259309 let offset = get_u64_le index_data (lin * index_entry_size) in
260310 let nbytes = get_u64_le index_data (lin * index_entry_size + 8) in
261261-262262- (* 0xFFFFFFFFFFFFFFFF means empty chunk *)
263263- if offset < max_int && nbytes > 0 then begin
264264- let compressed = String.sub shard_data offset nbytes in
265265- let raw = apply_inner_codecs arr.store.codecs
266266- meta.inner_codecs compressed in
267267-268268- (* Copy overlapping pixels into output buffer *)
269269- let copy_lo = Array.init ndim (fun d ->
270270- max chunk_pixel_start.(d) start.(d)) in
271271- let copy_hi = Array.init ndim (fun d ->
272272- min chunk_pixel_stop.(d) stop.(d)) in
273273-274274- (* Iterate over overlapping pixels *)
275275- let idx = Array.make ndim 0 in
276276- let rec copy dim =
277277- if dim = ndim then begin
278278- (* Source offset within the decoded inner chunk *)
279279- let src_off = ref 0 in
280280- let src_stride = ref elem_size in
281281- for d = ndim - 1 downto 0 do
282282- src_off := !src_off +
283283- (idx.(d) - chunk_pixel_start.(d)) * !src_stride;
284284- src_stride := !src_stride * inner_chunk_shape.(d)
285285- done;
286286- (* Dest offset within output buffer *)
287287- let dst_off = ref 0 in
288288- let dst_stride = ref elem_size in
289289- for d = ndim - 1 downto 0 do
290290- dst_off := !dst_off +
291291- (idx.(d) - start.(d)) * !dst_stride;
292292- dst_stride := !dst_stride * shape.(d)
293293- done;
294294- Bytes.blit_string raw !src_off out_buf !dst_off elem_size
295295- end else begin
296296- for i = copy_lo.(dim) to copy_hi.(dim) - 1 do
297297- idx.(dim) <- i;
298298- copy (dim + 1)
299299- done
300300- end
301301- in
302302- copy 0
303303- end
304304- end;
305305- Lwt.return_unit
306306- end else begin
307307- let tasks = ref [] in
311311+ if offset < max_int && nbytes > 0 then
312312+ needed_chunks := (offset, nbytes,
313313+ chunk_pixel_start, chunk_pixel_stop) :: !needed_chunks
314314+ end
315315+ end else
308316 for i = 0 to inner_per_shard.(dim) - 1 do
309317 inner_idx.(dim) <- i;
310310- tasks := iter_inner (Array.copy inner_idx) (dim + 1) :: !tasks
311311- done;
312312- Lwt.join !tasks
313313- end
318318+ collect_inner (Array.copy inner_idx) (dim + 1)
319319+ done
314320 in
315315- iter_inner (Array.make ndim 0) 0
321321+ collect_inner (Array.make ndim 0) 0;
322322+323323+ let chunks = !needed_chunks in
324324+ if chunks = [] then Lwt.return_unit
325325+ else match shard_data_opt with
326326+ | Some full ->
327327+ (* Already have the full shard — just decompress in place *)
328328+ List.iter (fun (offset, nbytes, cps, cpe) ->
329329+ decode_inner arr.store.codecs meta.inner_codecs
330330+ full offset nbytes cps cpe
331331+ ndim start shape elem_size inner_chunk_shape out_buf
332332+ ) chunks;
333333+ Lwt.return_unit
334334+ | None ->
335335+ (* Group nearby sub-chunks into merged byte ranges.
336336+ Sort by offset, then merge when gap < 64KB. *)
337337+ let sorted = List.sort (fun (a,_,_,_) (b,_,_,_) -> compare a b) chunks in
338338+ let max_gap = 65536 in
339339+ (* Build groups: each group is (range_start, range_end, chunk list) *)
340340+ let groups = ref [] in
341341+ let cur_start = ref 0 in
342342+ let cur_end = ref 0 in
343343+ let cur_chunks = ref [] in
344344+ List.iter (fun ((off, nb, _, _) as chunk) ->
345345+ if !cur_chunks = [] then begin
346346+ cur_start := off;
347347+ cur_end := off + nb;
348348+ cur_chunks := [chunk]
349349+ end else if off - !cur_end <= max_gap then begin
350350+ cur_end := max !cur_end (off + nb);
351351+ cur_chunks := chunk :: !cur_chunks
352352+ end else begin
353353+ groups := (!cur_start, !cur_end, !cur_chunks) :: !groups;
354354+ cur_start := off;
355355+ cur_end := off + nb;
356356+ cur_chunks := [chunk]
357357+ end
358358+ ) sorted;
359359+ if !cur_chunks <> [] then
360360+ groups := (!cur_start, !cur_end, !cur_chunks) :: !groups;
361361+362362+ (* Fetch each group in parallel *)
363363+ let group_tasks = List.map (fun (g_start, g_end, g_chunks) ->
364364+ let+ data = arr.store.fetch shard_url
365365+ ~off:g_start ~len:(g_end - g_start) () in
366366+ List.iter (fun (offset, nbytes, cps, cpe) ->
367367+ decode_inner arr.store.codecs meta.inner_codecs
368368+ data (offset - g_start) nbytes cps cpe
369369+ ndim start shape elem_size inner_chunk_shape out_buf
370370+ ) g_chunks
371371+ ) !groups in
372372+ Lwt.join group_tasks
316373 in
317374 shard_tasks := task :: !shard_tasks
318375 end else begin
+7-1
zarr-v3/lib/store.mli
···11(** Pure OCaml Zarr v3 store reader.
2233+ {b Warning:} This library was vibe-coded with AI assistance and has not
44+ been thoroughly reviewed or tested. Use at your own risk and expect
55+ breaking changes.
66+37 Reads sharded Zarr v3 arrays over HTTP with pluggable codecs and
48 fetch functions. Platform-independent — bring your own HTTP client
59 and decompressors.
···23272428type fetch = string -> ?off:int -> ?len:int -> unit -> string Lwt.t
2529(** [fetch url ?off ?len ()] fetches bytes from [url].
2626- If [off] and [len] are provided, fetches a byte range.
3030+ If [off] and [len] are provided, fetches byte range [off..off+len-1].
3131+ If only [len] is provided (no [off]), fetches the last [len] bytes
3232+ (suffix range, i.e. HTTP [bytes=-len]).
2733 Returns the response body as a string. *)
28342935type codec = string -> string
+56-3
zarr-v3/test/test_live.ml
···6161 zi.zone zi.origin_easting zi.origin_northing zi.pixel_size;
62626363 (* Test full fetch_region pipeline *)
6464- Printf.printf "\n=== Testing fetch_region (small bbox near Cambridge) ===\n%!";
6464+ Printf.printf "\n=== Testing v1 fetch_region (small bbox near Cambridge) ===\n%!";
6565 let bbox = Geotessera.{ min_lon = 0.11; min_lat = 52.19;
6666 max_lon = 0.13; max_lat = 52.21 } in
6767- let* (mat, h, w, bounds) = Tessera_zarr.fetch_region ~store bbox in
6767+ let progress msg = Printf.printf " [v1 progress] %s\n%!" msg in
6868+ let* (mat, h, w, bounds) = Tessera_zarr.fetch_region ~progress ~store bbox in
6869 Printf.printf " mosaic: %d x %d (%d rows, %d cols in mat)\n%!" h w mat.Linalg.rows mat.Linalg.cols;
6970 Printf.printf " bounds: S=%.4f N=%.4f W=%.4f E=%.4f\n%!"
7071 bounds.min_lat bounds.max_lat bounds.min_lon bounds.max_lon;
···7273 let v0 = Linalg.mat_get mat 0 0 in
7374 let v1 = Linalg.mat_get mat 0 1 in
7475 Printf.printf " first pixel: feat[0]=%.4f, feat[1]=%.4f\n%!" v0 v1;
7575- Printf.printf " (should be non-zero dequantized values)\n%!";
7676+ let mid = (h / 2) * w + (w / 2) in
7777+ let vm0 = Linalg.mat_get mat mid 0 in
7878+ let vm1 = Linalg.mat_get mat mid 1 in
7979+ Printf.printf " center pixel: feat[0]=%.4f, feat[1]=%.4f\n%!" vm0 vm1;
8080+ (* Count non-zero pixels *)
8181+ let nz = ref 0 in
8282+ for i = 0 to h * w - 1 do
8383+ if Float.abs (Linalg.mat_get mat i 0) > 0.001 then incr nz
8484+ done;
8585+ Printf.printf " non-zero pixels: %d / %d\n%!" !nz (h * w);
8686+8787+ (* === V2 store test === *)
8888+ Printf.printf "\n=== Opening v2 store ===\n%!";
8989+ let v2_base = "https://dl2.geotessera.org/zarr/v2/store.zarr" in
9090+ let* store2 = Zarr_v3.Store.open_store ~fetch:Zarr_v3_unix.fetch
9191+ ~codecs:Zarr_v3_unix.codecs v2_base in
9292+9393+ let entries2 = Zarr_v3.Store.store_meta store2 in
9494+ Printf.printf "V2 consolidated metadata: %d entries\n%!" (List.length entries2);
9595+9696+ (* Open v2 embeddings to check shape *)
9797+ Printf.printf "\n=== V2 utm31/embeddings ===\n%!";
9898+ let* emb2 = Zarr_v3.Store.open_array store2 "utm31/embeddings" in
9999+ let em2 = Zarr_v3.Store.array_meta emb2 in
100100+ Printf.printf " shape = %s\n"
101101+ (String.concat " x " (Array.to_list (Array.map string_of_int em2.shape)));
102102+ Printf.printf " sharded = %b, chunk_shape = %s\n" em2.is_sharded
103103+ (String.concat " x " (Array.to_list (Array.map string_of_int em2.chunk_shape)));
104104+ (match em2.inner_chunk_shape with
105105+ | Some s -> Printf.printf " inner_chunk_shape = %s\n"
106106+ (String.concat " x " (Array.to_list (Array.map string_of_int s)))
107107+ | None -> Printf.printf " (no inner chunks)\n");
108108+109109+ (* Test fetch_region with v2 store *)
110110+ Printf.printf "\n=== V2 fetch_region (small bbox near Cambridge, year=2024) ===\n%!";
111111+ let progress msg = Printf.printf " [progress] %s\n%!" msg in
112112+ let* (mat2, h2, w2, bounds2) =
113113+ Tessera_zarr.fetch_region ~progress ~year:2024 ~store:store2 bbox in
114114+ Printf.printf " mosaic: %d x %d (%d rows, %d cols in mat)\n%!" h2 w2 mat2.Linalg.rows mat2.Linalg.cols;
115115+ Printf.printf " bounds: S=%.4f N=%.4f W=%.4f E=%.4f\n%!"
116116+ bounds2.min_lat bounds2.max_lat bounds2.min_lon bounds2.max_lon;
117117+ let v0_2 = Linalg.mat_get mat2 0 0 in
118118+ let v1_2 = Linalg.mat_get mat2 0 1 in
119119+ Printf.printf " first pixel: feat[0]=%.4f, feat[1]=%.4f\n%!" v0_2 v1_2;
120120+ let mid2 = (h2 / 2) * w2 + (w2 / 2) in
121121+ let vm0_2 = Linalg.mat_get mat2 mid2 0 in
122122+ let vm1_2 = Linalg.mat_get mat2 mid2 1 in
123123+ Printf.printf " center pixel: feat[0]=%.4f, feat[1]=%.4f\n%!" vm0_2 vm1_2;
124124+ let nz2 = ref 0 in
125125+ for i = 0 to h2 * w2 - 1 do
126126+ if Float.abs (Linalg.mat_get mat2 i 0) > 0.001 then incr nz2
127127+ done;
128128+ Printf.printf " non-zero pixels: %d / %d\n%!" !nz2 (h2 * w2);
7612977130 Printf.printf "\n=== Done ===\n%!";
78131 Lwt.return_unit