bimap

    A bidirectional map (bijection) with reverse lookup, insertion order, index access, and a sorted BiBTreeMap variant - MoonBit port of Rust's bimap

    bimap
    bidirectional
    bijection
    data-structure
    ordered
    Download zip
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    72

    No dependencies

    #moonbit-bimap

    License CI

    A bidirectional map (bijection) for MoonBit — a port of Rust's bimap crate / Guava BiMap, extended with insertion-order preservation and index-based access (which neither Rust nor Guava provides).

    A BiMap[L, R] keeps keys and values in one-to-one correspondence: you can look up left→right and right→left, and every insertion maintains the bijection invariant.

    let m = @aurasuisui/bimap.new()
    m.insert("alice", "admin") |> ignore
    m.insert("bob", "user") |> ignore

    // Forward and reverse lookup:
    println(m.get_by_left("alice")) // Some("admin")
    println(m.get_by_right("user")) // Some("bob")

    // Index access (insertion order preserved):
    println(m.get_index(0)) // Some(("alice", "admin"))

    #Why a BiMap? (vs the built-in Map and vs indexmap)

    Featurebuilt-in MapBiMapindexmap
    key → value✅✅✅
    value → key (reverse)❌✅❌
    index access get_index(i)❌✅✅
    keys unique✅✅✅
    values also unique (bijection)❌✅❌
    preserves insertion orderimpl-defined✅✅
    Eq/Hash semanticsorder-independentorder-independentorder-sensitive

    BiMap and indexmap solve orthogonal problems — Bi = bidirectional (one-to-one, reverse lookup); Index = positional access. They share only the underlying hash table (as any two maps share arrays). This package is a fresh, dependency-free library, not a fork or rename of indexmap.

    #Features

    • Bidirectional lookup — get_by_left / get_by_right, contains_left / contains_right
    • Bijection-enforcing insertion — insert returns an Overwritten enum describing what was displaced (including the classic C4 collapse, see below)
    • Non-overwriting insertion — insert_no_overwrite returns Result[Unit, (L, R)]
    • Insertion-order iteration — iter() yields pairs in the order left keys were inserted
    • Index-based access — get_index(i), get_index_of_left, get_index_of_right, first(), last()
    • Inverse copy — to_inverse() -> BiMap[R, L] (a copy, not a live view)
    • Predicate filtering — retain(pred) keeps only the matching pairs in O(n), preserving their relative insertion order (a port of Rust bimap's retain)
    • Sorted bijection — BiBTreeMap: the same contract on a sorted engine (ascending by left key, range queries, first/last = min/max key), keys need only Compare; differential-tested against Rust bimap's BiBTreeMap
    • Set views + entry helpers — contains_pair(l, r), snapshot arrays left_keys() / right_values() (insertion order), and the stateless get_or_insert_left / get_or_insert_right — original extensions (Rust bimap v0.6.3 has none of them; its same-named right_values() is a lazy unordered iterator, ours is an ordered snapshot)
    • Standard traits — Debug, Default, Show, Eq/Hash (order-independent), ToJson, plus QuickCheck Arbitrary
    • JSON round-trip — from_json / from_json_with (both map types, v0.3.0) decode {"<left>": <right>, ...} objects with strict bijection validation: two left keys mapping to the same right raise DuplicateRightValue (the whole parse fails — no silent eviction); the JSON text key order becomes BiMap insertion order; decode errors pass through as Decode

    #Installation

    Add the dependency to your project's moon.mod:

    import { "aurasuisui/bimap@0.3.0", }

    Then import it in the relevant moon.pkg:

    import { "aurasuisui/bimap", }

    #The five insertion cases (C0–C4)

    Inserting (l, r) into a bijection has five sub-cases — the crux of a correct BiMap:

    CaseConditioninsert returnslen change
    C0neither l nor r presentNeither+1
    C1the exact pair (l, r) already presentPair(l, r)0
    C2l was bound to r'≠r; r freeLeft(l, r')0
    C3r was bound to l'≠l; l freeRight(l', r)0
    C4l→r' and l'→r both existBoth((l,r'), (l',r))−1

    C4 collapses two pairs into one — insert can reduce the map's size! This mirrors Rust bimap's Overwritten::Both exactly.

    let m = @aurasuisui/bimap.new()
    m.insert("a", 1) |> ignore // Neither {a↔1}
    m.insert("b", 2) |> ignore // Neither {a↔1, b↔2}
    m.insert("a", 4) |> ignore // Left(a, 1) {a↔4, b↔2}
    m.insert("c", 2) |> ignore // Right(b, 2) {a↔4, c↔2}
    let r = m.insert("a", 2) // Both((a,4),(c,2)) {a↔2} — len 2→1!

    #Gotchas

    1. insert can shrink the map (C4 collapse). Check the returned Overwritten if you need to know what was displaced.
    2. Eq and Hash are order-independent. A BiMap is a set of pairs; two maps with the same pairs in different insertion order are equal and hash the same. This is the opposite of the author's indexmap, whose Eq/Hash are order-sensitive. The hash is hardened (2026-08): it folds the sorted per-pair fingerprint sequence (the pair set's canonical form), so map-level collisions reduce to equal fingerprint multisets — engineering one now requires hash-level control over the keys (e.g. shifting h(l) up by t while shifting h(r) down by K·t). Remaining boundaries: fingerprint-level collisions are still possible in principle, and hashing a whole map costs O(n log n) (the sort). Fine for collections; if you hash very large BiMaps hot, cache the result.
    3. to_inverse() returns a copy, not a live view. Mutating the inverse does not affect the original (MoonBit's ownership model favors copies over shared live views; this matches Rust bimap's method-based access rather than Guava's live inverse()).
    4. ToJson keys use l.to_string() (L : Show), so String keys serialize verbatim.
    5. Don't mutate the map while an iterator is active — iterators are fail-fast (they snapshot a mutation counter and abort if the map changes mid-iteration).
    6. from_array resolves duplicate pairs by "last wins" (via insert), matching Rust's FromIterator.
    7. A rebind (C2) keeps the left key's insertion position — rebinding l to a new right value does not move l to the end of the order. This is an intentional, order-preserving extension over Rust's remove-then-reinsert behavior (see CHANGELOG).
    8. BiMap is not thread-safe. It is mutable and its iterators are fail-fast; concurrent reads/writes from multiple threads are undefined behavior. Use one BiMap per thread, or guard shared access with external synchronization.
    9. Removal shifts the insertion order — O(n) worst case. remove_by_left / remove_by_right keep the order array dense by shifting later entries, so a single removal costs O(len) in the worst case, and removing n pairs in insertion (head-first) order is O(n²) overall (bench-measured, see Performance). For bulk clear-outs, drain in reverse insertion order (each shift degenerates to O(1)) or rebuild (from_array / copy).
    10. from_json validates strictly; parse_key must be total. A JSON object with two left keys decoding to the same right raises DuplicateRightValue instead of silently evicting a pair (the opposite of Rust bimap's serde, which overwrites). The parse_key you pass to from_json_with has no raise channel — absorb parse failures inside it (e.g. catch @string.parse_int and fall back to a default). And R = Json is type-level impossible: Json has no Hash/Compare, which the right side of both map types requires.

    #API Overview

    CategoryMethods
    Constructnew(), with_capacity(n), from_array(pairs), default(), copy(), from_json(json), from_json_with(json, parse_key)
    Querylen(), is_empty(), capacity()
    Insertinsert(l, r) -> Overwritten, insert_no_overwrite(l, r) -> Result[Unit,(L,R)]
    Forwardget_by_left(l), contains_left(l), remove_by_left(l) -> R?
    Reverseget_by_right(r), contains_right(r), remove_by_right(r) -> L?
    Indexget_index(i), get_index_of_left(l), get_index_of_right(r), first(), last()
    Iterateiter(), lefts(), rights(), into_array()
    Viewscontains_pair(l, r), left_keys(), right_values()
    Entryget_or_insert_left(l, r) -> R, get_or_insert_right(r, l) -> L
    Bulkretain(pred)
    Convertto_inverse() -> BiMap[R, L]
    TraitsDebug, Default, Show, Hash, Eq, ToJson, Arbitrary

    #JSON round-trip (from_json)

    ToJson has a strict inverse (v0.3.0): from_json / from_json_with decode a JSON object { "<left>": <right-json>, ... } back into a bijection, with strict bijection validation — a right-value conflict raises BiMapDecodeError::DuplicateRightValue (payload: right value + both conflicting left keys), value decode errors pass through as Decode and take priority, and duplicate keys are last-wins. For BiMap use the free function; BiBTreeMap is reached by method call (MoonBit free functions cannot overload by return type). Both raise BiMapDecodeError — catch it, or let it propagate:

    let json = @json.parse("{\"bob\":2,\"alice\":1}") // text -> Json

    // BiMap: free function; insertion order = JSON text key order
    let m : @aurasuisui/bimap.BiMap[String, Int] = @aurasuisui/bimap.from_json(json)

    // BiBTreeMap: method call; sorted by left key (input key order irrelevant)
    let s : @aurasuisui/bimap.BiBTreeMap[String, Int] =
    @aurasuisui/bimap.BiBTreeMap::from_json(json)

    // Int-keyed text: from_json_with + a TOTAL parse_key (P1-2: no raise channel —
    // absorb parse failures inside, e.g. fall back to a default):
    let ints = @json.parse("{\"10\":10,\"20\":20}")
    let n : @aurasuisui/bimap.BiMap[Int, Int] = @aurasuisui/bimap.from_json_with(
    ints,
    fn(k) { @string.parse_int(k) catch { _ => 0 } },
    )

    The round-trip is lossless: from_json(m.to_json()) == m (set equality), and for BiMap the iteration order survives as the text key order. parse_key must be total. R = Json is type-level impossible (Json lacks Hash/Compare). See docs/SPEC.md §12 and the runnable cmd/json_roundtrip example.

    #BiBTreeMap — the sorted variant

    BiBTreeMap[L, R] is the same bijection on a sorted engine: two core SortedMap tables (ordered by left / by right). Sorted order replaces insertion order — no index access, first()/last() return the smallest/largest left key, and range(lo, hi) slices by left key (inclusive on both ends; the reverse-side range is deferred — see CHANGELOG). The whole C0–C4 insertion contract, retain, the M2 views/entry helpers, Eq/Hash set semantics, and fail-fast iterators carry over unchanged; keys only need Compare (no Hash required), and the snapshot/iteration/copy methods are all zero-bound. Differential-tested against the real Rust bimap BiBTreeMap v0.6.3 (golden + 6000-op stream + exact sorted terminal state).

    let m = @aurasuisui/bimap.BiBTreeMap::from_array([("b", 2), ("a", 1), ("c", 3)])
    println(m.into_array()) // [("a", 1), ("b", 2), ("c", 3)] — ascending by left
    println(m.first()) // Some(("a", 1)) — smallest left key
    println(m.range("a", "b").to_array()) // [("a", 1), ("b", 2)] — [lo, hi] inclusive

    CategoryMethods
    Constructnew(), from_array(pairs), default(), copy(), from_json(json), from_json_with(json, parse_key)
    Querylen(), is_empty(), first(), last(), range(lo, hi) -> Iter
    Insertinsert(l, r) -> Overwritten, insert_no_overwrite(l, r) -> Result[Unit,(L,R)]
    Forwardget_by_left(l), contains_left(l), remove_by_left(l) -> R?
    Reverseget_by_right(r), contains_right(r), remove_by_right(r) -> L?
    Viewscontains_pair(l, r), left_keys(), right_values()
    Entryget_or_insert_left(l, r) -> R, get_or_insert_right(r, l) -> L
    Bulkretain(pred)
    Iterateiter(), into_array()
    Convertto_inverse() -> BiBTreeMap[R, L]
    TraitsDebug, Default, Show, Hash, Eq, ToJson, Arbitrary

    #Design

    • Two inverse Robin Hood hash tables (forward: L→R, backward: R→L) keep the bijection.
    • One shared order array + positions map tracks left-key insertion order, enabling index access without a second order structure on the backward table.
    • All mutations funnel through private put_pair / remove_by_left / remove_by_right helpers that maintain the invariants: ∀(l,r)∈forward ⟺ backward[r]==l, and five consistent counters.
    • The Robin Hood engine is adapted from the author's aurasuisui/indexmap (see below).

    #Examples

    Runnable example packages live in cmd/:

    • cmd/username_email — username ↔ email bidirectional lookup, iteration, and a rebind
    • cmd/country_code — country name ↔ ISO code ("China" ↔ "CN"), reverse lookup, index access, and non-overwriting insert
    • cmd/json_roundtrip — from_json/from_json_with on both map types, strict-conflict error handling, and the lossless text round-trip (runs against the published @0.3.0 after release)

    Note: the cmd/* example packages are standalone modules (they import the published aurasuisui/bimap and are not part of the root package — the repo ships no workspace manifest). To run one, make the package resolvable (e.g. after moon publish) and run moon run cmd/<name>.

    #Performance

    Measured with bench/ (official @bench framework, native backend, --release, benchmarking the published package) on an AMD Ryzen 7 7840H, Windows 11, moonc v0.10.8 (2026-08-19). Median of 5 samples, Int ↔ Int maps at n = 100 000, per-operation cost:

    operationBiMapbuilt-in Map
    insert (fresh pair)≈ 2 980 ns≈ 410 ns
    get_by_left (hit)≈ 260 ns≈ 130 ns (get)
    get_by_right (hit)≈ 330 ns—
    insert_no_overwrite (conflict)≈ 300 ns—
    remove_by_left (tail-first drain)¹≈ 1 510 ns≈ 396 ns²
    into_array (full traversal)≈ 495 ns/pair—

    ¹ Derived: insert+remove_all-reverse (≈ 4 490 ns/op) minus insert-fresh; draining in tail order makes each order shift O(1). ² Built-in Map figure is insert+remove combined, not directly split.

    Read the numbers as the constant-factor price of bidirectionality + insertion order + index access: lookups run ≈2× the built-in map, inserts ≈7× (every write touches two hash tables, the order array, and the positions map). Reverse lookup stays within 25% of forward lookup. Worst case — bulk removal in insertion order: order_remove is an O(n) shift, so draining n pairs head-first is O(n²) overall (measured ≈ 133 µs/op at n = 10 000, ≈ 90× the tail-first drain). Drain in reverse insertion order, or rebuild — see Gotcha #9.

    Reproduce with moon run --release bench/main.mbt (see bench/README.md; single-machine numbers, indicative of constant factors, not absolute speed).

    #Development

    moon check # type check moon test # run all 368 tests moon fmt # format moon build # build

    The five-step CI pipeline runs: moon fmt --check → moon check → moon info && git diff --exit-code → moon test → moon build.

    See CONTRIBUTING.md for the architecture deep-dive and test conventions.

    #Known Issues

    • Fail-fast abort is not in-process testable. Mutating a map mid-iteration triggers abort, which the MoonBit test framework cannot catch as a passing assertion (a panicking test is reported as failed, not as "expected panic"). The version-snapshot + abort logic in src/bimap_iter.mbt is verified by inspection and by a manual reproduction (documented there); all other iterator behavior is fully tested.

    #Acknowledgements & Licensing

    • The Robin Hood hash-table engine is adapted from the author's aurasuisui/indexmap (Apache-2.0).
    • The BiMap semantics (insert/insert_no_overwrite, Overwritten, bidirectional lookup) are ported from the Rust bimap crate (MIT / Apache-2.0), with conceptual reference to Guava BiMap (Apache-2.0). Order preservation and index access are original additions.

    #License

    Apache 2.0 — see LICENSE.

    Built for the 2026 MoonBit Open Source Ecosystem Hackathon (August).

    BiMapDecodeError

    pub(all) suberror BiMapDecodeError {
    Decode(
    JsonDecodeError
    )
    DuplicateRightValue(String, String, String)
    } derive(Eq,
    Debug
    )

    Error raised by from_json / from_json_with.

    Toolchain ruling (moonc v0.10.12): a plain enum cannot be raised ([4127] "Type is not an error type") — error types must be declared as suberror. The public surface (name, variants, payloads, derives) matches docs/plans/v0.3.0.md §2 exactly.

    BiBTreeMap

    type BiBTreeMap[L, R]

    A bidirectional map (bijection) between left keys of type L and right values of type R, kept in SORTED order: iter() and the snapshot accessors yield pairs ascending by left key, first() / last() return the smallest / largest left key, and range(lo, hi) slices by left key (inclusive both ends). Unlike BiMap, there is no insertion order and no index access.

    Both sides stay unique: inserting a pair whose left OR right already exists displaces the conflicting pair(s), exactly like BiMap (the same C0–C4 semantics and the same Overwritten result enum).
    impl Default for BiBTreeMap[L, R]
    impl Eq for BiBTreeMap[L, R]
    impl Hash for BiBTreeMap[L, R]
    impl Show for BiBTreeMap[L, R]
    impl ToJson for BiBTreeMap[L, R]
    impl Debug for BiBTreeMap[L, R]
    impl Arbitrary for BiBTreeMap[L, R]

    BiBTreeMap::contains_left

    fn[L : Compare + Eq, R] BiBTreeMap::contains_left(self : BiBTreeMap[L, R], l : L) -> Bool

    Return true if left key l is present.

    BiBTreeMap::contains_pair

    fn[L : Compare + Eq, R : Eq] BiBTreeMap::contains_pair(self : BiBTreeMap[L, R], l : L, r : R) -> Bool

    Return true if the exact pair (l, r) is present — the C1 condition get_by_left(l) == Some(r). O(log n): only the forward table is consulted. R needs only Eq (for the ==), not Compare — Compare implies Eq but the forward lookup never compares right values.

    BiBTreeMap::contains_right

    fn[L, R : Compare + Eq] BiBTreeMap::contains_right(self : BiBTreeMap[L, R], r : R) -> Bool

    Return true if right value r is present.

    BiBTreeMap::copy

    fn[L, R] BiBTreeMap::copy(self : BiBTreeMap[L, R]) -> BiBTreeMap[L, R]

    Return an independent deep copy (both SortedMaps are copied structurally). Mutating the copy does not affect the original (and vice versa). Zero trait bounds.

    BiBTreeMap::first

    fn[L, R] BiBTreeMap::first(self : BiBTreeMap[L, R]) -> (L, R)?

    Return the pair with the SMALLEST left key, or None if the map is empty. (Unlike BiMap::first, which returns the earliest-inserted pair — see SPEC §11.)

    BiBTreeMap::from_array

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::from_array(pairs : Array[(L, R)]) -> BiBTreeMap[L, R]

    Build a BiBTreeMap from an array of pairs. Pairs are inserted in order through insert, so on a conflict the LATER pair wins (aligning with Rust FromIterator). For example from_array([("a",1), ("a",2)]) yields { "a" <-> 2 }.

    BiBTreeMap::from_json

    Deserialize a BiBTreeMap from a JSON object (String keys; the sorted map's order is self-canonical, so the input key order is irrelevant — SPEC §12.1 item 2). Duplicate text keys are last-wins (§3.2).

    Strict bijection validation, error priority and Decode passthrough are identical to BiMap::from_json. Call through the method (P1-1): BiBTreeMap::from_json(...) — the @aurasuisui/bimap.from_json free function covers only BiMap.

    BiBTreeMap::from_json_with

    fn[K : Compare + Eq, R :
    FromJson
    + Compare + Eq] BiBTreeMap::from_json_with(json : Json, parse_key : (String) -> K) -> BiBTreeMap[K, R] raise BiMapDecodeError

    Deserialize a BiBTreeMap from a JSON object, mapping each object key through parse_key : (String) -> K before inserting. parse_key MUST be total (P1-2) — see BiMap::from_json_with. parse_key collisions are last-wins, like duplicate text keys (§3.2, P2 ruling).

    All conflict semantics are identical to BiMap::from_json_with — in particular the dedup pass walks the SURVIVING pairs in text order, so a DuplicateRightValue payload names the text-earlier left key as the occupied one. Keys need only Compare (no Hash); the intermediate dedup structures use @sorted_map.SortedMap accordingly.

    BiBTreeMap::get_by_left

    fn[L : Compare + Eq, R] BiBTreeMap::get_by_left(self : BiBTreeMap[L, R], l : L) -> R?

    Return the right value bound to left key l (forward lookup).

    BiBTreeMap::get_by_right

    fn[L, R : Compare + Eq] BiBTreeMap::get_by_right(self : BiBTreeMap[L, R], r : R) -> L?

    Return the left key bound to right value r (reverse lookup).

    BiBTreeMap::get_or_insert_left

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::get_or_insert_left(self : BiBTreeMap[L, R], l : L, r : R) -> R

    Return the right value currently bound to left key l; if l is absent, insert the pair (l, r) and return r. The sorted twin of BiMap::get_or_insert_left with identical semantics (see SPEC §6.8): the get path does not mutate (no version bump); the insert path runs full insert semantics — a right value already bound to another left key is taken over by a C3 eviction (NOT "insert only when both sides are free").

    BiBTreeMap::get_or_insert_right

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::get_or_insert_right(self : BiBTreeMap[L, R], r : R, l : L) -> L

    Return the left key currently bound to right value r; if r is absent, insert the pair (l, r) and return l. The sorted twin of BiMap::get_or_insert_right with identical semantics (see SPEC §6.8): the get path does not mutate (no version bump); the insert path runs full insert semantics — a left key already bound to another right value is rebinding by a C2 eviction (NOT "insert only when both sides are free").

    BiBTreeMap::insert

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::insert(self : BiBTreeMap[L, R], l : L, r : R) -> Overwritten[L, R]

    Insert the pair (l, r), displacing any conflicting pair(s), and report what was overwritten via the returned Overwritten value.

    Cases (identical to BiMap::insert and to Rust bimap's BiBTreeMap):
    • C0 neither side present → Neither, length +1.
    • C1 the exact pair already present → Pair(l, r), no change (idempotent).
    • C2 l already bound to another right → Left(l, old_r), length unchanged.
    • C3 r already bound to another left → Right(old_l, r), length unchanged.
    • C4 both sides conflict → Both((l, old_r), (old_l, r)), length −1 (the two old pairs collapse into one).

    BiBTreeMap::insert_no_overwrite

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::insert_no_overwrite(self : BiBTreeMap[L, R], l : L, r : R) -> Result[Unit, (L, R)]

    Insert (l, r) only if NEITHER side is already present. If the left key or the right value already exists, the map is left unchanged and the attempted pair is returned in Err((l, r)); otherwise the pair is inserted and Ok(()) is returned. Aligns with Rust bimap's BiBTreeMap::insert_no_overwrite.

    BiBTreeMap::into_array

    fn[L, R] BiBTreeMap::into_array(self : BiBTreeMap[L, R]) -> Array[(L, R)]

    Return all pairs as an array in ascending left-key order.

    BiBTreeMap::is_empty

    fn[L, R] BiBTreeMap::is_empty(self : BiBTreeMap[L, R]) -> Bool

    Return true if the map contains no pairs.

    BiBTreeMap::iter

    fn[L, R] BiBTreeMap::iter(self : BiBTreeMap[L, R]) -> Iter[(L, R)]

    Return a lazy fail-fast iterator over (left, right) pairs in ASCENDING left-key order. Aborts if the map is mutated during iteration.

    BiBTreeMap::last

    fn[L, R] BiBTreeMap::last(self : BiBTreeMap[L, R]) -> (L, R)?

    Return the pair with the LARGEST left key, or None if the map is empty. O(n): walks the sorted iterator to its end. (Unlike BiMap::last, which returns the most-recently-inserted pair — see SPEC §11.)

    BiBTreeMap::left_keys

    fn[L, R] BiBTreeMap::left_keys(self : BiBTreeMap[L, R]) -> Array[L]

    Return a snapshot of the left keys as an array, in ascending order. This is an independent COPY: mutating the returned array does not affect the map. Zero trait bounds — the sorted twin of BiMap::left_keys.

    BiBTreeMap::len

    fn[L, R] BiBTreeMap::len(self : BiBTreeMap[L, R]) -> Int

    Return the number of pairs in the map.

    BiBTreeMap::new

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::new() -> BiBTreeMap[L, R]

    Create a new, empty BiBTreeMap. Both Compare bounds are needed to construct the two empty SortedMaps.

    BiBTreeMap::range

    fn[L : Compare + Eq, R] BiBTreeMap::range(self : BiBTreeMap[L, R], lo : L, hi : L) -> Iter[(L, R)]

    Return a lazy fail-fast iterator over the pairs whose left key lies in [lo, hi] — INCLUSIVE on both ends (the boundary semantics of core SortedMap::range, which is the engine this wraps; Rust's left_range(a..b) idiom is end-EXCLUSIVE, so ours matches a..=b — see SPEC §11). Pairs come in ascending left-key order. Aborts if the map is mutated during iteration.

    The reverse-side analogue (Rust's right_range) is deliberately deferred to v0.2.x (recorded in SPEC §11).

    BiBTreeMap::remove_by_left

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::remove_by_left(self : BiBTreeMap[L, R], l : L) -> R?

    Remove the pair keyed by left key l, returning its right value if present. Cleans up both tables.

    BiBTreeMap::remove_by_right

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::remove_by_right(self : BiBTreeMap[L, R], r : R) -> L?

    Remove the pair keyed by right value r, returning its left key if present. Cleans up both tables.

    BiBTreeMap::retain

    fn[L : Compare + Eq, R : Compare + Eq] BiBTreeMap::retain(self : BiBTreeMap[L, R], f : (L, R) -> Bool) -> Unit

    Keep only the pairs satisfying f, removing the rest (a port of Rust bimap's BiBTreeMap::retain, btree.rs). The predicate is evaluated exactly once per pair, in ascending left-key order, on a snapshot; the kept pairs stay sorted. If nothing is removed this is a no-op and the version is NOT bumped (active iterators stay valid). The predicate must not mutate the map.

    Mutation chokepoint #3 (bulk removal): per-pair removal from the two SortedMaps (each removal is O(log n); no order array to compact here).

    BiBTreeMap::right_values

    fn[L, R] BiBTreeMap::right_values(self : BiBTreeMap[L, R]) -> Array[R]

    Return a snapshot of the right values as an array, in the ascending order OF THEIR LEFT KEYS. This is an independent COPY: mutating the returned array does not affect the map. Zero trait bounds.

    (Same-name-different-contract vs Rust: bimap's BiBTreeMap::right_values() is a lazy iterator ascending by RIGHT value — this is an eager snapshot ascending by left key; see SPEC §11.)

    BiBTreeMap::to_inverse

    fn[L, R] BiBTreeMap::to_inverse(self : BiBTreeMap[L, R]) -> BiBTreeMap[R, L]

    Return an independent copy with the two sides swapped: a BiBTreeMap[R, L] whose left keys are this map's right values. The swap is exact because the two tables are mutual inverses: the new forward table is a copy of this backward table (ordered by old right = new left), and vice versa — so the result is a VALID sorted bijection built without re-insertion. This is a COPY, not a live view. Zero trait bounds.

    BiMap

    type BiMap[L, R]

    A bidirectional map maintaining a one-to-one correspondence (a bijection) between left keys of type L and right values of type R.

    Both sides are unique: inserting a pair whose left OR right already exists displaces the conflicting pair(s). Lookups work in both directions (get_by_left / get_by_right), and the insertion order of left keys is preserved and indexable (get_index, get_index_of_left, first, last).
    impl Default for BiMap[L, R]
    impl Eq for BiMap[L, R]
    impl Hash for BiMap[L, R]
    impl Show for BiMap[L, R]
    impl ToJson for BiMap[L, R]
    impl Debug for BiMap[L, R]
    impl Arbitrary for BiMap[L, R]

    BiMap::capacity

    fn[L, R] BiMap::capacity(self : BiMap[L, R]) -> Int

    Return the current bucket capacity of the underlying forward table.

    BiMap::contains_left

    fn[L : Hash + Eq, R] BiMap::contains_left(self : BiMap[L, R], l : L) -> Bool

    Return true if left key l is present.

    BiMap::contains_pair

    fn[L : Hash + Eq, R : Eq] BiMap::contains_pair(self : BiMap[L, R], l : L, r : R) -> Bool

    Return true if the exact pair (l, r) is present — i.e. the C1 condition get_by_left(l) == Some(r). O(1): only the forward table is consulted, so R needs Eq (for the comparison) but no Hash. (An original extension: Rust bimap v0.6.3 has no exact-pair membership query.)

    BiMap::contains_right

    fn[L, R : Hash + Eq] BiMap::contains_right(self : BiMap[L, R], r : R) -> Bool

    Return true if right value r is present.

    BiMap::copy

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::copy(self : BiMap[L, R]) -> BiMap[L, R]

    Return an independent deep copy. Mutating the copy does not affect the original (and vice versa). Insertion order is preserved.

    BiMap::first

    fn[L : Hash + Eq, R] BiMap::first(self : BiMap[L, R]) -> (L, R)?

    Return the earliest-inserted pair, or None if the map is empty.

    BiMap::from_array

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::from_array(pairs : Array[(L, R)]) -> BiMap[L, R]

    Build a BiMap from an array of pairs. Pairs are inserted in order through insert, so on a conflict the LATER pair wins (aligning with Rust FromIterator). For example from_array([("a",1), ("a",2)]) yields { "a" <-> 2 }.

    BiMap::from_json

    fn[R :
    FromJson
    + Hash + Eq] BiMap::from_json(json : Json) -> BiMap[String, R] raise BiMapDecodeError

    Deserialize a BiMap from a JSON object. Object keys become String left keys; the map's insertion order equals the TEXT key order of the JSON (core Map preserves order — the M1 ordering smoke test pins this), and duplicate text keys are last-wins (§3.2).

    Strict bijection validation: two different left keys decoding to the same right value raise DuplicateRightValue — the whole parse fails, no partial map, no silent C3 eviction (the deviation from Rust bimap's serde, which silently overwrites; SPEC §12.1). Non-object input or any value decode failure raises Decode with core's @json.JsonDecodeError passed through; value decode errors take priority over conflict detection (§3.5). For a String-keyed map from_json(m.to_json()) == m holds (Eq set semantics) with the same pair order.

    BiMap::from_json_with

    fn[K : Hash + Eq, R :
    FromJson
    + Hash + Eq] BiMap::from_json_with(json : Json, parse_key : (String) -> K) -> BiMap[K, R] raise BiMapDecodeError

    Deserialize a BiMap from a JSON object, mapping each object key through parse_key : (String) -> K before inserting.

    parse_key MUST be total (P1-2): the signature has no raise channel, so a parse failure must be absorbed inside parse_key (e.g. catch @string.parse_int and fall back to a default value). parse_key collisions (two text keys mapping to the same logical key) are last-wins, like duplicate text keys (§3.2, P2 ruling). All other semantics — text order preservation, strict right-value conflict errors, error priority — are identical to BiMap::from_json.

    BiMap::get_by_left

    fn[L : Hash + Eq, R] BiMap::get_by_left(self : BiMap[L, R], l : L) -> R?

    Return the right value bound to left key l (forward lookup).

    BiMap::get_by_right

    fn[L, R : Hash + Eq] BiMap::get_by_right(self : BiMap[L, R], r : R) -> L?

    Return the left key bound to right value r (reverse lookup).

    BiMap::get_index

    fn[L : Hash + Eq, R] BiMap::get_index(self : BiMap[L, R], i : Int) -> (L, R)?

    Return the pair at insertion-order index i, or None if i is out of bounds.

    BiMap::get_index_of_left

    fn[L : Hash + Eq, R] BiMap::get_index_of_left(self : BiMap[L, R], l : L) -> Int?

    Return the insertion-order index of left key l, or None if absent.

    BiMap::get_index_of_right

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::get_index_of_right(self : BiMap[L, R], r : R) -> Int?

    Return the insertion-order index of the pair whose right value is r, or None if absent. This is a convenience for get_index_of_left(get_by_right(r)): the order array only stores left keys, so a right value's index is the index of the left key it is paired with.

    BiMap::get_or_insert_left

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::get_or_insert_left(self : BiMap[L, R], l : L, r : R) -> R

    Return the right value currently bound to left key l; if l is absent, insert the pair (l, r) and return r.

    Original extension: verified absent from bimap-rs 0.6.3 — unlike retain this is NOT a port, so there is no differential oracle and the semantics are pinned by tests (SPEC §6).

    Surprise surface: when the insert path runs (l absent), full insert semantics apply — after the call l ↔ r is guaranteed to hold, but if r was already bound to another left key, that pair is evicted by a C3 takeover (this is NOT "insert only when both sides are free"). When l is present the map is NOT mutated (no version bump; active iterators stay valid) and its current right value is returned.

    BiMap::get_or_insert_right

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::get_or_insert_right(self : BiMap[L, R], r : R, l : L) -> L

    Return the left key currently bound to right value r; if r is absent, insert the pair (l, r) and return l.

    Original extension: verified absent from bimap-rs 0.6.3 — unlike retain this is NOT a port, so there is no differential oracle and the semantics are pinned by tests (SPEC §6).

    Surprise surface: when the insert path runs (r absent), full insert semantics apply — after the call l ↔ r is guaranteed to hold, but if l was already bound to another right value, that pair is evicted by a C2 rebind (this is NOT "insert only when both sides are free"). When r is present the map is NOT mutated (no version bump; active iterators stay valid) and its current left key is returned.

    BiMap::insert

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::insert(self : BiMap[L, R], l : L, r : R) -> Overwritten[L, R]

    Insert the pair (l, r), displacing any conflicting pair(s), and report what was overwritten via the returned Overwritten value.

    Cases:
    • C0 neither side present → Neither, length +1.
    • C1 the exact pair already present → Pair(l, r), no change (idempotent).
    • C2 l already bound to another right → Left(l, old_r), length unchanged.
    • C3 r already bound to another left → Right(old_l, r), length unchanged.
    • C4 both sides conflict → Both((l, old_r), (old_l, r)), length −1 (the two old pairs collapse into one).

    BiMap::insert_no_overwrite

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::insert_no_overwrite(self : BiMap[L, R], l : L, r : R) -> Result[Unit, (L, R)]

    Insert (l, r) only if NEITHER side is already present. If the left key or the right value already exists, the map is left unchanged and the attempted pair is returned in Err((l, r)); otherwise the pair is inserted and Ok(()) is returned. Aligns with Rust bimap::insert_no_overwrite.

    BiMap::into_array

    fn[L : Hash + Eq, R] BiMap::into_array(self : BiMap[L, R]) -> Array[(L, R)]

    Return all pairs as an array in left-key insertion order.

    BiMap::is_empty

    fn[L, R] BiMap::is_empty(self : BiMap[L, R]) -> Bool

    Return true if the map contains no pairs.

    BiMap::iter

    fn[L : Hash + Eq, R] BiMap::iter(self : BiMap[L, R]) -> Iter[(L, R)]

    Return a lazy iterator over (left, right) pairs in left-key insertion order. Supports for (l, r) in m { ... }. Aborts if the map is mutated during iteration.

    BiMap::last

    fn[L : Hash + Eq, R] BiMap::last(self : BiMap[L, R]) -> (L, R)?

    Return the most-recently-inserted pair, or None if the map is empty.

    BiMap::left_keys

    fn[L, R] BiMap::left_keys(self : BiMap[L, R]) -> Array[L]

    Return a snapshot of the left keys as an array, in insertion order. This is an independent COPY: mutating the returned array does not affect the map.

    Zero trait bounds — this is the whole point vs lefts(): usable from generic code that holds a BiMap[L, R] without any Hash/Eq on either side (see SPEC §6).

    BiMap::lefts

    fn[L : Hash + Eq, R] BiMap::lefts(self : BiMap[L, R]) -> Iter[L]

    Return a lazy iterator over the left keys in insertion order. Aborts if the map is mutated during iteration.

    BiMap::len

    fn[L, R] BiMap::len(self : BiMap[L, R]) -> Int

    Return the number of pairs in the map.

    BiMap::new

    fn[L : Hash + Eq, R] BiMap::new() -> BiMap[L, R]

    Create a new, empty BiMap with default capacity.

    BiMap::remove_by_left

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_left(self : BiMap[L, R], l : L) -> R?

    Remove the pair keyed by left key l, returning its right value if present. Cleans up both tables and the order/position bookkeeping.

    BiMap::remove_by_right

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_right(self : BiMap[L, R], r : R) -> L?

    Remove the pair keyed by right value r, returning its left key if present. Cleans up both tables and the order/position bookkeeping.

    BiMap::retain

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::retain(self : BiMap[L, R], f : (L, R) -> Bool) -> Unit

    Keep only the pairs satisfying f, removing the rest (a port of Rust bimap's retain, which exists on both of its map types). The predicate is evaluated exactly once per pair, in insertion order, on a snapshot; the kept pairs preserve their relative insertion order. If nothing is removed this is a no-op and the version is NOT bumped (active iterators stay valid). The predicate must not mutate the map.

    Mutation chokepoint #3 (bulk removal): per-pair removal from forward/backward/positions followed by an in-place compaction of order (which fixes the positions indices as it goes) — O(n) overall, avoiding the O(n²) cost of head-first per-element removal (README Gotcha #9).

    BiMap::right_values

    fn[L : Hash + Eq, R] BiMap::right_values(self : BiMap[L, R]) -> Array[R]

    Return a snapshot of the right values as an array, in the insertion order of their left keys. This is an independent COPY: mutating the returned array does not affect the map.

    Reading a right value requires resolving its left key in the forward table, hence L : Hash + Eq; R itself is unconstrained.

    (Rust bimap's same-named right_values() is a lazy iterator in hash-table order; this is an insertion-ordered eager snapshot — see SPEC §6.6.)

    BiMap::rights

    fn[L : Hash + Eq, R] BiMap::rights(self : BiMap[L, R]) -> Iter[R]

    Return a lazy iterator over the right values in insertion order. Aborts if the map is mutated during iteration.

    BiMap::to_inverse

    fn[L : Hash + Eq, R : Hash + Eq] BiMap::to_inverse(self : BiMap[L, R]) -> BiMap[R, L]

    Return an independent copy with the two sides swapped: a BiMap[R, L] whose left keys are this map's right values (in the same pair order). This is a COPY, not a live view — mutating it does not affect the original. (This differs from Guava's live inverse() and matches Rust bimap's method-based reverse access.)

    BiMap::with_capacity

    fn[L : Hash + Eq, R] BiMap::with_capacity(cap : Int) -> BiMap[L, R]

    Create a new, empty BiMap with capacity for at least cap pairs.

    Overwritten

    pub enum Overwritten[L, R] {
    Neither
    Left(L, R)
    Right(L, R)
    Both((L, R), (L, R))
    Pair(L, R)
    } derive(Eq,
    Debug
    )

    The result of insert, describing which existing pair(s) were displaced. Mirrors Rust bimap::Overwritten.

    • Neither — a brand-new pair; nothing was displaced (case C0).
    • Left(l, old_r) — the left key l was already bound to old_r, which is displaced (case C2).
    • Right(old_l, r) — the right value r was already bound to old_l, which is displaced (case C3).
    • Both((l, old_r), (old_l, r)) — both sides conflicted; the two old pairs collapse into the single new pair and the length decreases by one (case C4).
    • Pair(l, r) — the exact pair already existed; the insert is idempotent (case C1).

    VERSION

    let VERSION : String

    Library version string.

    from_json

    fn[R :
    FromJson
    + Hash + Eq] from_json(json : Json) -> BiMap[String, R] raise BiMapDecodeError

    Deserialize a BiMap from a JSON object (String keys, text order preserved). See BiMap::from_json.

    Free function covers ONLY BiMap (P1-1): MoonBit free functions cannot be distinguished by return type, so BiMap and BiBTreeMap cannot share the same @aurasuisui/bimap.from_json name — use BiBTreeMap::from_json(...) for the sorted variant.

    from_json_with

    fn[K : Hash + Eq, R :
    FromJson
    + Hash + Eq] from_json_with(json : Json, parse_key : (String) -> K) -> BiMap[K, R] raise BiMapDecodeError

    Deserialize a BiMap from a JSON object, parsing each key from String (e.g. integer keys). See BiMap::from_json_with; parse_key must be total (P1-2). Free function covers ONLY BiMap (P1-1) — use BiBTreeMap::from_json_with(...) for the sorted variant.

    new

    fn[L : Hash + Eq, R] new() -> BiMap[L, R]

    Create a new, empty BiMap.

    with_capacity

    fn[L : Hash + Eq, R] with_capacity(cap : Int) -> BiMap[L, R]

    Create a new, empty BiMap with capacity for at least cap pairs.