Skip to content

Irmin lwt shim#2406

Draft
balat wants to merge 26 commits into
mirage:eiofrom
balat:irmin-lwt-shim
Draft

Irmin lwt shim#2406
balat wants to merge 26 commits into
mirage:eiofrom
balat:irmin-lwt-shim

Conversation

@balat

@balat balat commented May 6, 2026

Copy link
Copy Markdown
Member

WIP. Not ready for review

This PR adds irmin-lwt, a thin shim that exposes the familiar Lwt-typed Irmin 3 API on top of Irmin 4's direct-style
implementation.

Important limitation: The application's main loop must be Eio:

let () =
  Eio_main.run @@ fun env ->
  Eio.Switch.run @@ fun sw ->
  Irmin.Backend.Watch.set_watch_switch sw;  (* if you use watches *)
  Lwt_eio.with_event_loop ~clock:env#clock @@ fun _ ->
  Lwt_main.run @@ fun () ->
  ...

TL;DR for reviewers — the PR is smaller than the diff suggests

The diff shows ~18 400 lines added. ~14 700 of them are mechanically verifiable copies of Irmin 3 source code, not new code to review. The remaining ~3 700 lines are the actual shim implementation plus tests, docs, and opam metadata.

If you have 30 minutes for this PR, read these commits only:

Commit What Lines
add Lwt_to_eio adapter functors Lwt ↔ Eio bridges, used by every shim 313
add Wrap_store Generic Lwt wrap of any Eio Generic_key.S. The core of the shim. 816
add Maker_v2 Thin glue calling Wrap_store on Lwt-typed Makers 40
top-level Irmin_lwt.{ml,mli} + dune file Public API surface 680
8 per-package shim commits (mem, pack, fs, chunk, containers, git, client, tezos) Per-backend wrappers ~1 200

Total to review carefully: ~3 050 lines.

The other commits are either pure file copies or trivial mechanical adaptations:

Commit What How to verify
copy Lwt-typed Irmin 3 sources verbatim from main 83 files, byte-identical to main:src/irmin/. git diff main:src/irmin/X src/irmin-lwt/core/X → empty for each file
reformat imported sources with project ocamlformat (0.29.0) Same files, dune fmt applied (main is on 0.26.2). git show <commit> --stat shows only the files touched by dune fmt
strip Store.Make and Tree.Make implementations 4 files reduced to include Sigs. Reason: replaced by Wrap_store. Read commit message, inspect the 4 small files
alias [Closed] to [Irmin.Closed] 4-line patch Read the change
reduce non-Lwt modules to re-exports of Irmin 4 12 files reduced to one-line include Irmin.X. Each file has 1 line
alias [Remote.t] to [Irmin.remote] 1-line semantic patch Read the change
irmin-lwt-test: import Irmin 3 test harness verbatim from main 12 files byte-identical to main:src/irmin-test/. git diff main:src/irmin-test/X src/irmin-lwt/test/X → empty
irmin-lwt-test: adapt harness to Irmin_lwt + reformat 3 substantive patches across the harness (rename Irmin → Irmin_lwt, add Lwt.Syntax, replace one Conf.ty call with Conf.of_string), plus a dune fmt. ~120 effective lines
CHANGES and the doc commit One CHANGES entry and three .mld / .md docs Read briefly

The commits are designed so each can be reviewed independently with a clear acceptance criterion. The actual shim work is small; the rest is the necessary Irmin 3 Lwt-typed surface re-hosted to make the shim self-contained.

What's in this PR

Eight new opam packages, plus the core shim and a test harness:

Package Role
irmin-lwt Lwt-typed top-level API + the Wrap_store / Lwt_to_eio machinery
irmin-lwt-test Port of irmin-test for Lwt-typed stores
irmin-lwt-mem In-memory backend
irmin-lwt-pack On-disk pack backend (full surface: GC, snapshots, integrity, stats)
irmin-lwt-fs Filesystem backend
irmin-lwt-chunk Chunking meta-backend
irmin-lwt-containers Mergeable data structures (Counter / Lww_register / Blob_log)
irmin-lwt-git Git backend (Mem + FS)
irmin-lwt-client RPC client for irmin-server
irmin-lwt-tezos Tezos schema (V1 pre-hash preserved, on-disk wire-compatible with irmin-tezos)

