Skip to content
forked from ocaml-ppx/cinaps

Trivial Metaprogramming tool using the OCaml toplevel

License

Notifications You must be signed in to change notification settings

janestreet/cinaps

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 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.

About

Trivial Metaprogramming tool using the OCaml toplevel

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • OCaml 97.3%
  • Makefile 1.7%
  • Standard ML 1.0%