// Copyright (c) 2026 moonbit-bimap contributors
// SPDX-License-Identifier: Apache-2.0
// BiMap core: two hash tables (forward L->R and backward R->L) plus a single
// insertion-order array and position map for the left keys.
//
// Invariant chokepoints: EVERY mutation funnels through `put_pair` (insert
// path) or `remove_by_left` / `remove_by_right` (removal path). These are the
// only places that touch forward + backward + order + positions together, so
// the bijection invariant ("two tables are mutual inverses, five counts agree")
// is maintained in exactly one place per operation kind.
///|
/// 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`).
struct BiMap[L, R] {
forward : HashTab[L, R]
backward : HashTab[R, L]
order : Array[L]
positions : Map[L, Int]
mut len : Int
mut version : Int
}
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
///|
/// Create a new, empty `BiMap` with default capacity.
pub fn[L : Hash + Eq, R] BiMap::new() -> BiMap[L, R] {
BiMap::with_capacity(MIN_CAPACITY)
}
///|
/// Create a new, empty `BiMap` with capacity for at least `cap` pairs.
pub fn[L : Hash + Eq, R] BiMap::with_capacity(cap : Int) -> BiMap[L, R] {
let real_cap = if cap < MIN_CAPACITY {
MIN_CAPACITY
} else {
next_power_of_two_impl(cap)
}
{
forward: HashTab::with_capacity(real_cap),
backward: HashTab::with_capacity(real_cap),
order: [],
positions: Map([], capacity=real_cap),
len: 0,
version: 0,
}
}
// ---------------------------------------------------------------------------
// Private: order maintenance
// ---------------------------------------------------------------------------
///|
/// Append left key `l` to the insertion order and record its position.
fn[L : Hash + Eq, R] BiMap::order_push(self : BiMap[L, R], l : L) -> Unit {
self.order.push(l)
self.positions[l] = self.order.length() - 1
}
///|
/// Shift-remove left key `l` from the insertion order, fixing the positions of
/// all later keys (O(n)). No-op if `l` is not present.
fn[L : Hash + Eq, R] BiMap::order_remove(self : BiMap[L, R], l : L) -> Unit {
match self.positions.get(l) {
None => ()
Some(pos) => {
let mut i = pos
let last_idx = self.order.length() - 1
while i < last_idx {
let next = self.order[i + 1]
self.order[i] = next
self.positions[next] = i
i = i + 1
}
self.order.pop() |> ignore
self.positions.remove(l) |> ignore
}
}
}
// ---------------------------------------------------------------------------
// Private: the single insert chokepoint
// ---------------------------------------------------------------------------
///|
/// Insert the pair `(l, r)`, displacing conflicting pairs, and return the
/// displaced `(old_right?, old_left?)`. Handles cases C0/C2/C3/C4; the caller
/// (`insert`) short-circuits the exact-reinsert case C1 before reaching here.
///
/// This is the ONLY function that mutates forward + backward + order +
/// positions on the insert path, which keeps the bijection invariant local.
fn[L : Hash + Eq, R : Hash + Eq] BiMap::put_pair(
self : BiMap[L, R],
l : L,
r : R,
) -> (R?, L?) {
let old_r = self.forward.get(l)
let old_l = self.backward.get(r)
self.version = self.version + 1
match (old_r, old_l) {
(None, None) => {
// C0: fresh pair.
self.forward.insert(l, r) |> ignore
self.backward.insert(r, l) |> ignore
self.order_push(l)
self.len = self.len + 1
(None, None)
}
(Some(r0), None) => {
// C2: l rebinds from r0 to r; r0 loses its left key. l keeps its order
// position (rebinding does not change insertion order).
self.backward.remove(r0) |> ignore
self.forward.insert(l, r) |> ignore
self.backward.insert(r, l) |> ignore
(Some(r0), None)
}
(None, Some(l0)) => {
// C3: r was bound to l0; l takes it over. l0 is removed entirely and l
// enters at the end of the order. Net length unchanged.
self.forward.remove(l0) |> ignore
self.backward.remove(r) |> ignore
self.order_remove(l0)
self.forward.insert(l, r) |> ignore
self.backward.insert(r, l) |> ignore
self.order_push(l)
(None, Some(l0))
}
(Some(r0), Some(l0)) => {
// C4: both (l, r0) and (l0, r) exist and collapse into the single pair
// (l, r). Here l != l0 and r != r0 (C1 is short-circuited by the caller).
// l keeps its order position; l0 is removed. Net length decreases by 1.
self.forward.remove(l0) |> ignore
self.backward.remove(r0) |> ignore
self.order_remove(l0)
self.forward.insert(l, r) |> ignore
self.backward.insert(r, l) |> ignore
self.len = self.len - 1
(Some(r0), Some(l0))
}
}
}
// ---------------------------------------------------------------------------
// Insertion
// ---------------------------------------------------------------------------
///|
/// 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).
pub fn[L : Hash + Eq, R : Hash + Eq] BiMap::insert(
self : BiMap[L, R],
l : L,
r : R,
) -> Overwritten[L, R] {
// C1 short-circuit: the exact pair already exists → idempotent, no change.
// This must be checked before put_pair, otherwise (Some(r), Some(l)) would
// be mis-handled as a C4 collapse.
match self.forward.get(l) {
Some(r0) =>
if r0 == r {
Overwritten::Pair(l, r)
} else {
self.finish_insert(l, r)
}
None => self.finish_insert(l, r)
}
}
///|
/// Run `put_pair` and map the displaced pair(s) onto the `Overwritten` enum.
fn[L : Hash + Eq, R : Hash + Eq] BiMap::finish_insert(
self : BiMap[L, R],
l : L,
r : R,
) -> Overwritten[L, R] {
let (old_r, old_l) = self.put_pair(l, r)
match (old_r, old_l) {
(None, None) => Overwritten::Neither
(Some(r0), None) => Overwritten::Left(l, r0)
(None, Some(l0)) => Overwritten::Right(l0, r)
(Some(r0), Some(l0)) => Overwritten::Both((l, r0), (l0, r))
}
}
// ---------------------------------------------------------------------------
// Bidirectional lookup
// ---------------------------------------------------------------------------
///|
/// Return the right value bound to left key `l` (forward lookup).
pub fn[L : Hash + Eq, R] BiMap::get_by_left(self : BiMap[L, R], l : L) -> R? {
self.forward.get(l)
}
///|
/// Return the left key bound to right value `r` (reverse lookup).
pub fn[L, R : Hash + Eq] BiMap::get_by_right(self : BiMap[L, R], r : R) -> L? {
self.backward.get(r)
}
///|
/// Return `true` if left key `l` is present.
pub fn[L : Hash + Eq, R] BiMap::contains_left(
self : BiMap[L, R],
l : L,
) -> Bool {
self.forward.contains(l)
}
///|
/// Return `true` if right value `r` is present.
pub fn[L, R : Hash + Eq] BiMap::contains_right(
self : BiMap[L, R],
r : R,
) -> Bool {
self.backward.contains(r)
}
// ---------------------------------------------------------------------------
// Removal (the removal chokepoints)
// ---------------------------------------------------------------------------
///|
/// Remove the pair keyed by left key `l`, returning its right value if present.
/// Cleans up both tables and the order/position bookkeeping.
pub fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_left(
self : BiMap[L, R],
l : L,
) -> R? {
match self.forward.get(l) {
None => None
Some(r) => {
self.forward.remove(l) |> ignore
self.backward.remove(r) |> ignore
self.order_remove(l)
self.len = self.len - 1
self.version = self.version + 1
Some(r)
}
}
}
///|
/// Remove the pair keyed by right value `r`, returning its left key if present.
/// Cleans up both tables and the order/position bookkeeping.
pub fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_right(
self : BiMap[L, R],
r : R,
) -> L? {
match self.backward.get(r) {
None => None
Some(l) => {
self.backward.remove(r) |> ignore
self.forward.remove(l) |> ignore
self.order_remove(l)
self.len = self.len - 1
self.version = self.version + 1
Some(l)
}
}
}
// ---------------------------------------------------------------------------
// Size queries
// ---------------------------------------------------------------------------
///|
/// Return the number of pairs in the map.
pub fn[L, R] BiMap::len(self : BiMap[L, R]) -> Int {
self.len
}
///|
/// Return `true` if the map contains no pairs.
pub fn[L, R] BiMap::is_empty(self : BiMap[L, R]) -> Bool {
self.len == 0
}
///|
/// Return the current bucket capacity of the underlying forward table.
pub fn[L, R] BiMap::capacity(self : BiMap[L, R]) -> Int {
self.forward.capacity()
}
// ---------------------------------------------------------------------------
// Snapshot
// ---------------------------------------------------------------------------
///|
/// Return all pairs as an array in left-key insertion order.
pub fn[L : Hash + Eq, R] BiMap::into_array(self : BiMap[L, R]) -> Array[(L, R)] {
let result : Array[(L, R)] = []
let mut i = 0
while i < self.order.length() {
let l = self.order[i]
match self.forward.get(l) {
Some(r) => result.push((l, r))
None => ()
}
i = i + 1
}
result
}