Skip to content

Trivial Metaprogramming tool using the OCaml toplevel

License

Notifications You must be signed in to change notification settings

ocaml-ppx/cinaps

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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:

$ 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

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:

(*$*)
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))

(*$*)

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,
    (* -*- mode: tuareg -*- *)
    include StdLabels
    include Printf
    
    let all_fields = [ "name", "string"; "age", "int" ]
        
  2. In foo.ml,
    (*$ #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"(*$*)
        
  3. In stubs.h,
    /*$ #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);/*$*/
        

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.