[ANN] ocamlgrep 0.1.1

We are happy to announce the first public release of ocamlgrep, a tool for structural grepping of OCaml code.

This tool has existed within the walls of LexiFi for a long time and we were keen on open-sourcing it, but it was not in a form that could be used by the wider public. Now, thanks to the work of @mjambon, we are able to to finally do so.

To install:

$ opam install ocamlgrep

The idea behind the tool is simple: you call it from within your Dune project[1] with a query (after having built all .cmt/.cmti artifacts, eg by doing dune build @check), and the tool returns the list of matches it can find in the source tree. A query is syntactically an OCaml expression, possibly with holes __ in it. Some examples follow to give an idea of how the tool is used in practice.

The following query searches for the anti-pattern List.rev e1 @ e2 (where e1 and e2 are arbitrary expressions.

$ ocamlgrep 'List.rev __ @ __`

Note that as the tool works at the level of the OCaml AST, it will also match expressions of the form (@) (e1 |> List.rev) e2, since they produce the same AST.

The syntax of type constraints (e : ty) is overloaded to impose a type condition on the search term. For example, the following search query looks for function calls where the first argument is an int and the second one a string.

$ ocamlgrep '__ (__ : int) (__ : string)'

The holes can be numbered, __1, __2, etc, to express repetitions of the same term. For example, the following query searches for a pattern matching on an option that sends Some x to Some x (ie reconstructing the same value):

$ ocamlgrep 'match __ with Some __1 -> Some __1 | None -> __'

We can also look for applications of the polymorphic operator = applied to float arguments:

$ ocamlgrep '(__ : float) = __'

Historically, this tool has been useful for large-scale refactorings and linting of our codebase. Nowadays, such refactorings can often be done using AI agents. However, the tool is still able to do things that seem a bit beyond of what AI agents can do today, eg to look for applications of polymorphic functions where one of the arguments is of a specific type. This was for example useful when migrating our codebase to no-flat-float-array mode, where we wanted to make sure that polymorphic array operations were not being applied to float array values.

Happy grepping!

Cheers,
Nicolas


  1. Only Dune projects are supported for now. Adding support for other build systems should not be very hard, issues and/or PRs are welcome. ↩︎

33 Likes

I’ve just tried it on a large codebase to find bad polymorphic comparisons and it worked really well, thanks a lot for sharing this!

6 Likes

Thanks for sharing this!

I was curious whether the project adopted any mechanisms for normalizing expressions that are distinct at the AST level but semantically equivalent.

This is interesting to me because I’ve found a similar need when writing a PPX. For instance, it’s a pain to have to write a matcher for function applications that may appear as f a (b c) or f a @@ b c.

I ended up writing a ppxlib AST transformer to address my issues but wasn’t confident that I solved the problem exhaustively, and it seemed to me like the kind of utility function that would benefit from a canonical implementation in Compiler_libs or ppxlib. Maybe it is there and I just don’t know about it. :slight_smile:

1 Like

Call me old fashioned but I hope that people who develop refactoring tools and those who fund them are not going to think that way.

There’s nothing comparable between a tool that operates deterministically based on the logic of a system and one which has absolutely no formal idea of said logic and performs changes on what it deems statistically plausible.

18 Likes

No, we don’t do anything special in this direction. But note that since we match the user query against the typed AST stored in the compilation artifacts, any normalisation done by the compiler itself will be taken into account.

For example, in the typed AST, the expression a @@ b c is represented in exactly the same way as a (b c). And so the tool will match either indistinctly.

Cheers,
Nicolas

1 Like

Looks useful!
Any way to use it without dune? Can it be pointed to where the cmx/cmo files are located?

1 Like

Not currently. The tool needs to know where to find .cmt/.cmti files and if I’m not wrong it also needs to understand the library dependency graph of your project in order to correctly set up the load path.

Cheers,
Nicolas

Self-plug, but you could also use ocamlmig, if you want to rewrite rather than merely grep for these bad comparisons + fix by hand. For instance, I used this to replace all uses of plain = in one of my projects.

2 Likes

We could support another build system. What we need is:

  • a list of cmt files - these are the files that are scanned by ocamlgrep
  • the list of “include dirs” for all the libraries used by the project i.e. the location of cmi files - this is used to resolve type aliases; without this, ocamlgrep still mostly works, though
  • a way to recover the real source file paths from the paths found in the cmt files (in the case of Dune, a path found as e.g. src/foo.ml means <workspace root>/_build/<context>/src/foo.ml but we translate it into <workspace root>/src/foo.ml if such a file exists (which may not be the case if foo.ml was generated via Dune)).

We obtain all this info from Dune using dune describe workspace. @rixed which build system do you have in mind and does it provide the info we need?

You can probably use .merlin if it is present. Some build systems generate it, sometimes it is hand-written.

Obviously, it will only work if .cmt files are generated and in the same place as the .cmi, .cmo … files

Is possible to invoke the tool manually with this information ?

Except for finding the source files, what you require doesn’t look very different from what a compiler invocation needs.

Allowing to provide this information manually via cli flags would be a good first step (and would invert the burden of supporting everyone’s pet build system out there).

2 Likes

an important feature missing from the announcement but i checked and it works: it supports queries across open modules.

open Lwt.Infix

… >>= …

will trigger for Lwt.Infix.( >>= ) (and for ( >>= )).

3 Likes

Indeed. The tool matches against the typed AST, where all names have been fully resolved by the compiler, so matching transparently works across opens, local opens, etc.

Cheers,
Nicolas

1 Like

Nice, thanks for the feedback! FWIW, we have another internal tool at LexiFi to track use of polymorphic (structural or physical) equality/comparison, with some nice features such as marking polymorphic functions exposed by some module as applying those operation internally on some of their type variables, etc. Hopefully we’ll find time some day to publish it as well!

2 Likes

Ocamlgrep could locate cmt files, cmi files, and source files by receiving a list of folders where it should look for them like Merlin does. It could support the .merlin file format (S for sources, B for cmt and cmi files; PKG could be forwarded to ocamlfind query). The same info could be also specified on the command line (--source-dir, --build-dir, --findlib-package).

We would need to distinguish the “build dirs” that provide cmi files for external libraries from those that provide the cmt files we want to scan. What we want to scan is usually some kind of workspace, project, or a subproject (folder within a project). Here’s a problem:

It would be best if a request to scan a subproject would only scan the cmt files for the subproject rather than scanning all the cmt files and discarding the results for source files outside the subproject. Here’s an example:

.
├── _build
│   ├── foo.cmt
│   └── test.cmt
├── src
│   └── foo.ml
└── tests
    └── test.ml

This made-up example is meant to illustrate that there’s no standard mapping between cmt files and their source files. If I run ocamlgrep failwith tests/ (+ options to provide build dirs, source dirs, etc.), how should it know to scan test.cmt and not foo.cmt?

I don’t have a good answer. Here are some ideas:

  • “the build system should provide a mapping : source file path → cmt file path”: not going to happen soon enough
  • “the user should provide a mapping : source file path → cmt file path”: ok, but how?
  • “provide all the cmt files on the ocamlgrep command line”: maybe feasible with some scripting to wrap around ocamlgrep?
  • “scanning subprojects is only supported by Dune and <list supported build systems>”

Providing the ability to explicitly specify things on the command line is a good idea, as well as best-effort modes.

Regarding this:

I don’t think you’d get into deep problems by simply:

  1. Recursively list the compilation unit names in tests
  2. Match them by name the build dir (possibly conditional on a --x-dir to exclude directories). If there are ambiguities you can disambiguate them at the cost of loading matching .cmt files and looking up that field.

In 2. however, compilation may have been done under the (entirely undocumented in the manual as far as I can tell and still broken) ocaml implementation of BUILD_PATH_PREFIX_MAP. So you may want to factor that in (allowing to specify it on the cli and/or look it up in the env) and give up if the map is not bijective (I think the not so great spec should have required this, I want both my reproducible artefacts and be able to trace back to their source).

1 Like

You will probably find the information in cmt_infos.cmt_sourcefile. In your example, _build/test.cmt should point to tests/test.ml.
Thus, you could match the command line path tests/ against the cmt_sourcefile.
Note: the cmt_sourcefile may not point to the original sourcefile. It is the one that actually corresponds to the cmt (see Unit_info.input_source_file). This can be combined with cmt_infos.cmt_builddir to gather a more complete sourcepath.

A real world example can be observed in Frama-C (noticed when using the dead_code_analyzer):
file _build/default/src/.frama_c_kernel.objs/byte/frama_c_kernel__Special_hooks.cmt points to src/kernel_internals/runtime/special_hooks.pp.ml. Notice how the kernel_internals/runtime/ part is absent from the cmt’s path.
The original sourcefile is src/kernel_internals/runtime/special_hooks.ml. Preprocessing added an extra .pp in the cmt_sourcefile.

This is cool. I just used it to check how a certain type was used in a large code base I am refactoring: ocamlgrep '(__ : Foo.t). This is useful. What is a bit surprising to me (but probably not a problem) is that it showed foo in the body of functions but not the (foo : Foo.t) argument further up in the function.

I opened two issues that would make the experience a bit nicer for me:

Thanks for sharing this tool!

1 Like

Currently we match the search term (which is syntactically an expression) against an expression in the source code. The (foo : Foo.t) argument is not syntactically an expression, but a pattern. I don’t see why we couldn’t match it as well, but it is not done at the moment.

Cheers,
Nicolas

1 Like

This approach requires listing the files in each of the build dirs so we can fish out the relevant cmt files. Maybe this is fast enough on most projects, I don’t know.

(possibly conditional on a --x-dir to exclude directories)

Providing such options on the command line would be a bit cumbersome, especially if this needs to be done every time when working on a given large project.

Having an ocamlgrep-specific config file is also a possibility but it doesn’t seem like a reasonable burden to place on a user.

I didn’t know about this. Thanks for the info.