diff --git a/quest/m2/ts-model/rangemap.md b/quest/m2/ts-model/rangemap.md index 27c0919f4..88a77a3b9 100644 --- a/quest/m2/ts-model/rangemap.md +++ b/quest/m2/ts-model/rangemap.md @@ -11,7 +11,6 @@ proven by differential tests against native Rust. At the pin it refuses 11 times ## Plan -[B-tree searches calling `Ord` as std's do](/quest/m3/ts-btree-search-order.md) is -related: a search by a borrowed form calls that form's `Ord`, so the comparisons it makes -should match std's. A field of std's iterator type is the runtime's `rs.Iter`, as a local +The runtime's B-tree calls `Ord::cmp` as std's does, `key.cmp(k)` node by node, so a +search by a borrowed form calls that form's `Ord` where std's would. A field of std's iterator type is the runtime's `rs.Iter`, as a local of that type already is. diff --git a/quest/m3/README.md b/quest/m3/README.md index 98c0d1b86..572b298bb 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -22,5 +22,4 @@ generic std gap is planned here even when moq never calls it. - [rs.Slice](/quest/m3/ts-slice-object.md) - references to arrays of other values are a runtime slice too, and pointers compare by where they point - [Writes through iter_mut without a Place per item](/quest/m3/ts-iter-mut-places.md) - a loop over `iter_mut()` of copy values writes the slice's array by index - [rs.String](/quest/m3/ts-string-capacity.md) - `String`, `Box`, and `&str` are a runtime type holding std's capacity -- [A B-tree's searches call Ord as std's do](/quest/m3/ts-btree-search-order.md) - a B-tree's lookups and inserts call a key's own `Ord` in std's order and number - [Slices not yet written](/quest/m3/ts-uninit-slices.md) - `Box::new_uninit_slice` and `write_clone_of_slice` translate diff --git a/quest/m3/ts-btree-search-order.md b/quest/m3/ts-btree-search-order.md deleted file mode 100644 index 3052975f7..000000000 --- a/quest/m3/ts-btree-search-order.md +++ /dev/null @@ -1,18 +0,0 @@ -# [L] A B-tree's searches call Ord as std's do - -## Goal - -[rust2-ts](/ts/README.md)'s `BTreeMap` and `BTreeSet` call a key's own `Ord::cmp` in the -order and number std's do in `insert`, `get`, `remove`, `extend`, `range`, and the set -operators that search, and none where std's call none, as `pop_first`, `pop_last`, and -`extract_if` remove and rebalance, and answer the same where the tree is out of `Ord`'s order, as a -`collect` sorted by a disagreeing `PartialOrd::lt` leaves it, proven by differential tests -against native Rust with a logging `Ord`. - -## Plan - -The runtime keeps a B+ tree of 64-key nodes searched by bisection, where std's keeps -nodes of at most 11 keys, searched linearly, with values beside keys in every node, so -their calls differ for any key but the first. Matching them means std's node layout and -search, which the TS bench's `btree/` workloads must weigh before it lands; keys whose -impls are derived or std's could keep the faster tree. diff --git a/ts/README.md b/ts/README.md index 043a77111..648549913 100644 --- a/ts/README.md +++ b/ts/README.md @@ -498,7 +498,9 @@ A write of a value of no bytes, through a `&mut` or a pointer, reads and writes A collection keeps Rust's key semantics. A hash map or set keeps a key that is a JS number, string, or boolean in a JS `Map`, which compares those as Rust's `Eq` does, and finds any other key, a 64-bit one past 2^53 included, through its translated `Hash` and -`Eq`; a B-tree orders every key by its translated `Ord`. std's impls are the runtime's: an +`Eq`; a B-tree orders every key by its translated `Ord`, calling `cmp` on the keys std's +does, in the same order, so a tree a `collect` sorted by a `lt` that disagrees answers as +std's does. std's impls are the runtime's: an `Option` orders `None` first, an `Option` and a `Poll` hash their variant and then what they hold, an `Arc` compares, orders, and hashes as what it holds, so a map of `Arc` finds a `&str`, a `Cell` and a `RefCell` compare and order as what they hold, a `RefCell` borrowing each, a one-time cell compares its `Option`, and @@ -975,12 +977,15 @@ they need. the long-lived slot, which made Chrome's 8-byte leading-ones varints 1.06-1.09x `main`'s time. A slice's bytes stay in the slot, since returning them gave back frame decode. The function as a value is an arrow that rebuilds the `Result`. -- A `BTreeMap` is a B+ tree of up to 64 keys a node, O(log n) as std's B-tree is: its - leaves hold the keys and values and link to their neighbors, which iterators walk from - either end, and its branches hold each child's lower bound. A node's slots shift in a - loop, which beat `splice` 2-3x at these sizes. On `just ts bench`'s `btree/` workloads - it takes 0.5-0.8x the time two sorted arrays did, from 16 keys to 4096, in Node, Bun, - and Chrome; 64 keys a node measured best of 16 to 128. A hash map's JS primitive keys +- A `BTreeMap` is std's B-tree, ported: nodes of up to 11 keys with their values, + children below internal ones, each searched from its first key by `key.cmp(k)`, split, + merged, and evened out with a sibling where std's are, built by `collect` and + `split_off` as std's builds them, and cloned node for node, so a key's own `cmp` runs + on the same keys in the same order as std's. Iterators climb and descend leaf edges as + std's do. On `just ts bench`'s `btree/` workloads it takes 0.8-1.0x the time the B+ + tree of 64-key nodes it replaced did in Chrome, 1.0-1.3x in Node and Firefox, within + their noise, and 1.3-1.6x in Bun, from 16 keys to 4096; bisecting a node's keys measured + no faster. A hash map's JS primitive keys are a JS `Map`'s, the engine's own hash table; its other keys go into a second `Map` by their translated hash, chained by `Eq`. A hash map, set, or table holds only what std guarantees of its capacity, at least its length and what `with_capacity` and `reserve` @@ -1010,9 +1015,8 @@ they need. so `append` is O(1) and `split_off` walks from the nearer end; each node is the place `iter_mut` and `front_mut` lend of an item JS copies. Dropping a whole map or set drops its entries in the runtime's order, keys' for a B-tree, not std's node by node: std leaves that order - undocumented, and matching it would mean porting std's 11-key node layout, measured 1.5x - slower ([#272](https://github.com/moq-dev/rust2/pull/272)). Maintainer's decision on - 2026-10-30; a map's `IntoIter` still drops in std's order. + undocumented ([#272](https://github.com/moq-dev/rust2/pull/272)). Maintainer's decision + on 2026-10-30; a map's `IntoIter` still drops in std's order. - A slice's `sort` and `sort_unstable` are std's driftsort and ipnsort, ported to the runtime with the choices std makes by the element's size on wasm32 and whether it is `Freeze` and `Copy`, so each calls the comparator in std's order and leaves equal diff --git a/ts/fixtures/lang/collections/expected/collections.Probe.ts b/ts/fixtures/lang/collections/expected/collections.Probe.ts new file mode 100644 index 000000000..fb7cefc66 --- /dev/null +++ b/ts/fixtures/lang/collections/expected/collections.Probe.ts @@ -0,0 +1,56 @@ +// Generated by rust2-ts. Do not edit. + +import * as rs from "./rs.ts"; + +/** + * A B-tree key that logs each comparison std makes of it, in order: `cmp` and `eq` by its + * low 10 bits, `le` and `gt` alike, but `lt`, which a set's `collect` sorts by, by those + * bits with the low 2 flipped, and `ne`, which `collect` drops all but the last of a run of + * equal keys by, by all 16. A collected tree is then out of `cmp`'s order, or holds keys + * `cmp` finds equal. + */ +export interface Probe { + _0: rs.u16; + _1: rs.Cell; +} + +export function log(self: Probe, other: Probe, by: rs.u32): void { + const call: rs.u32 = ((((((self._0 as rs.u32) << 16) >>> 0) ^ other._0 as rs.u32) >>> 0) ^ by) >>> 0; + rs.cellSet(self._1, rs.u32WrappingAdd(rs.u32WrappingMul(self._1.v, 16777619), call)); +} + +export function low(self: Probe): rs.u16 { + return self._0 & 1023; +} + +export function eq$cmp_PartialEq(self: Probe, other: Probe): boolean { + log(self, other, 256); + return low(self) === low(other); +} + +export function ne$cmp_PartialEq(self: Probe, other: Probe): boolean { + log(self, other, 512); + return self._0 !== other._0; +} + +export function lt$cmp_PartialOrd(self: Probe, other: Probe): boolean { + log(self, other, 1024); + return (low(self) ^ 3) < (low(other) ^ 3); +} + +export function le$cmp_PartialOrd(self: Probe, other: Probe): boolean { + log(self, other, 1280); + return low(self) <= low(other); +} + +export function gt$cmp_PartialOrd(self: Probe, other: Probe): boolean { + log(self, other, 1536); + return low(self) > low(other); +} + +export function cmp$cmp_Ord(self: Probe, other: Probe): rs.Ordering { + log(self, other, 2048); + return rs.cmp(low(self), low(other)); +} + +//# sourceMappingURL=collections.Probe.ts.map diff --git a/ts/fixtures/lang/collections/expected/collections.ts b/ts/fixtures/lang/collections/expected/collections.ts index 1fad85295..b85b4194e 100644 --- a/ts/fixtures/lang/collections/expected/collections.ts +++ b/ts/fixtures/lang/collections/expected/collections.ts @@ -18,6 +18,7 @@ import * as Nibble from "./collections.Nibble.ts"; import * as Odd from "./collections.Odd.ts"; import * as Pair from "./collections.Pair.ts"; import * as Picky from "./collections.Picky.ts"; +import * as Probe from "./collections.Probe.ts"; import * as Quirky from "./collections.Quirky.ts"; import * as Rank from "./collections.Rank.ts"; import * as Right from "./collections.Right.ts"; @@ -25,7 +26,7 @@ import * as Tally from "./collections.Tally.ts"; import * as Window from "./collections.Window.ts"; import * as Wrap from "./collections.Wrap.ts"; import { thenSome$0quywni } from "./core/bool.ts"; -import { unwrapOr$1szdyfq as option_unwrapOr$1szdyfq, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1pdxnel as option_map$1pdxnel, map$093awy8 as option_map$093awy8, map$0aug5m6 as option_map$0aug5m6, map$16w3l9s as option_map$16w3l9s, map$1y26nmr as option_map$1y26nmr, map$14ju55r as option_map$14ju55r, mapOr$14runhi as option_mapOr$14runhi, mapOr$0f7cus0 as option_mapOr$0f7cus0, mapOr$07jk2xl as option_mapOr$07jk2xl, mapOr$1rg5gkk as option_mapOr$1rg5gkk, mapOr$1lwzqm5 as option_mapOr$1lwzqm5, mapOr$0pgapuk as option_mapOr$0pgapuk, mapOr$13kt6x8 as option_mapOr$13kt6x8, mapOr$031uhqp as option_mapOr$031uhqp, mapOr$18dcmgz as option_mapOr$18dcmgz, mapOr$0vxw5sq as option_mapOr$0vxw5sq, mapOr$1d4bp7c as option_mapOr$1d4bp7c, mapOr$188iunt as option_mapOr$188iunt, mapOr$0955beb as option_mapOr$0955beb, mapOr$1h9hu6n as option_mapOr$1h9hu6n, clone$14nprly as option_clone$14nprly, flatten$0ux6gqw as option_flatten$0ux6gqw } from "./core/option.ts"; +import { unwrapOr$1szdyfq as option_unwrapOr$1szdyfq, unwrapOr$1fr498u as option_unwrapOr$1fr498u, map$1pdxnel as option_map$1pdxnel, map$093awy8 as option_map$093awy8, map$0aug5m6 as option_map$0aug5m6, map$16w3l9s as option_map$16w3l9s, map$1y26nmr as option_map$1y26nmr, map$14ju55r as option_map$14ju55r, mapOr$14runhi as option_mapOr$14runhi, mapOr$0f7cus0 as option_mapOr$0f7cus0, mapOr$0vkblkh as option_mapOr$0vkblkh, mapOr$07jk2xl as option_mapOr$07jk2xl, mapOr$1rg5gkk as option_mapOr$1rg5gkk, mapOr$1lwzqm5 as option_mapOr$1lwzqm5, mapOr$0pgapuk as option_mapOr$0pgapuk, mapOr$13kt6x8 as option_mapOr$13kt6x8, mapOr$1wei0sy as option_mapOr$1wei0sy, mapOr$031uhqp as option_mapOr$031uhqp, mapOr$18dcmgz as option_mapOr$18dcmgz, mapOr$0vxw5sq as option_mapOr$0vxw5sq, mapOr$1d4bp7c as option_mapOr$1d4bp7c, mapOr$188iunt as option_mapOr$188iunt, mapOr$05q90i2 as option_mapOr$05q90i2, mapOr$0955beb as option_mapOr$0955beb, mapOr$1h9hu6n as option_mapOr$1h9hu6n, clone$14nprly as option_clone$14nprly, flatten$0ux6gqw as option_flatten$0ux6gqw } from "./core/option.ts"; import { eq$1a91ieh as result_eq$1a91ieh, eq$122baxc as result_eq$122baxc } from "./core/result.ts"; import { put } from "./harness.ts"; @@ -5985,4 +5986,302 @@ export function btreeExtracts(kind: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): } } +/** + * std's B-tree compares keys only as it searches its nodes, each from its first key on, + * which `Probe` logs: of maps and sets grown by inserts that split nodes at every height, + * then searched, removed from until nodes merge and take from their siblings, extended, + * entered, ranged, popped, split, cloned, extracted from, and retained, and of the set + * operations that search, of sets of sizes that make std look keys up rather than merge. + * Then of a set and map `collect` leaves out of `cmp`'s order, which std searches as is. + * Each answer is followed by the log. + */ +export function btreeSearches(kind: rs.u8, data: rs.ByteSlice, out: rs.ByteVec): void { + let _35!: rs.usize; + let _139!: [Probe.Probe, Probe.Probe]; + let range!: rs.Iter<[Probe.Probe, rs.u16]>; + let _159!: [Probe.Probe, rs.u16] | undefined; + let _452!: [Probe.Probe, Probe.Probe]; + const _28: (a0: rs.u16 | undefined) => rs.u16 = (v: rs.u16 | undefined): rs.u16 => { + return option_mapOr$1wei0sy(v, 65535, (v2: rs.u16): rs.u16 => { + return v2; + }); + }; + const log: rs.Cell = new rs.Cell(0); + const at: (a0: rs.usize) => rs.u8 = (i3: rs.usize): rs.u8 => { + return option_unwrapOr$1fr498u(rs.byteSliceGet(data, i3), 0); + }; + const put: (a0: rs.ByteVec, a1: rs.u16) => undefined = (out2: rs.ByteVec, answer: rs.u16): undefined => { + rs.iterExtendBytes(out2, rs.iterSlice(rs.u16ToBeBytes(answer).reverse()), undefined, true); + rs.iterExtendBytes(out2, rs.iterSlice(rs.u32ToBeBytes(rs.cellReplace(log, 0)).reverse()), undefined, true); + }; + const n: rs.usize = rs.usizeAdd(at(0) as rs.usize, 1); + const stride: rs.usize = rs.usizeAdd(rs.usizeMul(at(1) as rs.usize, 2), 1); + const key: (a0: rs.usize) => Probe.Probe = (i4: rs.usize): Probe.Probe => { + return { _0: rs.usizeAdd(rs.usizeMul(i4, stride), at(2) as rs.usize) & 0xffff & 1023, _1: log }; + }; + const probe: (a0: rs.u8) => Probe.Probe = (b7: rs.u8): Probe.Probe => { + return { _0: rs.u16Add(rs.u16Mul(b7 as rs.u16, 4), b7 >> 6 as rs.u16), _1: log }; + }; + b0: { + const _29: rs.u8 = rs.u8Rem(kind, 4); + if (_29 === 0) { + const map: rs.BTreeMap = rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeMap; + let iterStart: rs.usize = 0; + const iterEnd: rs.usize = n; + while ((_35 = iterStart++) < iterEnd) { + const i: rs.usize = _35; + const _41: Probe.Probe = key(i); + const part31 = option_unwrapOr$1szdyfq(rs.btreeInsert(map, _41, i & 0xffff), 65535); + put(out, part31); + } + for (const b of rs.byteSliceView(rs.byteSliceRange(data, rs.min(data.length, 3)))) { + const _68: Probe.Probe = probe(b); + const part33 = rs.btreeGet(map, _68); + const part34 = _28(part33); + put(out, part34); + const _78: Probe.Probe = key(b as rs.usize); + const part35 = rs.btreeContains(map, _78) ? 1 : 0; + put(out, part35); + const _89: Probe.Probe = key(rs.usizeMul(b as rs.usize, 3)); + const part37 = option_unwrapOr$1szdyfq(rs.btreeRemove(map, _89), 65535); + put(out, part37); + const _98: Probe.Probe = probe(b); + const _95: rs.Place = rs.entryOrInsertMut(rs.entry(map, _98), 7); + _95.v = rs.u16Add(_95.v, 1); + const part38 = rs.btreeLen(map) & 0xffff; + put(out, part38); + } + const _110: rs.Iter<[Probe.Probe, rs.u16]> = rs.iterMap(new rs.Range(0, rs.usizeDiv(n, 2)), (i5: rs.usize): [Probe.Probe, rs.u16] => { + return [key(rs.usizeMul(i5, 5)), i5 & 0xffff] as [Probe.Probe, rs.u16]; + }); + rs.btreeExtend(map, _110); + const part40 = rs.btreeLen(map) & 0xffff; + put(out, part40); + const part41 = at(3); + const _125: Probe.Probe = probe(part41); + const part42 = at(4); + const _124: [Probe.Probe, Probe.Probe] = [_125, probe(part42)]; + const lo: Probe.Probe = _125; + const hi: Probe.Probe = _124[1]; + if (!(Probe.low(lo) <= Probe.low(hi))) { + _139 = [hi, lo] as [Probe.Probe, Probe.Probe]; + } else { + _139 = [lo, hi] as [Probe.Probe, Probe.Probe]; + } + const lo2: Probe.Probe = _139[0]; + const hi2: Probe.Probe = _139[1]; + const _146: rs.u8 = rs.u8Rem(at(5), 3); + if (_146 === 0) { + range = rs.btreeRange(map, new rs.Range(lo2, hi2), Probe.eq$cmp_PartialEq, Probe.gt$cmp_PartialOrd); + } else if (_146 === 1) { + const _154: rs.RangeInclusive = new rs.RangeInclusive(lo2, hi2); + range = rs.btreeRange(map, _154, Probe.eq$cmp_PartialEq, Probe.gt$cmp_PartialOrd); + } else { + range = rs.btreeRange(map, [{ tag: "Excluded", _0: lo2 } as rs.RangeBound, { tag: "Unbounded" } as rs.RangeBound] as [rs.RangeBound, rs.RangeBound], Probe.eq$cmp_PartialEq, Probe.gt$cmp_PartialOrd); + } + while (true) { + if (!(rs.u8Rem(at(6), 2) === 0)) { + _159 = rs.iterToOption(range.nextBack()); + } else { + _159 = rs.iterToOption(range.next()); + } + if (_159 !== undefined) { + put(out, _159[0]._0); + } else { + const part43 = option_mapOr$05q90i2(rs.btreePop(map), 65535, (_23: [Probe.Probe, rs.u16]): rs.u16 => { + return _23[0]._0; + }); + put(out, part43); + const part44 = option_mapOr$05q90i2(rs.btreePop(map, true), 65535, (_24: [Probe.Probe, rs.u16]): rs.u16 => { + return _24[0]._0; + }); + put(out, part44); + const copy: rs.BTreeMap = rs.btreeClone(map, undefined, undefined); + const part45 = at(7); + const part46 = rs.btreeGet(copy, probe(part45)); + const part47 = _28(part46); + put(out, part47); + const part48 = at(8); + const _206: Probe.Probe = probe(part48); + const right: rs.BTreeMap = rs.btreeSplitOff(map, _206); + const part49 = rs.usizeAdd(rs.usizeMul(rs.btreeLen(map), 1000), rs.btreeLen(right)) & 0xffff; + put(out, part49); + const part50 = at(9); + const _233: Probe.Probe = probe(part50); + const part51 = rs.btreeGet(right, _233); + const part52 = _28(part51); + put(out, part52); + const part53 = at(10); + const _243: Probe.Probe = probe(part53); + const part54 = at(11); + const _242: rs.Range = new rs.Range(_243, probe(part54)); + const part55 = rs.iterCount(rs.btreeExtractIfMut(right, _242, (k9: Probe.Probe, _38: rs.Place): boolean => { + return rs.u16Rem(k9._0, 3) === 0; + }, Probe.le$cmp_PartialOrd, Probe.lt$cmp_PartialOrd)) & 0xffff; + put(out, part55); + rs.btreeRetainMut(map, (k10: Probe.Probe, _39: rs.Place): boolean => { + return rs.u16Rem(k10._0, 4) !== 0; + }); + const part56 = rs.btreeLen(map) & 0xffff; + put(out, part56); + const iter3: rs.Iter = rs.iterStepBy(new rs.Range(0, n), 3, undefined, 4294967295); + while (true) { + const _271: rs.usize | rs.Done = iter3.next(); + if (_271 === rs.DONE) { + break; + } + const _282: Probe.Probe = key(_271); + const part57 = option_unwrapOr$1szdyfq(rs.btreeRemove(map, _282), 65535); + put(out, part57); + } + const part58 = rs.btreeLen(map) & 0xffff; + put(out, part58); + break b0; + } + } + } + if (_29 === 1) { + const _292: rs.Iter = rs.iterMap(new rs.Range(0, n), key); + const large: rs.BTreeSet = rs.btreeCollectKeys(rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeSet, rs.iterToVec(_292, undefined, 8, true).items, 8, true, Probe.lt$cmp_PartialOrd, (a4: Probe.Probe, b13: Probe.Probe): boolean => { + return !Probe.ne$cmp_PartialEq(a4, b13); + }); + const small: rs.BTreeSet = rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeSet; + for (const _297 of rs.byteSliceView(data)) { + const _307: Probe.Probe = probe(_297); + const part21 = rs.btreeAdd(small, _307) ? 1 : 0; + put(out, part21); + } + const iter5: rs.Iter<[rs.BTreeSet, rs.BTreeSet]> = rs.iterSlice([[small, large] as [rs.BTreeSet, rs.BTreeSet], [large, small] as [rs.BTreeSet, rs.BTreeSet]]); + while (true) { + const _319: [rs.BTreeSet, rs.BTreeSet] | rs.Done = iter5.next(); + if (_319 === rs.DONE) { + break; + } + const part22 = rs.iterCount(rs.btreeIntersection(_319[0], _319[1])) & 0xffff; + put(out, part22); + const part23 = rs.iterCount(rs.btreeDifference(_319[0], _319[1])) & 0xffff; + put(out, part23); + const part24 = rs.iterCount(rs.btreeUnion(_319[0], _319[1])) & 0xffff; + put(out, part24); + const part25 = rs.iterCount(rs.btreeSymmetricDifference(_319[0], _319[1])) & 0xffff; + put(out, part25); + const part26 = rs.btreeIsSubset(_319[0], _319[1]) ? 1 : 0; + put(out, part26); + const part27 = rs.btreeIsDisjoint(_319[0], _319[1]) ? 1 : 0; + put(out, part27); + } + const part28 = rs.btreeLen(rs.btreeFromSorted(rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeSet, rs.btreeIntersection(small, large), (a5: Probe.Probe, b14: Probe.Probe): boolean => { + return !Probe.ne$cmp_PartialEq(a5, b14); + })) & 0xffff; + put(out, part28); + for (const b3 of rs.byteSliceView(data)) { + const _380: Probe.Probe = probe(b3); + const part29 = rs.btreeContains(large, _380) ? 1 : 0; + put(out, part29); + const _390: Probe.Probe = key(b3 as rs.usize); + const part30 = option_mapOr$0vkblkh(rs.btreeKey(large, _390), 65535, (k6: Probe.Probe): rs.u16 => { + return k6._0; + }); + put(out, part30); + } + } else if (_29 === 2) { + const _395: rs.Iter = rs.iterChain(rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_22: rs.u8): Probe.Probe => { + return probe(_22); + }), rs.iterMap(new rs.Range(0, n), key)); + const set: rs.BTreeSet = rs.btreeCollectKeys(rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeSet, rs.iterToVec(_395, undefined, 8, true).items, 8, true, Probe.lt$cmp_PartialOrd, (a2: Probe.Probe, b11: Probe.Probe): boolean => { + return !Probe.ne$cmp_PartialEq(a2, b11); + }); + const part6 = rs.btreeLen(set) & 0xffff; + put(out, part6); + for (const b4 of rs.byteSliceView(data)) { + const _421: Probe.Probe = probe(b4); + const part7 = rs.btreeContains(set, _421) ? 1 : 0; + put(out, part7); + const _431: Probe.Probe = key(b4 as rs.usize); + const part8 = option_mapOr$0vkblkh(rs.btreeKey(set, _431), 65535, (k2: Probe.Probe): rs.u16 => { + return k2._0; + }); + put(out, part8); + } + const part9 = at(3); + const _438: Probe.Probe = probe(part9); + const part10 = at(4); + const _437: [Probe.Probe, Probe.Probe] = [_438, probe(part10)]; + const lo3: Probe.Probe = _438; + const hi3: Probe.Probe = _437[1]; + if (!(Probe.low(lo3) <= Probe.low(hi3))) { + _452 = [hi3, lo3] as [Probe.Probe, Probe.Probe]; + } else { + _452 = [lo3, hi3] as [Probe.Probe, Probe.Probe]; + } + const _461: rs.RangeInclusive = new rs.RangeInclusive(_452[0], _452[1]); + const iter8: rs.Iter = rs.btreeRangeKeys(set, _461, Probe.eq$cmp_PartialEq, Probe.gt$cmp_PartialOrd); + while (true) { + const _463: Probe.Probe | rs.Done = iter8.next(); + if (_463 === rs.DONE) { + break; + } + put(out, _463._0); + } + const _472: rs.Iter = rs.iterMap(new rs.Range(0, n), key); + const sorted: rs.BTreeSet = rs.btreeCollectKeys(rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeSet, rs.iterToVec(_472, undefined, 8, true).items, 8, true, Probe.lt$cmp_PartialOrd, (a3: Probe.Probe, b12: Probe.Probe): boolean => { + return !Probe.ne$cmp_PartialEq(a3, b12); + }); + const part11 = rs.iterCount(rs.btreeIntersection(set, sorted)) & 0xffff; + put(out, part11); + const part12 = rs.iterCount(rs.btreeDifference(set, sorted)) & 0xffff; + put(out, part12); + const part13 = rs.btreeIsSubset(set, sorted) ? 1 : 0; + put(out, part13); + for (const b5 of rs.byteSliceView(data)) { + const _509: Probe.Probe = probe(b5 ^ 85); + const part14 = rs.btreeAdd(set, _509) ? 1 : 0; + put(out, part14); + const _520: Probe.Probe = probe(b5); + const part15 = rs.btreeDelete(set, _520) ? 1 : 0; + put(out, part15); + } + const part16 = at(5); + const _529: Probe.Probe = probe(part16); + const part17 = option_mapOr$0vkblkh(rs.btreeReplace(set, _529), 65535, (k3: Probe.Probe): rs.u16 => { + return k3._0; + }); + put(out, part17); + const part18 = at(6); + const _542: Probe.Probe = probe(part18); + const part19 = option_mapOr$0vkblkh(rs.btreeKey(set, _542, true), 65535, (k4: Probe.Probe): rs.u16 => { + return k4._0; + }); + put(out, part19); + const part20 = rs.iterFold(rs.iterMap(rs.btreeKeys(set), (k5: Probe.Probe): rs.u16 => { + return k5._0; + }), 0, (a0: rs.u16, a1: rs.u16): rs.u16 => { + return rs.u16WrappingAdd(a0, a1); + }); + put(out, part20); + } else { + const _556: rs.Iter<[Probe.Probe, rs.u16]> = rs.iterMap(rs.iterSlice(rs.byteSliceView(data)), (_2: rs.u8): [Probe.Probe, rs.u16] => { + const b8: rs.u8 = _2; + return [{ _0: (((b8 as rs.u16) << 8) & 0xffff) | (b8 & 7) as rs.u16, _1: log }, b8 as rs.u16] as [Probe.Probe, rs.u16]; + }); + const map2: rs.BTreeMap = rs.btreeCollect(rs.btreeNew(Probe.cmp$cmp_Ord) as rs.BTreeMap, rs.iterToVec(_556, undefined, 12, true).items, 12, true, (a: Probe.Probe, b9: Probe.Probe): boolean => { + return !Probe.ne$cmp_PartialEq(a, b9); + }); + const part = rs.btreeLen(map2) & 0xffff; + put(out, part); + for (const b6 of rs.byteSliceView(data)) { + const _581: Probe.Probe = { _0: (b6 & 7) as rs.u16, _1: log }; + const part2 = rs.btreeGet(map2, _581); + const part3 = _28(part2); + put(out, part3); + const _592: Probe.Probe = { _0: (b6 as rs.u16) & 7, _1: log }; + const part4 = option_unwrapOr$1szdyfq(rs.btreeRemove(map2, _592), 65535); + put(out, part4); + } + const part5 = rs.btreeLen(map2) & 0xffff; + put(out, part5); + } + } +} + //# sourceMappingURL=collections.ts.map diff --git a/ts/fixtures/lang/collections/expected/core/option.ts b/ts/fixtures/lang/collections/expected/core/option.ts index 8c46ae06e..0210b408c 100644 --- a/ts/fixtures/lang/collections/expected/core/option.ts +++ b/ts/fixtures/lang/collections/expected/core/option.ts @@ -6,6 +6,7 @@ import type * as ByVal from "../collections.ByVal.ts"; import type * as Mixed from "../collections.Mixed.ts"; import type * as Nibble from "../collections.Nibble.ts"; import type * as Picky from "../collections.Picky.ts"; +import type * as Probe from "../collections.Probe.ts"; import type * as Rank from "../collections.Rank.ts"; import type * as Tally from "../collections.Tally.ts"; @@ -79,6 +80,13 @@ export function mapOr$0f7cus0(self: Nibble.Nibble | undefined, default_: rs.u8, return f(self); } +export function mapOr$0vkblkh(self: Probe.Probe | undefined, default_: rs.u16, f: (a0: Probe.Probe) => rs.u16): rs.u16 { + if (self === undefined) { + return default_; + } + return f(self); +} + export function mapOr$07jk2xl(self: Tally.Tally | undefined, default_: rs.u8, f: (a0: Tally.Tally) => rs.u8): rs.u8 { if (self === undefined) { return default_; @@ -114,6 +122,13 @@ export function mapOr$13kt6x8(self: [rs.u8, rs.u8] | undefined, default_: Uint8A return f(self); } +export function mapOr$1wei0sy(self: rs.u16 | undefined, default_: rs.u16, f: (a0: rs.u16) => rs.u16): rs.u16 { + if (self === undefined) { + return default_; + } + return f(self); +} + export function mapOr$031uhqp(self: rs.u16 | undefined, default_: rs.u8, f: (a0: rs.u16) => rs.u8): rs.u8 { if (self === undefined) { return default_; @@ -149,6 +164,13 @@ export function mapOr$188iunt(self: rs.usize | undefined, default_: rs.u8, f: (a return f(self); } +export function mapOr$05q90i2(self: [Probe.Probe, rs.u16] | undefined, default_: rs.u16, f: (a0: [Probe.Probe, rs.u16]) => rs.u16): rs.u16 { + if (self === undefined) { + return default_; + } + return f(self); +} + export function mapOr$0955beb(self: [Rank.Rank, Tally.Tally] | undefined, default_: rs.u8, f: (a0: [Rank.Rank, Tally.Tally]) => rs.u8): rs.u8 { if (self === undefined) { return default_; diff --git a/ts/fixtures/lang/collections/expected/lib.ts b/ts/fixtures/lang/collections/expected/lib.ts index 5e84e9cac..135eb357a 100644 --- a/ts/fixtures/lang/collections/expected/lib.ts +++ b/ts/fixtures/lang/collections/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rs from "./rs.ts"; -import { hashMaps, sets, btreeMaps, deques, dequeRanges, vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders, extracts, btreeExtracts } from "./collections.ts"; +import { hashMaps, sets, btreeMaps, deques, dequeRanges, vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders, extracts, btreeExtracts, btreeSearches } from "./collections.ts"; import type * as Bumped from "./collections.Bumped.ts"; import type * as Logged from "./collections.Logged.ts"; import type * as Picky from "./collections.Picky.ts"; @@ -100,7 +100,7 @@ export function cases(): rs.Vec<[string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.By vecs(data2, out2); }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::stores", (_212: rs.u8, data3: rs.ByteSlice, out3: rs.ByteVec): undefined => { stores(data3, out3); - }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::optionals", optionals as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::set_ops", setOps as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::unlike_eqs", unlikeEqs as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::own_nes", ownNes as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::cell_eqs", cellEqs as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::unlike_compares", unlikeCompares as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::ref_orders", refOrders as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::deque_ranges", dequeRanges as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::extracts", extracts as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::btree_extracts", btreeExtracts as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); + }] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::optionals", optionals as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::set_ops", setOps as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::unlike_eqs", unlikeEqs as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::own_nes", ownNes as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::cell_eqs", cellEqs as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::unlike_compares", unlikeCompares as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::ref_orders", refOrders as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::deque_ranges", dequeRanges as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::extracts", extracts as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::btree_extracts", btreeExtracts as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined], ["collections::btree_searches", btreeSearches as (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined] as [string, (a0: rs.u8, a1: rs.ByteSlice, a2: rs.ByteVec) => undefined]]); } //# sourceMappingURL=lib.ts.map diff --git a/ts/fixtures/lang/collections/src/collections.rs b/ts/fixtures/lang/collections/src/collections.rs index a9f4096f3..113a87279 100644 --- a/ts/fixtures/lang/collections/src/collections.rs +++ b/ts/fixtures/lang/collections/src/collections.rs @@ -2360,3 +2360,195 @@ pub fn btree_extracts(kind: u8, data: &[u8], out: &mut Vec) { } } } + +/// A B-tree key that logs each comparison std makes of it, in order: `cmp` and `eq` by its +/// low 10 bits, `le` and `gt` alike, but `lt`, which a set's `collect` sorts by, by those +/// bits with the low 2 flipped, and `ne`, which `collect` drops all but the last of a run of +/// equal keys by, by all 16. A collected tree is then out of `cmp`'s order, or holds keys +/// `cmp` finds equal. +#[derive(Clone, Copy)] +struct Probe<'a>(u16, &'a std::cell::Cell); + +impl Probe<'_> { + fn log(&self, other: &Self, by: u32) { + let call = u32::from(self.0) << 16 ^ u32::from(other.0) ^ by; + self.1.set(self.1.get().wrapping_mul(0x01000193).wrapping_add(call)); + } + + fn low(&self) -> u16 { + self.0 & 0x3ff + } +} + +impl PartialEq for Probe<'_> { + fn eq(&self, other: &Self) -> bool { + self.log(other, 0x100); + self.low() == other.low() + } + + fn ne(&self, other: &Self) -> bool { + self.log(other, 0x200); + self.0 != other.0 + } +} + +impl Eq for Probe<'_> {} + +impl PartialOrd for Probe<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + self.log(other, 0x300); + Some(self.low().cmp(&other.low())) + } + + fn lt(&self, other: &Self) -> bool { + self.log(other, 0x400); + self.low() ^ 3 < other.low() ^ 3 + } + + fn le(&self, other: &Self) -> bool { + self.log(other, 0x500); + self.low() <= other.low() + } + + fn gt(&self, other: &Self) -> bool { + self.log(other, 0x600); + self.low() > other.low() + } +} + +impl Ord for Probe<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.log(other, 0x800); + self.low().cmp(&other.low()) + } +} + +/// std's B-tree compares keys only as it searches its nodes, each from its first key on, +/// which `Probe` logs: of maps and sets grown by inserts that split nodes at every height, +/// then searched, removed from until nodes merge and take from their siblings, extended, +/// entered, ranged, popped, split, cloned, extracted from, and retained, and of the set +/// operations that search, of sets of sizes that make std look keys up rather than merge. +/// Then of a set and map `collect` leaves out of `cmp`'s order, which std searches as is. +/// Each answer is followed by the log. +pub fn btree_searches(kind: u8, data: &[u8], out: &mut Vec) { + let log = std::cell::Cell::new(0u32); + let at = |i: usize| data.get(i).copied().unwrap_or(0); + let put = |out: &mut Vec, answer: u16| { + out.extend(answer.to_le_bytes()); + out.extend(log.replace(0).to_le_bytes()); + }; + // Distinct keys spread over 0..1024 by an odd stride, so each lands anywhere in the tree. + let n = usize::from(at(0)) + 1; + let stride = usize::from(at(1)) * 2 + 1; + let key = |i: usize| Probe((i * stride + usize::from(at(2))) as u16 & 0x3ff, &log); + let probe = |b: u8| Probe(u16::from(b) * 4 + u16::from(b >> 6), &log); + let some = |v: Option<&u16>| v.map_or(0xffff, |v| *v); + match kind % 4 { + 0 => { + let mut map: BTreeMap = BTreeMap::new(); + for i in 0..n { + let old = map.insert(key(i), i as u16); + put(out, old.unwrap_or(0xffff)); + } + for &b in &data[data.len().min(3)..] { + put(out, some(map.get(&probe(b)))); + put(out, map.contains_key(&key(usize::from(b))) as u16); + put(out, map.remove(&key(usize::from(b) * 3)).unwrap_or(0xffff)); + *map.entry(probe(b)).or_insert(7) += 1; + put(out, map.len() as u16); + } + map.extend((0..n / 2).map(|i| (key(i * 5), i as u16))); + put(out, map.len() as u16); + let (lo, hi) = (probe(at(3)), probe(at(4))); + let (lo, hi) = if lo.low() <= hi.low() { (lo, hi) } else { (hi, lo) }; + let mut range = match at(5) % 3 { + 0 => map.range(lo..hi), + 1 => map.range(lo..=hi), + _ => map.range((Bound::Excluded(lo), Bound::Unbounded)), + }; + while let Some((k, _)) = if at(6) % 2 == 0 { + range.next() + } else { + range.next_back() + } { + put(out, k.0); + } + put(out, map.pop_first().map_or(0xffff, |(k, _)| k.0)); + put(out, map.pop_last().map_or(0xffff, |(k, _)| k.0)); + let copy = map.clone(); + put(out, some(copy.get(&probe(at(7))))); + let mut right = map.split_off(&probe(at(8))); + put(out, (map.len() * 1000 + right.len()) as u16); + put(out, some(right.get(&probe(at(9))))); + let taken = right + .extract_if(probe(at(10))..probe(at(11)), |k, _| k.0 % 3 == 0) + .count(); + put(out, taken as u16); + map.retain(|k, _| k.0 % 4 != 0); + put(out, map.len() as u16); + for i in (0..n).step_by(3) { + put(out, map.remove(&key(i)).unwrap_or(0xffff)); + } + put(out, map.len() as u16); + } + 1 => { + // One set at least 16 times the other's size, which std looks each key of up. + let large: BTreeSet = (0..n).map(key).collect(); + let mut small: BTreeSet = BTreeSet::new(); + for &b in data { + put(out, small.insert(probe(b)) as u16); + } + for (x, y) in [(&small, &large), (&large, &small)] { + put(out, x.intersection(y).count() as u16); + put(out, x.difference(y).count() as u16); + put(out, x.union(y).count() as u16); + put(out, x.symmetric_difference(y).count() as u16); + put(out, x.is_subset(y) as u16); + put(out, x.is_disjoint(y) as u16); + } + put(out, (&small & &large).len() as u16); + for &b in data { + put(out, large.contains(&probe(b)) as u16); + put(out, large.get(&key(usize::from(b))).map_or(0xffff, |k| k.0)); + } + } + 2 => { + // Sorted by `lt` and deduplicated by `ne`, out of `cmp`'s order. + let mut set: BTreeSet = data.iter().map(|&b| probe(b)).chain((0..n).map(key)).collect(); + put(out, set.len() as u16); + for &b in data { + put(out, set.contains(&probe(b)) as u16); + put(out, set.get(&key(usize::from(b))).map_or(0xffff, |k| k.0)); + } + let (lo, hi) = (probe(at(3)), probe(at(4))); + let (lo, hi) = if lo.low() <= hi.low() { (lo, hi) } else { (hi, lo) }; + for k in set.range(lo..=hi) { + put(out, k.0); + } + let sorted: BTreeSet = (0..n).map(key).collect(); + put(out, set.intersection(&sorted).count() as u16); + put(out, set.difference(&sorted).count() as u16); + put(out, set.is_subset(&sorted) as u16); + for &b in data { + put(out, set.insert(probe(b ^ 0x55)) as u16); + put(out, set.remove(&probe(b)) as u16); + } + put(out, set.replace(probe(at(5))).map_or(0xffff, |k| k.0)); + put(out, set.take(&probe(at(6))).map_or(0xffff, |k| k.0)); + put(out, set.iter().map(|k| k.0).fold(0, u16::wrapping_add)); + } + _ => { + // Sorted by `cmp`, but keys it finds equal stay where `ne` tells them apart. + let mut map: BTreeMap = data + .iter() + .map(|&b| (Probe(u16::from(b) << 8 | u16::from(b & 7), &log), u16::from(b))) + .collect(); + put(out, map.len() as u16); + for &b in data { + put(out, some(map.get(&Probe(u16::from(b & 7), &log)))); + put(out, map.remove(&Probe(u16::from(b) & 7, &log)).unwrap_or(0xffff)); + } + put(out, map.len() as u16); + } + } +} diff --git a/ts/fixtures/lang/collections/src/lib.rs b/ts/fixtures/lang/collections/src/lib.rs index 1147c0f69..9b0d32573 100644 --- a/ts/fixtures/lang/collections/src/lib.rs +++ b/ts/fixtures/lang/collections/src/lib.rs @@ -32,5 +32,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("collections::deque_ranges", collections::deque_ranges), ("collections::extracts", collections::extracts), ("collections::btree_extracts", collections::btree_extracts), + ("collections::btree_searches", collections::btree_searches), ] } diff --git a/ts/runtime/rs/collections.ts b/ts/runtime/rs/collections.ts index 7d20ecacd..93b77cf1d 100644 --- a/ts/runtime/rs/collections.ts +++ b/ts/runtime/rs/collections.ts @@ -6,8 +6,8 @@ // // A key compares as Rust's `Eq` or `Ord` says, never by JS identity: a hash collection // finds a key that is a JS object through its translated `Hash` and `Eq`, and a B-tree -// orders every key by its translated `Ord`. Which of those calls the runtime makes, and -// how often, are its own, not std's. +// orders every key by its translated `Ord`. Which `Hash` and `Eq` calls the runtime makes, +// and how often, are its own, not std's; a B-tree makes std's `Ord` calls. // // No key, value, or item is ever `undefined`, which the translator refuses, so // `undefined` is a missing one. @@ -1420,35 +1420,32 @@ export function tableDrain(t: HashTable, d?: (item: T) => void): Iter { // BTreeMap and BTreeSet /** - * The most keys a node holds. A B+ tree's leaves hold its keys and values in order, and - * its branches each child's lower bound, so every operation but a split or merge shifts - * at most this many array slots after `log_{NODE/2} n` searches of one node each. + * The most keys a node holds, std's: `2 * B - 1` of its `B` of 6. Each node is std's, + * split, merged, and evened out where std's is, so a search reads the same keys in the same + * order. */ -const NODE = 64; -/** The fewest keys a node other than the root holds, half of `NODE`, as a literal a bundler can drop. */ -const MIN = 32; +const CAPACITY = 11; +/** The fewest keys a node other than the root holds after std's removals. */ +const MIN_LEN = 5; -/** A leaf: its keys in order, each key's value at the same index, and the leaves beside it. */ -interface Leaf { +/** + * A node, as std's: its keys in order, each key's value at the same index, and of an + * internal node, its children, one more than its keys, each holding the keys between the + * two around it. Each knows its parent and its index among the parent's children. + */ +interface Node { k: K[]; v: V[]; - prev: Leaf | undefined; - next: Leaf | undefined; -} - -/** A branch: its children, and each one's lower bound, which the first child's is not. */ -interface Branch { - k: K[]; - c: Node[]; + e: Node[] | undefined; + p: Node | undefined; + i: number; } -type Node = Leaf | Branch; - -/** `BTreeMap`, and `BTreeSet` as a map to `undefined`: a B+ tree ordered by `Ord`. */ +/** `BTreeMap`, and `BTreeSet` as a map to `undefined`: std's B-tree, ordered by `Ord`. */ export interface BTreeMap { /** The root, which is a leaf where `h` is 0. */ t: Node; - /** The height: how many branches are above each leaf. */ + /** The height: how many internal nodes are above each leaf. */ h: number; /** How many keys it holds. */ n: number; @@ -1464,8 +1461,20 @@ export interface BTreeMap { /** `BTreeSet`. */ export type BTreeSet = BTreeMap; -function leaf(k: K[] = [], v: V[] = []): Leaf { - return { k, v, prev: undefined, next: undefined }; +/** An empty node, internal if it has children `e`, which it becomes the parent of. */ +function node(e?: Node[]): Node { + const n: Node = { k: [], v: [], e, p: undefined, i: 0 }; + if (e !== undefined) adopt(n); + return n; +} + +/** Makes `n` the parent of each of its children from index `from` on, at its index. */ +function adopt(n: Node, from = 0): void { + const e = n.e as Node[]; + for (let i = from; i < e.length; i++) { + e[i].p = n; + e[i].i = i; + } } /** @@ -1473,53 +1482,76 @@ function leaf(k: K[] = [], v: V[] = []): Leaf { * inferred from `cmp`: a slice's order takes any `ArrayLike`. */ export function btreeNew(cmp: (a: NoInfer, b: NoInfer) => Ordering): BTreeMap { - return { t: leaf(), h: 0, n: 0, c: cmp, r: false }; + return { t: node(), h: 0, n: 0, c: cmp, r: false }; } -/** The index of `key` in `keys` if there, or else the bitwise not of where it would go. */ -function search(keys: K[], key: K, cmp: (a: K, b: K) => Ordering): number { - let lo = 0; - let hi = keys.length; - while (lo < hi) { - const mid = (lo + hi) >>> 1; - const c = cmp(keys[mid], key); - if (c === 0) return mid; - if (c < 0) lo = mid + 1; - else hi = mid; +/** + * The index a function that finds a key or edge leaves here beside the node it returns, + * which its caller reads before anything else runs. + */ +let idx = 0; + +/** + * std's `find_key_index`: the index of the first of `n`'s keys from `from` on that `key` is + * not greater than, by `key.cmp(k)`, if equal, or else the bitwise not of the edge before it. + */ +function find(cmp: (a: K, b: K) => Ordering, n: Node, key: K, from = 0): number { + const keys = n.k; + const len = keys.length; + for (let i = from; i < len; i++) { + const order = cmp(key, keys[i]); + if (order === 0) return i; + if (order < 0) return ~i; } - return ~lo; + return ~len; } -/** The index of the child of `b` whose keys `key` would be among. */ -function child(b: Branch, key: K, cmp: (a: K, b: K) => Ordering): number { - let lo = 1; - let hi = b.k.length; - while (lo < hi) { - const mid = (lo + hi) >>> 1; - if (cmp(b.k[mid], key) <= 0) lo = mid + 1; - else hi = mid; +/** + * std's `search_tree`: the node holding a key equal to `key`, with `idx` its index, or else + * the leaf it would go in, with `idx` the bitwise not of the edge it goes at. + */ +function seek(m: BTreeMap, key: K): Node { + const cmp = m.c; + let n = m.t; + for (;;) { + const i = find(cmp, n, key); + if (i >= 0 || n.e === undefined) { + idx = i; + return n; + } + n = n.e[~i]; } - return lo - 1; } -/** The leaf `key` would be in. */ -function leafOf(m: BTreeMap, key: K): Leaf { - let node = m.t; - for (let h = m.h; h > 0; h--) { - const b = node as Branch; - node = b.c[child(b, key, m.c)]; +/** The first leaf, or with `last`, the last. */ +function end(m: BTreeMap, last: boolean): Node { + let n = m.t; + while (n.e !== undefined) n = n.e[last ? n.e.length - 1 : 0]; + return n; +} + +/** std's `next_leaf_edge` of the key at index `i` of `n`: the leaf, with `idx` the edge's index. */ +function nextEdge(n: Node, i: number): Node { + if (n.e === undefined) { + idx = i + 1; + return n; } - return node as Leaf; + n = n.e[i + 1]; + while (n.e !== undefined) n = n.e[0]; + idx = 0; + return n; } -/** The first leaf, or with `last`, the last. */ -function end(m: BTreeMap, last: boolean): Leaf { - let node = m.t; - for (let h = m.h; h > 0; h--) { - const c = (node as Branch).c; - node = c[last ? c.length - 1 : 0]; +/** std's `next_back_leaf_edge` of the key at index `i` of `n`: the leaf, with `idx` the edge's index. */ +function backEdge(n: Node, i: number): Node { + if (n.e === undefined) { + idx = i; + return n; } - return node as Leaf; + n = n.e[i]; + while (n.e !== undefined) n = n.e[n.e.length - 1]; + idx = n.k.length; + return n; } export function btreeLen(m: BTreeMap): number { @@ -1530,34 +1562,31 @@ export function btreeLen(m: BTreeMap): number { export function btreeGet(m: BTreeMap, key: K): V | undefined; export function btreeGet(m: BTreeMap, key: K, none: None): V | None; export function btreeGet(m: BTreeMap, key: K, none?: None): V | None | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - return i < 0 ? none : l.v[i]; + const n = seek(m, key); + return idx < 0 ? none : n.v[idx]; } /** `BTreeMap::get_mut` of a value JS copies: its place, if the map holds `key`. */ export function btreeGetMut(m: BTreeMap, key: K): Place | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - return i < 0 ? undefined : new AtIndex(l.v, i); + const n = seek(m, key); + return idx < 0 ? undefined : new AtIndex(n.v, idx); } /** `BTreeMap::get_key_value`: the equal key the map holds, and its value. */ export function btreeGetKeyValue(m: BTreeMap, key: K): [K, V] | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - return i < 0 ? undefined : [l.k[i], l.v[i]]; + const n = seek(m, key); + return idx < 0 ? undefined : [n.k[idx], n.v[idx]]; } /** `BTreeSet::get`, and `BTreeSet::take` if `take`: the equal key the set holds, or else `none`. */ export function btreeKey(m: BTreeMap, key: K, take?: boolean): K | undefined; export function btreeKey(m: BTreeMap, key: K, take: boolean, none: None): K | None; export function btreeKey(m: BTreeMap, key: K, take = false, none?: None): K | None | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) return none; - const held = l.k[i]; - if (take) removeAt(m, l, i); + const held = n.k[i]; + if (take) removeAt(m, n, i); return held; } @@ -1565,53 +1594,105 @@ export function btreeKey(m: BTreeMap, key: K, take = false, none?: N export function btreeReplace(m: BTreeSet, key: K): K | undefined; export function btreeReplace(m: BTreeSet, key: K, none: None): K | None; export function btreeReplace(m: BTreeSet, key: K, none?: None): K | None | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) { - insertAt(m, l, ~i, key, undefined); + insertAt(m, n, ~i, key, undefined); return none; } - const old = l.k[i]; - l.k[i] = key; - if (bounding(l, i)) rebound(m, key, old); + const old = n.k[i]; + n.k[i] = key; return old; } export function btreeContains(m: BTreeMap, key: K): boolean { - return search(leafOf(m, key).k, key, m.c) >= 0; + seek(m, key); + return idx >= 0; } /** `BTreeMap::index`: `map[&key]`. */ export function btreeIndex(m: BTreeMap, key: K, at?: Location): V { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - if (i < 0) panic("no entry found for key", at, true); - return l.v[i]; + const n = seek(m, key); + if (idx < 0) panic("no entry found for key", at, true); + return n.v[idx]; } /** `BTreeMap::insert`: the old value, or else `none`. An equal key already there stays, and `key` drops. */ export function btreeInsert(m: BTreeMap, key: K, value: V, k?: (key: K) => void): V | undefined; export function btreeInsert(m: BTreeMap, key: K, value: V, k: ((key: K) => void) | undefined, none: None): V | None; export function btreeInsert(m: BTreeMap, key: K, value: V, k?: (key: K) => void, none?: None): V | None | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) { - insertAt(m, l, ~i, key, value); + insertAt(m, n, ~i, key, value); return none; } k?.(key); - const old = l.v[i]; - l.v[i] = value; + const old = n.v[i]; + n.v[i] = value; return old; } -/** Puts `key` and `value` at index `i` of `l`, the leaf it goes in, and splits what overflows. */ -function insertAt(m: BTreeMap, l: Leaf, i: number, key: K, value: V): void { +/** + * std's `insert_recursing`: puts `key` and `value` at edge `i` of leaf `l`, splitting a full + * node about std's `splitpoint` and putting its middle key into its parent in turn, or a + * new root. Returns the leaf the key went in, with `idx` its index. + */ +function insertAt(m: BTreeMap, l: Node, i: number, key: K, value: V): Node { m.r = true; m.n++; - put(l.k, i, key); - put(l.v, i, value); - if (l.k.length > NODE) settle(m, key); + let n = l; + let edge: Node | undefined; + let leaf: Node | undefined; + let index = 0; + for (;;) { + let into = n; + let j = i; + let right: Node | undefined; + if (n.k.length === CAPACITY) { + const mid = i < 5 ? 4 : i <= 6 ? 5 : 6; + right = split(n, mid); + if (i > mid) { + into = right; + j = i - mid - 1; + } + } + put(into.k, j, key); + put(into.v, j, value); + if (edge !== undefined) { + put(into.e as Node[], j + 1, edge); + adopt(into, j + 1); + } + if (leaf === undefined) { + leaf = into; + index = j; + } + if (right === undefined) break; + // The middle key, left last in `n`, goes up. + key = n.k.pop() as K; + value = n.v.pop() as V; + edge = right; + if (n.p === undefined) { + const root = node([n, right]); + root.k.push(key); + root.v.push(value); + m.t = root; + m.h++; + break; + } + i = n.i; + n = n.p; + } + idx = index; + return leaf; +} + +/** Moves the keys of `n` after index `mid`, and the children after them, into a new node, which it returns. */ +function split(n: Node, mid: number): Node { + const right = node(n.e?.splice(mid + 1)); + right.k = n.k.splice(mid + 1); + right.v = n.v.splice(mid + 1); + return right; } /** Puts `item` at index `i` of `a`, after moving the rest up: in a node, faster than `splice`. */ @@ -1631,59 +1712,208 @@ function take(a: T[], i: number): void { /** `BTreeSet::insert`: whether `key` was not there yet. An equal key already there stays, and `key` drops. */ export function btreeAdd(m: BTreeSet, key: K, k?: (key: K) => void): boolean { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i >= 0) { k?.(key); return false; } - insertAt(m, l, ~i, key, undefined); + insertAt(m, n, ~i, key, undefined); return true; } -/** Takes the key and value at index `i` of `l` out, and merges the leaf if too small. */ -function removeAt(m: BTreeMap, l: Leaf, i: number): void { - const key = l.k[i]; - const bound = bounding(l, i); +/** + * std's `remove_kv_tracking`: takes the key and value at index `i` of `n` out, an internal + * node's by moving the last key before it in its place, and rebalances as std's does. + * Returns the leaf edge where it was, the leaf, with `idx` the edge's index. + */ +function removeAt(m: BTreeMap, n: Node, i: number): Node { m.n--; - take(l.k, i); - take(l.v, i); - if (l.k.length < MIN && m.h > 0) settle(m, key); - if (bound) rebound(m, key, key); + if (n.e === undefined) return removeLeaf(m, n, i); + let l = n.e[i]; + while (l.e !== undefined) l = l.e[l.e.length - 1]; + const last = l.k.length - 1; + const key = l.k[last]; + const value = l.v[last]; + // Rebalancing may move the key being replaced, which is the next one after the hole. + let hole = removeLeaf(m, l, last); + let j = idx; + while (j === hole.k.length) { + j = hole.i; + hole = hole.p as Node; + } + hole.k[j] = key; + hole.v[j] = value; + return nextEdge(hole, j); +} + +/** + * std's `remove_leaf_kv`: takes index `i` out of leaf `n`, and if that leaves it too small, + * merges it with a sibling or takes one key from it, the left one if it has one, then fixes + * each ancestor that leaves too small. Returns where the key was, as `removeAt`. + */ +function removeLeaf(m: BTreeMap, n: Node, i: number): Node { + take(n.k, i); + take(n.v, i); + const p = n.p; + if (n.k.length >= MIN_LEN || p === undefined) { + idx = i; + return n; + } + let pos = n; + if (n.i > 0) { + const left = (p.e as Node[])[n.i - 1]; + if (left.k.length + 1 + n.k.length <= CAPACITY) { + i += left.k.length + 1; + merge(p, n.i - 1); + pos = left; + } else { + stealLeft(p, n.i - 1, 1); + i++; + } + } else if (n.k.length + 1 + (p.e as Node[])[1].k.length <= CAPACITY) { + merge(p, 0); + } else { + stealRight(p, 0, 1); + } + if (!fixUp(pos.p as Node)) popRoot(m); + idx = i; + return pos; +} + +/** + * std's `fix_node_and_affected_ancestors`: merges `n` and each ancestor that left too + * small with a sibling, or evens it out with one. False if it emptied the internal root. + */ +function fixUp(n: Node): boolean { + for (;;) { + const len = n.k.length; + if (len >= MIN_LEN) return true; + const p = n.p; + if (p === undefined) return len > 0; + const e = p.e as Node[]; + const j = n.i > 0 ? n.i - 1 : 0; + if (e[j].k.length + 1 + e[j + 1].k.length <= CAPACITY) { + merge(p, j); + n = p; + } else { + if (n.i > 0) stealLeft(p, j, MIN_LEN - len); + else stealRight(p, j, MIN_LEN - len); + return true; + } + } +} + +/** std's `pop_internal_level`: the root's first child becomes the root. */ +function popRoot(m: BTreeMap): void { + m.t = (m.t.e as Node[])[0]; + m.t.p = undefined; + m.h--; +} + +/** std's `do_merge`: moves key `j` of `p`, then its right child's keys and children, into its left child. */ +function merge(p: Node, j: number): void { + const e = p.e as Node[]; + const left = e[j]; + const right = e[j + 1]; + const old = left.k.length; + left.k.push(p.k[j]); + left.v.push(p.v[j]); + append(left.k, right.k, 0); + append(left.v, right.v, 0); + take(p.k, j); + take(p.v, j); + take(e, j + 1); + adopt(p, j + 1); + if (left.e !== undefined) { + append(left.e, right.e as Node[], 0); + adopt(left, old + 1); + } } /** - * Whether index `i` of `l` may hold a key a branch bounds a child by: a leaf's first, - * but for the first leaf's, whose bounds no lookup reads, as `pop_first` takes. + * std's `bulk_steal_left`: moves the last `count` keys of the left child of key `j` of `p` + * through it into its right child, the first of them into `p`, with their children. */ -function bounding(l: Leaf, i: number): boolean { - return i === 0 && l.prev !== undefined; +function stealLeft(p: Node, j: number, count: number): void { + const e = p.e as Node[]; + const left = e[j]; + const right = e[j + 1]; + const keep = left.k.length - count; + passRight(left.k, right.k, p.k, j, keep, count); + passRight(left.v, right.v, p.v, j, keep, count); + if (right.e !== undefined) { + const from = left.e as Node[]; + shiftUp(right.e, count); + for (let t = 0; t < count; t++) right.e[t] = from[keep + 1 + t]; + from.length = keep + 1; + adopt(right); + } +} + +/** Moves `from`'s items from `keep` on through index `j` of `parent` to the front of `to`, as `stealLeft` moves keys. */ +function passRight(from: T[], to: T[], parent: T[], j: number, keep: number, count: number): void { + shiftUp(to, count); + for (let t = 0; t < count - 1; t++) to[t] = from[keep + 1 + t]; + to[count - 1] = parent[j]; + parent[j] = from[keep]; + from.length = keep; } /** - * Bounds each child by its first key where `old`, a key the tree gave back, which may - * change as it drops, bounds it: on the path to `key`, which `old` is equal to. + * std's `bulk_steal_right`: moves the first `count` keys of the right child of key `j` of + * `p` through it into its left child, the last of them into `p`, with their children. */ -function rebound(m: BTreeMap, key: K, old: K): void { - let node = m.t; - for (let h = m.h; h > 0; h--) { - const b = node as Branch; - const j = child(b, key, m.c); - node = b.c[j]; - if (b.k[j] !== old) continue; - let first = node; - for (let d = h - 1; d > 0; d--) first = (first as Branch).c[0]; - b.k[j] = first.k[0]; +function stealRight(p: Node, j: number, count: number): void { + const e = p.e as Node[]; + const left = e[j]; + const right = e[j + 1]; + const old = left.k.length; + passLeft(right.k, left.k, p.k, j, count); + passLeft(right.v, left.v, p.v, j, count); + if (left.e !== undefined) { + const from = right.e as Node[]; + for (let t = 0; t < count; t++) left.e.push(from[t]); + shiftDown(from, count); + adopt(left, old + 1); + adopt(right); } } +/** Moves `from`'s first `count` items through index `j` of `parent` to the end of `to`, as `stealRight` moves keys. */ +function passLeft(from: T[], to: T[], parent: T[], j: number, count: number): void { + to.push(parent[j]); + for (let t = 0; t < count - 1; t++) to.push(from[t]); + parent[j] = from[count - 1]; + shiftDown(from, count); +} + +/** Moves each item of `a` up `count` slots, leaving the first `count` to be written. */ +function shiftUp(a: T[], count: number): void { + const n = a.length; + for (let t = 0; t < count; t++) a.push(a[n - 1]); + for (let i = n - 1; i >= 0; i--) a[i + count] = a[i]; +} + +/** Takes the first `count` items out of `a`, moving the rest down. */ +function shiftDown(a: T[], count: number): void { + const n = a.length - count; + for (let i = 0; i < n; i++) a[i] = a[i + count]; + a.length = n; +} + +/** Pushes `b`'s items from `from` on onto `a`. */ +function append(a: T[], b: T[], from: number): void { + for (let i = from; i < b.length; i++) a.push(b[i]); +} + /** `BTreeMap::remove_entry`. */ export function btreeRemoveEntry(m: BTreeMap, key: K): [K, V] | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) return undefined; - const entry: [K, V] = [l.k[i], l.v[i]]; - removeAt(m, l, i); + const entry: [K, V] = [n.k[i], n.v[i]]; + removeAt(m, n, i); return entry; } @@ -1691,147 +1921,87 @@ export function btreeRemoveEntry(m: BTreeMap, key: K): [K, V] | unde export function btreeRemove(m: BTreeMap, key: K, k?: (key: K) => void): V | undefined; export function btreeRemove(m: BTreeMap, key: K, k: ((key: K) => void) | undefined, none: None): V | None; export function btreeRemove(m: BTreeMap, key: K, k?: (key: K) => void, none?: None): V | None | undefined { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) return none; - const held = l.k[i]; - const value = l.v[i]; - removeAt(m, l, i); + const held = n.k[i]; + const value = n.v[i]; + removeAt(m, n, i); k?.(held); return value; } /** `BTreeSet::remove`: whether `key` was there. The key it held drops. */ export function btreeDelete(m: BTreeSet, key: K, k?: (key: K) => void): boolean { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const n = seek(m, key); + const i = idx; if (i < 0) return false; - const held = l.k[i]; - removeAt(m, l, i); + const held = n.k[i]; + removeAt(m, n, i); k?.(held); return true; } -/** - * Splits or merges the node on `key`'s path that a change left too large or too small, - * and each above it that this leaves so in turn, finding them again from the root. - */ -function settle(m: BTreeMap, key: K): void { - if (m.h > 0) settleBelow(m, m.t as Branch, m.h, key); - const t = m.t; - if (t.k.length > NODE) { - const right = split(t, m.h === 0); - m.t = { k: [t.k[0], right.k[0]], c: [t, right] }; - m.h++; - } else if (m.h > 0 && (t as Branch).c.length === 1) { - m.t = (t as Branch).c[0]; - m.h--; - } -} - -/** `settle` below `b`, a branch at height `h`. */ -function settleBelow(m: BTreeMap, b: Branch, h: number, key: K): void { - const i = child(b, key, m.c); - const c = b.c[i]; - if (h > 1) settleBelow(m, c as Branch, h - 1, key); - if (c.k.length > NODE) { - const right = split(c, h === 1); - b.k.splice(i + 1, 0, right.k[0]); - b.c.splice(i + 1, 0, right); - } else if (c.k.length < MIN) { - const j = i > 0 ? i - 1 : i; - balance(b, j, h === 1); - } -} - -/** Takes the second half of `node` into a new node beside it, and returns that. */ -function split(node: Node, isLeaf: boolean): Node { - const mid = node.k.length >> 1; - if (!isLeaf) { - const b = node as Branch; - return { k: b.k.splice(mid), c: b.c.splice(mid) }; - } - const l = node as Leaf; - const right = leaf(l.k.splice(mid), l.v.splice(mid)); - right.prev = l; - right.next = l.next; - if (l.next !== undefined) l.next.prev = right; - l.next = right; - return right; +/** An internal node with no keys and one child, and below it another, down to an empty leaf: `height` of them above it. */ +function pillar(height: number): Node { + let n = node(); + for (let h = 0; h < height; h++) n = node([n]); + return n; } -/** Evens out children `j` and `j + 1` of `b`, one of which is too small, or merges them. */ -function balance(b: Branch, j: number, isLeaf: boolean): void { - const left = b.c[j]; - const right = b.c[j + 1]; - const total = left.k.length + right.k.length; - if (total <= NODE) { - left.k.push(...right.k); - if (isLeaf) { - const l = left as Leaf; - const r = right as Leaf; - l.v.push(...r.v); - l.next = r.next; - if (r.next !== undefined) r.next.prev = l; - } else { - (left as Branch).c.push(...(right as Branch).c); +/** + * std's `bulk_push` into `m`, an empty tree: `keys` and their `values`, in order and + * without equal keys, each pushed onto the last leaf, or where that is full, onto the + * lowest node above it with room, with a new right child of empty nodes, before the right + * border takes enough from its left siblings. + */ +function bulk(m: BTreeMap, keys: K[], values: V[]): void { + m.t = node(); + m.h = 0; + m.n = keys.length; + let leaf = m.t; + for (let j = 0; j < keys.length; j++) { + if (leaf.k.length < CAPACITY) { + leaf.k.push(keys[j]); + leaf.v.push(values[j]); + continue; } - b.k.splice(j + 1, 1); - b.c.splice(j + 1, 1); - return; + let open = leaf.p; + let h = 1; + for (; open !== undefined && open.k.length === CAPACITY; h++) open = open.p; + if (open === undefined) { + open = node([m.t]); + m.t = open; + m.h++; + } + const e = open.e as Node[]; + open.k.push(keys[j]); + open.v.push(values[j]); + e.push(pillar(h - 1)); + adopt(open, e.length - 1); + leaf = open; + while (leaf.e !== undefined) leaf = leaf.e[leaf.e.length - 1]; } - const half = total >> 1; - if (left.k.length < half) { - const n = half - left.k.length; - left.k.push(...right.k.splice(0, n)); - if (isLeaf) (left as Leaf).v.push(...(right as Leaf).v.splice(0, n)); - else (left as Branch).c.push(...(right as Branch).c.splice(0, n)); - } else { - const n = left.k.length - half; - right.k.unshift(...left.k.splice(half, n)); - if (isLeaf) (right as Leaf).v.unshift(...(left as Leaf).v.splice(half, n)); - else (right as Branch).c.unshift(...(left as Branch).c.splice(half, n)); + // std's `fix_right_border_of_plentiful`. + for (let n = m.t; n.e !== undefined; n = n.e[n.e.length - 1]) { + const short = MIN_LEN - n.e[n.e.length - 1].k.length; + if (short > 0) stealLeft(n, n.k.length - 1, short); } - b.k[j + 1] = right.k[0]; -} - -/** The tree of `keys` and their `values`, in order and without equal keys, built whole. */ -function build(m: BTreeMap, keys: K[], values: V[]): void { - m.n = keys.length; - // As few nodes per level as hold them, evened out, so each holds at least half. - const chunks = (n: number) => { - const count = Math.max(1, Math.ceil(n / NODE)); - return Array.from({ length: count }, (_, i) => [Math.floor((i * n) / count), Math.floor(((i + 1) * n) / count)]); - }; - let level: Node[] = []; - let prev: Leaf | undefined; - for (const [from, to] of chunks(keys.length)) { - const l = leaf(keys.slice(from, to), values.slice(from, to)); - l.prev = prev; - if (prev !== undefined) prev.next = l; - prev = l; - level.push(l); - } - let h = 0; - while (level.length > 1) { - level = chunks(level.length).map(([from, to]) => { - const c = level.slice(from, to); - return { k: c.map((node) => node.k[0]), c }; - }); - h++; - } - m.t = level[0]; - m.h = h; } /** Each key in order, and each one's value. */ function flatten(m: BTreeMap): [K[], V[]] { const keys: K[] = []; const values: V[] = []; - for (let l: Leaf | undefined = end(m, false); l !== undefined; l = l.next) { - keys.push(...l.k); - values.push(...l.v); - } + const visit = (n: Node) => { + for (let i = 0; i < n.k.length; i++) { + if (n.e !== undefined) visit(n.e[i]); + keys.push(n.k[i]); + values.push(n.v[i]); + } + if (n.e !== undefined) visit(n.e[n.k.length]); + }; + visit(m.t); return [keys, values]; } @@ -1884,28 +2054,75 @@ export function btreeClear(m: BTreeMap, k?: (key: K) => void, v?: (v btreeDrop(old, k, v); return; } - m.t = leaf(); + m.t = node(); m.h = 0; m.n = 0; m.r = false; } /** - * `split_off` of a map or set: the entries from `key` on, moved in order into a tree of - * their own. Like std's, the new tree has a root unless `m` was empty. + * `split_off` of a map or set, as std's: the entries from `key` on, cut from each node on + * the path to it into a column of empty nodes, whose borders then even out. Like std's, + * the new tree has a root unless `m` was empty. */ export function btreeSplitOff(m: BTreeMap, key: K): BTreeMap { const out = btreeNew(m.c); if (m.n === 0) return out; - const [keys, values] = flatten(m); - const i = search(keys, key, m.c); - const phys = i < 0 ? ~i : i; - build(out, keys.slice(phys), values.slice(phys)); - build(m, keys.slice(0, phys), values.slice(0, phys)); + out.t = pillar(m.h); + out.h = m.h; out.r = true; + let left = m.t; + let right = out.t; + for (;;) { + const i = find(m.c, left, key); + const cut = i < 0 ? ~i : i; + right.k = left.k.splice(cut); + right.v = left.v.splice(cut); + if (left.e === undefined) break; + const e = right.e as Node[]; + e.push(...left.e.splice(cut + 1)); + adopt(right, 1); + left = left.e[cut]; + right = e[0]; + } + fixBorder(m, true); + fixBorder(out, false); + out.n = count(out.t); + m.n -= out.n; return out; } +/** How many keys `n` and the nodes below it hold. */ +function count(n: Node): number { + let total = n.k.length; + if (n.e !== undefined) for (const c of n.e) total += count(c); + return total; +} + +/** + * std's `fix_right_border` of `m`, or else its `fix_left_border`: drops empty roots, and + * merges each node along the border with its sibling, or has it take enough from it. + */ +function fixBorder(m: BTreeMap, last: boolean): void { + while (m.h > 0 && m.t.k.length === 0) popRoot(m); + if (m.t.k.length === 0) return; + for (let n = m.t; n.e !== undefined; ) { + const j = last ? n.k.length - 1 : 0; + const left = n.e[j]; + const right = n.e[j + 1]; + const short = MIN_LEN + 1 - (last ? right : left).k.length; + if (left.k.length + 1 + right.k.length <= CAPACITY) { + merge(n, j); + n = left; + } else { + if (short > 0 && last) stealLeft(n, j, short); + else if (short > 0) stealRight(n, j, short); + n = last ? right : left; + } + } + while (m.h > 0 && m.t.k.length === 0) popRoot(m); +} + /** * `retain`, which visits the keys in order and keeps each entry `f` returns true for, * dropping each other one as `f` rejects it. @@ -1919,39 +2136,22 @@ export function btreeRetainMut(m: BTreeMap, f: (key: K, value: Place retainBTree(m, f as (key: K, value: V | Place) => boolean, true, k, v); } -/** `retain`, handing `f` each value or, if `lend`, its place, which the map is built from again. */ +/** `retain`, as std's: `extract_if` of the whole map, handing `f` each value or, if `lend`, its place. */ function retainBTree(m: BTreeMap, f: (key: K, value: V | Place) => boolean, lend: boolean, k?: (key: K) => void, v?: (value: V) => void): void { - const [keys, values] = flatten(m); - let kept = 0; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const keep = f(key, lend ? new AtIndex(values, i) : values[i]); - const value = values[i]; - if (!keep) { - dropPair(key, value, k, v); - continue; - } - keys[kept] = key; - values[kept] = value; - kept++; - } - if (kept < keys.length || lend) { - keys.length = kept; - values.length = kept; - build(m, keys, values); - } + const it = new BTreeExtractIf(m, undefined, (key, value) => !f(key, value), lend, false); + for (let entry = it.next(); entry !== DONE; entry = it.next()) dropPair(entry[0], entry[1], k, v); } /** - * An iterator of a B-tree's entries, keys, or values, from index `fi` of leaf `fl` up to - * before index `bi` of leaf `bl`. The front moves to the next leaf as it passes a leaf's - * last key, and the back to the previous leaf as it passes a leaf's first key. One that + * An iterator of a B-tree's entries, keys, or values, from leaf edge `fi` of leaf `fl` up + * to leaf edge `bi` of leaf `bl`, as std's: the front climbs from a leaf's last edge to the + * next key, then steps to the first leaf edge after it, and the back likewise. One that * owns its entries drops as `HashIter` does. */ export class BTreeIter extends Iter implements Disposable { - fl: Leaf; + fl: Node; fi: number; - bl: Leaf; + bl: Node; bi: number; /** How many items are left, where it is known: an iterator of the whole map. */ private n: number; @@ -1961,17 +2161,9 @@ export class BTreeIter extends Iter implements Disposable { /** Whether its `last` reads from the back, as a map's `into_keys`' and `into_values`' do. */ readonly back: boolean; - constructor(pick: Pick, fl: Leaf, fi: number, bl: Leaf, bi: number, n = -1, k?: (key: K) => void, v?: (value: V) => void, back = false) { + constructor(pick: Pick, fl: Node, fi: number, bl: Node, bi: number, n = -1, k?: (key: K) => void, v?: (value: V) => void, back = false) { super(); this.pick = pick; - if (fi === fl.k.length && fl.next !== undefined) { - fl = fl.next; - fi = 0; - } - if (bi === 0 && bl.prev !== undefined) { - bl = bl.prev; - bi = bl.k.length; - } this.fl = fl; this.fi = fi; this.bl = bl; @@ -1984,42 +2176,40 @@ export class BTreeIter extends Iter implements Disposable { next(): T | Done { if (done(this)) return DONE; - const l = this.fl; - const i = this.fi++; - if (this.fi === l.k.length && l.next !== undefined) { - this.fl = l.next; - this.fi = 0; - } + const n = front(this); this.n--; - return this.picked(l, i); + return this.picked(n, idx); } override nextBack(): T | Done { if (done(this)) return DONE; - const l = this.bl; - const i = --this.bi; - if (this.bi === 0 && l.prev !== undefined) { - this.bl = l.prev; - this.bi = this.bl.k.length; + let n = this.bl; + let i = this.bi; + while (i === 0) { + i = n.i; + n = n.p as Node; } + i--; + this.bl = backEdge(n, i); + this.bi = idx; this.n--; - return this.picked(l, i); + return this.picked(n, i); } /** - * What it yields of index `i` of leaf `l`: of `into_keys` or `into_values`, the other + * What it yields of index `i` of node `n`: of `into_keys` or `into_values`, the other * half drops first. */ - private picked(l: Leaf, i: number): T { - if (this.pick === ENTRIES) return [l.k[i], l.v[i]] as T; - if (this.pick === PLACES) return [l.k[i], new AtIndex(l.v, i)] as T; - if (this.pick === VALUE_PLACES) return new AtIndex(l.v, i) as T; + private picked(n: Node, i: number): T { + if (this.pick === ENTRIES) return [n.k[i], n.v[i]] as T; + if (this.pick === PLACES) return [n.k[i], new AtIndex(n.v, i)] as T; + if (this.pick === VALUE_PLACES) return new AtIndex(n.v, i) as T; if (this.pick === KEYS) { - this.v?.(l.v[i]); - return l.k[i] as unknown as T; + this.v?.(n.v[i]); + return n.k[i] as unknown as T; } - this.k?.(l.k[i]); - return l.v[i] as unknown as T; + this.k?.(n.k[i]); + return n.v[i] as unknown as T; } override remaining(): Iterable { @@ -2045,23 +2235,32 @@ export class BTreeIter extends Iter implements Disposable { const keys: K[] = []; const values: V[] = []; while (!done(this)) { - const l = this.fl; - keys.push(l.k[this.fi]); - values.push(l.v[this.fi]); - this.fi++; - if (this.fi === l.k.length && l.next !== undefined) { - this.fl = l.next; - this.fi = 0; - } + const n = front(this); + keys.push(n.k[idx]); + values.push(n.v[idx]); } this.n = 0; dropPairs(keys, values, this.k, this.v); } } -/** Whether `it`'s front has met its back. */ +/** Whether `it`'s front has met its back: the same leaf edge, as only one lies between two keys. */ function done(it: BTreeIter): boolean { - return it.fl === it.bl ? it.fi >= it.bi : it.fi === 0 && it.bl.next === it.fl && it.bi === it.bl.k.length; + return it.fi === it.bi && it.fl === it.bl; +} + +/** Steps `it`'s front past its next key: the node holding that key, with `idx` its index. */ +function front(it: BTreeIter): Node { + let n = it.fl; + let i = it.fi; + while (i === n.k.length) { + i = n.i; + n = n.p as Node; + } + it.fl = nextEdge(n, i); + it.fi = idx; + idx = i; + return n; } function whole(m: BTreeMap, pick: Pick, k?: (key: K) => void, v?: (value: V) => void, back = false): Iter { @@ -2170,9 +2369,13 @@ export function btreeRangeKeys(m: BTreeMap, r: NoInfer */ type Key = K | (K extends (infer E)[] ? Slice : never); -/** The items of `m` within `r`, checked as std checks it: by `eq` and `gt`, or by `m`'s order. */ +/** + * The items of `m` within `r`, checked as std checks it: by `eq` and `gt`, or by `m`'s + * order. Its ends are found as std's `find_leaf_edges_spanning_range` finds them: down one + * path while both lie below the same edge, then each down its own. + */ function range(m: BTreeMap, r: Bounds, pick: Pick, name: string, eq?: Test, gt?: Test): Iter { - if (!m.r) return new BTreeIter(pick, m.t as Leaf, 0, m.t as Leaf, 0); + if (!m.r) return new BTreeIter(pick, m.t, 0, m.t, 0); const [start, stop] = bounds(r); if (start.tag !== "Unbounded" && stop.tag !== "Unbounded") { const s = start._0; @@ -2182,17 +2385,72 @@ function range(m: BTreeMap, r: Bounds, pick: Pick, name: strin } if (gt === undefined ? m.c(s, e) > 0 : gt(s, e)) panic(`range start is greater than range end in ${name}`, `btree_range_order_${name}`); } - const [fl, fi] = start.tag === "Unbounded" ? [end(m, false), 0] : edge(m, start._0, start.tag === "Excluded"); - const last = end(m, true); - const [bl, bi] = stop.tag === "Unbounded" ? [last, last.k.length] : edge(m, stop._0, stop.tag === "Included"); - return new BTreeIter(pick, fl, fi, bl, bi); + const lo = side(start); + const hi = side(stop); + for (let n = m.t; ; ) { + const fi = lower(m.c, n, lo); + const bi = upper(m.c, n, hi, fi); + if (fi < bi) { + let fl = n; + let bl = n; + let f = fi; + let b = bi; + while (fl.e !== undefined) { + fl = fl.e[f]; + f = lower(m.c, fl, lo); + bl = (bl.e as Node[])[b]; + b = upper(m.c, bl, hi, 0); + } + return new BTreeIter(pick, fl, f, bl, b); + } + if (n.e === undefined) return new BTreeIter(pick, n, fi, n, fi); + n = n.e[fi]; + } +} + +/** std's `SearchBound`s, of a range's end as its search narrows it. */ +const INCLUDED = 0; +const EXCLUDED = 1; +const ALL_INCLUDED = 2; +const ALL_EXCLUDED = 3; + +/** One end of a range as std's search narrows it: a `SearchBound` and, where it has one, its key. */ +interface Side { + tag: number; + key: K | undefined; } -/** Where the first key past `key` is if `past`, or else the first key not below it. */ -function edge(m: BTreeMap, key: K, past: boolean): [Leaf, number] { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - return [l, i < 0 ? ~i : past ? i + 1 : i]; +function side(b: RangeBound): Side { + if (b.tag === "Unbounded") return { tag: ALL_INCLUDED, key: undefined }; + return { tag: b.tag === "Included" ? INCLUDED : EXCLUDED, key: b._0 }; +} + +/** std's `find_lower_bound_index`: the edge of `n` a range from `s` starts at, narrowing `s` for the child below it. */ +function lower(cmp: (a: K, b: K) => Ordering, n: Node, s: Side): number { + if (s.tag === ALL_INCLUDED) return 0; + if (s.tag === ALL_EXCLUDED) return n.k.length; + const i = find(cmp, n, s.key as K); + if (i < 0) return ~i; + if (s.tag === INCLUDED) { + s.tag = ALL_EXCLUDED; + return i; + } + s.tag = ALL_INCLUDED; + return i + 1; +} + +/** std's `find_upper_bound_index`: the edge of `n` from `from` on that a range to `s` ends at, narrowing `s` likewise. */ +function upper(cmp: (a: K, b: K) => Ordering, n: Node, s: Side, from: number): number { + if (s.tag === ALL_INCLUDED) return n.k.length; + if (s.tag === ALL_EXCLUDED) return from; + const i = find(cmp, n, s.key as K, from); + if (i < 0) return ~i; + if (s.tag === INCLUDED) { + s.tag = ALL_EXCLUDED; + return i + 1; + } + s.tag = ALL_INCLUDED; + return i; } /** @@ -2205,8 +2463,8 @@ function edge(m: BTreeMap, key: K, past: boolean): [Leaf, numb */ class BTreeExtractIf extends Iter implements Disposable { private readonly m: BTreeMap; - /** The leaf of the entry it reads next, and its index, until it stops, as std's cursor. */ - l: Leaf | undefined; + /** std's cursor: the leaf edge before the entry it reads next, until it stops. */ + l: Node | undefined; i = 0; private readonly r: Bounds; private readonly f: (key: K, value: V | Place) => boolean; @@ -2228,19 +2486,16 @@ class BTreeExtractIf extends Iter implements Disposable { this.drop = drop; // std's reads the start only of a map with its root node. if (!m.r) return; - const start = other(r) ? r.startBound() : bounds(r)[0]; - if (start.tag === "Unbounded") this.at(end(m, false), 0); - else this.at(...edge(m, start._0, start.tag === "Excluded")); - } - - /** Moves to index `i` of `l`, or to the next leaf's first where `i` is past `l`'s last. */ - private at(l: Leaf, i: number): void { - if (i === l.k.length && l.next !== undefined) { - l = l.next; - i = 0; + const s = side(other(r) ? r.startBound() : bounds(r)[0]); + let n = m.t; + for (let i = lower(m.c, n, s); ; i = lower(m.c, n, s)) { + if (n.e === undefined) { + this.l = n; + this.i = i; + return; + } + n = n.e[i]; } - this.l = l; - this.i = i; } /** Whether `key` is within the end of `r`. */ @@ -2253,31 +2508,25 @@ class BTreeExtractIf extends Iter implements Disposable { } next(): T | Done { - const m = this.m; - for (let l = this.l; l !== undefined; l = this.l) { + for (let n = this.l; n !== undefined; n = this.l) { let i = this.i; // Stopped where it ends, past the end of `r`, or by a panic, as std's is. this.l = undefined; - if (i === l.k.length) return DONE; - const key = l.k[i]; + while (i === n.k.length) { + if (n.p === undefined) return DONE; + i = n.i; + n = n.p; + } + const key = n.k[i]; if (!this.within(key)) return DONE; - if (!this.f(key, this.places ? new AtIndex(l.v, i) : l.v[i])) { - this.at(l, i + 1); + if (!this.f(key, this.places ? new AtIndex(n.v, i) : n.v[i])) { + this.l = nextEdge(n, i); + this.i = idx; continue; } - const value = l.v[i]; - // A leaf left too small evens out with a neighbor or merges with it. Only one that - // merges into the leaf before it or takes from it moves what is left of it. - const prev = l.prev; - const before = prev?.k.length ?? 0; - removeAt(m, l, i); - if (prev !== undefined && prev.next !== l) { - i += before; - l = prev; - } else if (prev !== undefined) { - i += before - prev.k.length; - } - this.at(l, i); + const value = n.v[i]; + this.l = removeAt(this.m, n, i); + this.i = idx; return (this.keys ? key : [key, value]) as T; } return DONE; @@ -2296,12 +2545,17 @@ class BTreeExtractIf extends Iter implements Disposable { /** `Debug` of a B-tree's `ExtractIf`: the entry it reads next, if any, or of a set, with no `value`, its key. */ export function fmtBTreeExtractIf(it: Iter, f: Formatter, key: Debug, value?: Debug): FmtResult { - const { l, i } = it as BTreeExtractIf; + let { l: n, i } = it as BTreeExtractIf; + while (n !== undefined && i === n.k.length) { + i = n.i; + n = n.p; + } let peek: Fmt = (f) => f.out.writeStr("None"); - if (l !== undefined && i < l.k.length) { - const k: Fmt = (f) => key(l.k[i], f); - const kv: Fmt = value === undefined ? k : (f) => fmtTuple([k, (f) => value(l.v[i], f)], f); - peek = (f) => fmtDebugTupleFields(f, "Some", [kv]); + if (n !== undefined) { + const kv = n; + const k: Fmt = (f) => key(kv.k[i], f); + const both: Fmt = value === undefined ? k : (f) => fmtTuple([k, (f) => value(kv.v[i], f)], f); + peek = (f) => fmtDebugTupleFields(f, "Some", [both]); } return debugStructFinishNonExhaustive(debugStructField(fmtDebugStruct(f, "ExtractIf"), "peek", peek)); } @@ -2346,12 +2600,12 @@ export function btreeExtend( if (old !== undefined) v?.(old); return; } - const l = leafOf(m, item[0]); - const i = search(l.k, item[0], m.c); - if (i < 0) insertAt(m, l, ~i, item[0], item[1]); + const n = seek(m, item[0]); + const i = idx; + if (i < 0) insertAt(m, n, ~i, item[0], item[1]); else { - l.k[i] = item[0]; - l.v[i] = item[1]; + n.k[i] = item[0]; + n.v[i] = item[1]; } }; return vecExtending(m, 0, items, add, take, size, trusted); @@ -2364,10 +2618,10 @@ export function btreeExtendKeys(m: BTreeSet, keys: Iter, last = false, btreeAdd(m, key, k); return; } - const l = leafOf(m, key); - const i = search(l.k, key, m.c); - if (i < 0) insertAt(m, l, ~i, key, undefined); - else l.k[i] = key; + const n = seek(m, key); + const i = idx; + if (i < 0) insertAt(m, n, ~i, key, undefined); + else n.k[i] = key; }; return vecExtending(m, 0, keys, add, take, size, trusted); } @@ -2392,7 +2646,7 @@ export function btreeCollect(m: BTreeMap, entries: [K, V][], size: n values.push(value); } else dropPair(key, value, k, v); } - build(m, keys, values); + bulk(m, keys, values); // std's makes no root of none. m.r = n > 0; return m; @@ -2419,20 +2673,30 @@ export function btreeCollectKeys(m: BTreeSet, keys: K[], size: number, fre return m; } -/** `clone`, with each key and value through its `Clone`, or as itself without one. */ + +/** + * `clone`, of std's shape, node for node, with each key and value through its `Clone`, or as + * itself without one, in key order, each key before its value, as std's clones them. + */ export function btreeClone(m: BTreeMap, key: (k: K) => K = itself, value: (v: V) => V = itself): BTreeMap { - const keys: K[] = []; - const values: V[] = []; - // Each key, then its value, as std's clones them. - for (let l: Leaf | undefined = end(m, false); l !== undefined; l = l.next) { - for (let i = 0; i < l.k.length; i++) { - keys.push(key(l.k[i])); - values.push(value(l.v[i])); + const copy = (n: Node): Node => { + const e = n.e === undefined ? undefined : [copy(n.e[0])]; + const out = node(e); + for (let i = 0; i < n.k.length; i++) { + out.k.push(key(n.k[i])); + out.v.push(value(n.v[i])); + if (e !== undefined) e.push(copy((n.e as Node[])[i + 1])); } - } + if (e !== undefined) adopt(out); + return out; + }; const out = btreeNew(m.c); - build(out, keys, values); - out.r = m.n > 0; + // std's clones an empty map as a new one, with no root. + if (m.n === 0) return out; + out.t = copy(m.t); + out.h = m.h; + out.n = m.n; + out.r = true; return out; } @@ -2788,7 +3052,7 @@ export function btreeFromSorted(m: BTreeSet, keys: Iter, same?: Test key = next; } } - build( + bulk( m, held, held.map(() => undefined), @@ -2814,8 +3078,8 @@ export interface Entry { /** A hashed key's slot if it is occupied, and its hash. */ readonly s: Slot | undefined; readonly h: number | undefined; - /** A B-tree's leaf, and the key's index in it, or the complement of where it goes. */ - readonly l: Leaf | undefined; + /** A B-tree's node holding the key, and its index there, or else the leaf and the complement of where it goes. */ + readonly l: Node | undefined; readonly i: number; } @@ -2831,8 +3095,8 @@ export function entry(m: HashMap | BTreeMap, key: K, k?: (key: function locate(m: HashMap | BTreeMap, key: K): Entry { if (!isHash(m)) { - const l = leafOf(m, key); - const i = search(l.k, key, m.c); + const l = seek(m, key); + const i = idx; return { m, k: i < 0 ? key : l.k[i], o: i >= 0, s: undefined, h: undefined, l, i }; } if (!hashed(m, key)) return { m, k: key, o: m.p.has(key), s: undefined, h: undefined, l: undefined, i: 0 }; @@ -2849,11 +3113,8 @@ function fill(e: Entry, value: V): Place { m.p.set(e.k, value); return new AtKey(m.p, e.k); } - const l = e.l as Leaf; - const i = ~e.i; - insertAt(m, l, i, e.k, value); - // A split keeps the leaf's first half and moves the rest to the next leaf. - return i < l.k.length ? new AtIndex(l.v, i) : new AtIndex((l.next as Leaf).v, i - l.k.length); + const l = insertAt(m, e.l as Node, ~e.i, e.k, value); + return new AtIndex(l.v, idx); } /** Drop `e`, an `Entry`: a vacant one holds the key it was given. */ @@ -2958,7 +3219,7 @@ export function entryRemoveEntry(e: Entry): [K, V] { const m = e.m; if (isHash(m)) hold(m); const value = entryGet(e); - if (!isHash(m)) removeAt(m, e.l as Leaf, e.i); + if (!isHash(m)) removeAt(m, e.l as Node, e.i); else if (e.s !== undefined) unlink(m, e.h as number, e.s); else m.p.delete(e.k); return [e.k, value]; diff --git a/ts/tests/btree.ts b/ts/tests/btree.ts index 8b6819997..6ad651261 100644 --- a/ts/tests/btree.ts +++ b/ts/tests/btree.ts @@ -1,8 +1,8 @@ // The runtime's B-tree through a script of random operations, large enough to split, merge, // and rebalance its nodes at every height, and to empty it again: inserts, removals, pops, // lookups, ranges and iterators walked from both ends, `retain`, `extract_if` read part way, -// and `clone`. Prints what -// each returns, which `runtime.rs` compares with std's `BTreeMap` running the same script. +// `clone`, `split_off`, and `collect`. Prints what each returns and which keys each compared, +// which `runtime.rs` compares with std's `BTreeMap` running the same script. // // usage: node btree.ts @@ -19,8 +19,21 @@ function rand(): number { return state; } -const cmp = (a: number, b: number): rs.Ordering => (a < b ? -1 : a > b ? 1 : 0); +// Each call of the keys' `cmp`, counted and hashed by its keys, which `runtime.rs` logs alike. +let calls = 0; +let hash = 0; +const cmp = (a: number, b: number): rs.Ordering => { + calls++; + hash = (Math.imul(hash, 31) + Math.imul(a, 65537) + b) >>> 0; + return a < b ? -1 : a > b ? 1 : 0; +}; const out: string[] = []; +/** `line`, and the calls of `cmp` since the last. */ +const say = (line: string) => { + out.push(`${line} #${calls} ${hash}`); + calls = 0; + hash = 0; +}; const show = (v: number | undefined) => (v === undefined ? "-" : String(v)); const walk = (it: rs.Iter<[number, number]>) => { const seen: number[] = []; @@ -33,46 +46,36 @@ const walk = (it: rs.Iter<[number, number]>) => { }; /** - * The tree's shape, which std's has no counterpart of: every leaf at height `h`, each - * node's keys in order and within its parent's bounds, the leaves linked in order, the - * count right, and every node but the root at least half as full as the fullest, so its - * height is logarithmic. + * The tree's shape, as std's keeps it: every leaf at height `h`, its keys in order, each + * node but the root holding 5 to 11, each child linked to its parent, and the count right. */ function check(m: rs.BTreeMap): void { - type Node = { k: number[]; c?: Node[]; prev?: Node; next?: Node }; + type Node = { k: number[]; e?: Node[]; p?: Node; i: number }; const tree = m as unknown as { t: Node; h: number; n: number }; - const leaves: Node[] = []; - let fewest = Infinity; - let most = 0; + const keys: number[] = []; const fail = (why: string) => { throw new Error(`B-tree of ${tree.n} at height ${tree.h}: ${why}`); }; - const visit = (node: Node, h: number, lo: number, hi: number, root: boolean) => { - const size = h === 0 ? node.k.length : (node.c as Node[]).length; - if (!root) fewest = Math.min(fewest, size); - most = Math.max(most, size); - if (h === 0) { - node.k.forEach((key, i) => { - if (key < lo || key >= hi || (i > 0 && key <= node.k[i - 1])) fail(`leaf key ${key} out of order`); - }); - leaves.push(node); - return; - } - const c = node.c as Node[]; - if (c.length !== node.k.length) fail("a branch's bounds and children differ in number"); - c.forEach((child, i) => { - const from = i === 0 ? lo : node.k[i]; - const to = i + 1 < c.length ? node.k[i + 1] : hi; - if (from < lo || to > hi || from >= to) fail(`branch bound ${from} out of order`); - visit(child, h - 1, from, to, false); + const visit = (node: Node, h: number, root: boolean) => { + if (node.k.length > 11 || (!root && node.k.length < 5)) fail(`a node of ${node.k.length}`); + if ((h === 0) !== (node.e === undefined)) fail("a leaf off the bottom"); + node.k.forEach((key, i) => { + if (node.e !== undefined) visit(node.e[i], h - 1, false); + keys.push(key); + }); + if (node.e === undefined) return; + if (node.e.length !== node.k.length + 1) fail("an internal node's keys and children differ in number"); + node.e.forEach((child, i) => { + if (child.p !== node || child.i !== i) fail(`child ${i} is linked to the wrong parent`); }); + visit(node.e[node.k.length], h - 1, false); }; - visit(tree.t, tree.h, -Infinity, Infinity, true); - leaves.forEach((l, i) => { - if (l.prev !== leaves[i - 1] || l.next !== leaves[i + 1]) fail(`leaf ${i} is linked out of order`); + if (tree.t.p !== undefined) fail("the root has a parent"); + visit(tree.t, tree.h, true); + keys.forEach((key, i) => { + if (i > 0 && key <= keys[i - 1]) fail(`key ${key} out of order`); }); - if (leaves.reduce((n, l) => n + l.k.length, 0) !== tree.n) fail("the count is off"); - if (fewest * 2 < most) fail(`a node of ${fewest} beside one of ${most}`); + if (keys.length !== tree.n) fail("the count is off"); if (tree.n === 0 && tree.h !== 0) fail("an empty tree above its root leaf"); } @@ -85,31 +88,40 @@ for (let step = 0; step < steps; step++) { const op = rand() % 10; const k = rand() % keys; if (op < 5) { - if (phase === 0 || (phase === 1 && k % 2 === 0)) out.push(`insert ${k} ${show(rs.btreeInsert(m, k, step))}`); - else out.push(`remove ${k} ${show(rs.btreeRemove(m, k))}`); + if (phase === 0 || (phase === 1 && k % 2 === 0)) say(`insert ${k} ${show(rs.btreeInsert(m, k, step))}`); + else say(`remove ${k} ${show(rs.btreeRemove(m, k))}`); } else if (op === 5) { - out.push(`get ${k} ${show(rs.btreeGet(m, k))}`); + say(`get ${k} ${show(rs.btreeGet(m, k))}`); } else if (op === 6) { const j = rand() % keys; const [lo, hi] = k < j ? [k, j] : [j, k]; const inclusive = rand() & 1; const r = inclusive ? new rs.RangeInclusive(lo, hi) : { start: lo, end: hi }; - out.push(`range ${lo} ${hi} ${inclusive} ${walk(rs.btreeRange(m, r))}`); + say(`range ${lo} ${hi} ${inclusive} ${walk(rs.btreeRange(m, r))}`); } else if (op === 7) { const last = (rand() & 1) === 1; - out.push(`pop ${last} ${rs.btreePop(m, last)?.join(" ") ?? "-"}`); + say(`pop ${last} ${rs.btreePop(m, last)?.join(" ") ?? "-"}`); } else if (op === 8) { - out.push(`remove ${k} ${show(rs.btreeRemove(m, k))}`); + say(`remove ${k} ${show(rs.btreeRemove(m, k))}`); } else if (step % 97 === 0) { const it = rs.btreeIter(m); const seen = walk(it); - out.push(`iter ${rs.btreeLen(m)} ${it.len()} ${seen}`); + say(`iter ${rs.btreeLen(m)} ${it.len()} ${seen}`); } else if (step % 1009 === 0) { const copy = rs.btreeClone(m); rs.btreeRetain(m, (key) => key % 7 !== k % 7); - out.push(`retain ${rs.btreeLen(m)} ${rs.btreeFirst(m)?.join(" ") ?? "-"} ${rs.btreeFirst(m, true)?.join(" ") ?? "-"}`); + say(`retain ${rs.btreeLen(m)} ${rs.btreeFirst(m)?.join(" ") ?? "-"} ${rs.btreeFirst(m, true)?.join(" ") ?? "-"}`); m = rand() & 1 ? copy : m; - out.push(`kept ${rs.btreeLen(m)}`); + say(`kept ${rs.btreeLen(m)}`); + } else if (step % 1301 === 0) { + const right = rs.btreeSplitOff(m, k); + say(`split ${k} ${rs.btreeLen(m)} ${rs.btreeLen(right)}`); + check(right); + m = rand() & 1 ? right : m; + } else if (step % 2003 === 0) { + const entries = rs.iterToVec(rs.btreeIter(m)).items; + m = rs.btreeCollect(rs.btreeNew(cmp), entries, 8, true, (a, b) => a === b); + say(`collect ${rs.btreeLen(m)}`); } else if (step % 211 === 0) { const j = rand() % keys; const [lo, hi] = k < j ? [k, j] : [j, k]; @@ -122,13 +134,13 @@ for (let step = 0; step < steps; step++) { if (entry === rs.DONE) break; taken.push(entry[0]); } - out.push(`extract ${lo} ${hi} ${rs.btreeLen(m)} ${taken.join(",")}`); + say(`extract ${lo} ${hi} ${rs.btreeLen(m)} ${taken.join(",")}`); check(m); m = rand() & 1 ? copy : m; } if (step % 499 === 0) check(m); } -out.push(`end ${rs.btreeLen(m)} ${rs.iterToVec(rs.btreeKeys(m)).items.join(",")}`); +say(`end ${rs.btreeLen(m)} ${rs.iterToVec(rs.btreeKeys(m)).items.join(",")}`); // Emptied from both ends, down to its root leaf. const drained: number[] = []; while (rs.btreeLen(m) > 0) { @@ -136,5 +148,5 @@ while (rs.btreeLen(m) > 0) { if (drained.length % 37 === 0) check(m); } check(m); -out.push(`drain ${drained.join(",")}`); +say(`drain ${drained.join(",")}`); console.log(out.join("\n")); diff --git a/ts/tests/runtime.rs b/ts/tests/runtime.rs index 22a58ec4b..e95802265 100644 --- a/ts/tests/runtime.rs +++ b/ts/tests/runtime.rs @@ -78,11 +78,44 @@ fn local_lazy() { assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); } -/// The runtime's B-tree runs `btree.ts`'s script of random operations as std's does. +/// The runtime's B-tree runs `btree.ts`'s script of random operations as std's does, calling +/// `Ord::cmp` on the same keys in the same order. #[test] fn btree_script() { + use std::cell::Cell; + use std::cmp::Ordering; use std::collections::BTreeMap; + thread_local! { + /// The calls of `Key::cmp` since the last line, and a hash of their keys. + static CALLS: Cell<(u32, u32)> = const { Cell::new((0, 0)) }; + } + /// A key whose `cmp` counts and hashes each call, as `btree.ts`'s does. + #[derive(Clone, Copy, PartialEq, Eq)] + struct Key(u32); + impl Ord for Key { + fn cmp(&self, other: &Self) -> Ordering { + CALLS.with(|c| { + let (n, h) = c.get(); + let h = h + .wrapping_mul(31) + .wrapping_add(self.0.wrapping_mul(65537)) + .wrapping_add(other.0); + c.set((n + 1, h)); + }); + self.0.cmp(&other.0) + } + } + impl PartialOrd for Key { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + let say = |want: &mut Vec, line: String| { + let (n, h) = CALLS.with(|c| c.replace((0, 0))); + want.push(format!("{line} #{n} {h}")); + }; + let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("btree.ts"); let output = Command::new("node") .arg(script) @@ -101,21 +134,21 @@ fn btree_script() { }; fn walk<'a>( rand: &mut impl FnMut() -> u32, - it: &mut impl DoubleEndedIterator, + it: &mut impl DoubleEndedIterator, ) -> String { let mut seen = Vec::new(); for _ in 0..40 { let item = if rand() & 1 == 1 { it.next() } else { it.next_back() }; let Some((k, _)) = item else { break }; - seen.push(k.to_string()); + seen.push(k.0.to_string()); } seen.join(",") } let show = |v: Option| v.map_or("-".to_string(), |v| v.to_string()); - let entry = |e: Option<(&u32, &u32)>| e.map_or("-".to_string(), |(k, v)| format!("{k} {v}")); + let entry = |e: Option<(&Key, &u32)>| e.map_or("-".to_string(), |(k, v)| format!("{} {v}", k.0)); let mut want = Vec::new(); - let mut m: BTreeMap = BTreeMap::new(); + let mut m: BTreeMap = BTreeMap::new(); let steps = 120_000; for step in 0..steps { let keys = 65_536; @@ -124,70 +157,85 @@ fn btree_script() { let k = rand() % keys; if op < 5 { if phase == 0 || (phase == 1 && k % 2 == 0) { - want.push(format!("insert {k} {}", show(m.insert(k, step)))); + say(&mut want, format!("insert {k} {}", show(m.insert(Key(k), step)))); } else { - want.push(format!("remove {k} {}", show(m.remove(&k)))); + say(&mut want, format!("remove {k} {}", show(m.remove(&Key(k))))); } } else if op == 5 { - want.push(format!("get {k} {}", show(m.get(&k).copied()))); + say(&mut want, format!("get {k} {}", show(m.get(&Key(k)).copied()))); } else if op == 6 { let j = rand() % keys; let (lo, hi) = if k < j { (k, j) } else { (j, k) }; let inclusive = rand() & 1; let seen = match inclusive { - 1 => walk(&mut rand, &mut m.range(lo..=hi)), - _ => walk(&mut rand, &mut m.range(lo..hi)), + 1 => walk(&mut rand, &mut m.range(Key(lo)..=Key(hi))), + _ => walk(&mut rand, &mut m.range(Key(lo)..Key(hi))), }; - want.push(format!("range {lo} {hi} {inclusive} {seen}")); + say(&mut want, format!("range {lo} {hi} {inclusive} {seen}")); } else if op == 7 { let last = rand() & 1 == 1; let popped = if last { m.pop_last() } else { m.pop_first() }; - want.push(format!( - "pop {last} {}", - popped.map_or("-".to_string(), |(k, v)| format!("{k} {v}")) - )); + say( + &mut want, + format!( + "pop {last} {}", + popped.map_or("-".to_string(), |(k, v)| format!("{} {v}", k.0)) + ), + ); } else if op == 8 { - want.push(format!("remove {k} {}", show(m.remove(&k)))); + say(&mut want, format!("remove {k} {}", show(m.remove(&Key(k))))); } else if step % 97 == 0 { let mut it = m.iter(); let seen = walk(&mut rand, &mut it); - want.push(format!("iter {} {} {seen}", m.len(), it.len())); + say(&mut want, format!("iter {} {} {seen}", m.len(), it.len())); } else if step % 1009 == 0 { let copy = m.clone(); - m.retain(|key, _| key % 7 != k % 7); - want.push(format!( - "retain {} {} {}", - m.len(), - entry(m.first_key_value()), - entry(m.last_key_value()) - )); + m.retain(|key, _| key.0 % 7 != k % 7); + say( + &mut want, + format!( + "retain {} {} {}", + m.len(), + entry(m.first_key_value()), + entry(m.last_key_value()) + ), + ); if rand() & 1 == 1 { m = copy; } - want.push(format!("kept {}", m.len())); + say(&mut want, format!("kept {}", m.len())); + } else if step % 1301 == 0 { + let right = m.split_off(&Key(k)); + say(&mut want, format!("split {k} {} {}", m.len(), right.len())); + if rand() & 1 == 1 { + m = right; + } + } else if step % 2003 == 0 { + m = m.iter().map(|(k, v)| (*k, *v)).collect(); + say(&mut want, format!("collect {}", m.len())); } else if step % 211 == 0 { let j = rand() % keys; let (lo, hi) = if k < j { (k, j) } else { (j, k) }; let limit = rand() % 400; let copy = m.clone(); - let taken: Vec = (m.extract_if(lo..hi, |key, value| (*key ^ *value) % 3 != 0)) + let taken: Vec = (m.extract_if(Key(lo)..Key(hi), |key, value| (key.0 ^ *value) % 3 != 0)) .take(limit as usize) - .map(|(k, _)| k.to_string()) + .map(|(k, _)| k.0.to_string()) .collect(); - want.push(format!("extract {lo} {hi} {} {}", m.len(), taken.join(","))); + say(&mut want, format!("extract {lo} {hi} {} {}", m.len(), taken.join(","))); if rand() & 1 == 1 { m = copy; } } } - let keys: Vec = m.keys().map(u32::to_string).collect(); - want.push(format!("end {} {}", m.len(), keys.join(","))); + let keys: Vec = m.keys().map(|k| k.0.to_string()).collect(); + say(&mut want, format!("end {} {}", m.len(), keys.join(","))); let mut drained = Vec::new(); while !m.is_empty() { let popped = if rand() & 1 == 1 { m.pop_last() } else { m.pop_first() }; - drained.push(popped.unwrap().0.to_string()); + drained.push(popped.unwrap().0.0.to_string()); } - want.push(format!("drain {}", drained.join(","))); + say(&mut want, format!("drain {}", drained.join(","))); for (got, want) in got.lines().zip(&want) { assert_eq!(got, want); }