Vinary Tree bindings use a small, versioned C resource ABI plus generated constants and hand-written language facades. UniFFI is not the runtime boundary. UniFFI is a good fit for record- and RPC-shaped APIs, but a result set here may be enormous and long-lived. Materializing it as a collection, or crossing the FFI boundary once per match, would violate the library's performance contract.
The architecture has three layers:
vinary-tree-interop defines retained, two-word resources and versioned
interfaces. A producer owns its data and publishes operations; a consumer
retains the resource without knowing the producer's Rust types.libdictenstein owns
dictionaries and CRUD, liblevenshtein owns edit-distance transducers and
phonetic automata, and related projects such as lling-llang and duallity
expose their own resource interfaces when cross-project composition needs
them.bindings/api.json; facade code is
not generated blindly from C declarations.This lets Java load a dictionary through libdictenstein, pass its
DictionaryResource directly to a liblevenshtein Transducer, and keep both
projects independently versioned and packaged. No serialization, term copying,
or monolithic native binding is involved.
VtResource is two machine words: a context pointer and a resource vtable. Its
base operations are retain, release, and query-interface. Handing a resource
between projects is therefore $\mathcal{O}(1)$ — sixteen bytes on a 64-bit
target, never a serialization. The mandatory vt.dictionary.v1 interface
supports byte, Unicode-scalar, and u64 unit domains and unit or optional-u64
values. Optional dictionary visit, compact-graph, snapshot-identity, and
entries-v1 interfaces add capabilities without enlarging the base resource;
vt.dict.entry.v1 is a finite lexicographic stream over one captured revision
with one explicit generation lease at a time. vt.scalar-wfst.1 carries
scalar-weighted transducers between sibling projects. One dictionary feeds
three consumers across two principal interfaces:
The dictionary interface deliberately models traversal rather than concrete dictionary classes:
Consequently DynamicDAWG, DoubleArrayTrie, SCDAWG views, persistent ARTrie variants, and future dictionary implementations remain libdictenstein types. Liblevenshtein does not duplicate their constructors, CRUD methods, persistence controls, or concrete class hierarchy. Composition APIs consume interfaces, not project-specific handles.
Host-defined providers are supported. Project-owned dictionaries and transducers preserve their native thread-safe, non-blocking behavior: the binding layer neither adds a global lock nor serializes independent calls. Foreign callbacks execute on the caller thread. Because an arbitrary callback cannot be assumed thread-safe, calls into one callback object are serialized unless that object declares itself parallel and reentrant. This gate protects only the callback; it does not serialize the library or other resources. Panics/exceptions must not cross the C ABI and are reported as provider errors.
Every query captures a retained dictionary revision before reading its root. That capture must be constant-cost regardless of dictionary size,
\mathrm{cost}(\mathtt{snapshot}) \;=\; \mathcal{O}(1)
\quad\text{— independent of } \lvert D \rvert ,
so copying the dictionary or holding a long-lived read lock are contract violations, not implementations. A cursor then:
Vec;The DynamicDAWG implementation uses immutable path-copied revisions and atomic root publication. That design is inspired by the persistent ARTrie snapshot principle, not by its storage representation: DynamicDAWG remains an in-memory structure while persistent ARTrie uses its own mmap/WAL machinery.
The contract is exercised at three boundaries:
Each test starts one cursor, partially consumes it, performs several mutations, then drains the same cursor and compares it with its query-start oracle. A fresh cursor must observe the new revision. The laws in full — with the persistence-theory derivation and the law ↔ formal-model ↔ test correspondence table — are docs/theory/snapshot-semantics.md.
The hot path is batch-oriented:
u64 arenas;C and C++ can borrow spans directly. JVM FFM exposes MemorySegment views in
the expert reducer path. Python exposes borrowed buffer views during the
callback. JavaScript runtimes use typed-array views where their runtime boundary
permits it. Ordinary safe iterators copy a term only when yielding it into the
host's managed ownership model.
Default batch size is 256, so transferring $n$ results costs
\left\lceil \frac{n}{256} \right\rceil
boundary crossings — never one per match — and a provider expanding a node
of out-degree $\deg(v)$ through the recommended edge batch pays
$\lceil \deg(v) / 256 \rceil$ crossings per expansion. Providers should
batch node expansion rather than
implementing a per-edge callback. Benchmark gates should measure boundary
crossings, allocations, and bytes copied in addition to throughput.
Native packages stay modular: one package per project plus the tiny shared interop package. Applications may dynamically link an installed shared library or statically link it where the language toolchain and license policy allow. Python wheels and the JVM artifact bundle the relevant native libraries; C/C++ exposes both CMake shared and static targets. No loader silently falls back to an unrelated system library.
JavaScript is the deliberate exception. A single @vinary-tree/vinary-tree
runtime owns one coherent native/WASM resource table and exposes namespaces for
the related projects. Project packages such as @vinary-tree/libdictenstein
and @vinary-tree/liblevenshtein are lightweight typed facades over that
runtime. This prevents incompatible WebAssembly instances from trying to pass
raw handles to one another.
The Node default should be a native N-API build. Explicit subpaths select browser WASM, WASI Preview 1, or a WASI component build. WASI gives a Node host filesystem capabilities only when the host preopens directories; it does not grant ambient filesystem access. Persistent ARTrie is enabled only for a WASI runtime with the required mmap/durability semantics. Browser persistence is a separate storage design and must not pretend to be the native WAL contract.
Only related Vinary Tree projects belong in this umbrella. For example,
libdictenstein, liblevenshtein, lling-llang, and duallity may compose;
an unrelated project such as libcpg must remain outside it.
The tiers describe support priority, not permission for one project to absorb another project's API.
| Tier | Languages | Facade direction |
|---|---|---|
| 1 | C17/C23, C++20/C++23 | Stable ABI and RAII spans/leases |
| 1 | Python 3.10-current | Iterator plus borrowed batch reducer |
| 1 | JVM: Java, Kotlin, Scala | Java 22 FFM API; JDK 25 LTS and current JDK tested |
| 1 | Clojure | Dedicated reducible/sequence facade over the JVM package |
| 1 | JavaScript, TypeScript, ClojureScript | Project facades over the shared umbrella runtime; CLJS mirrors the Clojure API where host semantics permit |
| 2 | .NET, Go, Swift, Ruby, Fortran | Idiomatic native facades over the same resource ABI |
| 3 | OCaml, Haskell, Lua | Maintained native packages over the same resource and leased-batch contracts |
FFM is the primary JVM backend. It avoids JNI glue, has been final since Java 22, and can represent the leased native views directly. A future Java 17/21 JNI fallback, if demand warrants it, must implement the same Java interfaces in a separate artifact rather than constrain the FFM design.
Clojure receives its own facade because IReduceInit, Seqable, keyword
options, and deterministic resource scopes are materially different from Java.
ClojureScript likewise receives a small facade with matching function names
and option maps. TypeScript supplies discriminated term domains, one-shot
iterators, and borrowed-batch types rather than exposing untyped JavaScript
objects.
Current implementation status is machine-checked in CI. Tier 1, Tier 2, and Tier 3 facades consume project resources and run the same cross-project query-start snapshot fixture. Generator-owned native mirrors additionally replay the entries-v1 identity, status, flag, operation-order, and LP64/ARM32 layout fixture. The tier is a maintenance and optimization priority; it is not a statement that lower-tier packages are incomplete.
Publication follows the dependency DAG — the shared interop package first, producers before consumers, the npm umbrella before its facades (the executable process is releasing-language-bindings.md):
The packages are independently releasable:
vinary-tree-interop and liblevenshtein on crates.io;pkg-config, and CMake config packages
vinary-tree-interop and liblevenshtein;vinary-tree-interop followed by
vinary-tree-liblevenshtein on PyPI;io.vinarytree:vinary-tree-interop followed by
io.vinarytree:liblevenshtein on Maven Central (and therefore
consumable through JFrog Artifactory mirrors);io.vinarytree/liblevenshtein-clojure on Clojars;@vinary-tree/interop, the umbrella
@vinary-tree/vinary-tree, and project facade
@vinary-tree/liblevenshtein;VinaryTree.Interop and VinaryTree.Liblevenshtein on NuGet;github.com/vinary-tree/liblevenshtein-rust/bindings/go/v4 and
github.com/vinary-tree/vinary-tree-interop/bindings/go/v4,
published with immutable module-subdirectory tags;VinaryTreeInterop and Liblevenshtein;vinary-tree-liblevenshtein on RubyGems;vinary-tree-interop and vinary-tree-liblevenshtein in the fpm
registry;vinary-tree-interop and vinary-tree-liblevenshtein through opam;vinary-tree-interop and vinary-tree-liblevenshtein on Hackage;vinary-tree-liblevenshtein on LuaRocks.Interop artifacts publish before producer/consumer artifacts. A binding release must pin an exact compatible interop version and test a real cross-project handoff, not merely compile each package independently.
All language binding features are opt-in. bindings-core enables the resource
consumer and cursor model, ffi adds the native ABI, and bindings-phonetic
adds project-owned phonetic APIs. Descriptive *-bindings features choose a
boundary; they do not compile another language runtime and do not restore
dictionary ownership to liblevenshtein.
| Level | Operating systems and architectures |
|---|---|
| Required | Linux x86_64 (AMD and Intel), Linux aarch64, macOS aarch64, Windows x86_64 |
| Best effort | FreeBSD x86_64/aarch64, NetBSD x86_64, OpenBSD x86_64, DragonFly BSD x86_64 |
| Experimental | Linux armv7, NetBSD/OpenBSD aarch64 |
Release artifacts never use target-cpu=native. Architecture-specific SIMD is
selected at runtime so one x86_64 artifact works on both AMD and Intel systems.
Adding an optional interface or tail vtable field is backward-compatible when the size/version handshake makes it discoverable. Changing a layout, ownership rule, status value, callback rule, or snapshot guarantee requires a new interface version; incompatible versions may coexist. Project APIs evolve above that boundary without requiring one monolithic release. The full change rules — four version counters, the additive-versus-fork decision table, worked examples — are the evolution policy.
llev_* C-ABI reference ·
resource consumer ·
WASM topology ·
snapshot semantics ·
binding trust model ·
findings ledger.ldict_* ·
lling-llang lling_* ·
duallity duallity_*.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |