Find and remove dead code and unused APIs in OCaml projects
1# Coding guidelines
2
3## 1 Overall philosophy
4
5* Strive for **small, orthogonal modules** that can be composed freely.
6* Depend only on the standard library (and occasionally on other “micro-libs”, e.g. `Fmt`, `Rresult`).
7* Preserve **purity** and determinism in the core; confine effects to thin I/O layers.
8* Offer **total functions** whenever feasible and expose *explicit* failure via `'a result`.
9* Maintain **API stability**: once an identifier is public, avoid breaking changes; add instead of mutate.
10
11---
12
13## 2 Project layout
14
15```
16my_lib/
17├─ dune-project
18├─ README.md – high-level overview & minimal example
19├─ CHANGES.md – human-written change-log
20├─ lib/ – library source code (alternative to src/)
21│ ├─ my_lib.mli – canonical public interface
22│ └─ my_lib.ml – implementation
23├─ bin/ – executable entry points
24└─ test/ – Alcotest or inline-tests
25```
26
27*Put the public interface in a single `.mli` whenever practical. Split only when the conceptual surface really warrants distinct compilation units. Using `lib/` and `bin/` directories instead of `src/` is acceptable for projects with both libraries and executables.*
28
29---
30
31## 3 Modules and sub-modules
32
33| Purpose | Idiom |
34| ---------------- | ------------------------------------------------------------------------------ |
35| Canonical type | `type t` (abstract) |
36| Pretty printer | `val pp : t Fmt.t` |
37| Conversions | `val to_string : t -> string`<br>`val of_string : string -> (t, error) result` (when meaningful) |
38| Equality & order | `val equal : t -> t -> bool`<br>`val compare : t -> t -> int` (when meaningful) |
39| Constructors | `val v : ... -> (t, error) result` (or pure `t` if infallible) |
40| Low-level view | `module Unsafe : sig … end` (optional) |
41
42*Keep the root module *flat*; introduce nested modules only for clearly separable concerns (e.g. `My_lib.Path`, `My_lib.Map`).*
43
44---
45
46## 4 Naming conventions
47
48* **Modules / files** – lower-case, underscore-separated file names (`my_lib.ml`), capitalised module names (`My_lib`).
49* **Values** – short but descriptive (`pp`, `v`, `find`, `with_…`).
50* **Labels** – prefer them only when they disambiguate (`~dir`, `~mode`); avoid gratuitous labels on simple data.
51* **Boolean arguments** – almost never positional; wrap in a variant or use a labelled argument (`?dry_run:bool`).
52* **Infix operators** – expose only if widespread (`>|=`) or central to the library (`Fpath.( / )`). Put them in a dedicated sub-module `Op`.
53
54---
55
56## 5 Types
57
58* Keep *representation hidden* (`type t`) and provide construction/destruction helpers.
59* Introduce **phantom parameters** to encode invariants if it avoids run-time checks.
60* Use **private types** when callers may inspect but not forge values (`type t = private string`).
61* For **enumerations**, use a closed variant; supply `val all : t list` and `val to_string`.
62* Reserve **exceptions** for programming errors (`Invalid_argument`, assertion failures). Operational errors travel in the `'error` part of `('a, error) result`.
63* **Formatting**: Use `Fmt` consistently for all output. Never mix `Printf` and `Fmt` - use `Fmt.pr` for user-facing messages, keep `Printf.printf` only for TTY progress display.
64* **Regular expressions**: Use the Re DSL instead of Re.Perl. Prefer `Re.(compile (seq [...]))` over `Re.Perl.compile_pat`.
65
66---
67
68## 6 Error handling pattern (`Rresult`)
69
70### Base Error Types
71
72```ocaml
73type error = [ `Msg of string | `Build_error of context ]
74val pp_error : error Fmt.t
75val v : ... -> (t, error) result
76```
77
78### Error Helper Functions Pattern
79
80Start implementation files with error helper functions (`err_*`) for consistent error messages:
81
82```ocaml
83(* Error helper functions *)
84let err fmt = Fmt.kstr (fun e -> Error (`Msg e)) fmt
85
86let err_file_read file msg = err "Failed to read %s: %s" file msg
87let err_file_write file msg = err "Failed to write %s: %s" file msg
88```
89
90Use `%a` with pretty printers for complex formatting:
91```ocaml
92let pp_build_error ppf ctx = Fmt.pf ppf "%s" ctx.output
93let err_build_failed ctx = err "Build failed:@.%a" pp_build_error ctx
94```
95
96For structured errors with context:
97```ocaml
98let err_build_error ctx = Error (`Build_error ctx)
99```
100
101### Library vs Main Separation
102
103**Critical Rule**: Library code never calls `exit` directly - only returns errors for main.ml to handle.
104
105```ocaml
106(* In library code - ALWAYS return structured errors *)
107let handle_build_failure ctx =
108 (* Display specific context *)
109 Fmt.pr "Loop detected - stopping to prevent infinite iterations.@.";
110 (* Return error for main.ml to handle *)
111 err_build_error ctx
112
113(* In main.ml - handle all exit logic *)
114match analyze mode root_dir files with
115| Ok result -> result
116| Error (`Build_error ctx) ->
117 System.display_build_failure_and_exit ctx (* Exits with build's code *)
118| Error e ->
119 Format.eprintf "Error: %a@." pp_error e;
120 exit 1 (* Generic error code *)
121```
122
123### Error Display Consistency
124
125Use unified display functions for consistent UX:
126```ocaml
127(* In system.ml *)
128let display_build_failure_and_exit ctx =
129 let all_warnings = (* parse build output *) in
130 Fmt.pr "%a with %d %s - full output:@."
131 Fmt.(styled (`Fg `Red) string) "Build failed"
132 (List.length all_warnings)
133 (if List.length all_warnings = 1 then "error" else "errors");
134 Fmt.pr "%a@." pp_build_output ctx;
135 let exit_code = get_build_exit_code ctx in
136 exit exit_code
137```
138
139### Guidelines
140
141* Accept a caller-provided buffer (`Buffer.t`) or formatter when the operation might produce extensive diagnostics.
142* Re-use `Fmt.failwith`/`Fmt.invalid_arg` for internal invariants; never expose raw `Printexc` traces.
143* **Never mix exit logic in library code** - always return structured errors that main.ml can handle appropriately.
144
145---
146
147## 7 Public API surface
148
1491. **Minimal** yet **composable**. Do not wrap the entire Unix API—wrap just enough to remove boilerplate.
1502. Expose **first-class values** rather than functors where a closure suffices.
1513. Keep *effectful* helpers separate (`My_lib_unix`, `My_lib_lwt`) to avoid dragging unwanted dependencies.
152
153---
154
155## 8 Interface documentation (`.mli`, `.mld`)
156
157*Document **every** public item in the `.mli`. Place docstrings **after** the item unless it spans multiple lines, using code-first voice.*
158
159### 8.1 Structure and style
160
161```
162(** {1 Overview}
163
164 One concise paragraph explaining the abstraction. Link to the README
165 for a tutorial.
166
167 {2 Types}
168
169 {3 Constructors}
170
171 {4 Accessors}
172
173 {5 Derived combinators}
174
175 {6 Pretty-printing}
176
177*)
178```
179
180* Use **section headings** (`{1 …}`, `{2 …}`) to group logically related functions.
181* Provide **usage examples** in `[{[ … ]}]` blocks that compile under `odoc-latency-level:normal`.
182* Annotate complexity (`@since`, `@deprecated`, `@raise`, `@see`) rigorously.
183* Keep comments declarative: *what* and *guarantees*, never implementation detail.
184
185---
186
187## 9 Testing & auxiliary artefacts
188
189* Unit tests with **Alcotest** (`test/`) mirroring the public API sections.
190* **Cram tests**: Use directory-based cram tests (`test/name.t/`) instead of inline `cat EOF` commands for complex file creation. This is cleaner and easier to maintain.
191* Provide a `tool/` directory of executable samples that double as integration tests.
192* Ship an `odig` metadata file so `odig odoc` builds HTML docs out-of-the-box.
193
194---
195
196## 10 Versioning & release process
197
198* Tag releases with `vN.N.N`; follow semantic versioning.
199* Maintain `CHANGES.md` with *user-visible* entries only.
200* Publish through `topkg` or Dune’s built-in release workflow; push docs to `gh-pages` via `odoc`.
201
202---
203
204## 11 Style Checklist
205
206| Step | Done |
207| ----------------------------------------------------------- | ---- |
208| `src/<lib>.mli` written first, fully documented | ☐ |
209| All errors returned as `'a result` with `Rresult` helpers | ☐ |
210| `pp`, `equal`, `compare`, `hash` supplied where meaningful | ☐ |
211| No hidden global side-effects; pure core separated | ☐ |
212| Example snippets compile under `dune build @doc` | ☐ |
213| Dependency list reviewed: stdlib ± {Fmt, Rresult, Alcotest} | ☐ |
214| `test/` provides Alcotest suites mirroring API sections | ☐ |
215
216Tick every box before publishing.