Philip2 ("fee-leap the second") is a compiler from Elm to OCaml, using the ReasonML/Bucklescript ecosystem. You can use it to do a semi-automated port of a codebase from Elm to Bucklescript/ReasonML.
To start translating your files:
npm install -g esy@0.4.9 # we build using esy
./install-elm-format # philip2 requires a modified version of elm-format.
esy install
esy build
esy test
./translate MyFile.elm > MyFile.ml
-
debug: run and then type OCaml code to get pretty-printed OCaml AST. This is useful for figuring out how to generate the code you're trying to create an AST for. (run ./debug, then type your code, then press Ctrl-D)
-
parse: a script that parses an Elm file and dumps philip2's understanding of the parse tree.
-
translate: a script to translate a single file from Elm to OCaml.
-
port: a script to help you port your project over to OCaml. Often, you will have to change your Elm code to make the compiler work (for example, the compiler doesn't support the points-free style of function declaration). This script was helpful to me as I changed my code hundreds of times as I developed philip2.
-
elm.ml: a parser for the modified json output of elm-format, as well as type definitions which match the type definitions in elm-format.
-
translate.ml: the compiler's main file. Reads input, calls elm.ml, then translates from the Elm AST and prints out OCaml.
- Create an empty bucklescript project using bucklescript-tea. A pretty good tutorial on this is: https://quernd.github.io/tutorials/tea-chess/index.html
- Copy Porting.ml into your repo. (update: you should be able to use Tablecloth instead now)
- Tweak Porting.ml until it you get it to convert something to Elm.
- Run bsb with the watcher to see errors in the generated files.
- Modify the source Elm until everything converts to OCaml cleanly. See tactics below. By doing this, you'll be able to make changes and validate that your old code continues to work, eg by running tests and unit tests.
- At some point (hopefully quite quickly) it becomes easier to edit the generated OCaml than the Elm.
- Edit the OCaml until it fully compiles.
- There are a number of ways to tweak the code generated by the compiler, namely by changing the following variables in translate.ml:
- config_function_patterns
- config_module_patterns
- config_post_process_patterns
- config_type_patterns
- post_process
These allow you to use regex to modify module, function, and type names, as well as a coarse-grained regex-across-the-whole-file (post_process).
-
Have a DontPort.elm file, in which you put things which don't make sense in OCaml, but which allow you make changes that don't exist in Elm.
-
You're probably going to take on some technical debt during the transition. Sorry.
-
Elm's dicts are more general than Belt's (
Belt.Map.String.t
vsDict String v
). Change your ElmDict Str x
toStrDict x
and then add StrDict to config_module_patterns. You can use our definition in Elm and Ocaml -
Because OCaml isn't great at errors, I found it useful to keep regenerating the OCaml code (using the
port
script) while tweaking things, esp file and type ordering. (However, this was necessary for me because I was writing the compiler alongside it; you may find it simpler to do a conversion once and update by hand afterwards). -
The compiler will use
type x = ... and y = ...
for any types that are adjacent. This should simplify using recursive types. -
The compiler doesn't do anything for recursive function definitions. Use
post_process_patterns
to renamelet myFunc
tolet rec myFunc
. -
Many functions, especially if you plan to use Belt, take arguments in a different order than Elm. To avoid the hassle, use a Porting.ml file which you open everywhere. The compiler will add a
open Porting!
to every file regardless. -
bs-json appears to be much nicer to use for decoders than Tea.Json.
-
You have to decide which standard libraries to use. We ended up porting lots of Elm code directly (eg using the compiler to translate it) rather than calling OCaml libraries (see https://reasonml.chat/t/advice-on-standard-library-to-use/1165) (update: you can now use Tablecloth for this)
-
Elm's Result takes parameters in a different order than Tea.Result and Belt.Result.
-
Check out the useful_elm and useful_ocaml directories (update: you can now use Tablecloth instead) .
This project has a code of conduct - please read and abide by it.
Philip2 is extremely alpha quality. Contributions are welcome to improve this and make it more automated and less semi-automated. In order of priority:
- add tests (read directory of Elm files, check that they are identical to the OCaml file of the same name)
- add CI
- fix bugs
PRs welcome!
PRs welcome (feel free to open an issue or PR to discuss your plans!)
- Philip2 will strip all your comments.
- Points-free style is not supported
- .mli files are not generated
++
does not take into account the type of its arguments: it just becomes^
when sometimes it should become@
. An easy improvement is to check the argument for list or string literals, or known list/string functions. I suspect we can also use the type system but I think this would be hard to make work.- Phillip2 generates OCaml syntax only. Add a flag for ReasonML syntax.
- In the pattern below, i1 and i2 are incorrectly flipped by the compiler. This is true of all literal lists in pattern match clauses.
match x with
| [i2; i1] -> 5
- Nested-else-ifs are reversed. Eg
if a then b
else if c then d
else if e then f
else g
becomes
if e then f
else if c then d
else if a then b
else g
- We should add an ability to target "flavors": whether the generated code uses functions and modules from Bucklescript functions, OCaml pervasives, Jane Street Base, or ported Elm libraries.
Files in the toplevel directory use the MIT license. Elm.ml is somewhat based on avh4/elm-format, which has a BSD. Some functions in useful_ocaml/Porting.ml are based on elm/core (BSD), and from the *.Extra packages in elm-community, which use a BSD license). Others may derive from elsewhere and have unclear providence.