API users replace Irmin.X with Irmin_lwt.X in their code; module shape, operation names and types are preserved (effectful ops return Lwt.t, like Irmin 3). A few configurations (config ~root ~clock, Gc.run ~domain_mgr, Snapshot.export ?on_disk) take Eio types directly; the shim does not hide Eio entirely.

Architecture in two pieces

  • Lwt_to_eio (src/irmin-lwt/core/lwt_to_eio.ml) — a small library of bridge adapter functors between the shim's Lwt-typed module types and Irmin 4's Eio-typed counterparts. Each adapter wraps a function through Lwt_eio.run_eio or Lwt_eio.Promise.await_lwt.
  • Wrap_store (src/irmin-lwt/core/wrap_store.ml, ~800 lines) the heart of the shim. Takes a user's Lwt-typed Schema, a parallel Eio-typed Schema_eio, and an Eio Inner : Generic_key.S; produces a Lwt-typed Generic_key.S whose effectful operations forward to Inner through Lwt_eio.run_eio. Every package goes through it.

How to review

The history is structured for review-friendliness:

Range What
1 opam files for all packages
2 Verbatim copy of 83 Lwt-typed sources from main:src/irmin/ (byte-identical, easy to verify)
3 dune fmt reformat (main is on ocamlformat 0.26.2, project is on 0.29.0)
4-7 Targeted adaptations: strip Store.Make/Tree.Make, alias Closed to Irmin.Closed, reduce non-Lwt modules to include Irmin.X, alias Remote.t to Irmin.remote
8-11 New code: Lwt_to_eio, Wrap_store, Maker_v2, Irmin_lwt.{ml,mli} + dune
12-13 irmin-lwt-test harness: verbatim copy from main:src/irmin-test/ then minor adaptations (module Irmin = Irmin_lwt, Conf.tyConf.of_string)
14-21 One commit per backend package (mem, pack, fs, chunk, containers, git, client, tezos), each self-contained: lib + opam + smoke test
22-23 Documentation (doc/irmin-lwt/index.mld + migration guide + LIMITATIONS.md) and CHANGES.md

Each commit message states whether the change is a literal copy, a reformat, a small adaptation, or new code, with rationale.

What is not supported

Documented in src/irmin-lwt/LIMITATIONS.md. Headlines:

  • Of_backend — not exposed. Routing a hand-rolled Lwt-typed Backend.S through Irmin 4 would require ~400 lines of bridges and a per-operation Lwt → Eio → Lwt round-trip. Backend authors should build against Irmin's direct-style Backend.S and re-wrap with Wrap_store.Make.
  • Generic_key.Maker (functor) — the module type is exposed (so backends like irmin-lwt-pack declare against it); only the functor implementation is dropped, for the same round-trip reason.
  • irmin-graphql, irmin-mirage*, irmin-cli, irmin-server — not ported. irmin-graphql is already Lwt-typed upstream; irmin-mirage-git is already Lwt-typed in its public API (Mirage_kv forces it); irmin-cli is a binary; irmin-server is consumed via irmin-lwt-client.

Tests

Each backend ships a smoke test under test/irmin-lwt-<pkg>/test.ml. On top of the smoke tests, the full irmin-test harness from main (ported as irmin-lwt-test) runs against four backends:

