[Ann] lwt-to-eio: Automating the mechanical parts of Lwt -> Eio migration

Hi everyone,

Like many of you, I’ve been looking into migrating my libraries from Lwt to Eio to take advantage of OCaml 5’s direct-style concurrency. While the manual migration guides are excellent, I found myself making the same mechanical changes over and over—flattening binds, unwrapping promises, and rewriting maps.

So, I built lwt-to-eio, a tool that automates the boring parts of this transition.

Repo: https://github.com/oug-t/lwt-to-eio

What it does (The Magic)

It parses your OCaml source code and performs AST-level rewrites to transform monadic Lwt patterns into direct-style Eio code.

Input (Lwt):

let fetch_data id =
  Lwt.bind (Db.get_user id) (fun user ->
    Db.get_posts user.id >>= (fun posts ->
      Lwt_list.map_p process posts
    )
  )

Output (Eio)

let fetch_data id =
  let user = Lwt_eio.Promise.await_lwt (Db.get_user id) in
  let posts = Lwt_eio.Promise.await_lwt (Db.get_posts user.id) in
  Eio.Fiber.List.map process posts

Current Status & Help Wanted

This is currently an MVP. It handles the most common patterns I encountered (binds, map_p, sleep, return), but there is plenty of surface area left to cover.

I’m looking for contributors to help add rules for other common patterns. I’ve tagged a few issues as “Good First Issues” if anyone wants to dip their toes into ppxlib and AST rewriting:

  • Lwt.catchtry/with: Transforming monadic error handling to direct style exceptions.

  • Lwt_io support: Mapping legacy IO functions to Eio buffers.

Feedback, PRs, and “It broke on my file!” reports are all very welcome.

Happy hacking!

3 Likes

Lwt 6.0 has just been released (announce thread) with support for direct-style usage via a Lwt_direct – see lwt_direct.mli. Have you considered having a version of your tool to convert from standard Lwt style to Lwt_direct?

I suppose the output on this example could be as follows:

let fetch_data id =
  let user = Lwt_direct.await (Db.get_user id) in
  let posts = Lwt_direct.await (Db.get_posts user.id) in
  Lwt_list.map_p process posts
4 Likes

i want to direct your attention to GitHub - tarides/ciao-lwt: Tools for migrating away from Lwt which is a project by @Juloo with a similar aim

1 Like

Thanks for the heads up, @gasche! I will look closely at the Lwt 6.0, the direct-style syntax seems much cleaner. A transformation to Lwt_direct seems like a great addition to the tool.

I’ll take a look at @Juloo’s work to see how they handle migration as well. Thanks @raphael-proust !!

I went head and implemented this!

Now the tool supports target ‘Lwt_direct’ specffically using new ‘–taget’ flag:

# Default (Eio)
dune exec lwt-to-eio -- file.ml

# Lwt 6.0 Direct Style
dune exec lwt-to-eio -- --target direct file.ml

It preserves the Lwt structure but swaps binds/operators for Lwt_direct.await.

Input:

Lwt.bind (DB.get_user id) (fun user -> ...)

Output (–target direct):

let user = Lwt_direct.await (Db.get_user id) in

Thanks again for the tip!

1 Like

I’ve taken a closer look on ‘ciao-lwt’. It’s a powerful tool, but fairly heavy due to reliance on ‘ocaml-index’.

I want to keep ‘lwt-to-eio’ as a lightweight alternative that can run without a full build environment. That said, I will definitely study their apporach to Lwt.catch for inspiration!