Vinary Tree libdictenstein for Ruby

The gem exposes full DynamicDAWG CRUD, immutable DoubleArrayTrie construction, SCDAWG substring search, persistent ARTrie CRUD/checkpoint/reopen, and persistent vocabulary reverse lookup. Every object implements with_resource, allowing an independently packaged liblevenshtein transducer to retain it in O(1).

Calls acquire only a short lifetime lease; operations on the same dictionary are not serialized. The project-owned native resource advertises parallel and reentrant access automatically.

Every dictionary is an Enumerable over immutable-revision Entry records:

dictionary = VinaryTree::Libdictenstein::DynamicDawg.new
dictionary.put("cat", 0)
dictionary.put("cut", nil)

dictionary.each do |entry|
  p [entry.key, entry.value]
  break if entry.key == "cat" # ensure closes the native cursor
end

keys = dictionary.keys
values = dictionary.values
snapshot = dictionary.entries

each returns an Enumerator without a block. entry_stream exposes manual next, cancel, and close for pull-driven bounded traversal.

Support and package contract

Property Contract
Binding Ruby
Languages/runtime Ruby 3.3+
Support tier Tier 2
Distribution RubyGems libdictenstein
Native boundary Fiddle over the stable C ABI
Canonical facade source bindings/ruby/lib/vinary_tree/libdictenstein

All tiers implement the same ownership, snapshot, status, and compatibility laws. The tier controls release gating rather than semantic quality. Start with the producer documentation hub, then use the ldict_* C ABI reference for exact preconditions, statuses, thread-safety, and complexity.

A host facade owns a project handle while exported snapshots cross projects only as retained versioned resources.

Installation and native loading

Install the distribution named above and its exact vinary-tree-interop dependency. Published managed packages carry or resolve supported native artifacts; source builds use the release library from target/release or the installed CMake/pkg-config package. Diagnose loading in this order: toolchain version, OS/CPU artifact, dependent package pin, loader path, then ABI/API handshake. Never silently load an arbitrary same-named system library.

Executable example and verification

The canonical checked example is bindings/ruby/test/test_conformance.rb. CI runs the public package path with:

ruby -Ibindings/ruby/lib bindings/ruby/test/test_conformance.rb

The example is also conformance evidence: it uses public constructors, checks membership/value behavior, exports a retained resource, and closes every owned handle. Cross-project suites pass that resource to liblevenshtein without serialization.

Public API, backends, and data domains

Concept Semantics
Dictionary handle Owns one mutable or immutable backend instance and exposes kind/capability introspection.
CRUD and batch mutation Text and u64 operations preserve optional values; empty-batch and partial-failure behavior follows the C reference.
Persistent maintenance checkpoint, compact, and clear are capability-gated and report unsupported operations explicitly.
Retained resource resource() lends vt.dictionary.v1; a consumer retains and snapshots it independently.
Snapshot Immutable revision with stable node identifiers, exact domains, bounded edge pages, and optional mapped values.

Dynamic DAWG supports mutable finite-term dictionaries; double-array tries are read-optimized static structures; SCDAWG indexes substrings; persistent ARTrie and vocabulary stores provide durable byte/Unicode/u64 domains. Select by reported kind and capabilities rather than assuming every operation exists.

Text APIs validate UTF-8 and traverse Unicode scalar values. Byte APIs retain arbitrary octets. Token APIs preserve the full u64 range. Optional dictionary values are represented separately from terminal membership, so None is not a sentinel and empty terms remain valid when supported.

Native collection surface

Every dictionary includes Enumerable. each returns an Enumerator without a block and yields host-owned Entry records in lexical order; its ensure path closes the cursor after exhaustion, break, or exception. entry_stream also exposes explicit next, cancel, and close, while entries, keys, and values provide materialized snapshot idioms. Binary strings, UTF-8 strings, and Array<Integer> preserve the three unit domains; nil remains distinct from every mapped integer.

The gem executable keeps construction and warmup outside the timed drain and prints one JSON record. Run its materialized, streaming, and early-cancel arms over the shared deterministic corpus with:

ruby bindings/ruby/bin/libdictenstein-collection-profile --arm materialized --entries 4096
ruby bindings/ruby/bin/libdictenstein-collection-profile --arm stream --entries 65536 --batch-size 256
ruby bindings/ruby/bin/libdictenstein-collection-profile --arm stream-cancel --entries 65536 --batch-size 64 --early-cancel 64

The pure Rust producer is the semantic and performance baseline: generic snapshot traversal, borrowed and snapshot-owning IntoIterator, optimized bulk FromIterator/Extend where infallible, named fallible variants for persistent stores, deterministic order, and reusable fold/visitor paths. Read the local Rust API audit and the family collection-protocol design.

Public spelling must remain native to this ecosystem. The shared engine standardizes laws and batching; it does not expose C handles, vtables, leases, or status codes to ordinary application code. Documentation may mark a protocol as shipped only after its language conformance and performance gates pass.

Ownership, snapshots, and resource handoff

Prefer block forms or ensure { dictionary.close }; close persistent stores explicitly.

An exported resource is borrowed until its base-vtable retain succeeds. A captured snapshot arrives owning one retain and may outlive the mutable dictionary. Later inserts, updates, removals, compaction, or checkpoints do not alter a pinned revision. Every successful retain has exactly one release, and failed construction transfers no ownership.

Errors and failure containment

Failures become typed Ruby exceptions with status and diagnostic. Branch on the typed status or exception, not diagnostic text. Invalid UTF-8, domain mismatch, unsupported capability, closed handle, bad path, allocation failure, provider fault, I/O failure, and contained panic are distinct cases. Copy thread-local diagnostics before another native call.

Concurrency and reentrancy

Independent handles and immutable snapshots are reentrant. Mutations follow the backend's advertised synchronization strategy; one host wrapper must not invent a stronger promise. Snapshot capture is a linearization point and never permits a torn root/count pair. Do not race close against another operation on the same handle, and do not retain callback/paging buffers after return.

Performance, durability, and marshalling

  • Use bulk construction for an initially empty dictionary and presorted input when available; unordered construction uses the optimized sort-plus-minimal path.
  • Batch mutations to amortize foreign-boundary crossings.
  • Export retained resources instead of serializing dictionaries between Vinary packages.
  • Keep byte, Unicode, and u64 domains explicit to avoid transcoding.
  • Treat checkpoint as durability, compact as representation maintenance, and close as ownership release; they are not interchangeable.

Security model

Treat paths, terms, values, page offsets, serialized files, and foreign callers as untrusted. Validate lengths before allocation, contain panics at the ABI, bound diagnostics and batches, prevent path traversal, and reject unknown enum values. See the FFI boundary analysis and family security model.

Compatibility and troubleshooting

The project ABI, project API revision, family ABI, interface version, package version, and persistent format version are separate counters. Negotiate each at its documented boundary. For unexpected behavior, record dictionary kind, capabilities, unit domain, persistence path, exact status, and copied diagnostic before reducing the operation sequence.

Maintainer checklist

  1. Update bindings/api.json before changing a public facade or package pin.
  2. Regenerate headers/constants and run the binding contract gate.
  3. Extend the executable, negative-path, leak, and cross-project tests.
  4. Update this guide when ownership, errors, capabilities, or platforms change.
  5. Render PlantUML headlessly and run math/link/documentation checks.
  6. Verify staged registry artifacts contain the guide and coherent pins.