Backend Harness irmin-test Tests passed Notes
irmin-lwt-mem 29 / 29
irmin-lwt-fs 28 / 28
irmin-lwt-chunk 29 / 29 combined with irmin-lwt-mem Atomic_write
irmin-lwt-git 28 / 28 Mem variant
irmin-lwt-pack ✗ (smoke only) 4 smoke 2 pack-specific quirks (Pending_flush on concurrent close, Big Yikes via Atomic_write watcher) — same constraints handled by main:test/irmin-pack with a dedicated suite
irmin-lwt-containers ✗ (smoke only) 3 smoke not a Generic_key.S backend (data structures)
irmin-lwt-client ✗ (smoke only) 1 smoke needs a running irmin-server, smoke spawns one in-process
irmin-lwt-tezos ✗ (smoke only) 1 smoke Contents = bytes; harness requires Contents.t = string

Total: 114 harness tests + 9 dedicated smoke tests = 123 tests.

Sample run:

$ dune test test/irmin-lwt-mem/ ...
basic set/get                  ok
branch and commit              ok
sync between repos             ok
remote E constructor           ok
dot output                     ok
Test Successful in 0.40s. 29 tests run.
chunked set/get small+big      ok
Test Successful in 0.51s. 29 tests run.
basic set/get / branch / persistence    ok      (fs)
Test Successful in 1.98s. 28 tests run.
basic set/get / branch / pack advanced  ok      (pack, smoke only)
git_commit passthrough         ok
Test Successful in 1.05s. 28 tests run.
set/get over Unix socket       ok      (client, against an in-process server)
tezos schema basic             ok

Migration

A step-by-step migration guide for users moving from Irmin 3 to Irmin 4 via the shim is in doc/irmin-lwt/migration.mld.

