Trivial Metaprogramming tool using the OCaml toplevel
OCaml
54
61 commits
updated Jul 29, 2026
* CINAPS - Cinaps Is Not A Preprocessing System
Cinaps is a trivial Metaprogramming tool for OCaml using the OCaml
toplevel.
It is intended for two purposes:
- when you want to include a bit of generated code in a file, but
writing a proper generator/ppx rewriter is not worth it
- when you have many repeated blocks of similar code in your program,
to help writing and maintaining them
It is not intended as a general preprocessor, and in particular cannot
only be used to generate static code that is independent of the
system.
** How does it work?
Cinaps is a purely textual tool. It recognizes special syntax of the
form =(*$ <ocaml-code> *)= in the input. =<ocaml-code>= is evaluated
and whatever it prints on the standard output is compared against what
follows in the file until the next =($ ... *)= form, in the same way
that expectation tests works.
A form ending with =$*)= stops the matching and switch back to plain
text mode. In particular the empty form =(*$*)= can be used to mark
the end of a generated block.
If the actual output doesn't match the expected one, cinaps creates a
=.corrected= file containing the actual output, diff the original file
against the actual output and exits with an error code. Other it
simply exits with error code 0.
For instance:
#+begin_src sh
$ cat file.ml
let x = 1
(*$ print_newline ();
List.iter (fun s -> Printf.printf "let ( %s ) = Pervasives.( %s )\n" s s)
["+"; "-"; "*"; "/"] *)
(*$*)
let y = 2
$ cinaps file.ml
---file.ml
+++file.ml.corrected
File "file.ml", line 5, characters 0-1:
let x = 1
(*$ print_newline ();
List.iter (fun s -> Printf.printf "let ( %s ) = Pervasives.( %s )\n" s s)
["+"; "-"; "*"; "/"] *)
+|let ( + ) = Pervasives.( + )
+|let ( - ) = Pervasives.( - )
+|let ( * ) = Pervasives.( * )
+|let ( / ) = Pervasives.( / )
(*$*)
let y = 2
$ echo $?
1
$ cp file.ml.corrected file.ml
$ cinaps file.ml
$ echo $?
0
#+end_src
You can also pass =-i= to override the file in place in case of
mismatch. For instance you can have a =cinaps= target in your build
system to refresh the files in your project.
** Capturing text from the input
In any form =(*$ ... *)= form, the variable =_last_text_block=
contains the contents of the text between the previous =(*$ ... *)=
form or beginning of file and the current form.
For instance you can use it to write a block of code and copy it to a
second block of code that is similar except for some simple
substitution:
#+begin_src ocaml
(*$*)
let rec power_int32 n p =
if Int32.equal p 0 then
Int32.one
else
Int32.mul n (power n (Int32.pred p))
(*$ print_string (Str.global_replace (Str.regexp "32") "64" _last_text_block) *)
let rec power_int64 n p =
if Int64.equal p 0 then
Int64.one
else
Int64.mul n (power n (Int64.pred p))
(*$*)
#+end_src
Now, whenever you modify =power_int32=, you can just run cinaps to update
the =power_int64= version.
** Sharing values across multiple files
The toplevel directive ~#use~ works in CINAPS, and can be used to read in values
from other files. For example,
1. In ~import.cinaps~,
#+BEGIN_SRC ocaml
(* -*- mode: tuareg -*- *)
include StdLabels
include Printf
let all_fields = [ "name", "string"; "age", "int" ]
#+END_SRC
2. In ~foo.ml~,
#+BEGIN_SRC ocaml
(*$ #use "import.cinaps";;
List.iter all_fields ~f:(fun (name, type_) ->
printf "\n\
external get_%s : unit -> %s = \"get_%s\"" name type_ name) *)
external get_name : unit -> string = "get_name"
external get_age : unit -> int = "get_age"(*$*)
#+END_SRC
3. In ~stubs.h~,
#+BEGIN_SRC C
/*$ #use "import.cinaps";;
List.iter all_fields ~f:(fun (name, _) ->
printf "\n\
extern value get_%s(void);" name) */
extern value get_name(void);
extern value get_age(void);/*$*/
#+END_SRC
Etc.
Note that the ~#use~ directive will read in OCaml from files of any extension.
~*.cinaps~ is a safe choice in the presence of jenga and dune, which by default
try to use all ~*.ml~ files in the directory for the executables or library.
** Automatic reformatting of CINAPS output
In files managed by automatic formatting tools such as ocp-indent or
ocamlformat, the code need not come out of CINAPs already formatted correctly.
~cinaps.exe -styler FOO~ uses ~FOO~ to reformat its output, before diffing
against the source file.
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
OCaml
98.1%
Makefile
1.7%
Trivial Metaprogramming tool using the OCaml toplevel
OCaml
54
61 commits
updated Jul 29, 2026
* CINAPS - Cinaps Is Not A Preprocessing System
Cinaps is a trivial Metaprogramming tool for OCaml using the OCaml
toplevel.
It is intended for two purposes:
- when you want to include a bit of generated code in a file, but
writing a proper generator/ppx rewriter is not worth it
- when you have many repeated blocks of similar code in your program,
to help writing and maintaining them
It is not intended as a general preprocessor, and in particular cannot
only be used to generate static code that is independent of the
system.
** How does it work?
Cinaps is a purely textual tool. It recognizes special syntax of the
form =(*$ <ocaml-code> *)= in the input. =<ocaml-code>= is evaluated
and whatever it prints on the standard output is compared against what
follows in the file until the next =($ ... *)= form, in the same way
that expectation tests works.
A form ending with =$*)= stops the matching and switch back to plain
text mode. In particular the empty form =(*$*)= can be used to mark
the end of a generated block.
If the actual output doesn't match the expected one, cinaps creates a
=.corrected= file containing the actual output, diff the original file
against the actual output and exits with an error code. Other it
simply exits with error code 0.
For instance:
#+begin_src sh
$ cat file.ml
let x = 1
(*$ print_newline ();
List.iter (fun s -> Printf.printf "let ( %s ) = Pervasives.( %s )\n" s s)
["+"; "-"; "*"; "/"] *)
(*$*)
let y = 2
$ cinaps file.ml
---file.ml
+++file.ml.corrected
File "file.ml", line 5, characters 0-1:
let x = 1
(*$ print_newline ();
List.iter (fun s -> Printf.printf "let ( %s ) = Pervasives.( %s )\n" s s)
["+"; "-"; "*"; "/"] *)
+|let ( + ) = Pervasives.( + )
+|let ( - ) = Pervasives.( - )
+|let ( * ) = Pervasives.( * )
+|let ( / ) = Pervasives.( / )
(*$*)
let y = 2
$ echo $?
1
$ cp file.ml.corrected file.ml
$ cinaps file.ml
$ echo $?
0
#+end_src
You can also pass =-i= to override the file in place in case of
mismatch. For instance you can have a =cinaps= target in your build
system to refresh the files in your project.
** Capturing text from the input
In any form =(*$ ... *)= form, the variable =_last_text_block=
contains the contents of the text between the previous =(*$ ... *)=
form or beginning of file and the current form.
For instance you can use it to write a block of code and copy it to a
second block of code that is similar except for some simple
substitution:
#+begin_src ocaml
(*$*)
let rec power_int32 n p =
if Int32.equal p 0 then
Int32.one
else
Int32.mul n (power n (Int32.pred p))
(*$ print_string (Str.global_replace (Str.regexp "32") "64" _last_text_block) *)
let rec power_int64 n p =
if Int64.equal p 0 then
Int64.one
else
Int64.mul n (power n (Int64.pred p))
(*$*)
#+end_src
Now, whenever you modify =power_int32=, you can just run cinaps to update
the =power_int64= version.
** Sharing values across multiple files
The toplevel directive ~#use~ works in CINAPS, and can be used to read in values
from other files. For example,
1. In ~import.cinaps~,
#+BEGIN_SRC ocaml
(* -*- mode: tuareg -*- *)
include StdLabels
include Printf
let all_fields = [ "name", "string"; "age", "int" ]
#+END_SRC
2. In ~foo.ml~,
#+BEGIN_SRC ocaml
(*$ #use "import.cinaps";;
List.iter all_fields ~f:(fun (name, type_) ->
printf "\n\
external get_%s : unit -> %s = \"get_%s\"" name type_ name) *)
external get_name : unit -> string = "get_name"
external get_age : unit -> int = "get_age"(*$*)
#+END_SRC
3. In ~stubs.h~,
#+BEGIN_SRC C
/*$ #use "import.cinaps";;
List.iter all_fields ~f:(fun (name, _) ->
printf "\n\
extern value get_%s(void);" name) */
extern value get_name(void);
extern value get_age(void);/*$*/
#+END_SRC
Etc.
Note that the ~#use~ directive will read in OCaml from files of any extension.
~*.cinaps~ is a safe choice in the presence of jenga and dune, which by default
try to use all ~*.ml~ files in the directory for the executables or library.
** Automatic reformatting of CINAPS output
In files managed by automatic formatting tools such as ocp-indent or
ocamlformat, the code need not come out of CINAPs already formatted correctly.
~cinaps.exe -styler FOO~ uses ~FOO~ to reformat its output, before diffing
against the source file.
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
OCaml
98.1%
Makefile
1.7%