this repo has no description
1// Tutorial test definitions for OCaml libraries
2// Each entry is a self-contained interactive tutorial
3
4const U = {
5 // ── Bunzli libraries ──
6 'fmt.0.9.0': '9901393f978b0a6627c5eab595111f50',
7 'fmt.0.10.0': 'd8140118651d08430f933d410a909e3b',
8 'fmt.0.11.0': '7663cce356513833b908ae5e4f521106',
9 'cmdliner.1.0.4': '0dd34259dc0892e543b03b3afb0a77fa',
10 'cmdliner.1.3.0': '258e7979b874502ea546e90a0742184a',
11 'cmdliner.2.0.0': '91c3d96cea9b89ddd24cf7b78786a5ca',
12 'cmdliner.2.1.0': 'f3e665d5388ac380a70c5ed67f465bbb',
13 'mtime.1.3.0': 'b6735658fd307bba23a7c5f21519b910',
14 'mtime.1.4.0': 'ebccfc43716c6da0ca4a065e60d0f875',
15 'mtime.2.1.0': '7db699c334606d6f66e65c8b515d298d',
16 'logs.0.7.0': '2c014cfbbee1d278b162002eae03eaa8',
17 'logs.0.10.0': '07a565e7588ce100ffd7c8eb8b52df07',
18 'uucp.14.0.0': '60e1409eb30c0650c4d4cbcf3c453e65',
19 'uucp.15.0.0': '6a96a3f145249f110bf14739c78e758c',
20 'uucp.16.0.0': '2bf0fbf12aa05c8f99989a759d2dc8cf',
21 'uucp.17.0.0': '58b9c48e9528ce99586b138d8f4778c2',
22 'uunf.14.0.0': 'cac36534f1bf353fd2192efd015dd0e6',
23 'uunf.17.0.0': '96704cd9810ea1ed504e4ed71cde82b0',
24 'astring.0.8.5': '1cdbe76f0ec91a6eb12bd0279a394492',
25 'jsonm.1.0.2': 'ac28e00ecd46c9464f5575c461b5d48f',
26 'xmlm.1.4.0': 'c4c22d0db3ea01343c1a868bab35e1b4',
27 'ptime.1.2.0': 'd57c69f3dd88b91454622c1841971354',
28 'react.1.2.2': 'f438ba61693a5448718c73116b228f3c',
29 'hmap.0.8.1': '753d7c421afb866e7ffe07ddea3b8349',
30 'gg.1.0.0': '02a9bababc92d6639cdbaf20233597ba',
31 'note.0.0.3': '2545f914c274aa806d29749eb96836fa',
32 'otfm.0.4.0': '4f870a70ee71e41dff878af7123b2cd6',
33 'vg.0.9.5': '0e2e71cfd8fe2e81bff124849421f662',
34 'bos.0.2.1': '0e04faa6cc5527bc124d8625bded34fc',
35 'fpath.0.7.3': '6c4fe09a631d871865fd38aa15cd61d4',
36 'uutf.1.0.4': 'ac04fa0671533316f94dacbd14ffe0bf',
37 'uuseg.14.0.0': '406ca4903030ee122ff6c61b61446ddc',
38 'uuseg.15.0.0': '62ea8502ec4e6c386a070cc75ec8377a',
39 'uuseg.16.0.0': '3a191102f91addba06efdd712ba037b2',
40 'uuseg.17.0.0': '7d9b8800252a9bec2a9be496e02eb9da',
41 'b0.0.0.6': 'bfc34a228f53ac5ced707eed285a6e5c',
42
43 // ── Serialization ──
44 'yojson.1.7.0': '0273d3484c1256a463fc6b5d822ba4ae',
45 'yojson.2.0.2': 'b02baa519ba5bedf95d1b42b5e66381a',
46 'yojson.2.1.2': '5efcef16114ee98834c3f4cf9a7f45b4',
47 'yojson.2.2.2': '739ca5bed6c1201d906f0f3132274687',
48 'yojson.3.0.0': 'e52f084da1b654e881d2dba81775b440',
49 'ezjsonm.1.1.0': '899976ac0dc15192e669f652bf29f29e',
50 'ezjsonm.1.2.0': 'b93294aee1f9361bfe1916f4127fa56c',
51 'ezjsonm.1.3.0': '98ee39eafcb78d7c102a291c7faa302e',
52 'sexplib0.v0.15.1':'f6fb7feeb446b4a67adb486a2392bf3e',
53 'sexplib0.v0.16.0':'8ec78baf83bdc6a0a181b58efb909869',
54 'sexplib0.v0.17.0':'08fe6d134ac413075564220297b2554f',
55 'csexp.1.5.2': '8443eb56f5227050537a4eb47b26fd10',
56 'base64.3.4.0': '9befb8850a0bcfb0556f8a7d2de8d3bd',
57 'base64.3.5.2': 'dee9a00f3ec355e7dab15121d7cb5a3c',
58
59 // ── Text / Parsing ──
60 're.1.10.4': '4697515ef0ed56df99029cfa8b6a4c1a',
61 're.1.11.0': '87fd99e341a1468e36de4973044ba1cb',
62 're.1.12.0': '3bc6cdc9f1fd39cd5ee61b89f423f51a',
63 're.1.13.2': 'e080307b8290a25f41d4ad87427c3cc0',
64 're.1.14.0': '7f3c1f0452e7156dea56c1a52e2096a4',
65 'angstrom.0.15.0':'12fe7a4d575b34f30551cf6eaaed4a0b',
66 'angstrom.0.16.1':'f46aa50b81b7e6a0dd7ee69d247920c0',
67 'tyre.0.5': 'ffdb349acdd211cf2699a689ed1491d3',
68 'tyre.1.0': 'e413ed92802108a275144c27e0f9efa8',
69
70 // ── Data structures ──
71 'containers.3.17': '62a1dfab4e79dda21e6775fc35bac90b',
72 'iter.1.7': '4aca16dd3c74db420f49a18cc54fd66f',
73 'iter.1.8': '7724b461d742c869a4abfaa870879763',
74 'iter.1.9': '73e7b9c9b638abf269affd1967509ce6',
75 'ocamlgraph.2.0.0':'bcfe5c830a54c4fc55121d6bc69d52d4',
76 'ocamlgraph.2.1.0':'e867dbcc2de571de4cb84d9a45e554bd',
77 'ocamlgraph.2.2.0':'ab9aa04f9746bf7c5b275cfddfc9dc20',
78
79 // ── Crypto / Encoding ──
80 'digestif.1.1.2': '33d25472185fc31bd41d277d488478f2',
81 'digestif.1.3.0': 'c3664212cf01a38aa9af7c54123056cf',
82 'hex.1.4.0': '0bff54cafa851851e4ddb617126d4ce6',
83 'hex.1.5.0': 'a46b45c6915570ff2966d96d9101258c',
84 'eqaf.0.9': '39499417427d1d35a028fc9101ecbfb2',
85 'eqaf.0.10': 'eed017d4f8c09e4fcabf2f9320361e64',
86
87 // ── Networking types ──
88 'uri.4.2.0': '473a4aaa6884b7d04af481a4bcf573e6',
89 'uri.4.4.0': '3f9567317844352b63256b5d7075e595',
90 'ipaddr.5.6.0': 'ca33bd0287b9b4cd9f67a4c6464b0bd9',
91 'ipaddr.5.6.1': '516728912d49b2b8b007b762f0cd985f',
92 'domain-name.0.4.1':'55bf622c2e1dacb9e5c7da2cf9195e95',
93 'domain-name.0.5.0':'e069e9e37be7c2d8264f41a661136c60',
94
95 // ── Math ──
96 'zarith.1.13': '5b98616ce2f37ecfbefd3d8c7c1f45a9',
97 'zarith.1.14': '3abb9b1ae0690526d21d9630f3f27153',
98
99 // ── Testing ──
100 'qcheck-core.0.25':'c338cf74d7ad14da542181619f55fbda',
101 'qcheck-core.0.27':'eb7a98de039353471656e141c6107fc3',
102 'qcheck-core.0.91':'c1307fa49614dc884aa0fec68b55c832',
103};
104
105// ── Factory: Fmt (same API across 0.9–0.11) ────────────────────────────
106function fmtTutorial(version, universe) {
107 return {
108 name: 'Fmt', version, opam: 'fmt',
109 description: 'OCaml Format pretty-printer combinators',
110 universe, require: ['fmt'],
111 sections: [
112 { title: 'String Formatting',
113 description: 'Fmt.str works like Printf.sprintf, building a string from a format string.',
114 steps: [
115 { code: 'Fmt.str "%d" 42;;', expect: '"42"',
116 description: 'Format an integer into a string' },
117 { code: 'Fmt.str "Hello, %s!" "world";;', expect: '"Hello, world!"',
118 description: 'Interpolate a string value' },
119 { code: 'Fmt.str "%d + %d = %d" 1 2 3;;', expect: '"1 + 2 = 3"',
120 description: 'Multiple format arguments' },
121 { code: 'Fmt.str "%a" Fmt.int 42;;', expect: '"42"',
122 description: 'Use a typed formatter with %a' },
123 ] },
124 { title: 'Typed Formatters',
125 description: 'Fmt provides typed formatter values (type \'a Fmt.t = Format.formatter -> \'a -> unit) for common types.',
126 steps: [
127 { code: 'Fmt.str "%a" Fmt.bool true;;', expect: '"true"',
128 description: 'Format a boolean' },
129 { code: 'Fmt.str "%a" Fmt.float 3.14;;', expect: '3.14',
130 description: 'Format a float' },
131 { code: 'Fmt.str "%a" Fmt.string "hi";;', expect: '"hi"',
132 description: 'Format a string with the string formatter' },
133 ] },
134 { title: 'Collection Formatters',
135 description: 'Fmt can format lists, options, pairs, and results with configurable separators.',
136 steps: [
137 { code: 'Fmt.str "%a" Fmt.(list int) [1; 2; 3];;', expect: '1',
138 description: 'Format a list of ints (default separator)' },
139 { code: 'Fmt.str "%a" Fmt.(list ~sep:comma int) [1; 2; 3];;', expect: '1, 2',
140 description: 'Format a list with comma separators' },
141 { code: 'Fmt.str "%a" Fmt.(list ~sep:(any " ") int) [10; 20];;', expect: '"10 20"',
142 description: 'Format with space separators using Fmt.any' },
143 { code: 'Fmt.str "%a" Fmt.(option int) (Some 5);;', expect: '5',
144 description: 'Format an option value' },
145 { code: 'Fmt.str "%a" Fmt.(option int) None;;', expect: '',
146 description: 'Format None (empty output by default)' },
147 { code: 'Fmt.str "%a" Fmt.(pair ~sep:comma int string) (42, "hi");;', expect: '42',
148 description: 'Format a pair' },
149 ] },
150 { title: 'Output to stdout',
151 description: 'Fmt.pr prints directly to stdout. Use @. for a newline flush.',
152 steps: [
153 { code: 'Fmt.pr "value: %d@." 42;;', expectStdout: 'value: 42',
154 description: 'Print formatted output to stdout' },
155 { code: 'Fmt.pr "%a@." Fmt.(list ~sep:sp int) [1; 2; 3];;', expectStdout: '1',
156 description: 'Print a list to stdout' },
157 ] },
158 { title: 'Combinators',
159 description: 'Higher-order combinators transform formatters.',
160 steps: [
161 { code: 'let pp_len = Fmt.using String.length Fmt.int;;', expect: 'Fmt.t',
162 description: 'Fmt.using transforms input before formatting' },
163 { code: 'Fmt.str "%a" pp_len "hello";;', expect: '"5"',
164 description: 'pp_len formats the length of a string' },
165 { code: 'Fmt.str "%a" (Fmt.Dump.list Fmt.int) [1; 2; 3];;', expect: '[1; 2; 3]',
166 description: 'Fmt.Dump formats with OCaml syntax (brackets)' },
167 ] },
168 ],
169 };
170}
171
172// ── Factory: Uucp (same API across 14–17, different Unicode version) ───
173function uucpTutorial(version, universe, unicodeVer) {
174 return {
175 name: 'Uucp', version, opam: 'uucp',
176 description: `Unicode character properties (Unicode ${unicodeVer})`,
177 universe, require: ['uucp'],
178 sections: [
179 { title: 'Unicode Version',
180 description: 'Each Uucp release tracks a specific Unicode standard version.',
181 steps: [
182 { code: 'Uucp.unicode_version;;', expect: `"${unicodeVer}"`,
183 description: 'Check which Unicode version this release implements' },
184 ] },
185 { title: 'General Category',
186 description: 'Uucp.Gc.general_category returns the Unicode General Category of a character as a polymorphic variant.',
187 steps: [
188 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0041);;', expect: '`Lu',
189 description: "'A' (U+0041) is an uppercase letter (Lu)" },
190 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0061);;', expect: '`Ll',
191 description: "'a' (U+0061) is a lowercase letter (Ll)" },
192 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0030);;', expect: '`Nd',
193 description: "'0' (U+0030) is a decimal digit (Nd)" },
194 { code: 'Uucp.Gc.general_category (Uchar.of_int 0x0020);;', expect: '`Zs',
195 description: "Space (U+0020) is a space separator (Zs)" },
196 ] },
197 { title: 'Script Detection',
198 description: 'Uucp.Script.script identifies which writing system a character belongs to.',
199 steps: [
200 { code: 'Uucp.Script.script (Uchar.of_int 0x03B1);;', expect: '`Grek',
201 description: "Greek alpha (U+03B1) is in the Greek script" },
202 { code: 'Uucp.Script.script (Uchar.of_int 0x4E16);;', expect: '`Hani',
203 description: "CJK character (U+4E16) is in the Han script" },
204 { code: 'Uucp.Script.script (Uchar.of_int 0x0041);;', expect: '`Latn',
205 description: "'A' is in the Latin script" },
206 ] },
207 { title: 'Character Properties',
208 description: 'Uucp provides boolean property lookups for whitespace, alphabetic characters, and more.',
209 steps: [
210 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x0020);;', expect: 'true',
211 description: 'Space is whitespace' },
212 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x0041);;', expect: 'false',
213 description: "'A' is not whitespace" },
214 { code: 'Uucp.White.is_white_space (Uchar.of_int 0x00A0);;', expect: 'true',
215 description: 'Non-breaking space (U+00A0) is whitespace' },
216 ] },
217 ],
218 };
219}
220
221// ── Factory: Uunf (same API, different Unicode version) ────────────────
222function uunfTutorial(version, universe, unicodeVer) {
223 return {
224 name: 'Uunf', version, opam: 'uunf',
225 description: `Unicode text normalization (Unicode ${unicodeVer})`,
226 universe, require: ['uunf'],
227 sections: [
228 { title: 'Unicode Version',
229 description: 'Each Uunf release implements normalization according to a specific Unicode version.',
230 steps: [
231 { code: 'Uunf.unicode_version;;', expect: `"${unicodeVer}"`,
232 description: 'Check the Unicode version' },
233 ] },
234 { title: 'Normalization Forms',
235 description: 'Unicode defines four normalization forms: NFC, NFD, NFKC, and NFKD. Uunf.create selects which form to use.',
236 steps: [
237 { code: 'let nfc = Uunf.create `NFC;;', expect: 'Uunf.t',
238 description: 'Create an NFC normalizer' },
239 { code: 'let nfd = Uunf.create `NFD;;', expect: 'Uunf.t',
240 description: 'Create an NFD normalizer (canonical decomposition)' },
241 { code: 'let nfkc = Uunf.create `NFKC;;', expect: 'Uunf.t',
242 description: 'Create an NFKC normalizer (compatibility composition)' },
243 { code: 'let nfkd = Uunf.create `NFKD;;', expect: 'Uunf.t',
244 description: 'Create an NFKD normalizer (compatibility decomposition)' },
245 ] },
246 { title: 'Adding Characters',
247 description: 'Feed characters to the normalizer with Uunf.add. It returns `Uchar for output characters and `Await when ready for more input.',
248 steps: [
249 { code: 'let n = Uunf.create `NFC;;', expect: 'Uunf.t',
250 description: 'Create a fresh NFC normalizer' },
251 { code: 'Uunf.add n (`Uchar (Uchar.of_int 0x0041));;', expect: '',
252 description: "Add 'A' to the normalizer" },
253 { code: 'Uunf.add n `End;;', expect: '',
254 description: 'Signal end of input' },
255 ] },
256 ],
257 };
258}
259
260// ── Factory: Mtime 1.x (1.3 and 1.4 share the same API) ───────────────
261function mtime1_3Tutorial(version, universe) {
262 // Mtime 1.3.0: no named constants (ns/ms/s), uses float conversion functions
263 return {
264 name: 'Mtime', version, opam: 'mtime',
265 description: 'Monotonic wall-clock time for OCaml',
266 universe, require: ['mtime'],
267 sections: [
268 { title: 'Time Span Basics',
269 description: 'Mtime.Span represents monotonic time durations in nanoseconds.',
270 steps: [
271 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span',
272 description: 'The zero-length span' },
273 { code: 'Mtime.Span.one;;', expect: 'Mtime.span',
274 description: 'One nanosecond' },
275 { code: 'Mtime.Span.max_span;;', expect: 'Mtime.span',
276 description: 'The maximum representable span' },
277 { code: 'Mtime.Span.of_uint64_ns 1_000_000_000L;;', expect: 'Mtime.span',
278 description: 'Create a 1-second span from nanoseconds' },
279 ] },
280 { title: 'Span Arithmetic',
281 description: 'Spans support addition, comparison, and absolute difference.',
282 steps: [
283 { code: 'let one_sec = Mtime.Span.of_uint64_ns 1_000_000_000L;;', expect: 'Mtime.span',
284 description: 'One second' },
285 { code: 'let two_sec = Mtime.Span.add one_sec one_sec;;', expect: 'Mtime.span',
286 description: 'Add two spans: 1s + 1s = 2s' },
287 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L',
288 description: '2 seconds in nanoseconds' },
289 { code: 'Mtime.Span.equal Mtime.Span.zero Mtime.Span.zero;;', expect: 'true',
290 description: 'Zero equals zero' },
291 { code: 'Mtime.Span.compare one_sec Mtime.Span.zero;;', expect: '1',
292 description: '1 second is greater than zero' },
293 ] },
294 { title: 'Float Conversions',
295 description: 'Convert spans to floating-point representations in various units.',
296 steps: [
297 { code: 'Mtime.Span.to_ns one_sec;;', expect: '1000000000.',
298 description: '1 second = 1e9 nanoseconds' },
299 { code: 'Mtime.Span.to_ms one_sec;;', expect: '1000.',
300 description: '1 second = 1000 milliseconds' },
301 { code: 'Mtime.Span.to_s one_sec;;', expect: '1.',
302 description: '1 second as a float' },
303 { code: 'Mtime.Span.to_us one_sec;;', expect: '1000000.',
304 description: '1 second = 1e6 microseconds' },
305 ] },
306 ],
307 };
308}
309
310function mtime1_4Tutorial(version, universe) {
311 // Mtime 1.4.0: has named constants (ns/ms/s) and is_shorter/is_longer
312 return {
313 name: 'Mtime', version, opam: 'mtime',
314 description: 'Monotonic wall-clock time for OCaml',
315 universe, require: ['mtime'],
316 sections: [
317 { title: 'Time Span Constants',
318 description: 'Mtime 1.4 added named constants for common time durations.',
319 steps: [
320 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span',
321 description: 'The zero-length span' },
322 { code: 'Mtime.Span.ns;;', expect: 'Mtime.span',
323 description: '1 nanosecond' },
324 { code: 'Mtime.Span.ms;;', expect: 'Mtime.span',
325 description: '1 millisecond' },
326 { code: 'Mtime.Span.s;;', expect: 'Mtime.span',
327 description: '1 second' },
328 { code: 'Mtime.Span.min;;', expect: 'Mtime.span',
329 description: '1 minute' },
330 ] },
331 { title: 'Span Arithmetic',
332 description: 'Spans support addition, scaling, and comparison.',
333 steps: [
334 { code: 'let two_sec = Mtime.Span.add Mtime.Span.s Mtime.Span.s;;', expect: 'Mtime.span',
335 description: '1s + 1s = 2s' },
336 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L',
337 description: '2 seconds in nanoseconds' },
338 { code: 'Mtime.Span.compare Mtime.Span.ms Mtime.Span.s;;', expect: '-1',
339 description: '1ms is less than 1s' },
340 { code: 'Mtime.Span.equal Mtime.Span.zero Mtime.Span.zero;;', expect: 'true',
341 description: 'Zero equals zero' },
342 ] },
343 { title: 'Conversions',
344 description: 'Convert spans to floating-point representations in various units.',
345 steps: [
346 { code: 'Mtime.Span.to_ms Mtime.Span.s;;', expect: '1000.',
347 description: '1 second = 1000 milliseconds' },
348 { code: 'Mtime.Span.to_s Mtime.Span.s;;', expect: '1.',
349 description: '1 second as a float' },
350 { code: 'Mtime.Span.of_uint64_ns 500_000_000L |> Mtime.Span.to_ms;;', expect: '500.',
351 description: '500ms round-trip through nanoseconds' },
352 ] },
353 ],
354 };
355}
356
357export const TUTORIALS = {
358 // ═══════════════════════════════════════════════════════════════════════
359 // Fmt
360 // ═══════════════════════════════════════════════════════════════════════
361 'fmt.0.9.0': fmtTutorial('0.9.0', U['fmt.0.9.0']),
362 'fmt.0.10.0': fmtTutorial('0.10.0', U['fmt.0.10.0']),
363 'fmt.0.11.0': fmtTutorial('0.11.0', U['fmt.0.11.0']),
364
365 // ═══════════════════════════════════════════════════════════════════════
366 // Cmdliner
367 // ═══════════════════════════════════════════════════════════════════════
368 'cmdliner.1.0.4': {
369 name: 'Cmdliner', version: '1.0.4', opam: 'cmdliner',
370 description: 'Declarative definition of command line interfaces (v1 API)',
371 universe: U['cmdliner.1.0.4'], require: ['cmdliner'],
372 sections: [
373 { title: 'Argument Info',
374 description: 'Cmdliner.Arg.info describes command-line arguments with names, docs, and metadata.',
375 steps: [
376 { code: 'let verbose_info = Cmdliner.Arg.info ["v"; "verbose"] ~doc:"Be verbose";;',
377 expect: 'Cmdliner.Arg.info', description: 'Create info for a --verbose/-v flag' },
378 { code: 'let name_info = Cmdliner.Arg.info [] ~docv:"NAME" ~doc:"The name";;',
379 expect: 'Cmdliner.Arg.info', description: 'Create info for a positional argument' },
380 ] },
381 { title: 'Argument Definitions',
382 description: 'Arguments are built from converters + info, then lifted into terms with Arg.value.',
383 steps: [
384 { code: 'Cmdliner.Arg.string;;', expect: 'string Cmdliner.Arg.conv',
385 description: 'Built-in string converter' },
386 { code: 'Cmdliner.Arg.int;;', expect: 'int Cmdliner.Arg.conv',
387 description: 'Built-in int converter' },
388 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"])));;',
389 expect: 'bool Cmdliner.Term.t', description: 'Define a boolean flag term' },
390 { code: 'let count = Cmdliner.Arg.(value (opt int 0 (info ["c";"count"])));;',
391 expect: 'int Cmdliner.Term.t', description: 'Define an optional int argument with default 0' },
392 ] },
393 { title: 'Terms (v1 API)',
394 description: 'In Cmdliner 1.0.x, Term.const and Term.($) combine argument terms into a program term.',
395 steps: [
396 { code: 'let greet = Cmdliner.Term.const (fun v n -> Printf.sprintf "%s%s" (if v then "HI " else "hi ") n);;',
397 expect: 'Cmdliner.Term.t', description: 'A constant function lifted into a term' },
398 { code: 'Cmdliner.Term.info "greet" ~doc:"A greeting program";;',
399 expect: 'Cmdliner.Term.info', description: 'Term.info describes the command (v1 API)' },
400 { code: 'Cmdliner.Term.eval;;', expect: 'Term.result',
401 description: 'Term.eval runs a term — available in 1.0.x' },
402 ] },
403 ],
404 },
405
406 'cmdliner.1.3.0': {
407 name: 'Cmdliner', version: '1.3.0', opam: 'cmdliner',
408 description: 'Declarative definition of command line interfaces (transitional)',
409 universe: U['cmdliner.1.3.0'], require: ['cmdliner'],
410 sections: [
411 { title: 'Argument Building',
412 description: 'Cmdliner 1.3 is a transitional release supporting both the old Term API and the new Cmd API.',
413 steps: [
414 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"] ~doc:"Be verbose")));;',
415 expect: 'bool Cmdliner.Term.t', description: 'Define a verbose flag' },
416 { code: 'let greeting = Cmdliner.Arg.(value (pos 0 string "world" (info [] ~docv:"NAME")));;',
417 expect: 'string Cmdliner.Term.t', description: 'Define a positional name argument' },
418 ] },
419 { title: 'New Cmd API (introduced in 1.1+)',
420 description: 'The Cmd module provides a structured way to define commands, replacing Term.info + Term.eval.',
421 steps: [
422 { code: 'Cmdliner.Cmd.info "hello" ~doc:"Say hello";;',
423 expect: 'Cmdliner.Cmd.info', description: 'Cmd.info creates command metadata' },
424 { code: 'let hello_t = Cmdliner.Term.(const (fun v n -> ()) $ verbose $ greeting);;',
425 expect: 'Cmdliner.Term.t', description: 'Combine arguments with Term.const and ($)' },
426 { code: 'Cmdliner.Cmd.v (Cmdliner.Cmd.info "hello") hello_t;;',
427 expect: 'Cmdliner.Cmd.t', description: 'Create a command from info + term' },
428 ] },
429 { title: 'Backward Compatibility',
430 description: 'The old Term.eval API still works in 1.3 for migration.',
431 steps: [
432 { code: 'Cmdliner.Term.eval;;', expect: 'Term.result',
433 description: 'Term.eval is still available (deprecated but functional)' },
434 { code: 'Cmdliner.Term.info "test";;', expect: 'Cmdliner.Term.info',
435 description: 'Term.info still works for backward compat' },
436 ] },
437 ],
438 },
439
440 'cmdliner.2.0.0': {
441 name: 'Cmdliner', version: '2.0.0', opam: 'cmdliner',
442 description: 'Declarative definition of command line interfaces (v2 API)',
443 universe: U['cmdliner.2.0.0'], require: ['cmdliner'],
444 sections: [
445 { title: 'Arguments',
446 description: 'Arguments are defined the same way as in earlier versions.',
447 steps: [
448 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"])));;',
449 expect: 'bool Cmdliner.Term.t', description: 'A boolean flag term' },
450 { code: 'let name = Cmdliner.Arg.(value (pos 0 string "world" (info [])));;',
451 expect: 'string Cmdliner.Term.t', description: 'A positional string argument' },
452 ] },
453 { title: 'Cmd Module (v2 API)',
454 description: 'In Cmdliner 2.x, Cmd replaces Term.info/Term.eval entirely.',
455 steps: [
456 { code: 'Cmdliner.Cmd.info "greet" ~doc:"Greet someone";;',
457 expect: 'Cmdliner.Cmd.info', description: 'Create command info' },
458 { code: 'let t = Cmdliner.Term.(const (fun _ _ -> ()) $ verbose $ name);;',
459 expect: 'Cmdliner.Term.t', description: 'Build the term' },
460 { code: 'let cmd = Cmdliner.Cmd.v (Cmdliner.Cmd.info "greet") t;;',
461 expect: 'Cmdliner.Cmd.t', description: 'Package into a command' },
462 { code: 'Cmdliner.Cmd.name cmd;;', expect: '"greet"',
463 description: 'Extract the command name' },
464 ] },
465 { title: 'Removed APIs',
466 description: 'Term.eval was removed in 2.0. Use Cmd.eval_value instead.',
467 steps: [
468 { code: 'Cmdliner.Cmd.eval_value;;', expect: 'eval_ok',
469 description: 'Cmd.eval_value is the new entry point' },
470 ] },
471 ],
472 },
473
474 'cmdliner.2.1.0': {
475 name: 'Cmdliner', version: '2.1.0', opam: 'cmdliner',
476 description: 'Declarative definition of command line interfaces (v2 API)',
477 universe: U['cmdliner.2.1.0'], require: ['cmdliner'],
478 sections: [
479 { title: 'Arguments',
480 description: 'Define typed command-line arguments with converters and info.',
481 steps: [
482 { code: 'let verbose = Cmdliner.Arg.(value (flag (info ["v";"verbose"] ~doc:"Increase verbosity")));;',
483 expect: 'bool Cmdliner.Term.t', description: 'A verbose flag' },
484 { code: 'let file = Cmdliner.Arg.(required (pos 0 (some string) None (info [] ~docv:"FILE")));;',
485 expect: 'string Cmdliner.Term.t', description: 'A required positional file argument' },
486 { code: 'let count = Cmdliner.Arg.(value (opt int 1 (info ["n";"count"] ~doc:"Repeat count")));;',
487 expect: 'int Cmdliner.Term.t', description: 'An optional integer with default' },
488 ] },
489 { title: 'Commands',
490 description: 'Commands combine a term with metadata. Groups can nest subcommands.',
491 steps: [
492 { code: 'let info = Cmdliner.Cmd.info "process" ~version:"1.0" ~doc:"Process files";;',
493 expect: 'Cmdliner.Cmd.info', description: 'Command info with version' },
494 { code: 'let t = Cmdliner.Term.(const (fun _ _ _ -> ()) $ verbose $ file $ count);;',
495 expect: 'Cmdliner.Term.t', description: 'Combine all arguments' },
496 { code: 'let cmd = Cmdliner.Cmd.v info t;;',
497 expect: 'Cmdliner.Cmd.t', description: 'Create the command' },
498 { code: 'Cmdliner.Cmd.name cmd;;', expect: '"process"',
499 description: 'Retrieve the command name' },
500 ] },
501 { title: 'Custom Converters',
502 description: 'Arg.conv creates custom argument converters from a parser/printer pair.',
503 steps: [
504 { code: 'let color_parser s = match s with "red" -> Ok `Red | "blue" -> Ok `Blue | _ -> Error (`Msg "unknown color");;',
505 expect: 'val color_parser', description: 'Define a parser function' },
506 { code: 'let color_pp ppf c = Format.pp_print_string ppf (match c with `Red -> "red" | `Blue -> "blue");;',
507 expect: 'val color_pp', description: 'Define a printer' },
508 { code: 'let color_conv = Cmdliner.Arg.conv (color_parser, color_pp);;',
509 expect: 'Cmdliner.Arg.conv', description: 'Build a custom converter' },
510 ] },
511 ],
512 },
513
514 // ═══════════════════════════════════════════════════════════════════════
515 // Mtime
516 // ═══════════════════════════════════════════════════════════════════════
517 'mtime.1.3.0': mtime1_3Tutorial('1.3.0', U['mtime.1.3.0']),
518 'mtime.1.4.0': mtime1_4Tutorial('1.4.0', U['mtime.1.4.0']),
519
520 'mtime.2.1.0': {
521 name: 'Mtime', version: '2.1.0', opam: 'mtime',
522 description: 'Monotonic wall-clock time for OCaml',
523 universe: U['mtime.2.1.0'], require: ['mtime'],
524 sections: [
525 { title: 'Time Span Constants',
526 description: 'Mtime.Span provides named constants for common durations.',
527 steps: [
528 { code: 'Mtime.Span.zero;;', expect: 'Mtime.span',
529 description: 'Zero-length span' },
530 { code: 'Mtime.Span.s;;', expect: 'Mtime.span',
531 description: '1 second' },
532 { code: 'Mtime.Span.min;;', expect: 'Mtime.span',
533 description: '1 minute' },
534 { code: 'Mtime.Span.hour;;', expect: 'Mtime.span',
535 description: '1 hour' },
536 ] },
537 { title: 'Span Arithmetic',
538 description: 'Spans support addition, comparison, and predicate-based comparisons (new in 2.x).',
539 steps: [
540 { code: 'let two_sec = Mtime.Span.add Mtime.Span.s Mtime.Span.s;;', expect: 'Mtime.span',
541 description: '1s + 1s' },
542 { code: 'Mtime.Span.to_uint64_ns two_sec;;', expect: '2000000000L',
543 description: '2 seconds in nanoseconds' },
544 { code: 'Mtime.Span.is_shorter Mtime.Span.ms ~than:Mtime.Span.s;;', expect: 'true',
545 description: '1ms is shorter than 1s (new in 2.x)' },
546 { code: 'Mtime.Span.is_longer Mtime.Span.hour ~than:Mtime.Span.min;;', expect: 'true',
547 description: '1 hour is longer than 1 minute (new in 2.x)' },
548 ] },
549 { title: 'New in 2.x: Float Conversions',
550 description: 'Mtime 2.x adds Span.of_float_ns for creating spans from floating-point nanoseconds.',
551 steps: [
552 { code: 'Mtime.Span.of_float_ns 1e9;;', expect: 'Some',
553 description: '1e9 ns = 1 second' },
554 { code: 'Mtime.Span.of_float_ns (-1.);;', expect: 'None',
555 description: 'Negative values return None' },
556 { code: 'Mtime.Span.of_float_ns infinity;;', expect: 'None',
557 description: 'Non-finite values return None' },
558 { code: 'Mtime.Span.to_float_ns Mtime.Span.s;;', expect: '1000000000.',
559 description: 'Convert 1 second to float nanoseconds' },
560 ] },
561 ],
562 },
563
564 // ═══════════════════════════════════════════════════════════════════════
565 // Logs
566 // ═══════════════════════════════════════════════════════════════════════
567 'logs.0.7.0': {
568 name: 'Logs', version: '0.7.0', opam: 'logs',
569 description: 'Logging infrastructure for OCaml',
570 universe: U['logs.0.7.0'], require: ['logs'],
571 sections: [
572 { title: 'Log Sources',
573 description: 'A Logs.Src.t identifies a log source with a name and optional documentation.',
574 steps: [
575 { code: 'let src = Logs.Src.create "myapp" ~doc:"My application";;', expect: 'Logs.src',
576 description: 'Create a named log source' },
577 { code: 'Logs.Src.name src;;', expect: '"myapp"',
578 description: 'Retrieve the source name' },
579 { code: 'Logs.Src.doc src;;', expect: '"My application"',
580 description: 'Retrieve the source documentation' },
581 ] },
582 { title: 'Log Levels',
583 description: 'Logs has five levels: App, Error, Warning, Info, Debug. The global level controls what gets logged.',
584 steps: [
585 { code: 'Logs.level ();;', expect: 'option',
586 description: 'Get the current global log level' },
587 { code: 'Logs.set_level (Some Logs.Debug);;', expect: 'unit',
588 description: 'Set the global level to Debug (most verbose)' },
589 { code: 'Logs.level ();;', expect: 'Some',
590 description: 'Verify the level was set' },
591 ] },
592 { title: 'Error Counting',
593 description: 'Logs tracks error and warning counts globally.',
594 steps: [
595 { code: 'Logs.err_count ();;', expect: 'int',
596 description: 'Count of errors logged so far' },
597 { code: 'Logs.warn_count ();;', expect: 'int',
598 description: 'Count of warnings logged so far' },
599 ] },
600 ],
601 },
602
603 'logs.0.10.0': {
604 name: 'Logs', version: '0.10.0', opam: 'logs',
605 description: 'Logging infrastructure for OCaml',
606 universe: U['logs.0.10.0'], require: ['logs'],
607 sections: [
608 { title: 'Log Sources',
609 description: 'Create and inspect named log sources.',
610 steps: [
611 { code: 'let src = Logs.Src.create "test" ~doc:"A test source";;', expect: 'Logs.src',
612 description: 'Create a log source' },
613 { code: 'Logs.Src.name src;;', expect: '"test"',
614 description: 'Get the source name' },
615 { code: 'Logs.Src.doc src;;', expect: '"A test source"',
616 description: 'Get the documentation string' },
617 { code: 'Logs.Src.list ();;', expect: 'Logs.src list',
618 description: 'List all registered sources' },
619 ] },
620 { title: 'Level Management',
621 description: 'Control log verbosity at the global and per-source levels.',
622 steps: [
623 { code: 'Logs.set_level (Some Logs.Info);;', expect: 'unit',
624 description: 'Set global level to Info' },
625 { code: 'Logs.level ();;', expect: 'Some',
626 description: 'Check the global level' },
627 { code: 'Logs.Src.set_level src (Some Logs.Debug);;', expect: 'unit',
628 description: 'Override the level for a specific source' },
629 { code: 'Logs.Src.level src;;', expect: 'Some',
630 description: 'Check the per-source level' },
631 ] },
632 { title: 'Error Tracking',
633 description: 'Logs maintains error and warning counters.',
634 steps: [
635 { code: 'Logs.err_count ();;', expect: 'int',
636 description: 'Number of errors logged' },
637 { code: 'Logs.warn_count ();;', expect: 'int',
638 description: 'Number of warnings logged' },
639 ] },
640 ],
641 },
642
643 // ═══════════════════════════════════════════════════════════════════════
644 // Uucp
645 // ═══════════════════════════════════════════════════════════════════════
646 'uucp.14.0.0': uucpTutorial('14.0.0', U['uucp.14.0.0'], '14.0.0'),
647 'uucp.15.0.0': uucpTutorial('15.0.0', U['uucp.15.0.0'], '15.0.0'),
648 'uucp.16.0.0': uucpTutorial('16.0.0', U['uucp.16.0.0'], '16.0.0'),
649 'uucp.17.0.0': uucpTutorial('17.0.0', U['uucp.17.0.0'], '17.0.0'),
650
651 // ═══════════════════════════════════════════════════════════════════════
652 // Uunf
653 // ═══════════════════════════════════════════════════════════════════════
654 'uunf.14.0.0': uunfTutorial('14.0.0', U['uunf.14.0.0'], '14.0.0'),
655 'uunf.17.0.0': uunfTutorial('17.0.0', U['uunf.17.0.0'], '17.0.0'),
656
657 // ═══════════════════════════════════════════════════════════════════════
658 // Astring
659 // ═══════════════════════════════════════════════════════════════════════
660 'astring.0.8.5': {
661 name: 'Astring', version: '0.8.5', opam: 'astring',
662 description: 'Alternative String module for OCaml',
663 universe: U['astring.0.8.5'], require: ['astring'],
664 sections: [
665 { title: 'String Splitting',
666 description: 'Astring.String provides powerful splitting functions that work with string separators.',
667 steps: [
668 { code: 'Astring.String.cuts ~sep:"," "a,b,c";;', expect: '["a"; "b"; "c"]',
669 description: 'Split on comma' },
670 { code: 'Astring.String.cuts ~sep:"::" "a::b::c";;', expect: '["a"; "b"; "c"]',
671 description: 'Split on multi-char separator' },
672 { code: 'Astring.String.cut ~sep:"=" "key=value";;', expect: 'Some ("key", "value")',
673 description: 'Cut at first separator occurrence' },
674 { code: 'Astring.String.cut ~rev:true ~sep:"." "a.b.c";;', expect: 'Some ("a.b", "c")',
675 description: 'Cut at last separator with ~rev:true' },
676 ] },
677 { title: 'String Building',
678 description: 'Concatenation and transformation functions.',
679 steps: [
680 { code: 'Astring.String.concat ~sep:"-" ["x"; "y"; "z"];;', expect: '"x-y-z"',
681 description: 'Join strings with separator' },
682 { code: 'Astring.String.concat ~sep:", " ["hello"; "world"];;', expect: '"hello, world"',
683 description: 'Join with comma-space' },
684 ] },
685 { title: 'String Testing',
686 description: 'Predicate functions for string content.',
687 steps: [
688 { code: 'Astring.String.is_prefix ~affix:"http" "http://example.com";;', expect: 'true',
689 description: 'Check for a prefix' },
690 { code: 'Astring.String.is_suffix ~affix:".ml" "main.ml";;', expect: 'true',
691 description: 'Check for a suffix' },
692 { code: 'Astring.String.is_prefix ~affix:"ftp" "http://example.com";;', expect: 'false',
693 description: 'Prefix not found' },
694 { code: 'Astring.String.find_sub ~sub:"world" "hello world";;', expect: 'Some 6',
695 description: 'Find substring position' },
696 ] },
697 { title: 'String Trimming',
698 description: 'Remove whitespace or specific characters from strings.',
699 steps: [
700 { code: 'Astring.String.trim " hello ";;', expect: '"hello"',
701 description: 'Trim whitespace from both ends' },
702 { code: 'Astring.String.trim ~drop:(fun c -> c = \'/\') "/path/to/";;', expect: '"path/to"',
703 description: 'Trim custom characters' },
704 ] },
705 { title: 'Substrings',
706 description: 'Astring.String.Sub provides zero-copy substring operations.',
707 steps: [
708 { code: 'Astring.String.Sub.(to_string (v "hello world" ~start:6));;', expect: '"world"',
709 description: 'Extract a substring from position 6' },
710 { code: 'Astring.String.Sub.(to_string (v "hello world" ~stop:5));;', expect: '"hello"',
711 description: 'Extract first 5 characters' },
712 ] },
713 ],
714 },
715
716 // ═══════════════════════════════════════════════════════════════════════
717 // Jsonm
718 // ═══════════════════════════════════════════════════════════════════════
719 'jsonm.1.0.2': {
720 name: 'Jsonm', version: '1.0.2', opam: 'jsonm',
721 description: 'Non-blocking streaming JSON codec for OCaml',
722 universe: U['jsonm.1.0.2'], require: ['jsonm'],
723 sections: [
724 { title: 'Decoding JSON Values',
725 description: 'Jsonm.decoder creates a streaming decoder. Each Jsonm.decode call returns one lexeme.',
726 steps: [
727 { code: 'let d = Jsonm.decoder (`String "42");;', expect: 'Jsonm.decoder',
728 description: 'Create a decoder from a JSON string' },
729 { code: 'Jsonm.decode d;;', expect: '`Lexeme (`Float 42.)',
730 description: 'Decode the number 42 (JSON numbers are floats)' },
731 { code: 'Jsonm.decode d;;', expect: '`End',
732 description: 'End of input' },
733 ] },
734 { title: 'Decoding Strings and Booleans',
735 description: 'JSON strings, booleans, and null each produce a single lexeme.',
736 steps: [
737 { code: 'let d2 = Jsonm.decoder (`String {|"hello"|});;', expect: 'Jsonm.decoder',
738 description: 'Decode a JSON string' },
739 { code: 'Jsonm.decode d2;;', expect: '`Lexeme (`String "hello")',
740 description: 'String lexeme' },
741 { code: 'let d3 = Jsonm.decoder (`String "true");;', expect: 'Jsonm.decoder',
742 description: 'Decode a JSON boolean' },
743 { code: 'Jsonm.decode d3;;', expect: '`Lexeme (`Bool true)',
744 description: 'Boolean lexeme' },
745 { code: 'let dn = Jsonm.decoder (`String "null");;', expect: 'Jsonm.decoder',
746 description: 'Decode null' },
747 { code: 'Jsonm.decode dn;;', expect: '`Lexeme `Null',
748 description: 'Null lexeme' },
749 ] },
750 { title: 'Decoding Arrays',
751 description: 'Arrays produce `As (array start) and `Ae (array end) lexemes around their elements.',
752 steps: [
753 { code: 'let da = Jsonm.decoder (`String "[1, 2, 3]");;', expect: 'Jsonm.decoder',
754 description: 'Create decoder for a JSON array' },
755 { code: 'Jsonm.decode da;;', expect: '`Lexeme `As',
756 description: 'Array start' },
757 { code: 'Jsonm.decode da;;', expect: '`Lexeme (`Float 1.)',
758 description: 'First element' },
759 { code: 'Jsonm.decode da;;', expect: '`Lexeme (`Float 2.)',
760 description: 'Second element' },
761 ] },
762 { title: 'Encoding JSON',
763 description: 'Jsonm.encoder creates an encoder that writes lexemes to a buffer.',
764 steps: [
765 { code: 'let buf = Buffer.create 64;;', expect: 'Buffer.t',
766 description: 'Create an output buffer' },
767 { code: 'let e = Jsonm.encoder (`Buffer buf);;', expect: 'Jsonm.encoder',
768 description: 'Create an encoder' },
769 { code: 'Jsonm.encode e (`Lexeme (`Float 42.));;', expect: '`Ok',
770 description: 'Encode a number' },
771 { code: 'Jsonm.encode e `End;;', expect: '`Ok',
772 description: 'End encoding' },
773 { code: 'Buffer.contents buf;;', expect: '42',
774 description: 'The buffer contains the JSON output' },
775 ] },
776 ],
777 },
778
779 // ═══════════════════════════════════════════════════════════════════════
780 // Xmlm
781 // ═══════════════════════════════════════════════════════════════════════
782 'xmlm.1.4.0': {
783 name: 'Xmlm', version: '1.4.0', opam: 'xmlm',
784 description: 'Streaming XML codec for OCaml',
785 universe: U['xmlm.1.4.0'], require: ['xmlm'],
786 sections: [
787 { title: 'Parsing XML Input',
788 description: 'Xmlm.make_input creates a streaming parser. Each Xmlm.input call returns one signal.',
789 steps: [
790 { code: 'let i = Xmlm.make_input (`String (0, "<root/>"));;', expect: 'Xmlm.input',
791 description: 'Create an input from a string' },
792 { code: 'Xmlm.input i;;', expect: '`Dtd',
793 description: 'First signal is the DTD (None for no doctype)' },
794 { code: 'Xmlm.input i;;', expect: '`El_start',
795 description: 'Element start: <root>' },
796 { code: 'Xmlm.input i;;', expect: '`El_end',
797 description: 'Element end: </root> (self-closing)' },
798 ] },
799 { title: 'Parsing with Attributes',
800 description: 'Element start signals include the tag name and attributes.',
801 steps: [
802 { code: 'let i2 = Xmlm.make_input (`String (0, {|<div class="main">text</div>|}));;',
803 expect: 'Xmlm.input', description: 'Parse XML with attributes' },
804 { code: 'Xmlm.input i2;;', expect: '`Dtd',
805 description: 'DTD signal' },
806 { code: 'Xmlm.input i2;;', expect: '`El_start',
807 description: 'Element start with attributes' },
808 { code: 'Xmlm.input i2;;', expect: '`Data "text"',
809 description: 'Text content' },
810 { code: 'Xmlm.input i2;;', expect: '`El_end',
811 description: 'Element end' },
812 ] },
813 { title: 'XML Output',
814 description: 'Xmlm can also write XML to a buffer.',
815 steps: [
816 { code: 'let buf = Buffer.create 64;;', expect: 'Buffer.t',
817 description: 'Create output buffer' },
818 { code: 'let o = Xmlm.make_output (`Buffer buf);;', expect: 'Xmlm.output',
819 description: 'Create an XML output' },
820 { code: 'Xmlm.output o (`Dtd None);;', expect: 'unit',
821 description: 'Write empty DTD' },
822 { code: 'Xmlm.output o (`El_start (("", "item"), []));;', expect: 'unit',
823 description: 'Start <item> element' },
824 { code: 'Xmlm.output o (`Data "hello");;', expect: 'unit',
825 description: 'Write text content' },
826 { code: 'Xmlm.output o `El_end;;', expect: 'unit',
827 description: 'Close the element' },
828 { code: 'Buffer.contents buf;;', expect: '<item>hello</item>',
829 description: 'The output XML' },
830 ] },
831 ],
832 },
833
834 // ═══════════════════════════════════════════════════════════════════════
835 // Ptime
836 // ═══════════════════════════════════════════════════════════════════════
837 'ptime.1.2.0': {
838 name: 'Ptime', version: '1.2.0', opam: 'ptime',
839 description: 'POSIX time for OCaml',
840 universe: U['ptime.1.2.0'], require: ['ptime'],
841 sections: [
842 { title: 'The Epoch',
843 description: 'Ptime.epoch represents 1970-01-01 00:00:00 UTC, the Unix epoch.',
844 steps: [
845 { code: 'Ptime.epoch;;', expect: 'Ptime.t',
846 description: 'The epoch timestamp' },
847 { code: 'Ptime.to_float_s Ptime.epoch;;', expect: '0.',
848 description: 'Epoch as float seconds = 0' },
849 ] },
850 { title: 'Creating Timestamps',
851 description: 'Ptime.of_date_time creates a timestamp from a date-time tuple.',
852 steps: [
853 { code: 'let t = Ptime.of_date_time ((2024, 1, 1), ((12, 0, 0), 0));;', expect: 'Some',
854 description: 'January 1, 2024, noon UTC' },
855 { code: 'let t = match t with Some t -> t | None -> assert false;;', expect: 'Ptime.t',
856 description: 'Unwrap the option' },
857 { code: 'Ptime.to_date_time t;;', expect: '(2024, 1, 1)',
858 description: 'Convert back to date-time tuple' },
859 { code: 'Ptime.to_rfc3339 t;;', expect: '2024-01-01',
860 description: 'Format as RFC 3339 string' },
861 ] },
862 { title: 'Time Arithmetic',
863 description: 'Add and subtract time spans from timestamps.',
864 steps: [
865 { code: 'let one_day = Ptime.Span.of_int_s (24 * 3600);;', expect: 'Ptime.span',
866 description: 'A span of one day (86400 seconds)' },
867 { code: 'let tomorrow = Ptime.add_span t one_day;;', expect: 'Some',
868 description: 'Add one day to our timestamp' },
869 { code: 'Ptime.to_rfc3339 (Option.get tomorrow);;', expect: '2024-01-02',
870 description: 'January 2nd' },
871 { code: 'Ptime.Span.to_int_s (Ptime.diff (Option.get tomorrow) t);;', expect: 'Some 86400',
872 description: 'Difference is exactly 86400 seconds' },
873 ] },
874 { title: 'Time Spans',
875 description: 'Ptime.Span represents durations in days and picoseconds.',
876 steps: [
877 { code: 'Ptime.Span.zero;;', expect: 'Ptime.span',
878 description: 'Zero duration' },
879 { code: 'Ptime.Span.of_int_s 3600;;', expect: 'Ptime.span',
880 description: 'One hour in seconds' },
881 { code: 'Ptime.Span.to_int_s (Ptime.Span.of_int_s 3600);;', expect: 'Some 3600',
882 description: 'Round-trip: int -> span -> int' },
883 { code: 'Ptime.Span.to_float_s (Ptime.Span.of_int_s 90);;', expect: '90.',
884 description: '90 seconds as a float' },
885 ] },
886 ],
887 },
888
889 // ═══════════════════════════════════════════════════════════════════════
890 // React
891 // ═══════════════════════════════════════════════════════════════════════
892 'react.1.2.2': {
893 name: 'React', version: '1.2.2', opam: 'react',
894 description: 'Declarative events and signals for OCaml (FRP)',
895 universe: U['react.1.2.2'], require: ['react'],
896 sections: [
897 { title: 'Creating Signals',
898 description: 'React.S.create returns a signal and a setter function. Signals always have a current value.',
899 steps: [
900 { code: 'let counter, set_counter = React.S.create 0;;', expect: 'React.signal',
901 description: 'Create a signal with initial value 0' },
902 { code: 'React.S.value counter;;', expect: '0',
903 description: 'Read the current value' },
904 { code: 'set_counter 42;;', expect: 'unit',
905 description: 'Update the signal value' },
906 { code: 'React.S.value counter;;', expect: '42',
907 description: 'The value has changed' },
908 ] },
909 { title: 'Derived Signals',
910 description: 'React.S.map creates a signal that automatically updates when its source changes.',
911 steps: [
912 { code: 'let doubled = React.S.map (fun x -> x * 2) counter;;', expect: 'React.signal',
913 description: 'A signal that is always 2x the counter' },
914 { code: 'React.S.value doubled;;', expect: '84',
915 description: '42 * 2 = 84' },
916 { code: 'set_counter 10;;', expect: 'unit',
917 description: 'Update the counter' },
918 { code: 'React.S.value doubled;;', expect: '20',
919 description: 'Doubled automatically updates: 10 * 2 = 20' },
920 ] },
921 { title: 'Combining Signals',
922 description: 'React.S.l2 combines two signals with a function. React.S.pair creates a signal of pairs.',
923 steps: [
924 { code: 'let name, set_name = React.S.create "world";;', expect: 'React.signal',
925 description: 'A name signal' },
926 { code: 'let greeting = React.S.l2 (fun n c -> Printf.sprintf "Hello %s (count=%d)" n c) name counter;;',
927 expect: 'React.signal', description: 'Combine name and counter' },
928 { code: 'React.S.value greeting;;', expect: '"Hello world (count=10)"',
929 description: 'The combined value' },
930 { code: 'set_name "OCaml";;', expect: 'unit',
931 description: 'Update the name' },
932 { code: 'React.S.value greeting;;', expect: '"Hello OCaml (count=10)"',
933 description: 'Greeting updates automatically' },
934 ] },
935 { title: 'Events',
936 description: 'React.E.create returns an event and a trigger. Unlike signals, events are discrete occurrences.',
937 steps: [
938 { code: 'let clicks, send_click = React.E.create ();;', expect: 'React.event',
939 description: 'Create a click event' },
940 { code: 'let click_count = React.S.hold 0 (React.E.map (fun _ -> React.S.value counter) clicks);;',
941 expect: 'React.signal', description: 'Hold the counter value at each click' },
942 ] },
943 ],
944 },
945
946 // ═══════════════════════════════════════════════════════════════════════
947 // Hmap
948 // ═══════════════════════════════════════════════════════════════════════
949 'hmap.0.8.1': {
950 name: 'Hmap', version: '0.8.1', opam: 'hmap',
951 description: 'Heterogeneous value maps for OCaml',
952 universe: U['hmap.0.8.1'], require: ['hmap'],
953 sections: [
954 { title: 'Creating Keys',
955 description: 'Hmap keys are created with Key.create. Each key carries a type witness, allowing the map to hold values of different types.',
956 steps: [
957 { code: 'let k_int : int Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key',
958 description: 'A key for int values' },
959 { code: 'let k_str : string Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key',
960 description: 'A key for string values' },
961 { code: 'let k_list : int list Hmap.key = Hmap.Key.create ();;', expect: 'Hmap.key',
962 description: 'A key for int list values' },
963 ] },
964 { title: 'Building Maps',
965 description: 'Start from Hmap.empty and add values with Hmap.add. Each key-value pair is type-safe.',
966 steps: [
967 { code: 'let m = Hmap.empty;;', expect: 'Hmap.t',
968 description: 'An empty heterogeneous map' },
969 { code: 'Hmap.is_empty m;;', expect: 'true',
970 description: 'Verify it is empty' },
971 { code: 'let m = m |> Hmap.add k_int 42 |> Hmap.add k_str "hello" |> Hmap.add k_list [1;2;3];;',
972 expect: 'Hmap.t', description: 'Add values of different types' },
973 { code: 'Hmap.cardinal m;;', expect: '3',
974 description: 'Three bindings in the map' },
975 ] },
976 { title: 'Querying Maps',
977 description: 'Hmap.find returns an option. The return type matches the key\'s type parameter.',
978 steps: [
979 { code: 'Hmap.find k_int m;;', expect: 'Some 42',
980 description: 'Find the int value — type-safe!' },
981 { code: 'Hmap.find k_str m;;', expect: 'Some "hello"',
982 description: 'Find the string value' },
983 { code: 'Hmap.find k_list m;;', expect: 'Some [1; 2; 3]',
984 description: 'Find the int list value' },
985 { code: 'Hmap.mem k_int m;;', expect: 'true',
986 description: 'Check membership' },
987 { code: 'let m2 = Hmap.rem k_int m;;', expect: 'Hmap.t',
988 description: 'Remove a binding' },
989 { code: 'Hmap.find k_int m2;;', expect: 'None',
990 description: 'Key is no longer bound' },
991 ] },
992 ],
993 },
994
995 // ═══════════════════════════════════════════════════════════════════════
996 // Gg
997 // ═══════════════════════════════════════════════════════════════════════
998 'gg.1.0.0': {
999 name: 'Gg', version: '1.0.0', opam: 'gg',
1000 description: 'Basic types for computer graphics in OCaml',
1001 universe: U['gg.1.0.0'], require: ['gg'],
1002 sections: [
1003 { title: '2D Vectors',
1004 description: 'Gg.V2 provides 2D vector operations. Vectors are immutable float pairs.',
1005 steps: [
1006 { code: 'let v = Gg.V2.v 3.0 4.0;;', expect: 'Gg.v2',
1007 description: 'Create a 2D vector (3, 4)' },
1008 { code: 'Gg.V2.x v;;', expect: '3.',
1009 description: 'X component' },
1010 { code: 'Gg.V2.y v;;', expect: '4.',
1011 description: 'Y component' },
1012 { code: 'Gg.V2.norm v;;', expect: '5.',
1013 description: 'Vector magnitude: sqrt(9 + 16) = 5' },
1014 ] },
1015 { title: 'Vector Arithmetic',
1016 description: 'Vectors support addition, subtraction, scalar multiplication, and dot products.',
1017 steps: [
1018 { code: 'let a = Gg.V2.v 1.0 0.0;;', expect: 'Gg.v2',
1019 description: 'Unit vector along x-axis' },
1020 { code: 'let b = Gg.V2.v 0.0 1.0;;', expect: 'Gg.v2',
1021 description: 'Unit vector along y-axis' },
1022 { code: 'Gg.V2.add a b |> Gg.V2.x;;', expect: '1.',
1023 description: 'Addition: (1,0) + (0,1) → x = 1' },
1024 { code: 'Gg.V2.dot a b;;', expect: '0.',
1025 description: 'Dot product of perpendicular vectors = 0' },
1026 { code: 'Gg.V2.smul 3.0 a |> Gg.V2.x;;', expect: '3.',
1027 description: 'Scalar multiply: 3 * (1,0) → x = 3' },
1028 ] },
1029 { title: '3D Vectors and Cross Product',
1030 description: 'Gg.V3 adds a third dimension and the cross product operation.',
1031 steps: [
1032 { code: 'let i = Gg.V3.v 1.0 0.0 0.0;;', expect: 'Gg.v3',
1033 description: 'X-axis unit vector' },
1034 { code: 'let j = Gg.V3.v 0.0 1.0 0.0;;', expect: 'Gg.v3',
1035 description: 'Y-axis unit vector' },
1036 { code: 'let k = Gg.V3.cross i j;;', expect: 'Gg.v3',
1037 description: 'Cross product i × j = k (z-axis)' },
1038 { code: 'Gg.V3.z k;;', expect: '1.',
1039 description: 'Z component is 1 (right-hand rule)' },
1040 ] },
1041 { title: 'Colors',
1042 description: 'Gg.Color represents colors in linear sRGB with alpha.',
1043 steps: [
1044 { code: 'Gg.Color.red;;', expect: 'Gg.color',
1045 description: 'Predefined red color' },
1046 { code: 'Gg.Color.r Gg.Color.red;;', expect: '1.',
1047 description: 'Red component = 1.0' },
1048 { code: 'Gg.Color.g Gg.Color.red;;', expect: '0.',
1049 description: 'Green component = 0.0' },
1050 { code: 'let c = Gg.Color.v 0.5 0.8 0.2 1.0;;', expect: 'Gg.color',
1051 description: 'Create a custom RGBA color' },
1052 { code: 'Gg.Color.a c;;', expect: '1.',
1053 description: 'Alpha component' },
1054 ] },
1055 ],
1056 },
1057
1058 // ═══════════════════════════════════════════════════════════════════════
1059 // Vg
1060 // ═══════════════════════════════════════════════════════════════════════
1061 'vg.0.9.5': {
1062 name: 'Vg', version: '0.9.5', opam: 'vg',
1063 description: 'Declarative 2D vector graphics for OCaml',
1064 universe: U['vg.0.9.5'], require: ['vg', 'gg'],
1065 sections: [
1066 { title: 'Building Paths',
1067 description: 'Vg.P builds immutable path values by chaining operations on P.empty.',
1068 steps: [
1069 { code: 'let p = Vg.P.empty;;', expect: 'Vg.path',
1070 description: 'Start with an empty path' },
1071 { code: 'let p = Vg.P.empty |> Vg.P.sub (Gg.P2.v 0. 0.) |> Vg.P.line (Gg.P2.v 1. 1.);;',
1072 expect: 'Vg.path', description: 'A line from (0,0) to (1,1)' },
1073 { code: 'let circ = Vg.P.empty |> Vg.P.circle (Gg.P2.v 0.5 0.5) 0.3;;',
1074 expect: 'Vg.path', description: 'A circle centered at (0.5, 0.5) with radius 0.3' },
1075 { code: 'let rect = Vg.P.empty |> Vg.P.rect (Gg.Box2.v (Gg.P2.v 0. 0.) (Gg.Size2.v 1. 1.));;',
1076 expect: 'Vg.path', description: 'A unit rectangle' },
1077 ] },
1078 { title: 'Creating Images',
1079 description: 'Vg.I constructs images from colors, paths, and compositing operations.',
1080 steps: [
1081 { code: 'let red_fill = Vg.I.const Gg.Color.red;;', expect: 'Vg.image',
1082 description: 'A solid red infinite image' },
1083 { code: 'let red_circle = Vg.I.cut circ red_fill;;', expect: 'Vg.image',
1084 description: 'Cut the red fill to the circle path' },
1085 { code: 'let blue_rect = Vg.I.cut rect (Vg.I.const Gg.Color.blue);;', expect: 'Vg.image',
1086 description: 'A blue rectangle' },
1087 ] },
1088 { title: 'Compositing Images',
1089 description: 'Vg.I.blend composites images. I.tr applies affine transforms via Gg.M3 matrices.',
1090 steps: [
1091 { code: 'let scene = Vg.I.blend red_circle blue_rect;;', expect: 'Vg.image',
1092 description: 'Blend circle over rectangle' },
1093 { code: 'Vg.I.void;;', expect: 'Vg.image',
1094 description: 'The empty (transparent) image' },
1095 { code: 'let moved = Vg.I.move (Gg.V2.v 0.5 0.5) red_circle;;', expect: 'Vg.image',
1096 description: 'Translate the circle by (0.5, 0.5)' },
1097 ] },
1098 ],
1099 },
1100
1101 // ═══════════════════════════════════════════════════════════════════════
1102 // Note
1103 // ═══════════════════════════════════════════════════════════════════════
1104 'note.0.0.3': {
1105 name: 'Note', version: '0.0.3', opam: 'note',
1106 description: 'Declarative events and signals for OCaml',
1107 universe: U['note.0.0.3'], require: ['note'],
1108 sections: [
1109 { title: 'Constant Signals',
1110 description: 'Note.S.const creates a signal with a fixed value. Signals always have a current value.',
1111 steps: [
1112 { code: 'let s = Note.S.const 42;;', expect: 'Note.signal',
1113 description: 'A constant signal with value 42' },
1114 { code: 'Note.S.value s;;', expect: '42',
1115 description: 'Read the signal value' },
1116 ] },
1117 { title: 'Mutable Signals',
1118 description: 'Note.S.create returns a signal and a setter function for updating the value.',
1119 steps: [
1120 { code: 'let counter, set_counter = Note.S.create 0;;', expect: 'Note.signal',
1121 description: 'Create a mutable signal starting at 0' },
1122 { code: 'Note.S.value counter;;', expect: '0',
1123 description: 'Initial value' },
1124 { code: 'set_counter 10;;', expect: 'unit',
1125 description: 'Update the value to 10' },
1126 { code: 'Note.S.value counter;;', expect: '10',
1127 description: 'Value has changed' },
1128 ] },
1129 { title: 'Signal Transformations',
1130 description: 'Note.S.map and Note.S.l2 derive new signals from existing ones.',
1131 steps: [
1132 { code: 'let doubled = Note.S.map (( * ) 2) counter;;', expect: 'Note.signal',
1133 description: 'A derived signal: always 2x the counter' },
1134 { code: 'Note.S.value doubled;;', expect: '20',
1135 description: '10 * 2 = 20' },
1136 { code: 'let label = Note.S.map (fun n -> Printf.sprintf "count=%d" n) counter;;',
1137 expect: 'Note.signal', description: 'Map counter to a string label' },
1138 { code: 'Note.S.value label;;', expect: '"count=10"',
1139 description: 'Label reflects the current counter value' },
1140 { code: 'let sum = Note.S.l2 ( + ) counter doubled;;', expect: 'Note.signal',
1141 description: 'Combine two signals with l2' },
1142 { code: 'Note.S.value sum;;', expect: '30',
1143 description: '10 + 20 = 30' },
1144 ] },
1145 ],
1146 },
1147
1148 // ═══════════════════════════════════════════════════════════════════════
1149 // Otfm
1150 // ═══════════════════════════════════════════════════════════════════════
1151 'otfm.0.4.0': {
1152 name: 'Otfm', version: '0.4.0', opam: 'otfm',
1153 description: 'OpenType font decoder for OCaml',
1154 universe: U['otfm.0.4.0'], require: ['otfm'],
1155 sections: [
1156 { title: 'Decoder Creation',
1157 description: 'Otfm.decoder creates a decoder from font byte data. Most operations require valid font data.',
1158 steps: [
1159 { code: 'Otfm.decoder;;', expect: '-> Otfm.decoder',
1160 description: 'The decoder constructor (takes a `String source)' },
1161 { code: 'let d = Otfm.decoder (`String "");;', expect: 'Otfm.decoder',
1162 description: 'Create a decoder (with empty data for exploration)' },
1163 ] },
1164 { title: 'Querying Font Data',
1165 description: 'With valid font data, you can query tables, glyph counts, and PostScript names.',
1166 steps: [
1167 { code: 'Otfm.flavour d;;', expect: 'Error',
1168 description: 'Flavour fails on empty data (expected)' },
1169 { code: 'Otfm.postscript_name d;;', expect: '',
1170 description: 'PostScript name query (fails gracefully on empty data)' },
1171 { code: 'Otfm.glyph_count d;;', expect: '',
1172 description: 'Glyph count query' },
1173 ] },
1174 ],
1175 },
1176
1177 // ═══════════════════════════════════════════════════════════════════════
1178 // Fpath
1179 // ═══════════════════════════════════════════════════════════════════════
1180 'fpath.0.7.3': {
1181 name: 'Fpath', version: '0.7.3', opam: 'fpath',
1182 description: 'File system paths for OCaml',
1183 universe: U['fpath.0.7.3'], require: ['fpath'],
1184 sections: [
1185 { title: 'Creating Paths',
1186 description: 'Fpath.v creates a path from a string. Paths are validated on creation.',
1187 steps: [
1188 { code: 'Fpath.v "/usr/local/bin";;', expect: 'Fpath.t',
1189 description: 'Create an absolute path' },
1190 { code: 'Fpath.v "/usr/local/bin" |> Fpath.to_string;;', expect: '"/usr/local/bin"',
1191 description: 'Convert back to string' },
1192 { code: 'Fpath.v "src/main.ml";;', expect: 'Fpath.t',
1193 description: 'A relative path' },
1194 ] },
1195 { title: 'Path Composition',
1196 description: 'Fpath.(/) appends a segment. Paths compose naturally.',
1197 steps: [
1198 { code: 'Fpath.(v "/usr" / "local" / "bin") |> Fpath.to_string;;', expect: '"/usr/local/bin"',
1199 description: 'Build paths by appending segments' },
1200 { code: 'Fpath.(v "src" / "lib" / "main.ml") |> Fpath.to_string;;', expect: '"src/lib/main.ml"',
1201 description: 'Relative path composition' },
1202 ] },
1203 { title: 'Path Components',
1204 description: 'Extract parts of a path: parent directory, basename, filename.',
1205 steps: [
1206 { code: 'Fpath.v "/usr/local/bin" |> Fpath.parent |> Fpath.to_string;;', expect: '"/usr/local/"',
1207 description: 'Parent directory' },
1208 { code: 'Fpath.v "/usr/local/bin" |> Fpath.basename;;', expect: '"bin"',
1209 description: 'Basename (last segment)' },
1210 { code: 'Fpath.v "/usr/local/bin" |> Fpath.filename;;', expect: '"bin"',
1211 description: 'Filename (last non-empty segment)' },
1212 { code: 'Fpath.v "/a/b/" |> Fpath.basename;;', expect: '"b"',
1213 description: 'Basename of a directory path' },
1214 { code: 'Fpath.segs (Fpath.v "/a/b/c");;', expect: '[""; "a"; "b"; "c"]',
1215 description: 'All segments (empty first = absolute)' },
1216 ] },
1217 { title: 'File Extensions',
1218 description: 'Query and manipulate file extensions.',
1219 steps: [
1220 { code: 'Fpath.has_ext ".ml" (Fpath.v "main.ml");;', expect: 'true',
1221 description: 'Check for .ml extension' },
1222 { code: 'Fpath.get_ext (Fpath.v "archive.tar.gz");;', expect: '".gz"',
1223 description: 'Get the last extension' },
1224 { code: 'Fpath.get_ext ~multi:true (Fpath.v "archive.tar.gz");;', expect: '".tar.gz"',
1225 description: 'Get the full multi-extension' },
1226 { code: 'Fpath.rem_ext (Fpath.v "main.ml") |> Fpath.to_string;;', expect: '"main"',
1227 description: 'Remove the extension' },
1228 ] },
1229 { title: 'Path Properties',
1230 description: 'Test whether paths are absolute, relative, file paths, or directory paths.',
1231 steps: [
1232 { code: 'Fpath.is_abs (Fpath.v "/usr/bin");;', expect: 'true',
1233 description: 'Absolute path check' },
1234 { code: 'Fpath.is_rel (Fpath.v "src/main.ml");;', expect: 'true',
1235 description: 'Relative path check' },
1236 { code: 'Fpath.is_dir_path (Fpath.v "/usr/bin/");;', expect: 'true',
1237 description: 'Directory path (ends with /)' },
1238 { code: 'Fpath.is_file_path (Fpath.v "/usr/bin");;', expect: 'true',
1239 description: 'File path (does not end with /)' },
1240 { code: 'Fpath.normalize (Fpath.v "/a/b/../c") |> Fpath.to_string;;', expect: '"/a/c"',
1241 description: 'Normalize resolves .. components' },
1242 ] },
1243 ],
1244 },
1245
1246 // ═══════════════════════════════════════════════════════════════════════
1247 // Uutf
1248 // ═══════════════════════════════════════════════════════════════════════
1249 'uutf.1.0.4': {
1250 name: 'Uutf', version: '1.0.4', opam: 'uutf',
1251 description: 'Non-blocking streaming Unicode codec for OCaml',
1252 universe: U['uutf.1.0.4'], require: ['uutf'],
1253 sections: [
1254 { title: 'UTF-8 Decoding',
1255 description: 'Uutf.decoder creates a streaming decoder. Each Uutf.decode call returns one character or a signal.',
1256 steps: [
1257 { code: 'let d = Uutf.decoder ~encoding:`UTF_8 (`String "ABC");;', expect: 'Uutf.decoder',
1258 description: 'Create a UTF-8 decoder for "ABC"' },
1259 { code: 'Uutf.decode d;;', expect: '`Uchar',
1260 description: 'Decode first character: A' },
1261 { code: 'Uutf.decode d;;', expect: '`Uchar',
1262 description: 'Decode second character: B' },
1263 { code: 'Uutf.decode d;;', expect: '`Uchar',
1264 description: 'Decode third character: C' },
1265 { code: 'Uutf.decode d;;', expect: '`End',
1266 description: 'End of input' },
1267 ] },
1268 { title: 'Multi-byte Characters',
1269 description: 'UTF-8 encodes non-ASCII characters in multiple bytes. Uutf handles this transparently.',
1270 steps: [
1271 { code: 'let d2 = Uutf.decoder ~encoding:`UTF_8 (`String "caf\\xC3\\xA9");;',
1272 expect: 'Uutf.decoder', description: 'Decode "cafe" with e-acute (U+00E9)' },
1273 { code: 'Uutf.decode d2;;', expect: '`Uchar',
1274 description: 'c' },
1275 { code: 'Uutf.decode d2;;', expect: '`Uchar',
1276 description: 'a' },
1277 { code: 'Uutf.decode d2;;', expect: '`Uchar',
1278 description: 'f' },
1279 { code: 'Uutf.decode d2;;', expect: '`Uchar',
1280 description: 'e-acute (U+00E9, decoded from 2 bytes)' },
1281 ] },
1282 { title: 'UTF-8 Encoding',
1283 description: 'Uutf.encoder writes Unicode characters to a buffer in a specified encoding.',
1284 steps: [
1285 { code: 'let buf = Buffer.create 16;;', expect: 'Buffer.t',
1286 description: 'Create an output buffer' },
1287 { code: 'let e = Uutf.encoder `UTF_8 (`Buffer buf);;', expect: 'Uutf.encoder',
1288 description: 'Create a UTF-8 encoder' },
1289 { code: 'Uutf.encode e (`Uchar (Uchar.of_int 0x41));;', expect: '`Ok',
1290 description: "Encode 'A'" },
1291 { code: 'Uutf.encode e (`Uchar (Uchar.of_int 0xE9));;', expect: '`Ok',
1292 description: "Encode e-acute" },
1293 { code: 'Uutf.encode e `End;;', expect: '`Ok',
1294 description: 'Flush the encoder' },
1295 { code: 'Buffer.length buf;;', expect: '3',
1296 description: "A (1 byte) + e-acute (2 bytes) = 3 bytes" },
1297 ] },
1298 ],
1299 },
1300
1301 // ═══════════════════════════════════════════════════════════════════════
1302 // B0
1303 // ═══════════════════════════════════════════════════════════════════════
1304 'b0.0.0.6': {
1305 name: 'B0', version: '0.0.6', opam: 'b0',
1306 description: 'Software construction and deployment kit',
1307 universe: U['b0.0.0.6'], require: ['b0.std'],
1308 sections: [
1309 { title: 'File Paths (B0_std.Fpath)',
1310 description: 'B0_std provides its own Fpath module for file path manipulation.',
1311 steps: [
1312 { code: 'B0_std.Fpath.v "/usr/bin";;', expect: 'B0_std.Fpath.t',
1313 description: 'Create a path' },
1314 { code: 'B0_std.Fpath.(v "/usr" / "local" / "bin") |> B0_std.Fpath.to_string;;',
1315 expect: '"/usr/local/bin"', description: 'Path composition with (/)' },
1316 { code: 'B0_std.Fpath.basename (B0_std.Fpath.v "/usr/local/bin");;', expect: '"bin"',
1317 description: 'Get the basename' },
1318 { code: 'B0_std.Fpath.parent (B0_std.Fpath.v "/usr/local/bin") |> B0_std.Fpath.to_string;;',
1319 expect: '"/usr/local/"', description: 'Get parent directory' },
1320 ] },
1321 { title: 'Command Lines (B0_std.Cmd)',
1322 description: 'B0_std.Cmd builds command-line invocations declaratively.',
1323 steps: [
1324 { code: 'let cmd = B0_std.Cmd.(tool "ocamlfind" % "query" % "-format" % "%d" % "fmt");;',
1325 expect: 'B0_std.Cmd.t', description: 'Build a command line' },
1326 { code: 'B0_std.Cmd.to_list cmd;;', expect: '["ocamlfind"',
1327 description: 'Convert to a list of strings' },
1328 { code: 'B0_std.Cmd.is_empty B0_std.Cmd.empty;;', expect: 'true',
1329 description: 'Check for empty command' },
1330 ] },
1331 ],
1332 },
1333
1334 // ═══════════════════════════════════════════════════════════════════════
1335 // Bos
1336 // ═══════════════════════════════════════════════════════════════════════
1337 // ═══════════════════════════════════════════════════════════════════════
1338 // Yojson
1339 // ═══════════════════════════════════════════════════════════════════════
1340 'yojson.1.7.0': {
1341 name: 'Yojson', version: '1.7.0', opam: 'yojson',
1342 description: 'JSON parsing and printing for OCaml (1.x API)',
1343 universe: U['yojson.1.7.0'], require: ['yojson'],
1344 sections: [
1345 { title: 'Parsing JSON',
1346 description: 'Yojson.Safe.from_string parses a JSON string into an algebraic type.',
1347 steps: [
1348 { code: 'Yojson.Safe.from_string {|{"name": "Alice", "age": 30}|};;',
1349 expect: '`Assoc', description: 'Parse a JSON object' },
1350 { code: 'Yojson.Safe.from_string "[1, 2, 3]";;',
1351 expect: '`List', description: 'Parse a JSON array' },
1352 { code: 'Yojson.Safe.from_string "42";;',
1353 expect: '`Int 42', description: 'Parse a JSON number' },
1354 ] },
1355 { title: 'Building JSON',
1356 description: 'JSON values are polymorphic variants: `Null, `Bool, `Int, `Float, `String, `List, `Assoc.',
1357 steps: [
1358 { code: 'Yojson.Safe.to_string (`Assoc [("x", `Int 1); ("y", `Int 2)]);;',
1359 expect: '"x"', description: 'Serialize an object to string' },
1360 { code: 'Yojson.Safe.to_string (`List [`String "a"; `Bool true]);;',
1361 expect: '"a"', description: 'Serialize a list' },
1362 ] },
1363 { title: 'Util Module',
1364 description: 'Yojson.Safe.Util provides accessor functions for extracting values from JSON.',
1365 steps: [
1366 { code: 'let j = Yojson.Safe.from_string {|{"name": "Bob"}|};;', expect: '`Assoc',
1367 description: 'Parse a JSON object' },
1368 { code: 'Yojson.Safe.Util.member "name" j;;', expect: '`String "Bob"',
1369 description: 'Extract a field by name' },
1370 { code: 'Yojson.Safe.Util.member "name" j |> Yojson.Safe.Util.to_string;;',
1371 expect: '"Bob"', description: 'Extract as an OCaml string' },
1372 { code: 'Yojson.Safe.Util.keys (`Assoc [("a", `Int 1); ("b", `Int 2)]);;',
1373 expect: '["a"; "b"]', description: 'Get all keys of an object' },
1374 ] },
1375 ],
1376 },
1377
1378 'yojson.2.0.2': {
1379 name: 'Yojson', version: '2.0.2', opam: 'yojson',
1380 description: 'JSON parsing and printing for OCaml (2.x API)',
1381 universe: U['yojson.2.0.2'], require: ['yojson'],
1382 sections: [
1383 { title: 'Parsing JSON',
1384 description: 'Yojson 2.x removed biniou dependency. The core API is the same.',
1385 steps: [
1386 { code: 'Yojson.Safe.from_string {|{"key": "value"}|};;',
1387 expect: '`Assoc', description: 'Parse a JSON object' },
1388 { code: 'Yojson.Safe.from_string "true";;',
1389 expect: '`Bool true', description: 'Parse a boolean' },
1390 ] },
1391 { title: 'Building and Serializing',
1392 steps: [
1393 { code: 'Yojson.Safe.to_string (`Assoc [("n", `Int 42)]);;',
1394 expect: '"n"', description: 'Serialize to compact JSON' },
1395 { code: 'Yojson.Safe.pretty_to_string (`Assoc [("n", `Int 42)]);;',
1396 expect: '"n"', description: 'Pretty-print with indentation' },
1397 ] },
1398 { title: 'Util Accessors',
1399 description: 'Extract typed values from JSON trees.',
1400 steps: [
1401 { code: 'Yojson.Safe.Util.to_int (`Int 42);;', expect: '42',
1402 description: 'Extract an int' },
1403 { code: 'Yojson.Safe.Util.to_list (`List [`Int 1; `Int 2]);;',
1404 expect: '[`Int 1', description: 'Extract a list' },
1405 { code: 'Yojson.Safe.Util.to_bool (`Bool true);;', expect: 'true',
1406 description: 'Extract a bool' },
1407 { code: 'Yojson.Safe.Util.member "x" (`Assoc [("x", `Float 3.14)]);;',
1408 expect: '`Float 3.14', description: 'Navigate into an object' },
1409 ] },
1410 ],
1411 },
1412
1413 'yojson.2.1.2': {
1414 name: 'Yojson', version: '2.1.2', opam: 'yojson',
1415 description: 'JSON parsing and printing for OCaml',
1416 universe: U['yojson.2.1.2'], require: ['yojson'],
1417 sections: [
1418 { title: 'Parsing and Serializing',
1419 steps: [
1420 { code: 'Yojson.Safe.from_string {|[1, "two", true]|};;',
1421 expect: '`List', description: 'Parse a heterogeneous array' },
1422 { code: 'Yojson.Safe.to_string (`List [`Int 1; `String "two"; `Bool true]);;',
1423 expect: '1', description: 'Serialize back to JSON' },
1424 ] },
1425 { title: 'Util Navigation',
1426 steps: [
1427 { code: 'let data = Yojson.Safe.from_string {|{"users": [{"name": "A"}, {"name": "B"}]}|};;',
1428 expect: '`Assoc', description: 'Parse nested JSON' },
1429 { code: 'Yojson.Safe.Util.(member "users" data |> to_list |> List.map (member "name"));;',
1430 expect: '[`String "A"', description: 'Navigate and extract nested values' },
1431 { code: 'Yojson.Safe.Util.(member "users" data |> index 0 |> member "name" |> to_string);;',
1432 expect: '"A"', description: 'Index into arrays' },
1433 ] },
1434 ],
1435 },
1436
1437 'yojson.2.2.2': {
1438 name: 'Yojson', version: '2.2.2', opam: 'yojson',
1439 description: 'JSON parsing and printing for OCaml',
1440 universe: U['yojson.2.2.2'], require: ['yojson'],
1441 sections: [
1442 { title: 'Round-Trip JSON',
1443 steps: [
1444 { code: 'let j = `Assoc [("list", `List [`Int 1; `Int 2; `Int 3])];;',
1445 expect: '`Assoc', description: 'Build a JSON value' },
1446 { code: 'let s = Yojson.Safe.to_string j;;', expect: 'string',
1447 description: 'Serialize to string' },
1448 { code: 'Yojson.Safe.from_string s = j;;', expect: 'true',
1449 description: 'Round-trip preserves structure' },
1450 ] },
1451 { title: 'Util Combinators',
1452 steps: [
1453 { code: 'Yojson.Safe.Util.combine (`Assoc [("a", `Int 1)]) (`Assoc [("b", `Int 2)]);;',
1454 expect: '`Assoc', description: 'Merge two objects' },
1455 { code: 'Yojson.Safe.Util.to_assoc (`Assoc [("x", `Int 1)]);;',
1456 expect: '[("x"', description: 'Convert to association list' },
1457 { code: 'Yojson.Safe.Util.filter_member "name" [`Assoc [("name", `String "A")]; `Assoc []];;',
1458 expect: '[`String "A"', description: 'Filter objects by field presence' },
1459 ] },
1460 ],
1461 },
1462
1463 'yojson.3.0.0': {
1464 name: 'Yojson', version: '3.0.0', opam: 'yojson',
1465 description: 'JSON parsing and printing for OCaml (3.x, strict types)',
1466 universe: U['yojson.3.0.0'], require: ['yojson'],
1467 sections: [
1468 { title: 'Strict JSON Types',
1469 description: 'Yojson 3.0 removed non-standard Tuple and Variant constructors from Safe.t.',
1470 steps: [
1471 { code: 'Yojson.Safe.from_string {|{"clean": true}|};;',
1472 expect: '`Assoc', description: 'Parse standard JSON' },
1473 { code: 'Yojson.Safe.to_string (`Assoc [("v", `Intlit "999999999999999")]);;',
1474 expect: '999999999999999', description: 'Intlit preserves large integers as strings' },
1475 ] },
1476 { title: 'Util Module',
1477 steps: [
1478 { code: 'Yojson.Safe.Util.member "x" (`Assoc [("x", `Null)]);;',
1479 expect: '`Null', description: 'Access a null field' },
1480 { code: 'Yojson.Safe.Util.values (`Assoc [("a", `Int 1); ("b", `Int 2)]);;',
1481 expect: '[`Int 1', description: 'Extract all values' },
1482 { code: 'Yojson.Safe.Util.to_string_option (`Null);;', expect: 'None',
1483 description: 'Safe accessor returns None for wrong type' },
1484 { code: 'Yojson.Safe.Util.to_string_option (`String "hi");;', expect: 'Some "hi"',
1485 description: 'Safe accessor returns Some for correct type' },
1486 ] },
1487 ],
1488 },
1489
1490 // ═══════════════════════════════════════════════════════════════════════
1491 // Ezjsonm
1492 // ═══════════════════════════════════════════════════════════════════════
1493 'ezjsonm.1.1.0': {
1494 name: 'Ezjsonm', version: '1.1.0', opam: 'ezjsonm',
1495 description: 'Easy JSON manipulation for OCaml',
1496 universe: U['ezjsonm.1.1.0'], require: ['ezjsonm'],
1497 sections: [
1498 { title: 'Building Values',
1499 description: 'Ezjsonm provides typed constructors for JSON values.',
1500 steps: [
1501 { code: 'Ezjsonm.string "hello";;', expect: '`String "hello"',
1502 description: 'Create a JSON string' },
1503 { code: 'Ezjsonm.int 42;;', expect: '`Float 42.',
1504 description: 'Create a JSON number (stored as float internally)' },
1505 { code: 'Ezjsonm.bool true;;', expect: '`Bool true',
1506 description: 'Create a JSON boolean' },
1507 { code: 'Ezjsonm.list Ezjsonm.int [1; 2; 3];;', expect: '`A',
1508 description: 'Create a JSON array from a list' },
1509 ] },
1510 { title: 'Serialization',
1511 description: 'Convert values to and from strings.',
1512 steps: [
1513 { code: 'Ezjsonm.value_to_string (Ezjsonm.string "hi");;', expect: 'string',
1514 description: 'Serialize a value (JSON-encoded string with quotes)' },
1515 { code: 'Ezjsonm.value_to_string (Ezjsonm.list Ezjsonm.int [1;2;3]);;',
1516 expect: '[1', description: 'Serialize an array' },
1517 { code: 'Ezjsonm.value_from_string "42";;', expect: '`Float 42.',
1518 description: 'Parse a JSON value from string' },
1519 ] },
1520 { title: 'Extracting Values',
1521 description: 'get_* functions extract OCaml values from JSON.',
1522 steps: [
1523 { code: 'Ezjsonm.get_string (Ezjsonm.string "test");;', expect: '"test"',
1524 description: 'Extract a string' },
1525 { code: 'Ezjsonm.get_int (Ezjsonm.int 42);;', expect: '42',
1526 description: 'Extract an int' },
1527 { code: 'Ezjsonm.get_list Ezjsonm.get_int (Ezjsonm.list Ezjsonm.int [1;2;3]);;',
1528 expect: '[1; 2; 3]', description: 'Extract a list of ints' },
1529 ] },
1530 ],
1531 },
1532
1533 'ezjsonm.1.2.0': {
1534 name: 'Ezjsonm', version: '1.2.0', opam: 'ezjsonm',
1535 description: 'Easy JSON manipulation for OCaml',
1536 universe: U['ezjsonm.1.2.0'], require: ['ezjsonm'],
1537 sections: [
1538 { title: 'Building and Querying',
1539 steps: [
1540 { code: 'let doc = Ezjsonm.dict [("name", Ezjsonm.string "Alice"); ("age", Ezjsonm.int 30)];;',
1541 expect: '`O', description: 'Build a JSON object with dict' },
1542 { code: 'Ezjsonm.value_to_string doc;;', expect: 'string',
1543 description: 'Serialize the object to JSON' },
1544 { code: 'Ezjsonm.get_dict doc;;', expect: '[("name"',
1545 description: 'Extract as association list' },
1546 ] },
1547 { title: 'Navigating Documents',
1548 description: 'Ezjsonm.find navigates into nested JSON using a path of string keys.',
1549 steps: [
1550 { code: 'let j = Ezjsonm.from_string {|{"user": {"name": "Bob"}}|};;',
1551 expect: '`O', description: 'Parse a nested document' },
1552 { code: 'Ezjsonm.find j ["user"; "name"];;', expect: '`String "Bob"',
1553 description: 'Navigate by key path' },
1554 { code: 'Ezjsonm.mem j ["user"; "name"];;', expect: 'true',
1555 description: 'Check if a path exists' },
1556 { code: 'Ezjsonm.find_opt j ["user"; "email"];;', expect: 'None',
1557 description: 'Safe navigation returns None for missing paths' },
1558 ] },
1559 ],
1560 },
1561
1562 'ezjsonm.1.3.0': {
1563 name: 'Ezjsonm', version: '1.3.0', opam: 'ezjsonm',
1564 description: 'Easy JSON manipulation for OCaml',
1565 universe: U['ezjsonm.1.3.0'], require: ['ezjsonm'],
1566 sections: [
1567 { title: 'Value Constructors',
1568 steps: [
1569 { code: 'Ezjsonm.string "hello";;', expect: '`String',
1570 description: 'A JSON string value' },
1571 { code: 'Ezjsonm.unit ();;', expect: '`Null',
1572 description: 'JSON null' },
1573 { code: 'Ezjsonm.list Ezjsonm.string ["a"; "b"];;', expect: '`A',
1574 description: 'Array of strings' },
1575 ] },
1576 { title: 'Documents',
1577 description: 'Documents (Ezjsonm.t) must be arrays or objects at the top level.',
1578 steps: [
1579 { code: 'let doc = Ezjsonm.from_string {|{"x": [1, 2, 3]}|};;', expect: '`O',
1580 description: 'Parse a document' },
1581 { code: 'Ezjsonm.find doc ["x"] |> Ezjsonm.get_list Ezjsonm.get_int;;',
1582 expect: '[1; 2; 3]', description: 'Navigate and extract typed values' },
1583 { code: 'Ezjsonm.to_string ~minify:false doc;;', expect: 'string',
1584 description: 'Pretty-print a document' },
1585 ] },
1586 ],
1587 },
1588
1589 // ═══════════════════════════════════════════════════════════════════════
1590 // Sexplib0
1591 // ═══════════════════════════════════════════════════════════════════════
1592 'sexplib0.v0.15.1': {
1593 name: 'Sexplib0', version: 'v0.15.1', opam: 'sexplib0',
1594 description: 'S-expression type and printing (minimal, no parsing)',
1595 universe: U['sexplib0.v0.15.1'], require: ['sexplib0'],
1596 sections: [
1597 { title: 'S-expression Type',
1598 description: 'Sexplib0.Sexp.t has two constructors: Atom of string and List of t list.',
1599 steps: [
1600 { code: 'Sexplib0.Sexp.Atom "hello";;', expect: 'Sexplib0.Sexp.t',
1601 description: 'An atomic S-expression' },
1602 { code: 'Sexplib0.Sexp.List [Atom "add"; Atom "1"; Atom "2"];;',
1603 expect: 'Sexplib0.Sexp.t', description: 'A list S-expression' },
1604 ] },
1605 { title: 'Printing',
1606 description: 'to_string produces compact output, to_string_hum produces indented output.',
1607 steps: [
1608 { code: 'Sexplib0.Sexp.to_string (List [Atom "name"; Atom "Alice"]);;',
1609 expect: '"(name Alice)"', description: 'Compact string representation' },
1610 { code: 'Sexplib0.Sexp.to_string_hum (List [Atom "config"; List [Atom "port"; Atom "8080"]]);;',
1611 expect: '(config', description: 'Human-readable indented output' },
1612 ] },
1613 { title: 'Comparison',
1614 steps: [
1615 { code: 'Sexplib0.Sexp.equal (Atom "x") (Atom "x");;', expect: 'true',
1616 description: 'Structural equality' },
1617 { code: 'Sexplib0.Sexp.equal (Atom "x") (Atom "y");;', expect: 'false',
1618 description: 'Different atoms are not equal' },
1619 ] },
1620 ],
1621 },
1622
1623 'sexplib0.v0.16.0': {
1624 name: 'Sexplib0', version: 'v0.16.0', opam: 'sexplib0',
1625 description: 'S-expression type and printing (minimal, no parsing)',
1626 universe: U['sexplib0.v0.16.0'], require: ['sexplib0'],
1627 sections: [
1628 { title: 'Building S-expressions',
1629 steps: [
1630 { code: 'open Sexplib0.Sexp;; let s = List [Atom "person"; List [Atom "name"; Atom "Bob"]; List [Atom "age"; Atom "25"]];;',
1631 expect: 'Sexplib0.Sexp.t', description: 'Build a nested S-expression' },
1632 { code: 'Sexplib0.Sexp.to_string s;;', expect: '"(person (name Bob) (age 25))"',
1633 description: 'Serialize to compact string' },
1634 { code: 'Sexplib0.Sexp.to_string_hum s;;', expect: '(person',
1635 description: 'Pretty-print with indentation' },
1636 ] },
1637 { title: 'Error Messages',
1638 description: 'Sexp.message builds structured error S-expressions.',
1639 steps: [
1640 { code: 'Sexplib0.Sexp.message "invalid input" ["value", Atom "42"; "expected", Atom "string"];;',
1641 expect: 'Sexplib0.Sexp.t', description: 'Build a structured error message' },
1642 ] },
1643 ],
1644 },
1645
1646 'sexplib0.v0.17.0': {
1647 name: 'Sexplib0', version: 'v0.17.0', opam: 'sexplib0',
1648 description: 'S-expression type and printing (minimal, no parsing)',
1649 universe: U['sexplib0.v0.17.0'], require: ['sexplib0'],
1650 sections: [
1651 { title: 'S-expression Basics',
1652 steps: [
1653 { code: 'let open Sexplib0.Sexp in Atom "hello";;', expect: 'Sexplib0.Sexp.t',
1654 description: 'An atom' },
1655 { code: 'let open Sexplib0.Sexp in List [Atom "list"; List [Atom "1"; Atom "2"; Atom "3"]];;',
1656 expect: 'Sexplib0.Sexp.t', description: 'Nested S-expression' },
1657 { code: 'Sexplib0.Sexp.(to_string (List [Atom "a"; Atom "b"; Atom "c"]));;',
1658 expect: '"(a b c)"', description: 'Serialize to string' },
1659 ] },
1660 { title: 'Comparison and Equality',
1661 steps: [
1662 { code: 'Sexplib0.Sexp.compare (Atom "a") (Atom "b");;', expect: '-1',
1663 description: 'Lexicographic comparison' },
1664 { code: 'Sexplib0.Sexp.equal (List [Atom "x"]) (List [Atom "x"]);;', expect: 'true',
1665 description: 'Deep structural equality' },
1666 ] },
1667 ],
1668 },
1669
1670 // ═══════════════════════════════════════════════════════════════════════
1671 // Csexp
1672 // ═══════════════════════════════════════════════════════════════════════
1673 'csexp.1.5.2': {
1674 name: 'Csexp', version: '1.5.2', opam: 'csexp',
1675 description: 'Canonical S-expressions (length-prefixed binary format)',
1676 universe: U['csexp.1.5.2'], require: ['csexp'],
1677 sections: [
1678 { title: 'Encoding',
1679 description: 'Canonical S-expressions use length-prefixed format: "5:hello" instead of "hello".',
1680 steps: [
1681 { code: 'Csexp.to_string (Csexp.Atom "hello");;', expect: '"5:hello"',
1682 description: 'Encode an atom (5 bytes, colon, data)' },
1683 { code: 'Csexp.to_string (Csexp.List [Csexp.Atom "a"; Csexp.Atom "bc"]);;',
1684 expect: '"(1:a2:bc)"', description: 'Encode a list' },
1685 { code: 'Csexp.serialised_length (Csexp.Atom "test");;', expect: '6',
1686 description: '"1:test" would be wrong; "4:test" = 6 bytes' },
1687 ] },
1688 { title: 'Decoding',
1689 description: 'parse_string decodes canonical S-expressions.',
1690 steps: [
1691 { code: 'Csexp.parse_string "5:hello";;', expect: 'Ok',
1692 description: 'Parse a single atom' },
1693 { code: 'Csexp.parse_string "(1:a2:bc)";;', expect: 'Ok',
1694 description: 'Parse a list' },
1695 { code: 'Csexp.parse_string_many "1:a1:b";;', expect: 'Ok',
1696 description: 'Parse multiple S-expressions' },
1697 { code: 'Csexp.parse_string "bad";;', expect: 'Error',
1698 description: 'Invalid input returns Error' },
1699 ] },
1700 ],
1701 },
1702
1703 // ═══════════════════════════════════════════════════════════════════════
1704 // Base64
1705 // ═══════════════════════════════════════════════════════════════════════
1706 'base64.3.4.0': {
1707 name: 'Base64', version: '3.4.0', opam: 'base64',
1708 description: 'Base64 encoding and decoding for OCaml',
1709 universe: U['base64.3.4.0'], require: ['base64'],
1710 sections: [
1711 { title: 'Encoding',
1712 steps: [
1713 { code: 'Base64.encode_string "Hello, World!";;', expect: 'SGVsbG8sIFdvcmxkIQ==',
1714 description: 'Encode a string to base64' },
1715 { code: 'Base64.encode_string "";;', expect: '""',
1716 description: 'Empty string encodes to empty' },
1717 { code: 'Base64.encode_string "a";;', expect: 'YQ==',
1718 description: 'Single character with padding' },
1719 ] },
1720 { title: 'Decoding',
1721 steps: [
1722 { code: 'Base64.decode_exn "SGVsbG8sIFdvcmxkIQ==";;', expect: '"Hello, World!"',
1723 description: 'Decode base64 back to string' },
1724 { code: 'Base64.decode "YQ==";;', expect: 'Ok "a"',
1725 description: 'Safe decode returns result' },
1726 { code: 'Base64.decode "!!invalid!!";;', expect: 'Error',
1727 description: 'Invalid base64 returns Error' },
1728 ] },
1729 ],
1730 },
1731
1732 'base64.3.5.2': {
1733 name: 'Base64', version: '3.5.2', opam: 'base64',
1734 description: 'Base64 encoding and decoding for OCaml',
1735 universe: U['base64.3.5.2'], require: ['base64'],
1736 sections: [
1737 { title: 'Standard Encoding',
1738 steps: [
1739 { code: 'Base64.encode_string "OCaml";;', expect: 'T0NhbWw=',
1740 description: 'Encode "OCaml" to base64' },
1741 { code: 'Base64.decode_exn "T0NhbWw=";;', expect: '"OCaml"',
1742 description: 'Decode back to original' },
1743 ] },
1744 { title: 'Round-Trip',
1745 steps: [
1746 { code: 'let test s = Base64.decode_exn (Base64.encode_string s) = s;;',
1747 expect: 'val test', description: 'Define a round-trip test function' },
1748 { code: 'test "hello world";;', expect: 'true',
1749 description: 'Round-trip preserves data' },
1750 { code: 'test "";;', expect: 'true',
1751 description: 'Empty string round-trips' },
1752 { code: 'test "\\x00\\xff";;', expect: 'true',
1753 description: 'Binary data round-trips' },
1754 ] },
1755 ],
1756 },
1757
1758 'bos.0.2.1': {
1759 name: 'Bos', version: '0.2.1', opam: 'bos',
1760 description: 'Basic OS interaction for OCaml',
1761 universe: U['bos.0.2.1'], require: ['bos'],
1762 sections: [
1763 { title: 'Command Construction',
1764 description: 'Bos.Cmd builds shell commands declaratively with type-safe combinators.',
1765 steps: [
1766 { code: 'let cmd = Bos.Cmd.(v "echo" % "hello" % "world");;', expect: 'Bos.Cmd.t',
1767 description: 'Build: echo hello world' },
1768 { code: 'Bos.Cmd.to_string cmd;;', expect: 'echo',
1769 description: 'Convert to shell string' },
1770 { code: 'Bos.Cmd.to_list cmd;;', expect: '["echo"; "hello"; "world"]',
1771 description: 'Convert to argument list' },
1772 ] },
1773 { title: 'Command Combinators',
1774 description: 'Commands support appending, conditional inclusion, and inspection.',
1775 steps: [
1776 { code: 'let base = Bos.Cmd.(v "gcc" % "-O2");;', expect: 'Bos.Cmd.t',
1777 description: 'Base compiler command' },
1778 { code: 'let full = Bos.Cmd.(base % "-o" % "main" %% v "main.c");;', expect: 'Bos.Cmd.t',
1779 description: 'Append arguments and a sub-command' },
1780 { code: 'Bos.Cmd.to_list full;;', expect: '["gcc"',
1781 description: 'Full argument list' },
1782 { code: 'Bos.Cmd.line_tool full;;', expect: 'Some "gcc"',
1783 description: 'Extract the tool name' },
1784 { code: 'Bos.Cmd.is_empty Bos.Cmd.empty;;', expect: 'true',
1785 description: 'Empty command check' },
1786 ] },
1787 { title: 'Conditional Arguments',
1788 description: 'Bos.Cmd.on conditionally includes arguments.',
1789 steps: [
1790 { code: 'let debug = true;;', expect: 'true',
1791 description: 'A debug flag' },
1792 { code: 'Bos.Cmd.(v "gcc" %% on debug (v "-g") % "main.c") |> Bos.Cmd.to_list;;',
1793 expect: '["gcc"; "-g"; "main.c"]', description: 'Debug flag is included when true' },
1794 { code: 'Bos.Cmd.(v "gcc" %% on false (v "-g") % "main.c") |> Bos.Cmd.to_list;;',
1795 expect: '["gcc"; "main.c"]', description: 'Debug flag is omitted when false' },
1796 ] },
1797 ],
1798 },
1799
1800 // ═══════════════════════════════════════════════════════════════════════
1801 // Re (regular expressions)
1802 // ═══════════════════════════════════════════════════════════════════════
1803 're.1.10.4': {
1804 name: 'Re', version: '1.10.4', opam: 're',
1805 description: 'Regular expression library for OCaml',
1806 universe: U['re.1.10.4'], require: ['re'],
1807 sections: [
1808 { title: 'Compiling and Matching',
1809 description: 'Re works in two steps: build a regex value, then compile it before matching.',
1810 steps: [
1811 { code: 'let re = Re.Pcre.re "\\\\d+" |> Re.compile;;', expect: 'Re.re',
1812 description: 'Compile a PCRE-style regex for digits' },
1813 { code: 'Re.execp re "abc123";;', expect: 'true',
1814 description: 'Test if the string matches anywhere' },
1815 { code: 'Re.execp re "no digits";;', expect: 'false',
1816 description: 'No match returns false' },
1817 ] },
1818 { title: 'Extracting Matches',
1819 description: 'Re.exec returns a group object, and Re.Group.get extracts matched substrings.',
1820 steps: [
1821 { code: 'let g = Re.exec re "abc123def";;', expect: 'Re.Group.t',
1822 description: 'Execute and get the match group' },
1823 { code: 'Re.Group.get g 0;;', expect: '"123"',
1824 description: 'Group 0 is the whole match' },
1825 ] },
1826 { title: 'Finding All Matches',
1827 steps: [
1828 { code: 'Re.all re "a1b22c333" |> List.map (fun g -> Re.Group.get g 0);;',
1829 expect: '["1"; "22"; "333"]', description: 'Find all digit sequences' },
1830 { code: 'Re.split (Re.compile (Re.Pcre.re ",")) "a,b,c";;',
1831 expect: '["a"; "b"; "c"]', description: 'Split on comma' },
1832 ] },
1833 ],
1834 },
1835
1836 're.1.11.0': {
1837 name: 'Re', version: '1.11.0', opam: 're',
1838 description: 'Regular expression library for OCaml',
1839 universe: U['re.1.11.0'], require: ['re'],
1840 sections: [
1841 { title: 'PCRE Syntax',
1842 steps: [
1843 { code: 'let word = Re.Pcre.re "[a-zA-Z]+" |> Re.compile;;', expect: 'Re.re',
1844 description: 'Compile a word pattern' },
1845 { code: 'Re.all word "hello world" |> List.map (fun g -> Re.Group.get g 0);;',
1846 expect: '["hello"; "world"]', description: 'Find all words' },
1847 ] },
1848 { title: 'Replacement',
1849 steps: [
1850 { code: 'Re.replace_string (Re.compile (Re.Pcre.re "\\\\d+")) ~by:"N" "abc123def456";;',
1851 expect: '"abcNdefN"', description: 'Replace all digit sequences' },
1852 ] },
1853 { title: 'Combinatorial API',
1854 description: 'Re also has a combinator API for building regexes without string syntax.',
1855 steps: [
1856 { code: 'let re = Re.(seq [bos; rep1 digit; eos]) |> Re.compile;;', expect: 'Re.re',
1857 description: 'Match strings that are all digits' },
1858 { code: 'Re.execp re "12345";;', expect: 'true',
1859 description: 'All digits matches' },
1860 { code: 'Re.execp re "123abc";;', expect: 'false',
1861 description: 'Mixed string does not match' },
1862 ] },
1863 ],
1864 },
1865
1866 're.1.12.0': {
1867 name: 'Re', version: '1.12.0', opam: 're',
1868 description: 'Regular expression library for OCaml',
1869 universe: U['re.1.12.0'], require: ['re'],
1870 sections: [
1871 { title: 'Pattern Matching',
1872 steps: [
1873 { code: 'let email_re = Re.Pcre.re "[^@]+@[^@]+" |> Re.compile;;', expect: 'Re.re',
1874 description: 'Simple email pattern' },
1875 { code: 'Re.execp email_re "user@example.com";;', expect: 'true',
1876 description: 'Matches an email-like string' },
1877 { code: 'Re.execp email_re "not-an-email";;', expect: 'false',
1878 description: 'No @ sign means no match' },
1879 ] },
1880 { title: 'Groups',
1881 description: 'Capture groups extract sub-matches.',
1882 steps: [
1883 { code: 'let kv = Re.Pcre.re "(\\\\w+)=(\\\\w+)" |> Re.compile;;', expect: 'Re.re',
1884 description: 'Key=value pattern with groups' },
1885 { code: 'let g = Re.exec kv "name=Alice";;', expect: 'Re.Group.t',
1886 description: 'Execute the match' },
1887 { code: 'Re.Group.get g 1;;', expect: '"name"',
1888 description: 'Group 1: the key' },
1889 { code: 'Re.Group.get g 2;;', expect: '"Alice"',
1890 description: 'Group 2: the value' },
1891 ] },
1892 ],
1893 },
1894
1895 're.1.13.2': {
1896 name: 'Re', version: '1.13.2', opam: 're',
1897 description: 'Regular expression library for OCaml',
1898 universe: U['re.1.13.2'], require: ['re'],
1899 sections: [
1900 { title: 'Splitting and Replacing',
1901 steps: [
1902 { code: 'Re.split (Re.compile (Re.Pcre.re "\\\\s+")) "hello world foo";;',
1903 expect: '["hello"; "world"; "foo"]', description: 'Split on whitespace' },
1904 { code: 'Re.replace_string (Re.compile (Re.Pcre.re "[aeiou]")) ~by:"*" "hello";;',
1905 expect: '"h*ll*"', description: 'Replace vowels' },
1906 ] },
1907 { title: 'Posix Character Classes',
1908 steps: [
1909 { code: 'let re = Re.(rep1 alpha |> compile);;', expect: 'Re.re',
1910 description: 'Match alphabetic characters' },
1911 { code: 'Re.execp re "hello";;', expect: 'true',
1912 description: 'All alpha matches' },
1913 { code: 'Re.all re "abc123def" |> List.map (fun g -> Re.Group.get g 0);;',
1914 expect: '["abc"; "def"]', description: 'Find all alphabetic runs' },
1915 ] },
1916 ],
1917 },
1918
1919 're.1.14.0': {
1920 name: 'Re', version: '1.14.0', opam: 're',
1921 description: 'Regular expression library for OCaml',
1922 universe: U['re.1.14.0'], require: ['re'],
1923 sections: [
1924 { title: 'Combinatorial API',
1925 steps: [
1926 { code: 'let hex = Re.(alt [rg \'0\' \'9\'; rg \'a\' \'f\'; rg \'A\' \'F\']) |> Re.rep1 |> Re.compile;;',
1927 expect: 'Re.re', description: 'Match hex strings' },
1928 { code: 'Re.execp hex "deadBEEF";;', expect: 'true',
1929 description: 'Valid hex matches' },
1930 { code: 'Re.all hex "ff0099" |> List.map (fun g -> Re.Group.get g 0);;',
1931 expect: '["ff0099"]', description: 'Extract hex values' },
1932 ] },
1933 { title: 'Capture Groups',
1934 steps: [
1935 { code: 'let re = Re.Pcre.re "(\\\\d{4})-(\\\\d{2})" |> Re.compile;;',
1936 expect: 'Re.re', description: 'Date pattern with capture groups' },
1937 { code: 'let g = Re.exec re "date: 2024-01";;', expect: 'Re.Group.t',
1938 description: 'Execute match' },
1939 { code: 'Re.Group.get g 1;;', expect: '"2024"',
1940 description: 'First capture group (year)' },
1941 { code: 'Re.Group.get g 2;;', expect: '"01"',
1942 description: 'Second capture group (month)' },
1943 ] },
1944 ],
1945 },
1946
1947 // ═══════════════════════════════════════════════════════════════════════
1948 // Angstrom
1949 // ═══════════════════════════════════════════════════════════════════════
1950 'angstrom.0.15.0': {
1951 name: 'Angstrom', version: '0.15.0', opam: 'angstrom',
1952 description: 'Parser combinators for OCaml',
1953 universe: U['angstrom.0.15.0'], require: ['angstrom'],
1954 sections: [
1955 { title: 'Basic Parsers',
1956 description: 'Angstrom provides primitive parsers and combinators for building complex parsers.',
1957 steps: [
1958 { code: 'Angstrom.parse_string ~consume:Prefix (Angstrom.string "hello") "hello world";;',
1959 expect: 'Ok "hello"', description: 'Match a literal string' },
1960 { code: 'Angstrom.parse_string ~consume:All (Angstrom.string "hello") "hello";;',
1961 expect: 'Ok "hello"', description: 'Consume:All requires full input match' },
1962 { code: 'Angstrom.parse_string ~consume:All (Angstrom.string "hello") "hello world";;',
1963 expect: 'Error', description: 'Consume:All fails with leftover input' },
1964 ] },
1965 { title: 'Character Parsers',
1966 steps: [
1967 { code: 'let digits = Angstrom.take_while1 (function \'0\'..\'9\' -> true | _ -> false);;',
1968 expect: 'Angstrom.t', description: 'Parser for one or more digits' },
1969 { code: 'Angstrom.parse_string ~consume:Prefix digits "123abc";;',
1970 expect: 'Ok "123"', description: 'Consume digits, stop at letters' },
1971 ] },
1972 { title: 'Combinators',
1973 description: 'Combine parsers with sep_by, many, choice, and operators.',
1974 steps: [
1975 { code: 'let word = Angstrom.take_while1 (function \'a\'..\'z\' | \'A\'..\'Z\' -> true | _ -> false);;',
1976 expect: 'Angstrom.t', description: 'Parser for words' },
1977 { code: 'let csv = Angstrom.sep_by (Angstrom.char \',\') word;;', expect: 'Angstrom.t',
1978 description: 'Comma-separated words parser' },
1979 { code: 'Angstrom.parse_string ~consume:All csv "foo,bar,baz";;',
1980 expect: 'Ok ["foo"; "bar"; "baz"]', description: 'Parse CSV into a list' },
1981 { code: 'Angstrom.parse_string ~consume:Prefix (Angstrom.many (Angstrom.char \'a\')) "aaab";;',
1982 expect: 'Ok', description: 'many matches zero or more' },
1983 ] },
1984 ],
1985 },
1986
1987 'angstrom.0.16.1': {
1988 name: 'Angstrom', version: '0.16.1', opam: 'angstrom',
1989 description: 'Parser combinators for OCaml',
1990 universe: U['angstrom.0.16.1'], require: ['angstrom'],
1991 sections: [
1992 { title: 'Parsing Structured Data',
1993 steps: [
1994 { code: 'let is_digit c = c >= \'0\' && c <= \'9\';;', expect: 'val is_digit',
1995 description: 'Helper: digit predicate' },
1996 { code: 'let integer = Angstrom.(take_while1 is_digit >>| int_of_string);;',
1997 expect: 'Angstrom.t', description: 'Integer parser using >>| (map)' },
1998 { code: 'Angstrom.parse_string ~consume:Prefix integer "42rest";;',
1999 expect: 'Ok 42', description: 'Parse and convert to int' },
2000 ] },
2001 { title: 'Sequencing and Alternatives',
2002 description: 'Use *> to discard left, <* to discard right, <|> for alternatives.',
2003 steps: [
2004 { code: 'let bool_p = Angstrom.((string "true" >>| fun _ -> true) <|> (string "false" >>| fun _ -> false));;',
2005 expect: 'Angstrom.t', description: 'Boolean parser with alternatives' },
2006 { code: 'Angstrom.parse_string ~consume:All bool_p "true";;',
2007 expect: 'Ok true', description: 'Parse "true"' },
2008 { code: 'Angstrom.parse_string ~consume:All bool_p "false";;',
2009 expect: 'Ok false', description: 'Parse "false"' },
2010 ] },
2011 ],
2012 },
2013
2014 // ═══════════════════════════════════════════════════════════════════════
2015 // Tyre
2016 // ═══════════════════════════════════════════════════════════════════════
2017 'tyre.0.5': {
2018 name: 'Tyre', version: '0.5', opam: 'tyre',
2019 description: 'Typed regular expressions for OCaml',
2020 universe: U['tyre.0.5'], require: ['tyre'],
2021 sections: [
2022 { title: 'Basic Typed Matching',
2023 description: 'Tyre combines regex matching with type extraction.',
2024 steps: [
2025 { code: 'let re = Tyre.compile Tyre.int;;', expect: 'Tyre.re',
2026 description: 'Compile a typed regex for integers' },
2027 { code: 'Tyre.exec re "42";;', expect: 'Ok 42',
2028 description: 'Match and extract an int' },
2029 { code: 'Tyre.exec re "abc";;', expect: 'Error',
2030 description: 'Non-matching input returns Error' },
2031 ] },
2032 { title: 'Combining Patterns',
2033 description: 'Use <&> to sequence patterns (returns tuples) and *> or <* to discard parts.',
2034 steps: [
2035 { code: 'let re = Tyre.compile Tyre.(str "v" *> int);;', expect: 'Tyre.re',
2036 description: 'Match "v" prefix then extract an int' },
2037 { code: 'Tyre.exec re "v42";;', expect: 'Ok 42',
2038 description: 'Extract version number' },
2039 { code: 'let dim = Tyre.compile Tyre.(int <&> str "x" *> int);;', expect: 'Tyre.re',
2040 description: 'Match WxH dimension pattern' },
2041 { code: 'Tyre.exec dim "800x600";;', expect: 'Ok (800, 600)',
2042 description: 'Extract both dimensions as a tuple' },
2043 ] },
2044 ],
2045 },
2046
2047 'tyre.1.0': {
2048 name: 'Tyre', version: '1.0', opam: 'tyre',
2049 description: 'Typed regular expressions for OCaml',
2050 universe: U['tyre.1.0'], require: ['tyre'],
2051 sections: [
2052 { title: 'Typed Extraction',
2053 steps: [
2054 { code: 'Tyre.exec (Tyre.compile Tyre.int) "123";;', expect: 'Ok 123',
2055 description: 'Extract an integer' },
2056 { code: 'Tyre.exec (Tyre.compile Tyre.float) "3.14";;', expect: 'Ok 3.14',
2057 description: 'Extract a float' },
2058 { code: 'Tyre.exec (Tyre.compile Tyre.bool) "true";;', expect: 'Ok true',
2059 description: 'Extract a boolean' },
2060 ] },
2061 { title: 'Optional and Repeated',
2062 steps: [
2063 { code: 'let re = Tyre.compile Tyre.(opt int);;', expect: 'Tyre.re',
2064 description: 'Optional integer pattern' },
2065 { code: 'Tyre.exec re "42";;', expect: 'Ok (Some 42)',
2066 description: 'Present value gives Some' },
2067 { code: 'Tyre.exec re "";;', expect: 'Ok None',
2068 description: 'Empty input gives None' },
2069 ] },
2070 { title: 'Bidirectional: Eval',
2071 description: 'Tyre.eval converts values back to strings (unparse).',
2072 steps: [
2073 { code: 'Tyre.eval Tyre.int 42;;', expect: '"42"',
2074 description: 'Unparse an integer' },
2075 { code: 'Tyre.eval Tyre.(str "v" *> int) 3;;', expect: '"v3"',
2076 description: 'Unparse with literal prefix' },
2077 ] },
2078 ],
2079 },
2080
2081 // ═══════════════════════════════════════════════════════════════════════
2082 // Uuseg
2083 // ═══════════════════════════════════════════════════════════════════════
2084 'uuseg.14.0.0': {
2085 name: 'Uuseg', version: '14.0.0', opam: 'uuseg',
2086 description: 'Unicode text segmentation (Unicode 14.0.0)',
2087 universe: U['uuseg.14.0.0'], require: ['uuseg'],
2088 sections: [
2089 { title: 'Unicode Version',
2090 steps: [
2091 { code: 'Uuseg.unicode_version;;', expect: '"14.0.0"',
2092 description: 'Check the Unicode version' },
2093 ] },
2094 { title: 'Segmenter Creation',
2095 description: 'Uuseg.create makes a segmenter for grapheme clusters, words, or sentences.',
2096 steps: [
2097 { code: 'let seg = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t',
2098 description: 'Create a grapheme cluster segmenter' },
2099 { code: 'let wseg = Uuseg.create `Word;;', expect: 'Uuseg.t',
2100 description: 'Create a word segmenter' },
2101 ] },
2102 ],
2103 },
2104
2105 'uuseg.15.0.0': {
2106 name: 'Uuseg', version: '15.0.0', opam: 'uuseg',
2107 description: 'Unicode text segmentation (Unicode 15.0.0)',
2108 universe: U['uuseg.15.0.0'], require: ['uuseg'],
2109 sections: [
2110 { title: 'Unicode Version',
2111 steps: [
2112 { code: 'Uuseg.unicode_version;;', expect: '"15.0.0"',
2113 description: 'Check the Unicode version' },
2114 ] },
2115 { title: 'Segmenter Types',
2116 steps: [
2117 { code: 'let _ = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t',
2118 description: 'Grapheme cluster segmentation' },
2119 { code: 'let _ = Uuseg.create `Word;;', expect: 'Uuseg.t',
2120 description: 'Word segmentation' },
2121 { code: 'let _ = Uuseg.create `Sentence;;', expect: 'Uuseg.t',
2122 description: 'Sentence segmentation' },
2123 { code: 'let _ = Uuseg.create `Line_break;;', expect: 'Uuseg.t',
2124 description: 'Line break opportunity segmentation' },
2125 ] },
2126 ],
2127 },
2128
2129 'uuseg.16.0.0': {
2130 name: 'Uuseg', version: '16.0.0', opam: 'uuseg',
2131 description: 'Unicode text segmentation (Unicode 16.0.0)',
2132 universe: U['uuseg.16.0.0'], require: ['uuseg'],
2133 sections: [
2134 { title: 'Unicode Version',
2135 steps: [
2136 { code: 'Uuseg.unicode_version;;', expect: '"16.0.0"',
2137 description: 'Check the Unicode version' },
2138 ] },
2139 { title: 'Segmenter API',
2140 description: 'Feed Uchars to a segmenter and it reports segment boundaries.',
2141 steps: [
2142 { code: 'let seg = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t',
2143 description: 'Create a grapheme cluster segmenter' },
2144 { code: 'Uuseg.add seg (`Uchar (Uchar.of_int 0x0041));;', expect: '',
2145 description: "Add 'A' to the segmenter" },
2146 { code: 'Uuseg.add seg `End;;', expect: '',
2147 description: 'Signal end of input' },
2148 ] },
2149 ],
2150 },
2151
2152 'uuseg.17.0.0': {
2153 name: 'Uuseg', version: '17.0.0', opam: 'uuseg',
2154 description: 'Unicode text segmentation (Unicode 17.0.0)',
2155 universe: U['uuseg.17.0.0'], require: ['uuseg'],
2156 sections: [
2157 { title: 'Unicode Version',
2158 steps: [
2159 { code: 'Uuseg.unicode_version;;', expect: '"17.0.0"',
2160 description: 'Check the Unicode version' },
2161 ] },
2162 { title: 'Segmenter Types',
2163 steps: [
2164 { code: 'let _ = Uuseg.create `Grapheme_cluster;;', expect: 'Uuseg.t',
2165 description: 'Grapheme cluster boundaries' },
2166 { code: 'let _ = Uuseg.create `Word;;', expect: 'Uuseg.t',
2167 description: 'Word boundaries' },
2168 { code: 'let _ = Uuseg.create `Sentence;;', expect: 'Uuseg.t',
2169 description: 'Sentence boundaries' },
2170 ] },
2171 ],
2172 },
2173
2174 // ═══════════════════════════════════════════════════════════════════════
2175 // Containers
2176 // ═══════════════════════════════════════════════════════════════════════
2177 'containers.3.17': {
2178 name: 'Containers', version: '3.17', opam: 'containers',
2179 description: 'A modular extension of the OCaml standard library',
2180 universe: U['containers.3.17'], require: ['containers'],
2181 sections: [
2182 { title: 'CCList Advanced',
2183 steps: [
2184 { code: 'CCList.product (fun a b -> (a, b)) [1; 2] ["a"; "b"];;',
2185 expect: '[(1, "a")', description: 'Cartesian product' },
2186 { code: 'CCList.pure 42;;', expect: '[42]',
2187 description: 'Wrap a value in a singleton list' },
2188 ] },
2189 { title: 'CCString',
2190 steps: [
2191 { code: 'CCString.take 5 "hello world";;', expect: '"hello"',
2192 description: 'Take first 5 characters' },
2193 { code: 'CCString.drop 6 "hello world";;', expect: '"world"',
2194 description: 'Drop first 6 characters' },
2195 { code: 'CCString.chop_prefix ~pre:"http://" "http://example.com";;',
2196 expect: 'Some "example.com"', description: 'Remove prefix if present' },
2197 { code: 'CCString.chop_suffix ~suf:".ml" "main.ml";;',
2198 expect: 'Some "main"', description: 'Remove suffix if present' },
2199 ] },
2200 ],
2201 },
2202
2203 // ═══════════════════════════════════════════════════════════════════════
2204 // Iter
2205 // ═══════════════════════════════════════════════════════════════════════
2206 'iter.1.7': {
2207 name: 'Iter', version: '1.7', opam: 'iter',
2208 description: 'Simple, efficient iterators for OCaml',
2209 universe: U['iter.1.7'], require: ['iter'],
2210 sections: [
2211 { title: 'Creating Iterators',
2212 description: 'Iter.t is (\'a -> unit) -> unit — a continuation-based iterator.',
2213 steps: [
2214 { code: 'Iter.of_list [1; 2; 3] |> Iter.to_list;;', expect: '[1; 2; 3]',
2215 description: 'Round-trip through Iter' },
2216 { code: 'Iter.(1 -- 5) |> Iter.to_list;;', expect: '[1; 2; 3; 4; 5]',
2217 description: 'Integer range (inclusive)' },
2218 { code: 'Iter.init (fun i -> i * i) |> Iter.take 5 |> Iter.to_list;;',
2219 expect: '[0; 1; 4; 9; 16]', description: 'Infinite sequence, take first 5' },
2220 ] },
2221 { title: 'Transformations',
2222 steps: [
2223 { code: 'Iter.(1 -- 10) |> Iter.filter (fun x -> x mod 2 = 0) |> Iter.to_list;;',
2224 expect: '[2; 4; 6; 8; 10]', description: 'Filter even numbers' },
2225 { code: 'Iter.(1 -- 5) |> Iter.map (fun x -> x * 2) |> Iter.to_list;;',
2226 expect: '[2; 4; 6; 8; 10]', description: 'Map doubling' },
2227 { code: 'Iter.(1 -- 5) |> Iter.fold (+) 0;;', expect: '15',
2228 description: 'Fold to compute sum' },
2229 ] },
2230 ],
2231 },
2232
2233 'iter.1.8': {
2234 name: 'Iter', version: '1.8', opam: 'iter',
2235 description: 'Simple, efficient iterators for OCaml',
2236 universe: U['iter.1.8'], require: ['iter'],
2237 sections: [
2238 { title: 'Iterator Basics',
2239 steps: [
2240 { code: 'Iter.empty |> Iter.to_list;;', expect: '[]',
2241 description: 'Empty iterator' },
2242 { code: 'Iter.singleton 42 |> Iter.to_list;;', expect: '[42]',
2243 description: 'Single-element iterator' },
2244 { code: 'Iter.repeat 3 |> Iter.take 4 |> Iter.to_list;;', expect: '[3; 3; 3; 3]',
2245 description: 'Infinite repetition, take 4' },
2246 ] },
2247 { title: 'Flat Map and Product',
2248 steps: [
2249 { code: 'Iter.(1 -- 3) |> Iter.flat_map (fun x -> Iter.of_list [x; x*10]) |> Iter.to_list;;',
2250 expect: '[1; 10; 2; 20; 3; 30]', description: 'Flat map' },
2251 { code: 'Iter.product (Iter.of_list [1;2]) (Iter.of_list ["a";"b"]) |> Iter.to_list;;',
2252 expect: '[(1, "a")', description: 'Cartesian product' },
2253 ] },
2254 ],
2255 },
2256
2257 'iter.1.9': {
2258 name: 'Iter', version: '1.9', opam: 'iter',
2259 description: 'Simple, efficient iterators for OCaml',
2260 universe: U['iter.1.9'], require: ['iter'],
2261 sections: [
2262 { title: 'Aggregation',
2263 steps: [
2264 { code: 'Iter.(1 -- 100) |> Iter.fold (+) 0;;', expect: '5050',
2265 description: 'Sum 1 to 100' },
2266 { code: 'Iter.(1 -- 10) |> Iter.length;;', expect: '10',
2267 description: 'Count elements' },
2268 { code: 'Iter.of_list ["hello"; "world"] |> Iter.for_all (fun s -> String.length s > 3);;',
2269 expect: 'true', description: 'Check a predicate for all elements' },
2270 { code: 'Iter.of_list [1; 2; 3] |> Iter.exists (fun x -> x > 2);;',
2271 expect: 'true', description: 'Check if any element matches' },
2272 ] },
2273 { title: 'Conversion',
2274 steps: [
2275 { code: 'Iter.of_list [1; 2; 3] |> Iter.to_rev_list;;', expect: '[3; 2; 1]',
2276 description: 'Convert to reversed list' },
2277 { code: 'Iter.of_list [("a",1); ("b",2)] |> Iter.to_hashtbl;;', expect: 'Hashtbl',
2278 description: 'Convert to hashtable' },
2279 ] },
2280 ],
2281 },
2282
2283 // ═══════════════════════════════════════════════════════════════════════
2284 // OCamlgraph
2285 // ═══════════════════════════════════════════════════════════════════════
2286 'ocamlgraph.2.0.0': {
2287 name: 'OCamlgraph', version: '2.0.0', opam: 'ocamlgraph',
2288 description: 'Graph library for OCaml',
2289 universe: U['ocamlgraph.2.0.0'], require: ['ocamlgraph'],
2290 sections: [
2291 { title: 'Building Graphs',
2292 description: 'Graph.Pack.Digraph provides an easy-to-use imperative directed graph. Vertices must be reused (not re-created).',
2293 steps: [
2294 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 1 in let v2 = G.V.create 2 in let v3 = G.V.create 3 in G.add_edge g v1 v2; G.add_edge g v2 v3; G.nb_vertex g;;',
2295 expect: '3', description: 'Create a graph with 3 vertices' },
2296 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 1 in let v2 = G.V.create 2 in let v3 = G.V.create 3 in G.add_edge g v1 v2; G.add_edge g v1 v3; G.nb_edges g;;',
2297 expect: '2', description: '2 edges from vertex 1' },
2298 ] },
2299 ],
2300 },
2301
2302 'ocamlgraph.2.1.0': {
2303 name: 'OCamlgraph', version: '2.1.0', opam: 'ocamlgraph',
2304 description: 'Graph library for OCaml',
2305 universe: U['ocamlgraph.2.1.0'], require: ['ocamlgraph'],
2306 sections: [
2307 { title: 'Imperative Graphs',
2308 steps: [
2309 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v1 = G.V.create 10 in let v2 = G.V.create 20 in G.add_edge g v1 v2; G.mem_edge g v1 v2;;',
2310 expect: 'true', description: 'Check edge existence' },
2311 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in G.add_edge g (G.V.create 1) (G.V.create 2); G.add_edge g (G.V.create 2) (G.V.create 3); G.add_edge g (G.V.create 3) (G.V.create 1); G.nb_edges g;;',
2312 expect: '3', description: 'A cycle with 3 edges' },
2313 ] },
2314 ],
2315 },
2316
2317 'ocamlgraph.2.2.0': {
2318 name: 'OCamlgraph', version: '2.2.0', opam: 'ocamlgraph',
2319 description: 'Graph library for OCaml',
2320 universe: U['ocamlgraph.2.2.0'], require: ['ocamlgraph'],
2321 sections: [
2322 { title: 'Graph Operations',
2323 steps: [
2324 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let vs = Array.init 6 G.V.create in for i = 0 to 4 do G.add_edge g vs.(i) vs.(i+1) done; G.nb_vertex g;;',
2325 expect: '6', description: 'A chain of 6 vertices' },
2326 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let vs = Array.init 6 G.V.create in for i = 0 to 4 do G.add_edge g vs.(i) vs.(i+1) done; G.nb_edges g;;',
2327 expect: '5', description: '5 edges in the chain' },
2328 { code: 'let module G = Graph.Pack.Digraph in let g = G.create () in let v0 = G.V.create 0 in let v1 = G.V.create 1 in let v2 = G.V.create 2 in G.add_edge g v0 v1; G.add_edge g v0 v2; G.out_degree g v0;;',
2329 expect: '2', description: 'Out-degree of vertex 0' },
2330 ] },
2331 ],
2332 },
2333
2334 // ═══════════════════════════════════════════════════════════════════════
2335 // Digestif
2336 // ═══════════════════════════════════════════════════════════════════════
2337 'digestif.1.1.2': {
2338 name: 'Digestif', version: '1.1.2', opam: 'digestif',
2339 description: 'Cryptographic hash functions for OCaml',
2340 universe: U['digestif.1.1.2'], require: ['digestif'],
2341 sections: [
2342 { title: 'SHA-256',
2343 description: 'Digestif.SHA256 provides SHA-256 hashing with hex encoding.',
2344 steps: [
2345 { code: 'Digestif.SHA256.digest_string "hello" |> Digestif.SHA256.to_hex;;',
2346 expect: '2cf24dba', description: 'SHA-256 of "hello"' },
2347 { code: 'Digestif.SHA256.digest_string "" |> Digestif.SHA256.to_hex;;',
2348 expect: 'e3b0c442', description: 'SHA-256 of empty string' },
2349 { code: 'let h1 = Digestif.SHA256.digest_string "test" in let h2 = Digestif.SHA256.digest_string "test" in Digestif.SHA256.equal h1 h2;;',
2350 expect: 'true', description: 'Same input produces same hash (constant-time equal)' },
2351 ] },
2352 { title: 'MD5',
2353 steps: [
2354 { code: 'Digestif.MD5.digest_string "hello" |> Digestif.MD5.to_hex;;',
2355 expect: '5d41402a', description: 'MD5 of "hello"' },
2356 ] },
2357 ],
2358 },
2359
2360 'digestif.1.3.0': {
2361 name: 'Digestif', version: '1.3.0', opam: 'digestif',
2362 description: 'Cryptographic hash functions for OCaml',
2363 universe: U['digestif.1.3.0'], require: ['digestif'],
2364 sections: [
2365 { title: 'Multiple Algorithms',
2366 steps: [
2367 { code: 'Digestif.SHA256.digest_string "OCaml" |> Digestif.SHA256.to_hex;;',
2368 expect: 'string', description: 'SHA-256 hash' },
2369 { code: 'Digestif.SHA512.digest_string "OCaml" |> Digestif.SHA512.to_hex;;',
2370 expect: 'string', description: 'SHA-512 hash' },
2371 { code: 'Digestif.SHA1.digest_string "OCaml" |> Digestif.SHA1.to_hex;;',
2372 expect: 'string', description: 'SHA-1 hash' },
2373 ] },
2374 { title: 'HMAC',
2375 description: 'HMAC provides keyed hashing for authentication.',
2376 steps: [
2377 { code: 'Digestif.SHA256.hmac_string ~key:"secret" "message" |> Digestif.SHA256.to_hex;;',
2378 expect: 'string', description: 'HMAC-SHA256 with a key' },
2379 { code: 'let h1 = Digestif.SHA256.hmac_string ~key:"k" "m" in let h2 = Digestif.SHA256.hmac_string ~key:"k" "m" in Digestif.SHA256.equal h1 h2;;',
2380 expect: 'true', description: 'Same key+message = same HMAC' },
2381 ] },
2382 ],
2383 },
2384
2385 // ═══════════════════════════════════════════════════════════════════════
2386 // Hex
2387 // ═══════════════════════════════════════════════════════════════════════
2388 'hex.1.4.0': {
2389 name: 'Hex', version: '1.4.0', opam: 'hex',
2390 description: 'Hex encoding and decoding for OCaml',
2391 universe: U['hex.1.4.0'], require: ['hex'],
2392 sections: [
2393 { title: 'Encoding and Decoding',
2394 steps: [
2395 { code: 'Hex.of_string "Hello";;', expect: '`Hex',
2396 description: 'Encode string to hex' },
2397 { code: 'Hex.to_string (`Hex "48656c6c6f");;', expect: '"Hello"',
2398 description: 'Decode hex to string' },
2399 { code: 'Hex.hexdump_s (Hex.of_string "Hello, World!");;', expect: '4865',
2400 description: 'Hexdump for debugging' },
2401 ] },
2402 ],
2403 },
2404
2405 'hex.1.5.0': {
2406 name: 'Hex', version: '1.5.0', opam: 'hex',
2407 description: 'Hex encoding and decoding for OCaml',
2408 universe: U['hex.1.5.0'], require: ['hex'],
2409 sections: [
2410 { title: 'Hex Encoding',
2411 steps: [
2412 { code: 'Hex.of_string "\\x00\\xff";;', expect: '`Hex "00ff"',
2413 description: 'Binary data to hex' },
2414 { code: 'Hex.to_string (`Hex "00ff");;', expect: 'string',
2415 description: 'Hex back to binary' },
2416 { code: 'Hex.show (Hex.of_string "AB");;', expect: '"4142"',
2417 description: 'Show hex representation' },
2418 ] },
2419 ],
2420 },
2421
2422 // ═══════════════════════════════════════════════════════════════════════
2423 // Eqaf
2424 // ═══════════════════════════════════════════════════════════════════════
2425 'eqaf.0.9': {
2426 name: 'Eqaf', version: '0.9', opam: 'eqaf',
2427 description: 'Constant-time string comparison for OCaml',
2428 universe: U['eqaf.0.9'], require: ['eqaf'],
2429 sections: [
2430 { title: 'Constant-Time Comparison',
2431 description: 'Eqaf.equal compares strings in constant time, preventing timing attacks.',
2432 steps: [
2433 { code: 'Eqaf.equal "secret" "secret";;', expect: 'true',
2434 description: 'Equal strings' },
2435 { code: 'Eqaf.equal "secret" "wrong!";;', expect: 'false',
2436 description: 'Different strings' },
2437 { code: 'Eqaf.equal "" "";;', expect: 'true',
2438 description: 'Empty strings are equal' },
2439 ] },
2440 ],
2441 },
2442
2443 'eqaf.0.10': {
2444 name: 'Eqaf', version: '0.10', opam: 'eqaf',
2445 description: 'Constant-time string comparison for OCaml',
2446 universe: U['eqaf.0.10'], require: ['eqaf'],
2447 sections: [
2448 { title: 'Constant-Time Operations',
2449 steps: [
2450 { code: 'Eqaf.equal "abc" "abc";;', expect: 'true',
2451 description: 'Same strings (constant time)' },
2452 { code: 'Eqaf.equal "abc" "xyz";;', expect: 'false',
2453 description: 'Different strings (same timing as equal case)' },
2454 { code: 'Eqaf.compare_be "a" "b";;', expect: '-1',
2455 description: 'Constant-time big-endian comparison' },
2456 ] },
2457 ],
2458 },
2459
2460 // ═══════════════════════════════════════════════════════════════════════
2461 // Uri
2462 // ═══════════════════════════════════════════════════════════════════════
2463 'uri.4.2.0': {
2464 name: 'Uri', version: '4.2.0', opam: 'uri',
2465 description: 'URI parsing and manipulation for OCaml',
2466 universe: U['uri.4.2.0'], require: ['uri'],
2467 sections: [
2468 { title: 'Parsing URIs',
2469 steps: [
2470 { code: 'let u = Uri.of_string "https://example.com:8080/path?q=1#frag";;', expect: 'Uri.t',
2471 description: 'Parse a full URI' },
2472 { code: 'Uri.scheme u;;', expect: 'Some "https"',
2473 description: 'Extract the scheme' },
2474 { code: 'Uri.host u;;', expect: 'Some "example.com"',
2475 description: 'Extract the host' },
2476 { code: 'Uri.port u;;', expect: 'Some 8080',
2477 description: 'Extract the port' },
2478 { code: 'Uri.path u;;', expect: '"/path"',
2479 description: 'Extract the path' },
2480 { code: 'Uri.fragment u;;', expect: 'Some "frag"',
2481 description: 'Extract the fragment' },
2482 ] },
2483 { title: 'Query Parameters',
2484 steps: [
2485 { code: 'let u = Uri.of_string "http://example.com?a=1&b=2";;', expect: 'Uri.t',
2486 description: 'URI with query params' },
2487 { code: 'Uri.get_query_param u "a";;', expect: 'Some "1"',
2488 description: 'Get a single query parameter' },
2489 { code: 'Uri.query u;;', expect: '[("a"', description: 'Get all query parameters' },
2490 ] },
2491 { title: 'Building URIs',
2492 steps: [
2493 { code: 'Uri.make ~scheme:"https" ~host:"example.com" ~path:"/api" () |> Uri.to_string;;',
2494 expect: 'https://example.com/api', description: 'Build a URI from components' },
2495 { code: 'Uri.with_query\' (Uri.of_string "http://x.com") [("key", "val")] |> Uri.to_string;;',
2496 expect: 'key=val', description: 'Add query parameters' },
2497 ] },
2498 ],
2499 },
2500
2501 'uri.4.4.0': {
2502 name: 'Uri', version: '4.4.0', opam: 'uri',
2503 description: 'URI parsing and manipulation for OCaml',
2504 universe: U['uri.4.4.0'], require: ['uri'],
2505 sections: [
2506 { title: 'URI Components',
2507 steps: [
2508 { code: 'let u = Uri.of_string "ftp://user@host/file.txt";;', expect: 'Uri.t',
2509 description: 'Parse an FTP URI' },
2510 { code: 'Uri.scheme u;;', expect: 'Some "ftp"',
2511 description: 'FTP scheme' },
2512 { code: 'Uri.userinfo u;;', expect: 'Some "user"',
2513 description: 'Extract userinfo' },
2514 { code: 'Uri.host u;;', expect: 'Some "host"',
2515 description: 'Extract host' },
2516 { code: 'Uri.path u;;', expect: '"/file.txt"',
2517 description: 'Extract path' },
2518 ] },
2519 { title: 'URI Manipulation',
2520 steps: [
2521 { code: 'let u = Uri.of_string "http://example.com/old" in Uri.with_path u "/new" |> Uri.to_string;;',
2522 expect: 'http://example.com/new', description: 'Replace the path' },
2523 { code: 'Uri.resolve "http" (Uri.of_string "http://example.com/a/b") (Uri.of_string "../c") |> Uri.to_string;;',
2524 expect: 'example.com', description: 'Resolve a relative reference' },
2525 ] },
2526 ],
2527 },
2528
2529 // ═══════════════════════════════════════════════════════════════════════
2530 // Ipaddr
2531 // ═══════════════════════════════════════════════════════════════════════
2532 'ipaddr.5.6.0': {
2533 name: 'Ipaddr', version: '5.6.0', opam: 'ipaddr',
2534 description: 'IP address parsing and manipulation for OCaml',
2535 universe: U['ipaddr.5.6.0'], require: ['ipaddr'],
2536 sections: [
2537 { title: 'IPv4 Addresses',
2538 steps: [
2539 { code: 'Ipaddr.V4.of_string_exn "192.168.1.1";;', expect: 'Ipaddr.V4.t',
2540 description: 'Parse an IPv4 address' },
2541 { code: 'Ipaddr.V4.to_string (Ipaddr.V4.of_string_exn "10.0.0.1");;',
2542 expect: '"10.0.0.1"', description: 'Convert back to string' },
2543 { code: 'Ipaddr.V4.localhost |> Ipaddr.V4.to_string;;', expect: '"127.0.0.1"',
2544 description: 'Localhost constant' },
2545 ] },
2546 { title: 'IPv6 Addresses',
2547 steps: [
2548 { code: 'Ipaddr.V6.of_string_exn "::1" |> Ipaddr.V6.to_string;;', expect: '"::1"',
2549 description: 'IPv6 loopback' },
2550 { code: 'Ipaddr.V6.localhost |> Ipaddr.V6.to_string;;', expect: '"::1"',
2551 description: 'IPv6 localhost constant' },
2552 ] },
2553 { title: 'Generic IP',
2554 steps: [
2555 { code: 'Ipaddr.of_string_exn "192.168.1.1" |> Ipaddr.to_string;;',
2556 expect: '"192.168.1.1"', description: 'Parse any IP address' },
2557 { code: 'Ipaddr.of_string_exn "::1" |> Ipaddr.to_string;;',
2558 expect: '"::1"', description: 'Parse IPv6 through generic interface' },
2559 ] },
2560 ],
2561 },
2562
2563 'ipaddr.5.6.1': {
2564 name: 'Ipaddr', version: '5.6.1', opam: 'ipaddr',
2565 description: 'IP address parsing and manipulation for OCaml',
2566 universe: U['ipaddr.5.6.1'], require: ['ipaddr'],
2567 sections: [
2568 { title: 'Address Operations',
2569 steps: [
2570 { code: 'Ipaddr.V4.of_string "invalid";;', expect: 'Error',
2571 description: 'Invalid address returns Error' },
2572 { code: 'Ipaddr.V4.of_string "10.0.0.1";;', expect: 'Ok',
2573 description: 'Valid address returns Ok' },
2574 { code: 'Ipaddr.V4.(compare localhost (of_string_exn "127.0.0.1"));;', expect: '0',
2575 description: 'Localhost equals 127.0.0.1' },
2576 ] },
2577 { title: 'CIDR Prefixes',
2578 steps: [
2579 { code: 'let prefix = Ipaddr.V4.Prefix.of_string_exn "192.168.0.0/24";;',
2580 expect: 'Ipaddr.V4.Prefix.t', description: 'Parse a CIDR prefix' },
2581 { code: 'Ipaddr.V4.Prefix.mem (Ipaddr.V4.of_string_exn "192.168.0.42") prefix;;',
2582 expect: 'true', description: 'Address is in the prefix' },
2583 { code: 'Ipaddr.V4.Prefix.mem (Ipaddr.V4.of_string_exn "192.168.1.1") prefix;;',
2584 expect: 'false', description: 'Address is not in the prefix' },
2585 ] },
2586 ],
2587 },
2588
2589 // ═══════════════════════════════════════════════════════════════════════
2590 // Domain_name
2591 // ═══════════════════════════════════════════════════════════════════════
2592 'domain-name.0.4.1': {
2593 name: 'Domain_name', version: '0.4.1', opam: 'domain-name',
2594 description: 'Domain name parsing and validation for OCaml',
2595 universe: U['domain-name.0.4.1'], require: ['domain-name'],
2596 sections: [
2597 { title: 'Parsing Domain Names',
2598 steps: [
2599 { code: 'Domain_name.of_string_exn "example.com";;', expect: 'Domain_name.t',
2600 description: 'Parse a domain name' },
2601 { code: 'Domain_name.to_string (Domain_name.of_string_exn "www.example.com");;',
2602 expect: '"www.example.com"', description: 'Convert back to string' },
2603 { code: 'Domain_name.of_string "invalid..domain";;', expect: 'Error',
2604 description: 'Double dots are invalid' },
2605 ] },
2606 { title: 'Domain Name Operations',
2607 steps: [
2608 { code: 'Domain_name.of_string_exn "sub.example.com" |> Domain_name.count_labels;;',
2609 expect: '3', description: 'Count labels (sub, example, com)' },
2610 { code: 'Domain_name.equal (Domain_name.of_string_exn "A.COM") (Domain_name.of_string_exn "a.com");;',
2611 expect: 'true', description: 'Domain names are case-insensitive' },
2612 ] },
2613 ],
2614 },
2615
2616 'domain-name.0.5.0': {
2617 name: 'Domain_name', version: '0.5.0', opam: 'domain-name',
2618 description: 'Domain name parsing and validation for OCaml',
2619 universe: U['domain-name.0.5.0'], require: ['domain-name'],
2620 sections: [
2621 { title: 'Domain Names',
2622 steps: [
2623 { code: 'Domain_name.of_string_exn "mail.example.org";;', expect: 'Domain_name.t',
2624 description: 'Parse a domain name' },
2625 { code: 'Domain_name.of_string_exn "example.com" |> Domain_name.count_labels;;',
2626 expect: '2', description: 'Two labels' },
2627 { code: 'Domain_name.is_subdomain ~subdomain:(Domain_name.of_string_exn "sub.example.com") ~domain:(Domain_name.of_string_exn "example.com");;',
2628 expect: 'true', description: 'Check subdomain relationship' },
2629 ] },
2630 { title: 'Host Names',
2631 description: 'Domain_name.host_exn validates a domain name as a valid hostname.',
2632 steps: [
2633 { code: 'Domain_name.host_exn (Domain_name.of_string_exn "example.com");;',
2634 expect: 'Domain_name.t', description: 'Valid hostname' },
2635 { code: 'Domain_name.to_string (Domain_name.of_string_exn "example.com");;',
2636 expect: '"example.com"', description: 'Convert back to string' },
2637 ] },
2638 ],
2639 },
2640
2641 // ═══════════════════════════════════════════════════════════════════════
2642 // Zarith
2643 // ═══════════════════════════════════════════════════════════════════════
2644 'zarith.1.13': {
2645 name: 'Zarith', version: '1.13', opam: 'zarith',
2646 description: 'Arbitrary-precision integers and rationals for OCaml',
2647 universe: U['zarith.1.13'], require: ['zarith'],
2648 sections: [
2649 { title: 'Big Integers',
2650 description: 'Z.t represents arbitrary-precision integers.',
2651 steps: [
2652 { code: 'Z.of_int 42;;', expect: '42', description: 'Create from int' },
2653 { code: 'Z.of_string "999999999999999999999";;', expect: '999999999999999999999',
2654 description: 'Create from string (exceeds int range)' },
2655 { code: 'Z.add (Z.of_int 1) (Z.of_string "999999999999999999999");;',
2656 expect: '1000000000000000000000', description: 'Arbitrary-precision addition' },
2657 ] },
2658 { title: 'Arithmetic',
2659 steps: [
2660 { code: 'Z.mul (Z.of_int 1000000) (Z.of_int 1000000);;', expect: '1000000000000',
2661 description: 'Multiplication' },
2662 { code: 'Z.pow (Z.of_int 2) 100 |> Z.to_string;;', expect: '1267650600228229401496703205376',
2663 description: '2^100 as a string' },
2664 { code: 'Z.rem (Z.of_int 17) (Z.of_int 5);;', expect: '2',
2665 description: 'Remainder' },
2666 { code: 'Z.gcd (Z.of_int 12) (Z.of_int 18);;', expect: '6',
2667 description: 'Greatest common divisor' },
2668 ] },
2669 { title: 'Comparison',
2670 steps: [
2671 { code: 'Z.compare (Z.of_int 10) (Z.of_int 20);;', expect: '-1',
2672 description: '10 < 20' },
2673 { code: 'Z.equal Z.zero Z.zero;;', expect: 'true',
2674 description: 'Zero equals zero' },
2675 { code: 'Z.sign (Z.of_int (-5));;', expect: '-1',
2676 description: 'Sign of negative number' },
2677 ] },
2678 ],
2679 },
2680
2681 'zarith.1.14': {
2682 name: 'Zarith', version: '1.14', opam: 'zarith',
2683 description: 'Arbitrary-precision integers and rationals for OCaml',
2684 universe: U['zarith.1.14'], require: ['zarith'],
2685 sections: [
2686 { title: 'Big Integer Arithmetic',
2687 steps: [
2688 { code: 'Z.(of_int 2 ** 256) |> Z.to_string |> String.length;;',
2689 expect: '78', description: '2^256 has 78 digits' },
2690 { code: 'Z.probab_prime (Z.of_int 97) 25;;', expect: '2',
2691 description: '97 is prime (2 = definitely prime)' },
2692 { code: 'Z.probab_prime (Z.of_int 100) 25;;', expect: '0',
2693 description: '100 is composite (0 = definitely not prime)' },
2694 ] },
2695 { title: 'Rationals (Q module)',
2696 description: 'Q.t represents exact rational numbers.',
2697 steps: [
2698 { code: 'Q.of_ints 1 3;;', expect: '1/3',
2699 description: 'Create the fraction 1/3' },
2700 { code: 'Q.add (Q.of_ints 1 3) (Q.of_ints 1 6);;', expect: '1/2',
2701 description: '1/3 + 1/6 = 1/2 (auto-simplified)' },
2702 { code: 'Q.mul (Q.of_ints 2 3) (Q.of_ints 3 4);;', expect: '1/2',
2703 description: '2/3 * 3/4 = 1/2' },
2704 { code: 'Q.to_float (Q.of_ints 1 3);;', expect: '0.333333',
2705 description: 'Convert to float (approximate)' },
2706 ] },
2707 ],
2708 },
2709
2710 // ═══════════════════════════════════════════════════════════════════════
2711 // QCheck
2712 // ═══════════════════════════════════════════════════════════════════════
2713 'qcheck-core.0.25': {
2714 name: 'QCheck', version: '0.25', opam: 'qcheck-core',
2715 description: 'Property-based testing for OCaml',
2716 universe: U['qcheck-core.0.25'], require: ['qcheck-core'],
2717 sections: [
2718 { title: 'Generators',
2719 description: 'QCheck2.Gen provides random value generators with integrated shrinking.',
2720 steps: [
2721 { code: 'QCheck2.Gen.generate1 QCheck2.Gen.int;;', expect: 'int',
2722 description: 'Generate a random integer' },
2723 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.return 42);;', expect: '42',
2724 description: 'Constant generator always returns 42' },
2725 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.list QCheck2.Gen.small_int) |> List.length >= 0;;',
2726 expect: 'true', description: 'Generate a random list of small ints' },
2727 ] },
2728 { title: 'Property Tests',
2729 description: 'QCheck2.Test.make creates a test, check_exn runs it.',
2730 steps: [
2731 { code: 'let t = QCheck2.Test.make ~name:"commutative" QCheck2.Gen.(pair int int) (fun (a, b) -> a + b = b + a);;',
2732 expect: 'QCheck2.Test.t', description: 'Addition is commutative' },
2733 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit',
2734 description: 'Test passes (no exception)' },
2735 { code: 'let t2 = QCheck2.Test.make ~name:"rev rev" QCheck2.Gen.(list small_int) (fun l -> List.rev (List.rev l) = l);;',
2736 expect: 'QCheck2.Test.t', description: 'Double reverse is identity' },
2737 { code: 'QCheck2.Test.check_exn t2;;', expect: 'unit',
2738 description: 'Test passes' },
2739 ] },
2740 ],
2741 },
2742
2743 'qcheck-core.0.27': {
2744 name: 'QCheck', version: '0.27', opam: 'qcheck-core',
2745 description: 'Property-based testing for OCaml',
2746 universe: U['qcheck-core.0.27'], require: ['qcheck-core'],
2747 sections: [
2748 { title: 'Generators',
2749 steps: [
2750 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.oneof [QCheck2.Gen.return 1; QCheck2.Gen.return 2]);;',
2751 expect: 'int', description: 'Choose between generators randomly' },
2752 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.map (fun x -> x * 2) QCheck2.Gen.small_int);;',
2753 expect: 'int', description: 'Map over a generator' },
2754 ] },
2755 { title: 'Testing Properties',
2756 steps: [
2757 { code: 'let t = QCheck2.Test.make ~name:"sort idempotent" QCheck2.Gen.(list small_int) (fun l -> let s = List.sort compare l in List.sort compare s = s);;',
2758 expect: 'QCheck2.Test.t', description: 'Sorting is idempotent' },
2759 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit',
2760 description: 'Property holds' },
2761 { code: 'let t = QCheck2.Test.make ~count:1000 ~name:"length" QCheck2.Gen.(list small_int) (fun l -> List.length (List.rev l) = List.length l);;',
2762 expect: 'QCheck2.Test.t', description: 'Rev preserves length (1000 tests)' },
2763 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit',
2764 description: 'Passes all 1000 tests' },
2765 ] },
2766 ],
2767 },
2768
2769 'qcheck-core.0.91': {
2770 name: 'QCheck', version: '0.91', opam: 'qcheck-core',
2771 description: 'Property-based testing for OCaml',
2772 universe: U['qcheck-core.0.91'], require: ['qcheck-core'],
2773 sections: [
2774 { title: 'Generators and Tests',
2775 steps: [
2776 { code: 'QCheck2.Gen.generate1 (QCheck2.Gen.pair QCheck2.Gen.nat QCheck2.Gen.bool);;',
2777 expect: 'int * bool', description: 'Generate a pair of int and bool' },
2778 { code: 'let t = QCheck2.Test.make ~name:"assoc" QCheck2.Gen.(triple int int int) (fun (a, b, c) -> (a + b) + c = a + (b + c));;',
2779 expect: 'QCheck2.Test.t', description: 'Addition is associative' },
2780 { code: 'QCheck2.Test.check_exn t;;', expect: 'unit',
2781 description: 'Property holds' },
2782 ] },
2783 ],
2784 },
2785};