Code metrics

 Verbatim copy from main (Irmin 3)  : ~7 800 lines (core + harness)
 Re-exports from Irmin 4            :   ~800 lines
 New code (shim core + 8 packages)  : ~3 300 lines
   of which Wrap_store               :    816 lines
            Lwt_to_eio                :    313 lines
            Irmin_lwt.{ml,mli}        :    680 lines
            Maker_v2                  :     40 lines
            8 backend shims           : ~1 200 lines
 Smoke tests                        :    766 lines
 Documentation                      :    648 lines

 .mli files (subset of the above)   :  1 754 lines (~10%) (+ core/store_intf.ml (1 243) + core/tree_intf.ml (468) = 3500)
   of which irmin_lwt.mli            :    542 lines  (new -- public API)
            other core/*.mli         :    762 lines  (verbatim main)
            per-package shim .mli    :    316 lines  (new)
            test harness *.mli       :    134 lines  (verbatim main)

balat added 23 commits May 6, 2026 17:01
Adds opam metadata for [irmin-lwt] core and the eight backend / helper
packages: [irmin-lwt-mem], [irmin-lwt-pack], [irmin-lwt-fs],
[irmin-lwt-chunk], [irmin-lwt-containers], [irmin-lwt-git],
[irmin-lwt-client], [irmin-lwt-tezos], plus the test harness
[irmin-lwt-test].
This commit imports 83 module-type and module-implementation files
from [main:src/irmin/], unchanged. They form the Lwt-typed surface
layer of the shim:

- Module types and signatures: read_only, append_only, indexable,
  content_addressable, atomic_write, contents, branch, node, commit,
  metadata, schema, slice, remote, sync, dot, watch, storage, lock,
  lru, store, store_properties, tree, proof, object_graph, hash,
  path, info, conf, key, perms, type, diff, merge, metrics, version,
  backend, plus their *_intf.ml siblings.
- Implementation files imported verbatim: branch.ml, commit.ml,
  contents.ml, dot.ml, hash.ml, info.ml, lock.ml, lru.ml,
  metadata.ml, metrics.ml, node.ml, object_graph.ml, path.ml,
  perms.ml, proof.ml, remote.ml, schema.ml, slice.ml, storage.ml,
  store.ml, store_properties.ml, sync.ml, tree.ml, type.ml,
  watch.ml, plus the export_for_backends and import shims.

Each file is byte-identical to its main:src/irmin/<basename> source
at the time of import. Subsequent commits adapt them to the shim's
needs (project-wide ocamlformat reformat, stripping of [Make]
implementations, transparent re-exports of pure modules from
Irmin 4, signature tweaks).
The Irmin 3 ([main]) sources imported in the previous commit were
formatted under ocamlformat 0.26.2; the [eio] branch we are layering
on top uses 0.29.0. [dune fmt] reformats the imported files to match
the project's current style. No semantic change.
[Store.Make (B : Backend.S)] (~1300 lines, [main:src/irmin/store.ml])
and [Tree.Make (B : Backend.S)] (~2800 lines,
[main:src/irmin/tree.ml]) are removed from the shim. They are not
needed: a later commit adds [Wrap_store.Make], which wraps the
already-built Eio [Generic_key.S] from [Inner] back into a Lwt-typed
surface, delegating tree machinery to [Inner.Tree]. The two functor
declarations are also removed from [Store_intf.Sigs] and
[Tree_intf.Sigs]; only the module types stay.

[Json_tree] and the [type Remote.t += Store of ...] constructor are
preserved in [store.ml] (they operate on any [Store.S]). The rest of
the file is reduced to [include Store_intf].
[main:src/irmin/store_properties.ml] declared a fresh [exception
Closed]. With Irmin 4 layered underneath, exceptions raised by the
Eio backend through [Lwt_eio.run_eio] would be of type [Irmin.Closed]
(the Eio one), not the local fresh [Closed]. User code that wrote
[Lwt.catch ... (function Irmin.Closed -> ...)] would silently miss
those exceptions.

Aliasing the local [Closed] to [Irmin.Closed] makes the two
constructors equal -- pattern matching on either reaches the same
exception value.
Twelve modules in [main:src/irmin/] are pure (no [Lwt.t] / [Merge.t] /
no Lwt-typed sub-stores in their signatures): they're identical in
shape between Irmin 3 and Irmin 4. Rather than carry a separate copy
in the shim, each is reduced to a one-line transparent re-export of
the Irmin 4 module. The list:

- [path.ml]               -> [include Irmin.Path]
- [hash.ml]               -> [include Irmin.Hash]
- [info.ml]               -> [include Irmin.Info]
- [conf.ml] / [conf.mli]  -> re-export [Irmin.Backend.Conf]
- [key.ml]                -> [include Irmin.Key]
- [diff.ml]               -> [include Irmin.Diff]
- [perms.ml]              -> [include Irmin.Perms]
- [export_for_backends.ml] -> [include Irmin.Export_for_backends]
- [type.ml]               -> [include Repr] + a small [Defaultable]
                              local type
- [metrics.ml]            -> [include Irmin.Metrics]
- [append_only.ml]        -> collapsed to [include Append_only_intf]
                              (alongside its existing [_intf.ml])
- [read_only.ml]          -> collapsed to [include Read_only_intf]
…fication)

Without this alias, the shim's [Remote.t] and [Irmin.remote] are two
distinct extensible variants. Constructors added by Irmin 4 backends
(notably [Backend.E of endpoint] inside any [Inner] store) cannot be
re-exported from the shim's surface.

Declaring [type t = Irmin.remote = ..] makes them the same type and
lets [Wrap_store.Make] forward the inner [E] constructor with [type
Remote.t += E = Inner.E].
A collection of bidirectional adapter functors between the shim's
Lwt-typed module types and Irmin 4's Eio-typed module types. Each
adapter wraps a function via [Lwt_eio.run_eio] (Eio direct -> Lwt.t)
or [Lwt_eio.Promise.await_lwt] (Lwt.t -> Eio direct), depending on
direction.

Provides:

- [merge_of_eio] / [merge_to_eio] -- bridge [Irmin.Merge.t] to / from
  [Merge.t] for a given type descriptor.
- [Metadata] / [Contents] -- lift Lwt-typed Schema sub-modules to
  [Irmin.Metadata.S] / [Irmin.Contents.S].
- [Schema] / [Schema_extended] -- lift a Lwt [Schema.S] to
  [Irmin.Schema.S] / [Irmin.Schema.Extended] for backends like
  [Irmin_pack_unix.Maker] that need [Extended].
- [Indexable_of_eio] / [Atomic_write_of_eio] -- Eio store -> Lwt
  store wrappers, one operation per call.
- [Indexable_to_eio] / [Atomic_write_to_eio] -- mirror direction.
- [Content_addressable] / [Atomic_write] / [Append_only] -- Lwt
  Maker -> Eio Maker bridges, used to feed user-supplied Lwt
  backend Makers into [Irmin.Maker].

Used internally by [Wrap_store], [Maker_v2], and per-package
shims.
…S (new code)

[Wrap_store.Make (S : Schema.S) (Schema_eio : Irmin.Schema.S with ...)
(Inner : Irmin.Generic_key.S with module Schema = Schema_eio)]
produces a Lwt-typed [Generic_key.S] whose [Schema] is the user's
input [S] and whose effectful operations forward to [Inner] through
[Lwt_eio.run_eio].

This is the core of the shim. Every backend produced by [irmin-lwt]
goes through it: the per-package shim builds an Eio [Inner] and
passes it to [Wrap_store.Make] alongside the user's Lwt-typed
[Schema] and a parallel Eio-typed [Schema_eio] (constructed via
[Lwt_to_eio.Schema] or [Lwt_to_eio.Schema_extended]).

Wraps:
- types [repo, t, contents, contents_key, node_key, commit_key,
  hash, metadata, tree, ...] (passed through from [Inner])
- top-level read and write ops
  ([find / mem / set_exn / test_and_set / merge / ...]) - 35+ ops
- [Repo] sub-module (v / close / heads / branches / batch /
  export / import / ...)
- [Branch] sub-module (Atomic_write-like ops with watch callbacks
  bridged)
- [Head] / [Commit] / [Info] / [Status] / [Hash] sub-modules
- [Tree] sub-module (in-memory tree ops, sub-modules including
  [Proof] / [Private] / converters / [counters] hoisted to top
  for nominal type identity)
- [Backend] sub-module (Slice with iter callbacks bridged, Branch
  re-exposed, Repo with batch bridged, Remote, plus Node and
  Commit with their merges bridged via [merge_of_eio])
- [Sync.E] forwarded as [Inner.E] for cross-store remote endpoints

Result: the user gets a fully Lwt-flavoured [Generic_key.S] backed
by an Irmin 4 store.
[Maker_v2.Make (CA) (AW) (S)] is the Lwt-flavoured [Irmin.Maker]
implementation:

  module CA_eio (H) (V) = Lwt_to_eio.Content_addressable (CA) (H) (V)
  module AW_eio (K) (V) = Lwt_to_eio.Atomic_write (AW) (K) (V)
  module Schema_eio = Lwt_to_eio.Schema (S)
  module Inner_maker = Irmin.Maker (CA_eio) (AW_eio)
  module Inner = Inner_maker.Make (Schema_eio)
  include Wrap_store.Make (S) (Schema_eio) (Inner)

The user's Lwt-typed sub-Makers are bridged to Eio, fed to
[Irmin.Maker], and the resulting Eio store is wrapped back to a
Lwt-typed [Generic_key.S] via [Wrap_store.Make].
The shim's public entry point. The .mli is structured like
[main:src/irmin/irmin.mli] (same module layout, same modules
exposed at top-level), so application code that targets [Irmin.X]
in Irmin 3 can switch to [Irmin_lwt.X] with no other change.

The .ml ties everything together:
- [module Maker (CA) (AW) = struct ... module Make (S) =
  Maker_v2.Make (CA) (AW) (S) end]
- [module KV_maker = Maker.Make o Schema.KV]
- [Of_storage] using the Maker chain
- [module Lwt_to_eio] / [module Wrap_store] re-exposed for
  downstream backend packages
- [type remote = Remote.t = ..]
- [module Sync] re-exposed
- Closed alias re-exposed

[Of_backend] and the [Generic_key.Maker] functor are intentionally
*not* exposed -- see the documentation paragraphs in the .mli and
[LIMITATIONS.md] (added in a later commit).

Adds the [src/irmin-lwt/core/dune] file declaring the [irmin_lwt]
library.
Imports [main:src/irmin-test/] into [src/irmin-lwt/test/]
unchanged. The harness consumes a Lwt-typed [Irmin.S] / [Irmin.KV]
and runs the standard Irmin test suite (Generic_key tests, store
tests, watch tests, branch tests, etc.).

Each file is byte-identical to its main:src/irmin-test source. The
next commit adapts it to consume [Irmin_lwt] in place of [Irmin]
and reformats it under the project's ocamlformat 0.29.0.
Three substantive patches (~120 lines effective across ~3700 lines):

- [test/helpers.ml]: [module Irmin = Irmin_lwt] (replaces the
  [Irmin] reference in the harness with the shim's surface).
- [test/import.ml]: add [include Lwt.Syntax] and [( >>= )] /
  [( >|= )] aliases used elsewhere in the harness.
- [test/common.ml]: replace one call site that used
  [Irmin.Type.of_string (Conf.ty k)] with [Conf.of_string k]
  (the [Conf.ty] entry point exists on main but not on Irmin 4
  -- the new [Conf] surface exposes [of_string] on each key
  directly, with the same semantics).

Plus the dune file for the [irmin-lwt-test] library and an
ocamlformat 0.29.0 reformat of the imported files.
Wraps [Irmin_mem]'s Append_only / Content_addressable / Atomic_write
Makers, each operation forwarded through [Lwt_eio.run_eio]. Plugs
the Lwt-typed Makers into [Irmin_lwt.Maker] and [Irmin_lwt.KV_maker].

Smoke test exercises: basic set/get, branches and commits, sync
between two repos, [S.E] [Remote.t] constructor type-check, [Dot]
output, and the full [irmin-lwt-test] harness on top
(29/29 tests).
A Lwt shim over [Irmin_pack_unix]. The user's Lwt-typed [Schema.S]
is bridged to [Irmin.Schema.Extended] via
[Lwt_to_eio.Schema_extended], the inner Eio store is built with
[Irmin_pack_unix.Maker (Config).Make], and the result is re-wrapped
to a Lwt-typed [Generic_key.S] via [Wrap_store.Make].

[Maker.Make]'s output extends [Generic_key.S] with the irmin-pack-unix
specific surface, each Eio-direct effectful operation wrapped in
[Lwt.t] via [Lwt_eio.run_eio]:

- Integrity checks ([integrity_check], [integrity_check_inodes],
  [traverse_pack_file], [test_traverse_pack_file]).
- Chunking / lower / on-disk ([split], [is_split_allowed],
  [add_volume], [reload], [flush], [create_one_commit_store]).
- Statistics ([stats]).
- Garbage collection ([Gc.start_exn], [finalise_exn], [run] (with
  [?finished] callback bridged), [wait], [cancel], [is_finished],
  [behaviour], [is_allowed], [latest_gc_target]).
- Snapshots ([Snapshot.export] (with callback bridged),
  [Snapshot.Import.{v, save_elt, close}], plus the pure data
  types re-exported as type-equal to [Inner.Snapshot.*]).

A few entry points unavoidably leak Eio types because they take Eio
capabilities ([_ Eio.Domain_manager.t] for [Gc.start_exn] / [Gc.run]
/ [create_one_commit_store]; [Eio.Fs.dir_ty Eio.Path.t] for
[create_one_commit_store] / [Snapshot.export ?on_disk]); Lwt callers
must obtain these from their [Eio_main.run] runner.

Smoke test on a temporary directory: basic set/get, branch and
commit, persistence across reopen, plus the advanced surface
([flush], [integrity_check], [is_split_allowed], [Gc.is_allowed],
[Gc.is_finished], [stats]). [Gc.run] is not exercised by the smoke
test (would need a live [Eio.Domain_manager.t] and a non-trivial
repo state).
A Lwt-flavoured shim over [Irmin_fs_unix], mirroring the
[irmin-lwt-mem] structure. Each Eio-direct operation in the
Append_only and Atomic_write Makers is wrapped through
[Lwt_eio.run_eio]; Content_addressable is derived from Append_only
via [Irmin_lwt.Content_addressable.Make]; the result is plugged
into [Irmin_lwt.Maker / KV_maker].

[config ~root ~clock] takes Eio types ([_ Eio.Path.t] and
[_ Eio.Time.clock]) directly: the shim does not hide Eio in this
entry point. Lwt callers obtain these from their [Eio_main.run]
runner (typically [Eio.Stdenv.fs env] and [env#clock]).

Smoke test on a temporary directory: basic set/get, branch and
commit, persistence across reopen.
A Lwt-flavoured shim over [Irmin_chunk.Content_addressable], the
meta-backend that stores values cut into fixed-size chunks on top of
an [Append_only.Maker]. The functor takes a Lwt-typed
[Irmin_lwt.Append_only.Maker], lifts it to its Eio counterpart
through [Lwt_to_eio.Append_only], runs it through
[Irmin_chunk.Content_addressable] to obtain an Eio
[Irmin.Content_addressable.Maker], and bridges each operation back
to Lwt via [Lwt_eio.run_eio].

[Irmin_chunk.Conf] and [config] are re-exported transparently.

Smoke test combines chunking with [Irmin_lwt_mem]'s Append_only and
Atomic_write to build a chunked KV store; writes a small (3-byte)
and a large (5000-byte, above the chunk threshold) value, reads
both back.
Lwt-flavoured port of [irmin-containers] for [irmin-lwt]. Each data
structure is re-implemented on top of [Irmin_lwt]'s Lwt-typed Store
API; the merge logic is reused from the Eio side
([Irmin.Merge.counter] for the counter; [Irmin.Merge.option (v t
merge_eio)] for time-stamped values) and bridged to a [Merge.t] via
[Irmin_lwt.Lwt_to_eio.merge_of_eio].

Modules:

- [Counter] -- int64 counter with inc / dec / read.
- [Lww_register] -- last-writer-wins register parameterised by a
  [Time.S] and a value type.
- [Blob_log] -- append-only log of timestamped values.

Each module exposes [.Make (Backend : Irmin_lwt.KV_maker)], plus
[.FS] / [.Mem] instantiations on top of [Irmin_lwt_fs.KV] /
[Irmin_lwt_mem.KV].

[Linked_log] from [irmin-containers] is not ported: its merge
function performs reads on a content-addressable handle, and bridging
that to Lwt would require threading a Lwt-typed CAS handle through
the merge. Skipped for now.
A Lwt shim over [Irmin_git_unix]. The user provides a Lwt-typed
[Contents.S]; the shim bridges it to Eio via [Lwt_to_eio.Contents],
applies the git Maker, and wraps the resulting Eio store back to a
Lwt-typed [Generic_key.S] via [Wrap_store.Make]. Git-specific extras
([module Git], [git_commit], [git_of_repo], [repo_of_git], [remote])
are passed through unchanged.

The Lwt-side [Schema] reuses [Inner.Schema]'s pure modules (Hash,
Branch, Info, Path) directly because their module types have no
[Lwt.t] in either the Lwt or the Eio module type definitions. Only
[Metadata] needs a Lwt-bridged [merge].

Exposes:

- [Maker (G : Irmin_git.G)] producing [KV] / [Ref] convenience
  instantiations for any ocaml-git store.
- [Mem] = [Maker (Irmin_git.Mem)] -- in-memory git store.
- [FS] = [Maker (Git_unix.Store)] -- on-disk git store.

Smoke test on Mem: basic set/get, branch and commit, [git_commit]
passthrough verifying the underlying ocaml-git commit object is
retrievable from a commit handle.
A Lwt shim over [Irmin_client_unix]. The user provides only a
Lwt-typed [Contents.S]; the shim synthesises an Eio reference store
([Irmin_mem.KV.Make (V_eio)]) purely as a Schema carrier (the local
mem backend is never used since the client routes all ops over the
wire), feeds it to [Irmin_client_unix.Make_codec], and wraps the
result back to [Irmin_lwt.Generic_key.S] via [Wrap_store.Make].
Client extras ([connect], [reconnect], [dup], [Batch], [export],
[import], etc.) are passed through, with Eio-direct ones wrapped in
[Lwt.t] for caller convenience.

Modules:

- [Make_codec (Codec) (V)] -- Make parameterised by codec.
- [Make (V)] -- default binary codec.
- [Make_json (V)] -- JSON codec.
- [config] / [Error] re-exported from [Irmin_client_unix].

Smoke test spawns an [Irmin_server_unix] in an Eio fiber on a
Unix-domain socket, connects with the Lwt client, runs basic
set / get / find.
A Lwt shim exposing the Tezos-specific Irmin schema (BLAKE2B + Base58
hash, V1 pre-hashing for Node / Commit / Contents) on top of
[irmin-pack-unix], with a Lwt-typed Store API.

Implementation trick: instead of going through [Irmin_lwt_pack.Maker
(Conf).Make] -- which would replace the Tezos Schema's V1 pre-hashing
with the default [Generic_key.Make] via the
[Lwt_to_eio.Schema_extended] adapter -- the shim uses
[Irmin_tezos.Schema] directly as [Schema_eio] in [Wrap_store.Make],
constructing in parallel a Lwt-side [Schema_lwt] that reuses
[Schema.Hash / Branch / Info / Path] (pure modules, satisfy both Lwt
and Eio Schema.S constraints) and bridges [Schema.Metadata.merge]
and [Schema.Contents.merge] via [Lwt_to_eio.merge_of_eio]. The V1
pre-hashing is preserved end-to-end, so on-disk data is wire-
compatible with regular [irmin-tezos] data.

Smoke test: write a [bytes] value to a fresh temporary pack store
and read it back.
Three documentation artefacts:

- [doc/irmin-lwt/index.mld]: package landing page for irmin.org. Opens
  with a prominent warning that the main loop must be Eio (the central
  limitation of the shim: every operation goes through [Lwt_eio] and
  requires a running [Eio_main.run] scheduler at the top level). Lists
  each shim package with one usage example.
- [doc/irmin-lwt/migration.mld]: step-by-step migration guide from
  Irmin 3 (Lwt) to Irmin 4 via the shim. Restructures the main loop,
  swaps opam deps, renames module references, adjusts the
  configurations that take Eio types directly, wires up the watch
  switch when needed, lists workarounds for the dropped entry points
  ([Of_backend], [Generic_key.Maker] functor).
- [src/irmin-lwt/LIMITATIONS.md]: canonical list of what the shim does
  not support, with rationale for each entry ([Of_backend],
  [Generic_key.Maker] functor, [Watch.set_watch_switch] caveat,
  un-ported sister packages [graphql], [mirage], [cli], [server]).
balat added 3 commits May 6, 2026 17:51
Adds a [Irmin_lwt_test.Irmin_test.Suite] for [Irmin_lwt_fs] alongside
the existing smoke tests. The full Irmin test suite (basic, branches,
nodes, commits, slices, sync, watch, ...) runs against the
filesystem backend and passes 28/28.

Filesystem watching needs a [listen_dir_hook]; we bridge
[Irmin_watcher.hook] (Lwt-typed) to [Irmin.Backend.Watch.hook] (Eio
direct-style) and register it before [Lwt_eio.with_event_loop].
Combines [Irmin_lwt_chunk.Content_addressable (Irmin_lwt_mem.Append_only)]
with [Irmin_lwt_mem.Atomic_write] to obtain a full
[Irmin_lwt.Maker], and runs the harness on it: 29/29 tests pass.
…/28 tests)

[Irmin_lwt_git.Mem.KV] satisfies [Irmin_test.Generic_key]; running
the harness on it gives 28/28 tests passing (one less than mem
because Git has no [Metadata.None] -- the suite skips one test).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant