Contents
Mentat × mino integration — review & plan
Goal: give Mentat a Clojure-like scripting/data layer, the way Datomic is
driven from Clojure. mino (~/src/mino) is an embeddable Clojure-dialect
Lisp in ANSI C, not JVM Clojure — so “integrate like Datomic+Clojure” means
embed the mino interpreter in-process and expose Mentat’s store to it.
Decision (per maintainer): vendor the relevant mino sources into the Mentat
tree rather than add an external git/crate dependency. mino is built exactly
for this — ./mino task amalgamate emits a single dist/mino.c + dist/mino.h
whose stated purpose is “an embedder vendors the dist/ directory into their
tree.” MIT-licensed, CalVer.
1. Review findings
What mino gives us (the lucky part)
mino already ships mino.store (lib/mino/store.clj + C API), which is an
EAVT fact store with:
- accumulating fact log
[e a v tx instant op]+ materialized entity view, - schema with
:unique/:identity/ cardinality /:ref, - lookup-refs and upserts,
- durability (snapshot + append-only EDN WAL),
- a Clojure surface:
transact,read,entity,entities,with,put,retract,listen.
This is the same data model Mentat implements (EAVT over SQLite, schema
keywords, TypedValue). So the integration is not “reimplement Mentat in a
Lisp.” It’s “wire mino’s store-shaped Clojure API to real Mentat storage.”
mino C embedding API (from src/mino.h)
- Lifecycle:
mino_state_new/free,mino_env_new/free,mino_install_all. - Eval:
mino_eval_string,mino_eval(protected_exvariants that don’t leak into global error state),mino_call,mino_read. - Values in:
mino_int/float/string/keyword/symbol/map/vector/set, builders (mino_vector_builder_*,mino_map_builder_*). - Values out:
mino_to_int/float/string,mino_typeof,mino_is_*. - Host bridge:
mino_handle/mino_handle_ptr/mino_handle_tag(opaque host pointer wrapped as a Lisp value — this is how a MentatStorecrosses the boundary),mino_prim/mino_prim_argv(register a C function callable from Lisp),mino_env_set. - Errors:
mino_last_error,mino_last_error_map. - Store C API:
mino_store_open/deref/publish/checkpoint/close,mino_is_store.
Mentat Rust API (the call target)
mentat::Store::open(path),Store::transact(&str /* EDN */) -> TxReport.Conn::q_once/q_prepare/pull_attributes_for_entities/lookup_value_for_attribute,begin_transaction→InProgress,cache.- Queries and tx-data are EDN strings — and mino can already read/print EDN. That is the natural wire format between the two.
- Existing
ffi/crate (ffi/src/lib.rs,utils.rs) has C-FFI patterns to copy (C string handling, error marshalling).
Build reality check (verified on EC2 c7i.xlarge, AL2023, GCC 11.5)
- Static-lib embedding works, no amalgamation needed. Compile the
Makefile’s exact flat per-dir globs (NOT a recursive
find— that wrongly pullsvendor/bearssl/src/*,vendor/miniz/upstream/*,eval/bc/stencils/*which need private includes / are codegen inputs). Verified list:src/{eval,eval/bc,eval/bc/jit,diag,runtime,gc,public,values,collections,prim,interop,regex,async}/*.csrc/vendor/{imath,bearssl,miniz}/*.c, excludingmain.c. 128 files →ar rcs libmino.a(~4.1 MB).
- Include dirs:
-Isrc -Isrc/public -Isrc/runtime -Isrc/gc -Isrc/eval -Isrc/values -Isrc/collections -Isrc/prim -Isrc/async -Isrc/interop -Isrc/diag -Isrc/vendor/imath -Isrc/vendor/bearssl -Isrc/vendor/bearssl/inc -Isrc/vendor/miniz -Isrc/vendor/miniz/upstream. - CFLAGS
-std=c99 -O2 -DMINO_CPJIT=1 -Wno-array-bounds(GCC 11.5 array-bounds false positive; drop mino’s-Werror).-DMINO_CPJIT=0is the interpreter fallback if the JIT can’t build in CI. - Link:
cc app.c libmino.a -lm -lpthread. - End-to-end verified: an embedded C program eval’d
(+ 1 2)→ 3 AND drove the EAVT store:(mino.store/transact c {:alice {:age 30}})then(mino.store/read (mino.store/db c) :alice :age)→ 30. This proves the Datomic-shaped data layer is reachable from embedded C (→ Rust FFI). ./mino task amalgamateOOM-crashes (SIGKILL/exit 137) even with 7.6 GB free and no ulimit — a real bug in that task, not a resource cap. Skip it; build the static lib from the source tree instead (the recipe above). This is also better for Mentat:build.rs+ thecccrate compiles the vendoredsrc/directly, no separate generate step.
2. Architecture
Rust mentat crate ──(feature "mino")──► mentat-mino (new crate)
│
build.rs: cc vendored dist/mino.c
│
unsafe extern "C" bindings to mino.h
│
register prims: mentat-transact! / mentat-q / mentat-pull / mentat-open
│
mino_handle("mentat.store", *mut Store) ← Store crosses as opaque handle
│
user writes Clojure: (require 'mentat)
(def db (mentat/open "foo.db"))
(mentat/transact db [{:person/name "Alice"}])
(mentat/q db '[:find ?n :where [?e :person/name ?n]])
Data flow: Lisp value → print to EDN string (mino side) → Rust reads EDN via
edn crate / passes to Store::transact / q_once → results back as EDN
string → mino_read into a Lisp value. EDN is the ABI. No hand-marshalling
of every type across FFI; reuse what both sides already do.
ponytail: EDN-string round-trip is the naive-but-correct bridge (one
serialize + one parse per call). Fast enough for a scripting layer; if a
hot-loop caller shows up, add direct mino_val↔TypedValue marshalling then.
3. Plan (phased, each phase independently reviewable)
Phase 0 — vendor + build spike (VERIFIED on EC2)
1. Vendor the mino src/ tree (not the amalgamation — it’s broken) into
mentat-mino/vendor/mino/. Record the mino CalVer tag in VERSION.
2. New crate mentat-mino with a build.rs using the cc crate to compile
the verified flat-glob source list (see §1 build recipe) into a static lib;
no -Werror; MINO_CPJIT=1 with =0 fallback.
3. Minimal extern "C" block + smoke test: state_new →
eval_string("(+ 1 2)") → to_int == 3 → state_free.
Status: the C-level equivalent already passes on EC2 (SMOKE_OK,
store round-trip = 30). Remaining: port the smoke to Rust cargo test.
Phase 1 — expose Store as an opaque handle + open/transact
4. Register C prims via mino_prim_argv: mentat/open, mentat/transact.
open returns mino_handle(store_ptr, "mentat.store"); transact takes the
handle + an EDN string (mino prints the Lisp arg to EDN), calls
Store::transact, returns the TxReport as EDN.
5. Handle lifecycle: finalizer/tag so dropping the Lisp handle drops the Rust
Box<Store> (mirror mino’s store finalizer contract — GC sweep must not
double-free; keep ownership Rust-side, hand out a raw pointer, free in a
registered finalizer).
Gate: open in-memory, transact, reopen durable, read back (mirror
examples/embed_store.c).
Phase 2 — query + pull
6. mentat/q (EDN query string → Conn::q_once → results as EDN),
mentat/pull, mentat/lookup.
Gate: a .clj script transacts a schema + data and queries it.
Phase 3 — ergonomics + feature wiring
7. Add mino = { path = "mentat-mino", optional = true } and a mino feature
to the top mentat Cargo.toml ([features] mino = ["dep:mentat-mino"]).
Default OFF — pure-Rust users pay nothing.
8. Re-export a mentat::script module behind #[cfg(feature = "mino")]:
Interpreter::new(), eval(&str), bind_store(&mut Store).
9. A bundled mentat.clj prelude (require’d on init) giving Datomic-flavored
sugar over the raw prims.
Gate: cargo build (no feature) unchanged; cargo build --features mino
links; example REPL script runs against a real Mentat DB.
Phase 4 — docs + example
10. examples/mino_repl.rs, a short docs/mino.md, THIRD_PARTY_LICENSES note.
4. Open questions to settle before Phase 1
- JIT in CI: does
MINO_CPJIT=1build under Mentat’s CI toolchain, or ship with=0? (Built fine with GCC 11.5 on EC2; re-check Mentat’s CI.) - Threading: Mentat
Storeis!Sync-ish (holds a rusqliteConnection). mino states are per-thread; one interpreter ↔ oneStoreon one thread is the safe v1. Document it; don’t build cross-thread sharing (YAGNI). - EDN dialect drift: mino’s EDN printer vs Mentat’s
edncrate reader — verify keywords, instants (#inst), refs, andUuidround-trip. This is the one real risk; add a round-trip test early. - Vendor sync: pin the mino tag; a
Makefile/script target to regenerate the amalgamation so updates are one command, not a manual copy.
5. What we deliberately are NOT doing (yet)
- No JVM Clojure interop (mino is C; that’s the whole point).
- No direct
mino_val↔TypedValuefast path — EDN string bridge first. - No cross-thread / multi-Store-per-interpreter sharing.
- No re-implementation of Mentat query semantics in Lisp — Mentat stays the engine; mino is the language surface.