# Coding guidelines
## 1 Overall philosophy
* Strive for **small, orthogonal modules** that can be composed freely.
* Depend only on the standard library (and occasionally on other “micro-libs”, e.g. `Fmt`, `Rresult`).
* Preserve **purity** and determinism in the core; confine effects to thin I/O layers.
* Offer **total functions** whenever feasible and expose *explicit* failure via `'a result`.
* Maintain **API stability**: once an identifier is public, avoid breaking changes; add instead of mutate.
---
## 2 Project layout
```
my_lib/
├─ dune-project
├─ README.md – high-level overview & minimal example
├─ CHANGES.md – human-written change-log
├─ lib/ – library source code (alternative to src/)
│ ├─ my_lib.mli – canonical public interface
│ └─ my_lib.ml – implementation
├─ bin/ – executable entry points
└─ test/ – Alcotest or inline-tests
```
*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.*
---
## 3 Modules and sub-modules
| Purpose | Idiom |
| ---------------- | ------------------------------------------------------------------------------ |
| Canonical type | `type t` (abstract) |
| Pretty printer | `val pp : t Fmt.t` |
| Conversions | `val to_string : t -> string`
`val of_string : string -> (t, error) result` (when meaningful) |
| Equality & order | `val equal : t -> t -> bool`
`val compare : t -> t -> int` (when meaningful) |
| Constructors | `val v : ... -> (t, error) result` (or pure `t` if infallible) |
| Low-level view | `module Unsafe : sig … end` (optional) |
*Keep the root module *flat*; introduce nested modules only for clearly separable concerns (e.g. `My_lib.Path`, `My_lib.Map`).*
---
## 4 Naming conventions
* **Modules / files** – lower-case, underscore-separated file names (`my_lib.ml`), capitalised module names (`My_lib`).
* **Values** – short but descriptive (`pp`, `v`, `find`, `with_…`).
* **Labels** – prefer them only when they disambiguate (`~dir`, `~mode`); avoid gratuitous labels on simple data.
* **Boolean arguments** – almost never positional; wrap in a variant or use a labelled argument (`?dry_run:bool`).
* **Infix operators** – expose only if widespread (`>|=`) or central to the library (`Fpath.( / )`). Put them in a dedicated sub-module `Op`.
---
## 5 Types
* Keep *representation hidden* (`type t`) and provide construction/destruction helpers.
* Introduce **phantom parameters** to encode invariants if it avoids run-time checks.
* Use **private types** when callers may inspect but not forge values (`type t = private string`).
* For **enumerations**, use a closed variant; supply `val all : t list` and `val to_string`.
* Reserve **exceptions** for programming errors (`Invalid_argument`, assertion failures). Operational errors travel in the `'error` part of `('a, error) result`.
* **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.
* **Regular expressions**: Use the Re DSL instead of Re.Perl. Prefer `Re.(compile (seq [...]))` over `Re.Perl.compile_pat`.
---
## 6 Error handling pattern (`Rresult`)
### Base Error Types
```ocaml
type error = [ `Msg of string | `Build_error of context ]
val pp_error : error Fmt.t
val v : ... -> (t, error) result
```
### Error Helper Functions Pattern
Start implementation files with error helper functions (`err_*`) for consistent error messages:
```ocaml
(* Error helper functions *)
let err fmt = Fmt.kstr (fun e -> Error (`Msg e)) fmt
let err_file_read file msg = err "Failed to read %s: %s" file msg
let err_file_write file msg = err "Failed to write %s: %s" file msg
```
Use `%a` with pretty printers for complex formatting:
```ocaml
let pp_build_error ppf ctx = Fmt.pf ppf "%s" ctx.output
let err_build_failed ctx = err "Build failed:@.%a" pp_build_error ctx
```
For structured errors with context:
```ocaml
let err_build_error ctx = Error (`Build_error ctx)
```
### Library vs Main Separation
**Critical Rule**: Library code never calls `exit` directly - only returns errors for main.ml to handle.
```ocaml
(* In library code - ALWAYS return structured errors *)
let handle_build_failure ctx =
(* Display specific context *)
Fmt.pr "Loop detected - stopping to prevent infinite iterations.@.";
(* Return error for main.ml to handle *)
err_build_error ctx
(* In main.ml - handle all exit logic *)
match analyze mode root_dir files with
| Ok result -> result
| Error (`Build_error ctx) ->
System.display_build_failure_and_exit ctx (* Exits with build's code *)
| Error e ->
Format.eprintf "Error: %a@." pp_error e;
exit 1 (* Generic error code *)
```
### Error Display Consistency
Use unified display functions for consistent UX:
```ocaml
(* In system.ml *)
let display_build_failure_and_exit ctx =
let all_warnings = (* parse build output *) in
Fmt.pr "%a with %d %s - full output:@."
Fmt.(styled (`Fg `Red) string) "Build failed"
(List.length all_warnings)
(if List.length all_warnings = 1 then "error" else "errors");
Fmt.pr "%a@." pp_build_output ctx;
let exit_code = get_build_exit_code ctx in
exit exit_code
```
### Guidelines
* Accept a caller-provided buffer (`Buffer.t`) or formatter when the operation might produce extensive diagnostics.
* Re-use `Fmt.failwith`/`Fmt.invalid_arg` for internal invariants; never expose raw `Printexc` traces.
* **Never mix exit logic in library code** - always return structured errors that main.ml can handle appropriately.
---
## 7 Public API surface
1. **Minimal** yet **composable**. Do not wrap the entire Unix API—wrap just enough to remove boilerplate.
2. Expose **first-class values** rather than functors where a closure suffices.
3. Keep *effectful* helpers separate (`My_lib_unix`, `My_lib_lwt`) to avoid dragging unwanted dependencies.
---
## 8 Interface documentation (`.mli`, `.mld`)
*Document **every** public item in the `.mli`. Place docstrings **after** the item unless it spans multiple lines, using code-first voice.*
### 8.1 Structure and style
```
(** {1 Overview}
One concise paragraph explaining the abstraction. Link to the README
for a tutorial.
{2 Types}
{3 Constructors}
{4 Accessors}
{5 Derived combinators}
{6 Pretty-printing}
*)
```
* Use **section headings** (`{1 …}`, `{2 …}`) to group logically related functions.
* Provide **usage examples** in `[{[ … ]}]` blocks that compile under `odoc-latency-level:normal`.
* Annotate complexity (`@since`, `@deprecated`, `@raise`, `@see`) rigorously.
* Keep comments declarative: *what* and *guarantees*, never implementation detail.
---
## 9 Testing & auxiliary artefacts
* Unit tests with **Alcotest** (`test/`) mirroring the public API sections.
* **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.
* Provide a `tool/` directory of executable samples that double as integration tests.
* Ship an `odig` metadata file so `odig odoc` builds HTML docs out-of-the-box.
---
## 10 Versioning & release process
* Tag releases with `vN.N.N`; follow semantic versioning.
* Maintain `CHANGES.md` with *user-visible* entries only.
* Publish through `topkg` or Dune’s built-in release workflow; push docs to `gh-pages` via `odoc`.
---
## 11 Style Checklist
| Step | Done |
| ----------------------------------------------------------- | ---- |
| `src/.mli` written first, fully documented | ☐ |
| All errors returned as `'a result` with `Rresult` helpers | ☐ |
| `pp`, `equal`, `compare`, `hash` supplied where meaningful | ☐ |
| No hidden global side-effects; pure core separated | ☐ |
| Example snippets compile under `dune build @doc` | ☐ |
| Dependency list reviewed: stdlib ± {Fmt, Rresult, Alcotest} | ☐ |
| `test/` provides Alcotest suites mirroring API sections | ☐ |
Tick every box before publishing.