From 643637ed57190a6285233d21df8663f0413f2ba9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:41:48 -0800 Subject: [PATCH 1/5] chore(quest): claim quest/m3/ts-btree-extract-if.md Quest-Claim: 4a5195755169d3a0 From e4454b85d008e78db3f32b4c0f3d8af87f063f50 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:58:07 -0800 Subject: [PATCH 2/5] feat(ts): translate a B-tree's extract_if, with its Debug Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-btree-extract-if.md | 15 -- ts/README.md | 8 +- .../expected/collections.Logged.ts | 52 +++++ .../expected/collections.Window.ts | 26 +++ .../lang/collections/expected/collections.ts | 191 +++++++++++++++++- .../lang/collections/expected/harness.ts | 6 + ts/fixtures/lang/collections/expected/lib.ts | 33 ++- .../lang/collections/src/collections.rs | 163 +++++++++++++++ ts/fixtures/lang/collections/src/lib.rs | 1 + ts/runtime/rt/collections.ts | 151 +++++++++++++- ts/src/lower/collections.rs | 35 +++- ts/src/lower/fmt.rs | 10 + ts/src/lower/iter.rs | 2 +- ts/tests/btree.ts | 18 +- ts/tests/runtime.rs | 13 ++ 16 files changed, 692 insertions(+), 33 deletions(-) delete mode 100644 quest/m3/ts-btree-extract-if.md create mode 100644 ts/fixtures/lang/collections/expected/collections.Logged.ts create mode 100644 ts/fixtures/lang/collections/expected/collections.Window.ts diff --git a/quest/m3/README.md b/quest/m3/README.md index 4ba136599..f6d0fd118 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -26,7 +26,6 @@ generic std gap is planned here even when moq never calls it. - [Waiting: a slice object for a mutable part of other values](/quest/m3/ts-slice-object.md) - waits on a moq profile showing the `Proxy` view's traps matter, then decides on an `rt.ByteSlice`-like form - [Port the workflow guard to kixelated/quest](/quest/m3/quest-workflow-guard.md) - quest's environment guard parses YAML as this repository's does - [Debug of a str's searches](/quest/m3/ts-iter-source-debug.md) - `Debug` of `str`'s `Split` and its forms, `Matches`, `Lines`, and `SplitWhitespace` writes their searcher's state as std's does -- [A B-tree's extract_if](/quest/m3/ts-btree-extract-if.md) - `BTreeMap::extract_if` and `BTreeSet::extract_if` translate, with their `Debug` - [A vector keeps the capacity its pushes grew as it shrinks](/quest/m3/ts-vec-clear-capacity.md) - every step that drops a pushed `Vec`'s length keeps its capacity, as std guarantees - [A vector's exact capacity as it shrinks](/quest/m3/ts-vec-exact-capacity.md) - a deque made from a `Vec` splits its slices where std's does - [A string's exact capacity](/quest/m3/ts-string-capacity.md) - a `String` holds std's capacity, so a crate making a `Bytes` of one reads its capacity diff --git a/quest/m3/ts-btree-extract-if.md b/quest/m3/ts-btree-extract-if.md deleted file mode 100644 index d4ec21745..000000000 --- a/quest/m3/ts-btree-extract-if.md +++ /dev/null @@ -1,15 +0,0 @@ -# [S] A B-tree's extract_if - -## Goal - -[rust2-ts](/ts/README.md) translates `BTreeMap::extract_if` and `BTreeSet::extract_if`, -which it refuses now: each entry in the range its closure returns true for, taken out as it -is read in key order, the rest kept once it drops, and `Debug` of their `ExtractIf`, which -writes the entry it reads next as `peek`. Proven by differential tests against native Rust. - -## Plan - -The runtime's B-tree keeps its keys in linked leaves, which a removal while a `BTreeIter` -reads them may rebalance; the iterator has to survive that as std's cursor does. A map's -closure takes `&mut V`, lent as a place where JS copies the value, as `retain`'s is. No -consumer calls them yet; they are planned here as part of std. diff --git a/ts/README.md b/ts/README.md index 9bd156190..340b58e16 100644 --- a/ts/README.md +++ b/ts/README.md @@ -114,7 +114,7 @@ crate translation reaches is refused, and so are two versions of one crate it re | an iterator | `rt.Iter`, whose adaptors are runtime functions: `rt.iterMap(rt.iterSlice(v), f)` | | `a..b`, `a..=b` | `rt.Range`, `rt.RangeInclusive`, which are iterators too | | `a..`, `..b`, `..=b`, `Bound` | `{ start }`, `{ end }`, `{ end, inclusive: true }`; `rt.RangeBound`, a union tagged by variant | -| a range a drain or a B-tree's `range` takes | itself, or of a type other than std's ranges and a pair of `Bound`s, `rt.boundsOf` of it and its `start_bound` and `end_bound`, which the runtime calls where std's does; one that drops, the parameter of the call `rt.dropAfter` makes, which drops it once the call returns, as std drops it before it does anything JS can see | +| a range a drain or a B-tree's `range` or `extract_if` takes | itself, or of a type other than std's ranges and a pair of `Bound`s, `rt.boundsOf` of it and its `start_bound` and `end_bound`, which the runtime calls where std's does; one that drops, the parameter of the call `rt.dropAfter` makes, which drops it once the call returns, as std drops it before it does anything JS can see, or of an `extract_if`, which holds it, the parameter of a call that hands its iterator what drops it after its closure | | `HashMap`, `HashSet`, `BTreeMap`, `BTreeSet`, `VecDeque`, `LinkedList`, `BinaryHeap` | `rt.HashMap` and so on, each a plain object whose functions are the runtime's: `rt.hashGet(map, key)` | | `PeekMut` | `rt.PeekMut`, the `rt.Place` of a heap's greatest element, which sifts it down as it drops once `DerefMut` lent it | | `Ordering`, `DefaultHasher`, `RandomState`, `BuildHasherDefault` | `rt.Ordering`, which is `-1`, `0`, or `1`; `rt.Hasher`; `rt.RandomState`, the keys of the `DefaultHasher` it builds, never changed once made; `undefined`, as `PhantomData` is, whose `build_hasher` is `H::default()` | @@ -309,8 +309,8 @@ iterators, a `CharIndices`, a `String`'s `Drain`, and a slice's `Windows`, chunk splits, each writing what is left of its slice as std's does, emptied where std's `nth` past the end empties a `Windows`; a hash set's operations write the list a clone yields, and a `BTreeSet`'s the way std picked to make each, by the iterators it reads, the set it looks -keys up in, and the key it peeked or holds back. A list's `ExtractIf` writes the item it -reads next, and a hash map's and set's nothing. A slice's `chunks_mut` and its forms are +keys up in, and the key it peeked or holds back. A list's and a B-tree's `ExtractIf` write +the item or entry it reads next, until it stops, and a hash map's and set's nothing. A slice's `chunks_mut` and its forms are refused, as std writes their slice's address, and so are a `str`'s searches, `Split` and its forms, `Matches`, `MatchIndices`, `Lines`, and `SplitWhitespace`, as std writes its searcher's state in UTF-8 bytes. `Debug` of `Bound`, `Ordering`, `Reverse`, `AssertUnwindSafe`, and `ControlFlow` is @@ -771,7 +771,7 @@ a translation replaces it. A bound type keys a collection, alone or in a tuple, `Option`, or `Arc`, by the exports of its Rust impls: `hash$Hash(self, state: rt.Hasher): undefined` and `eq$cmp_PartialEq` for a `HashMap` or `HashSet`, `cmp$cmp_Ord` for a `BTreeMap` or `BTreeSet`, and -`partialCmp$cmp_PartialOrd` too for a B-tree's `range`. A derived impl's are the same exports. +`partialCmp$cmp_PartialOrd` too for a B-tree's `range` and `extract_if`. A derived impl's are the same exports. Each must agree with the Rust impl: `hash` writes what it writes, through `rt.hash8` to `rt.hash128`, `rt.hashI8` to `rt.hashI128`, `rt.hashUsize`, `rt.hashIsize`, `rt.hashStr`, and `rt.hashBytes`, so values `eq` finds equal hash alike, and diff --git a/ts/fixtures/lang/collections/expected/collections.Logged.ts b/ts/fixtures/lang/collections/expected/collections.Logged.ts new file mode 100644 index 000000000..ae95ab861 --- /dev/null +++ b/ts/fixtures/lang/collections/expected/collections.Logged.ts @@ -0,0 +1,52 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** + * A B-tree key ordered by its number's low byte, whose `PartialOrd`'s `le` and `lt`, which + * std checks a range's end by, see only that byte halved. Each of those and its drop add its + * whole number to the log. + */ +export class Logged { + _0: rt.u16; + _1: rt.RefCell; + + constructor(_0: rt.u16, _1: rt.RefCell) { + this._0 = _0; + this._1 = _1; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function drop$ops_Drop(self: Logged): void { + const _5: rt.RefCell = rt.refCellBorrowMut(self._1); + _5.value.push(self._0); + rt.refMutRelease(_5); +} + +export function eq$cmp_PartialEq(self: Logged, other: Logged): boolean { + return (self._0 & 255) === (other._0 & 255); +} + +export function le$cmp_PartialOrd(self: Logged, other: Logged): boolean { + const _6: rt.RefCell = rt.refCellBorrowMut(self._1); + _6.value.push(16384 | self._0); + rt.refMutRelease(_6); + return rt.u16Div(self._0 & 255, 2) <= rt.u16Div(other._0 & 255, 2); +} + +export function lt$cmp_PartialOrd(self: Logged, other: Logged): boolean { + const _6: rt.RefCell = rt.refCellBorrowMut(self._1); + _6.value.push(8192 | self._0); + rt.refMutRelease(_6); + return rt.u16Div(self._0 & 255, 2) < rt.u16Div(other._0 & 255, 2); +} + +export function cmp$cmp_Ord(self: Logged, other: Logged): rt.Ordering { + return rt.cmp(self._0 & 255, other._0 & 255); +} + +//# sourceMappingURL=collections.Logged.ts.map diff --git a/ts/fixtures/lang/collections/expected/collections.Window.ts b/ts/fixtures/lang/collections/expected/collections.Window.ts new file mode 100644 index 000000000..72a8dc0bc --- /dev/null +++ b/ts/fixtures/lang/collections/expected/collections.Window.ts @@ -0,0 +1,26 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** A range of its own, which logs each read of either bound. */ +export interface Window { + lo: rt.u16; + hi: rt.u16; + log: rt.RefCell; +} + +export function startBound$ops_RangeBounds_u16(self: Window): rt.RangeBound { + const _5: rt.RefCell = rt.refCellBorrowMut(self.log); + _5.value.push(40960); + rt.refMutRelease(_5); + return { tag: "Included", _0: self.lo } as rt.RangeBound; +} + +export function endBound$ops_RangeBounds_u16(self: Window): rt.RangeBound { + const _5: rt.RefCell = rt.refCellBorrowMut(self.log); + _5.value.push(45056); + rt.refMutRelease(_5); + return { tag: "Excluded", _0: self.hi } as rt.RangeBound; +} + +//# sourceMappingURL=collections.Window.ts.map diff --git a/ts/fixtures/lang/collections/expected/collections.ts b/ts/fixtures/lang/collections/expected/collections.ts index 8f771f53c..7a59a5127 100644 --- a/ts/fixtures/lang/collections/expected/collections.ts +++ b/ts/fixtures/lang/collections/expected/collections.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { copy$Option_collections_Tally, copy$array_collections_Quirky, copy$array_tuple2_u8_u16, copy$array_tuple2_u8_u8, copy$array_u8, copy$collections_Bumped, copy$collections_Quirky, copy$collections_Tally, copy$collections_Wrap_u8, copy$tuple2_u8_collections_Tally, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$tuple2_sync_Arc_collections_Picky_u8 } from "./lib.ts"; +import { copy$Option_collections_Tally, copy$array_collections_Quirky, copy$array_tuple2_u8_u16, copy$array_tuple2_u8_u8, copy$array_u8, copy$collections_Bumped, copy$collections_Quirky, copy$collections_Tally, copy$collections_Wrap_u8, copy$tuple2_u8_collections_Tally, copy$tuple2_u8_u16, copy$tuple2_u8_u8, drop$Option_tuple2_collections_Logged_collections_Logged, drop$ops_Range_collections_Logged, drop$tuple2_collections_Logged_collections_Logged, drop$tuple2_ops_Bound_collections_Logged_ops_Bound_collections_Logged, drop$tuple2_sync_Arc_collections_Picky_u8 } from "./lib.ts"; import type * as Boxed from "./collections.Boxed.ts"; import * as Bumped from "./collections.Bumped.ts"; import * as ByRef from "./collections.ByRef.ts"; @@ -12,6 +12,7 @@ import * as Dir from "./collections.Dir.ts"; import * as Fussy from "./collections.Fussy.ts"; import type * as Holder from "./collections.Holder.ts"; import * as Left from "./collections.Left.ts"; +import * as Logged from "./collections.Logged.ts"; import * as Mixed from "./collections.Mixed.ts"; import * as Nibble from "./collections.Nibble.ts"; import * as Odd from "./collections.Odd.ts"; @@ -21,10 +22,12 @@ import * as Quirky from "./collections.Quirky.ts"; import * as Rank from "./collections.Rank.ts"; import * as Right from "./collections.Right.ts"; import * as Tally from "./collections.Tally.ts"; +import * as Window from "./collections.Window.ts"; import * as Wrap from "./collections.Wrap.ts"; import { thenSome$0d5am30 } from "./core/bool.ts"; import { unwrapOr$1j4op5y as option_unwrapOr$1j4op5y, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr, map$0ltgf8p as option_map$0ltgf8p, map$1ajptuf as option_map$1ajptuf, map$0i8kclf as option_map$0i8kclf, map$09ua5et as option_map$09ua5et, map$0oc32h1 as option_map$0oc32h1, map$05nbzug as option_map$05nbzug, mapOr$1e4a1e0 as option_mapOr$1e4a1e0, mapOr$0q0ymkh as option_mapOr$0q0ymkh, mapOr$1ht002r as option_mapOr$1ht002r, mapOr$1xztl8h as option_mapOr$1xztl8h, mapOr$1w1gbwl as option_mapOr$1w1gbwl, mapOr$0bnr5nu as option_mapOr$0bnr5nu, mapOr$1nlkf9b as option_mapOr$1nlkf9b, mapOr$099335t as option_mapOr$099335t, mapOr$0z4mj8g as option_mapOr$0z4mj8g, mapOr$1surhce as option_mapOr$1surhce, mapOr$1t0muvr as option_mapOr$1t0muvr, mapOr$02k4ito as option_mapOr$02k4ito, mapOr$1to2ul4 as option_mapOr$1to2ul4, mapOr$0mmpeqx as option_mapOr$0mmpeqx, clone$14nprly as option_clone$14nprly, flatten$1nwzx42 as option_flatten$1nwzx42 } from "./core/option.ts"; import { eq$1dt73ez as result_eq$1dt73ez, eq$163neqy as result_eq$163neqy } from "./core/result.ts"; +import { put } from "./harness.ts"; /** A 64-bit key: small, or past 2^53, where it is no longer a JS number. */ export function wide(b: rt.u8): rt.u64 { @@ -5793,4 +5796,190 @@ export function extracts(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void } } +/** + * The range form `form` picks of `lo` and `hi`, which may be backwards or empty and + * excluded at both ends, neither of which `extract_if` panics on. + */ +export function form(form: rt.u8, lo: rt.u16, hi: rt.u16): [rt.RangeBound, rt.RangeBound] { + const _4: rt.u8 = rt.u8Rem(form, 8); + if (_4 === 0) { + return [{ tag: "Unbounded" } as rt.RangeBound, { tag: "Unbounded" } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 1) { + return [{ tag: "Included", _0: lo } as rt.RangeBound, { tag: "Excluded", _0: hi } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 2) { + return [{ tag: "Included", _0: lo } as rt.RangeBound, { tag: "Included", _0: hi } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 3) { + return [{ tag: "Included", _0: lo } as rt.RangeBound, { tag: "Unbounded" } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 4) { + return [{ tag: "Unbounded" } as rt.RangeBound, { tag: "Excluded", _0: hi } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 5) { + return [{ tag: "Unbounded" } as rt.RangeBound, { tag: "Included", _0: hi } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + if (_4 === 6) { + return [{ tag: "Excluded", _0: lo } as rt.RangeBound, { tag: "Excluded", _0: hi } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; + } + return [{ tag: "Excluded", _0: lo } as rt.RangeBound, { tag: "Included", _0: lo } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]; +} + +/** + * `extract_if` of B-tree maps and sets, large enough to merge and rebalance leaves as it + * takes entries out: each form of range, with values changed in place as each is read, + * read in full or part way, with what stays, the hint, and `Debug` of the iterator as it + * reads, which peeks past the range's end until it stops there. Then keys and values that + * log their drops and the `PartialOrd` tests std ends the range by, a closure and range + * that drop with the iterator, and a range of its own, whose start std reads only of a + * map with a root. + */ +export function btreeExtracts(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + const at: (a0: rt.usize) => rt.u8 = (i: rt.usize): rt.u8 => { + return option_unwrapOr$0r2bmpr(rt.byteSliceGet(data, i), 0); + }; + const n: rt.u16 = rt.u16Mul(at(0) as rt.u16, 2); + const _13: [rt.u16, rt.u16] = [rt.u16Mul(at(1) as rt.u16, 2), rt.u16Mul(at(2) as rt.u16, 2)]; + const lo: rt.u16 = _13[0]; + const hi: rt.u16 = _13[1]; + const _28: [rt.u16, rt.u8] = [rt.u8Add(rt.u8Rem(at(3), 5), 1) as rt.u16, at(4)]; + const every: rt.u16 = _28[0]; + const limit: rt.u8 = _28[1]; + const range: [rt.RangeBound, rt.RangeBound] = form(at(5), lo, hi); + const _44: rt.u8 = rt.u8Rem(kind, 3); + if (_44 === 0) { + const _47: rt.Iter<[rt.u16, rt.u16]> = rt.iterMap(new rt.Range(0, n), (k7: rt.u16): [rt.u16, rt.u16] => { + return [k7, k7 ^ 256] as [rt.u16, rt.u16]; + }); + const map: rt.BTreeMap = rt.btreeExtend(rt.btreeNew(rt.cmp) as rt.BTreeMap, _47, true, undefined, undefined, undefined, 4, true); + const it: rt.Iter<[rt.u16, rt.u16]> = rt.btreeExtractIfMut(map, range, (k8: rt.u16, v2: rt.Place): boolean => { + v2.v = rt.u16Add(v2.v, 1); + return rt.u16Rem(k8 ^ limit as rt.u16, every) === 0; + }); + const _62: [rt.usize, rt.usize | undefined] = it.sizeHint(); + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtBTreeExtractIf(it, f, rt.u16Debug, rt.u16Debug), (f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.usizeDebug(_62[0], f), (f: rt.Formatter) => rt.fmtOption(_62[1], f, rt.usizeDebug)], f)]; + put(out, rt.fmtFormat([args2[0], " ", args2[1]])); + const _72: rt.Iter<[rt.u16, rt.u16]> = it; + const _74: rt.usize = limit as rt.usize; + const taken: [rt.u16, rt.u16][] = rt.iterToArray(rt.iterTake(rt.iterBorrow(_72), _74), "next", 4); + const _83: [rt.usize, rt.usize | undefined] = it.sizeHint(); + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtList(taken, f, (value: [rt.u16, rt.u16], f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.u16Debug(value[0], f), (f: rt.Formatter) => rt.u16Debug(value[1], f)], f)), (f: rt.Formatter) => rt.fmtBTreeExtractIf(it, f, rt.u16Debug, rt.u16Debug), (f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.usizeDebug(_83[0], f), (f: rt.Formatter) => rt.fmtOption(_83[1], f, rt.usizeDebug)], f)]; + put(out, rt.fmtFormat([args4[0], " ", args4[1], " ", args4[2]])); + if (rt.u8Rem(limit, 2) === 0) { + const _104: [rt.u16, rt.u16] | undefined = rt.iterToOption(it.next()); + const args6: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtOption(_104, f, (value: [rt.u16, rt.u16], f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.u16Debug(value[0], f), (f: rt.Formatter) => rt.u16Debug(value[1], f)], f)), (f: rt.Formatter) => rt.fmtBTreeExtractIf(it, f, rt.u16Debug, rt.u16Debug)]; + put(out, rt.fmtFormat([args6[0], " ", rt.fmtSpec({ alternate: true }, args6[1])])); + } + const _122: rt.usize = rt.btreeLen(map); + const _125: [rt.u16, rt.u16][] = rt.iterToArray(rt.iterStepBy(rt.btreeIter(map), 7), "next", 8); + const args8: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(_122, f), (f: rt.Formatter) => rt.fmtList(_125, f, (value: [rt.u16, rt.u16], f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.u16Debug(value[0], f), (f: rt.Formatter) => rt.u16Debug(value[1], f)], f))]; + put(out, rt.fmtFormat([args8[0], " ", args8[1]])); + const taken2: [rt.u16, rt.u16][] = rt.iterToArray(rt.btreeExtractIfMut(map, undefined, (_217: rt.u16, v3: rt.Place): boolean => { + return rt.u16Rem(v3.v, 3) === 0; + }), "next", 4); + const args10: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtList(taken2, f, (value: [rt.u16, rt.u16], f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.u16Debug(value[0], f), (f: rt.Formatter) => rt.u16Debug(value[1], f)], f)), (f: rt.Formatter) => rt.fmtMap(rt.btreeIter(map), f, rt.u16Debug, rt.u16Debug)]; + put(out, rt.fmtFormat([args10[0], " ", args10[1]])); + } else if (_44 === 1) { + const _152: rt.Iter = rt.iterMap(new rt.Range(0, n), (k5: rt.u16): rt.u16 => { + return rt.u16Rem(rt.u16Mul(k5, 3), rt.u16Add(n, 1)); + }); + const set: rt.BTreeSet = rt.btreeExtendKeys(rt.btreeNew(rt.cmp) as rt.BTreeSet, _152, true, undefined, undefined, 2, true); + const it2: rt.Iter = rt.btreeExtractKeysIf(set, range, (k6: rt.u16): boolean => { + return rt.u16Rem(k6, every) !== 0; + }); + const args12: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtBTreeExtractIf(it2, f, rt.u16Debug)]; + put(out, rt.fmtFormat([args12[0]])); + const _174: rt.Iter = it2; + const _176: rt.usize = limit as rt.usize; + const taken3: rt.u16[] = rt.iterToArray(rt.iterTake(rt.iterBorrow(_174), _176), "next", 2); + const _185: [rt.usize, rt.usize | undefined] = it2.sizeHint(); + const args14: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtList(taken3, f, rt.u16Debug), (f: rt.Formatter) => rt.fmtBTreeExtractIf(it2, f, rt.u16Debug), (f: rt.Formatter) => rt.fmtTuple([(f: rt.Formatter) => rt.usizeDebug(_185[0], f), (f: rt.Formatter) => rt.fmtOption(_185[1], f, rt.usizeDebug)], f)]; + put(out, rt.fmtFormat([args14[0], " ", args14[1], " ", args14[2]])); + const args16: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtSet(rt.btreeKeys(set), f, rt.u16Debug)]; + put(out, rt.fmtFormat([args16[0]])); + const part3 = rt.btreeExtractKeysIf(rt.btreeNew(rt.cmp) as rt.BTreeSet, range, (_212: rt.u16): boolean => { + return true; + }); + const args18: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtBTreeExtractIf(part3, f, rt.u16Debug)]; + put(out, rt.fmtFormat([args18[0]])); + rt.btreeClear(set); + const part4 = rt.btreeExtractKeysIf(set, undefined, (_216: rt.u16): boolean => { + return true; + }); + const args20: rt.Fmt[] = [(f: rt.Formatter) => rt.fmtBTreeExtractIf(part4, f, rt.u16Debug)]; + put(out, rt.fmtFormat([args20[0]])); + } else { + const _241: rt.u16[] = []; + const log: rt.RefCell = new rt.RefCell(_241); + const _243: rt.Iter<[Logged.Logged, Logged.Logged]> = rt.iterMap(new rt.Range(0, rt.u16Div(n, 2)), (k: rt.u16): [Logged.Logged, Logged.Logged] => { + return [new Logged.Logged(k, log), new Logged.Logged(4096 | k, log)] as [Logged.Logged, Logged.Logged]; + }); + const map2: rt.BTreeMap = rt.btreeCollect(rt.btreeNew(Logged.cmp$cmp_Ord) as rt.BTreeMap, rt.iterToArray(_243, undefined, 16, true), 16, true, Logged.eq$cmp_PartialEq, rt.drop, rt.drop); + const guard: Logged.Logged = new Logged.Logged(2048, log); + const _253: [Logged.Logged, Logged.Logged] = [new Logged.Logged(2304 | rt.u16Div(lo, 2), log), new Logged.Logged(2304 | rt.u16Div(hi, 2), log)]; + const _266: [rt.RangeBound, rt.RangeBound] = [{ tag: "Excluded", _0: _253[0] } as rt.RangeBound, { tag: "Included", _0: _253[1] } as rt.RangeBound]; + const captured = guard; + const captured2 = every; + const _269: (a0: Logged.Logged, a1: Logged.Logged) => boolean = rt.closure((k2: Logged.Logged, v: Logged.Logged): boolean => { + v._0 = v._0 ^ 512; + return rt.u16Rem(k2._0, captured2) === 0; + }, (): void => { + captured[Symbol.dispose](); + }); + const it3: rt.Iter<[Logged.Logged, Logged.Logged]> = ((range2) => rt.btreeExtractIf(map2, range2, _269, Logged.le$cmp_PartialOrd, Logged.lt$cmp_PartialOrd, () => drop$tuple2_ops_Bound_collections_Logged_ops_Bound_collections_Logged(range2)))(_266); + const _272: rt.Iter<[Logged.Logged, Logged.Logged]> = it3; + const _274: rt.usize = rt.u8Rem(limit, 8) as rt.usize; + const taken4: [Logged.Logged, Logged.Logged][] = rt.iterToArray(rt.iterTake(rt.iterBorrow(_272), _274, drop$tuple2_collections_Logged_collections_Logged), "next", 16); + rt.drop(it3); + rt.iterExtendBytes(out, rt.iterFlatMap(rt.iterSlice(taken4), (_2: [Logged.Logged, Logged.Logged]): Uint8Array => { + return Uint8Array.of(_2[0]._0 & 0xff, _2[1]._0 & 0xff); + }, (value: Uint8Array) => rt.iterSlice(value), true, 2), undefined, true); + rt.dropAll(taken4, drop$tuple2_collections_Logged_collections_Logged); + const _287: rt.Range = new rt.Range(new Logged.Logged(2560 | rt.u16Div(lo, 4), log), new Logged.Logged(2560 | rt.u16Div(hi, 4), log)); + const it4: rt.Iter<[Logged.Logged, Logged.Logged]> = ((range3) => rt.btreeExtractIf(map2, range3, (_23: Logged.Logged, _38: Logged.Logged): boolean => { + return false; + }, Logged.le$cmp_PartialOrd, Logged.lt$cmp_PartialOrd, () => drop$ops_Range_collections_Logged(range3)))(_287); + const _302: [Logged.Logged, Logged.Logged] | undefined = rt.iterToOption(it4.next()); + rt.byteVecPush(out, _302 === undefined ? 1 : 0); + drop$Option_tuple2_collections_Logged_collections_Logged(_302); + rt.drop(it4); + rt.iterForEach(rt.btreeIter(map2, rt.drop, rt.drop), (a0: [Logged.Logged, Logged.Logged]): undefined => { + drop$tuple2_collections_Logged_collections_Logged(a0); + }, true); + const bytes: rt.BTreeMap = rt.btreeNew(rt.cmp) as rt.BTreeMap; + const window: (a0: rt.u16, a1: rt.u16) => Window.Window = (lo2: rt.u16, hi2: rt.u16): Window.Window => { + return { lo: lo2, hi: hi2, log }; + }; + const _315: Window.Window = window(0, 9); + rt.iterForEach(rt.btreeExtractIfMut(bytes, rt.boundsOf(_315, Window.startBound$ops_RangeBounds_u16, Window.endBound$ops_RangeBounds_u16), (_26: rt.u16, _310: rt.Place): boolean => { + return true; + }), (_a02: [rt.u16, rt.u8]): undefined => { + undefined; + }); + rt.btreeInsert(bytes, 1, 1); + rt.btreeRemove(bytes, 1); + const _326: Window.Window = window(0, 9); + rt.iterForEach(rt.btreeExtractIfMut(bytes, rt.boundsOf(_326, Window.startBound$ops_RangeBounds_u16, Window.endBound$ops_RangeBounds_u16), (_27: rt.u16, _311: rt.Place): boolean => { + return true; + }), (_a03: [rt.u16, rt.u8]): undefined => { + undefined; + }); + const _331: rt.Iter<[rt.u16, rt.u8]> = rt.iterMap(new rt.Range(0, n), (k3: rt.u16): [rt.u16, rt.u8] => { + return [k3, k3 & 0xff] as [rt.u16, rt.u8]; + }); + rt.btreeExtend(bytes, _331); + const _336: Window.Window = window(rt.u16Div(lo, 2), rt.u16Div(hi, 2)); + const _343: (a0: rt.u16, a1: rt.Place) => boolean = (k4: rt.u16, _314: rt.Place): boolean => { + return rt.u16Rem(k4, every) === 0; + }; + rt.iterExtendBytes(out, rt.iterMap(rt.iterSlice(rt.iterToArray(rt.btreeExtractIfMut(bytes, rt.boundsOf(_336, Window.startBound$ops_RangeBounds_u16, Window.endBound$ops_RangeBounds_u16), _343), "next", 4)), (_210: [rt.u16, rt.u8]): rt.u8 => { + return _210[1]; + }), undefined, true); + rt.iterExtendBytes(out, rt.iterFlatMap(rt.iterSlice(rt.refCellReplace(log, [])), (e: rt.u16): Uint8Array => { + return rt.u16ToBeBytes(e); + }, (value: Uint8Array) => rt.iterSlice(value), true, 2), undefined, true); + } +} + //# sourceMappingURL=collections.ts.map diff --git a/ts/fixtures/lang/collections/expected/harness.ts b/ts/fixtures/lang/collections/expected/harness.ts index 58ec1cf62..3b7bbcfb0 100644 --- a/ts/fixtures/lang/collections/expected/harness.ts +++ b/ts/fixtures/lang/collections/expected/harness.ts @@ -37,4 +37,10 @@ export function op(name: string): rt.u16 { } } +/** Write `s` and the byte no UTF-8 holds, which ends it. */ +export function put(out: rt.ByteVec, s: string): void { + rt.byteVecExtendFromSlice(out, rt.stringAsBytes(s)); + rt.byteVecPush(out, 255); +} + //# sourceMappingURL=harness.ts.map diff --git a/ts/fixtures/lang/collections/expected/lib.ts b/ts/fixtures/lang/collections/expected/lib.ts index 9b638ec66..0ee7c1fb0 100644 --- a/ts/fixtures/lang/collections/expected/lib.ts +++ b/ts/fixtures/lang/collections/expected/lib.ts @@ -1,8 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { hashMaps, sets, btreeMaps, deques, dequeRanges, vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders, extracts } from "./collections.ts"; +import { hashMaps, sets, btreeMaps, deques, dequeRanges, vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders, extracts, btreeExtracts } 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"; import type * as Quirky from "./collections.Quirky.ts"; import type * as Tally from "./collections.Tally.ts"; @@ -56,6 +57,34 @@ export function copy$tuple2_u8_u8(v: [rt.u8, rt.u8]): [rt.u8, rt.u8] { return [v[0], v[1]]; } +export function drop$Option_tuple2_collections_Logged_collections_Logged(_1: [Logged.Logged, Logged.Logged] | undefined): void { + if (_1 !== undefined) { + drop$tuple2_collections_Logged_collections_Logged(_1); + } +} + +export function drop$ops_Bound_collections_Logged(_1: rt.RangeBound): void { + if (_1.tag === "Included") { + (_1 as { tag: "Included"; _0: Logged.Logged })._0[Symbol.dispose](); + } else if (_1.tag === "Excluded") { + (_1 as { tag: "Excluded"; _0: Logged.Logged })._0[Symbol.dispose](); + } +} + +export function drop$ops_Range_collections_Logged(value: rt.Range): void { + rt.dropAll([value.start, value.end]); +} + +export function drop$tuple2_collections_Logged_collections_Logged(_1: [Logged.Logged, Logged.Logged]): void { + _1[0][Symbol.dispose](); + _1[1][Symbol.dispose](); +} + +export function drop$tuple2_ops_Bound_collections_Logged_ops_Bound_collections_Logged(_1: [rt.RangeBound, rt.RangeBound]): void { + drop$ops_Bound_collections_Logged(_1[0]); + drop$ops_Bound_collections_Logged(_1[1]); +} + export function drop$tuple2_sync_Arc_collections_Picky_u8(_1: [rt.Arc, rt.u8]): void { _1[0][Symbol.dispose](); } @@ -71,7 +100,7 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) vecs(data2, out2); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::stores", (_212: rt.u8, data3: rt.ByteSlice, out3: rt.ByteVec): undefined => { stores(data3, out3); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::optionals", optionals as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::set_ops", setOps as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::unlike_eqs", unlikeEqs as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::own_nes", ownNes as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::cell_eqs", cellEqs as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::unlike_compares", unlikeCompares as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::ref_orders", refOrders as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::deque_ranges", dequeRanges as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::extracts", extracts as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined]]; + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::optionals", optionals as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::set_ops", setOps as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::unlike_eqs", unlikeEqs as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::own_nes", ownNes as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::cell_eqs", cellEqs as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::unlike_compares", unlikeCompares as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::ref_orders", refOrders as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::deque_ranges", dequeRanges as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::extracts", extracts as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["collections::btree_extracts", btreeExtracts as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.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 5154d1280..a9f4096f3 100644 --- a/ts/fixtures/lang/collections/src/collections.rs +++ b/ts/fixtures/lang/collections/src/collections.rs @@ -2197,3 +2197,166 @@ pub fn extracts(kind: u8, data: &[u8], out: &mut Vec) { } } } + +/// A B-tree key ordered by its number's low byte, whose `PartialOrd`'s `le` and `lt`, which +/// std checks a range's end by, see only that byte halved. Each of those and its drop add its +/// whole number to the log. +struct Logged<'a>(u16, &'a std::cell::RefCell>); + +impl Drop for Logged<'_> { + fn drop(&mut self) { + self.1.borrow_mut().push(self.0); + } +} + +impl PartialEq for Logged<'_> { + fn eq(&self, other: &Self) -> bool { + self.0 & 0xff == other.0 & 0xff + } +} + +impl Eq for Logged<'_> {} + +impl PartialOrd for Logged<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + + fn le(&self, other: &Self) -> bool { + self.1.borrow_mut().push(0x4000 | self.0); + (self.0 & 0xff) / 2 <= (other.0 & 0xff) / 2 + } + + fn lt(&self, other: &Self) -> bool { + self.1.borrow_mut().push(0x2000 | self.0); + (self.0 & 0xff) / 2 < (other.0 & 0xff) / 2 + } +} + +impl Ord for Logged<'_> { + fn cmp(&self, other: &Self) -> Ordering { + (self.0 & 0xff).cmp(&(other.0 & 0xff)) + } +} + +/// A range of its own, which logs each read of either bound. +struct Window<'a> { + lo: u16, + hi: u16, + log: &'a std::cell::RefCell>, +} + +impl std::ops::RangeBounds for Window<'_> { + fn start_bound(&self) -> Bound<&u16> { + self.log.borrow_mut().push(0xa000); + Bound::Included(&self.lo) + } + + fn end_bound(&self) -> Bound<&u16> { + self.log.borrow_mut().push(0xb000); + Bound::Excluded(&self.hi) + } +} + +/// The range form `form` picks of `lo` and `hi`, which may be backwards or empty and +/// excluded at both ends, neither of which `extract_if` panics on. +fn form(form: u8, lo: u16, hi: u16) -> (Bound, Bound) { + use Bound::*; + match form % 8 { + 0 => (Unbounded, Unbounded), + 1 => (Included(lo), Excluded(hi)), + 2 => (Included(lo), Included(hi)), + 3 => (Included(lo), Unbounded), + 4 => (Unbounded, Excluded(hi)), + 5 => (Unbounded, Included(hi)), + 6 => (Excluded(lo), Excluded(hi)), + _ => (Excluded(lo), Included(lo)), + } +} + +/// `extract_if` of B-tree maps and sets, large enough to merge and rebalance leaves as it +/// takes entries out: each form of range, with values changed in place as each is read, +/// read in full or part way, with what stays, the hint, and `Debug` of the iterator as it +/// reads, which peeks past the range's end until it stops there. Then keys and values that +/// log their drops and the `PartialOrd` tests std ends the range by, a closure and range +/// that drop with the iterator, and a range of its own, whose start std reads only of a +/// map with a root. +pub fn btree_extracts(kind: u8, data: &[u8], out: &mut Vec) { + let at = |i: usize| data.get(i).copied().unwrap_or(0); + let n = u16::from(at(0)) * 2; + let (lo, hi) = (u16::from(at(1)) * 2, u16::from(at(2)) * 2); + let (every, limit) = (u16::from(at(3) % 5 + 1), at(4)); + let range = form(at(5), lo, hi); + match kind % 3 { + 0 => { + let mut map: BTreeMap = (0..n).map(|k| (k, k ^ 0x100)).collect(); + let mut it = map.extract_if(range, |k, v| { + *v += 1; + (*k ^ u16::from(limit)) % every == 0 + }); + crate::harness::put(out, &format!("{it:?} {:?}", it.size_hint())); + let taken: Vec<(u16, u16)> = it.by_ref().take(usize::from(limit)).collect(); + crate::harness::put(out, &format!("{taken:?} {it:?} {:?}", it.size_hint())); + if limit % 2 == 0 { + crate::harness::put(out, &format!("{:?} {it:#?}", it.next())); + } + drop(it); + crate::harness::put( + out, + &format!("{} {:?}", map.len(), map.iter().step_by(7).collect::>()), + ); + let taken: Vec<(u16, u16)> = map.extract_if(.., |_, v| *v % 3 == 0).collect(); + crate::harness::put(out, &format!("{taken:?} {map:?}")); + } + 1 => { + let mut set: BTreeSet = (0..n).map(|k| k * 3 % (n + 1)).collect(); + let mut it = set.extract_if(range, |k| k % every != 0); + crate::harness::put(out, &format!("{it:?}")); + let taken: Vec = it.by_ref().take(usize::from(limit)).collect(); + crate::harness::put(out, &format!("{taken:?} {it:?} {:?}", it.size_hint())); + drop(it); + crate::harness::put(out, &format!("{set:?}")); + // Neither a set never added to nor one cleared has a root. + let mut empty: BTreeSet = BTreeSet::new(); + crate::harness::put(out, &format!("{:?}", empty.extract_if(range, |_| true))); + set.clear(); + crate::harness::put(out, &format!("{:?}", set.extract_if(.., |_| true))); + } + _ => { + let log = std::cell::RefCell::new(Vec::new()); + let mut map: BTreeMap = (0..n / 2) + .map(|k| (Logged(k, &log), Logged(0x1000 | k, &log))) + .collect(); + let guard = Logged(0x800, &log); + let (start, end) = (Logged(0x900 | lo / 2, &log), Logged(0x900 | hi / 2, &log)); + let mut it = map.extract_if((Bound::Excluded(start), Bound::Included(end)), move |k, v| { + let _ = &guard; + v.0 ^= 0x200; + k.0 % every == 0 + }); + let taken: Vec<(Logged, Logged)> = it.by_ref().take(usize::from(limit % 8)).collect(); + drop(it); + out.extend(taken.iter().flat_map(|(k, v)| [k.0 as u8, v.0 as u8])); + drop(taken); + // Kept, each key in range is tested against the end, and the first past it. + let mut it = map.extract_if(Logged(0xa00 | lo / 4, &log)..Logged(0xa00 | hi / 4, &log), |_, _| false); + out.push(it.next().is_none() as u8); + drop(it); + // In key order, which std's drop of a whole tree does not keep, node by node. + map.into_iter().for_each(drop); + // A range of its own reads its start once, of a map with a root, and its end at each key. + let mut bytes: BTreeMap = BTreeMap::new(); + let window = |lo, hi| Window { lo, hi, log: &log }; + bytes.extract_if(window(0, 9), |_, _| true).for_each(drop); + bytes.insert(1, 1); + bytes.remove(&1); + bytes.extract_if(window(0, 9), |_, _| true).for_each(drop); + bytes.extend((0..n).map(|k| (k, k as u8))); + let taken: Vec<(u16, u8)> = bytes + .extract_if(window(lo / 2, hi / 2), |k, _| k % every == 0) + .collect(); + out.extend(taken.iter().map(|(_, v)| *v)); + out.extend(log.take().iter().flat_map(|e| e.to_be_bytes())); + } + } +} diff --git a/ts/fixtures/lang/collections/src/lib.rs b/ts/fixtures/lang/collections/src/lib.rs index 47c48ebbc..1147c0f69 100644 --- a/ts/fixtures/lang/collections/src/lib.rs +++ b/ts/fixtures/lang/collections/src/lib.rs @@ -31,5 +31,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("collections::ref_orders", collections::ref_orders), ("collections::deque_ranges", collections::deque_ranges), ("collections::extracts", collections::extracts), + ("collections::btree_extracts", collections::btree_extracts), ] } diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index a8e9a5f0b..1ca51daae 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -15,7 +15,7 @@ // A function that may drop a key, value, or item std's would, rather than return or keep // it, takes what drops each that does, as `k`, `v`, or `f`, and drops it where std's does. -import { ByteSlice, ByteVec, byteVecPush, byteVecReserve, type Capped, capHold, capOf, capReserve, capReserveExact, capSet, type CloneFrom, type Debug, debugStructField, debugStructFinishNonExhaustive, Err, type Fmt, type FmtResult, type Formatter, fmtDebugStruct, fmtDebugStructFields, fmtDebugTupleFields, fmtIter, fmtList, fmtSet, type None, Ok, type Place, panic, type RangeBound, type Result, rangeCheck, sliceSort, sliceView, vecCloneFrom, vecExact, vecMax, vecReserve, vecWithCapacity } from "../rt.ts"; +import { ByteSlice, ByteVec, byteVecPush, byteVecReserve, type Capped, capHold, capOf, capReserve, capReserveExact, capSet, type CloneFrom, type Debug, debugStructField, debugStructFinishNonExhaustive, Err, type Fmt, type FmtResult, type Formatter, fmtDebugStruct, fmtDebugStructFields, fmtDebugTupleFields, fmtIter, fmtList, fmtSet, fmtTuple, type None, Ok, type Place, panic, type RangeBound, type Result, rangeCheck, sliceSort, sliceView, vecCloneFrom, vecExact, vecMax, vecReserve, vecWithCapacity } from "../rt.ts"; import type { Hashing } from "./hash.ts"; import { type Limbs, lt, type u64, type usize } from "./int.ts"; import { @@ -2088,7 +2088,7 @@ export function btreeValues(m: BTreeMap, k?: (key: K) => void, v?: ( /** * A range of keys or indices, as its value is: `a..b`, `a..=b`, `a..` as `{ start }`, `..b` * as `{ end }`, `..` as `undefined`, and `(Bound, Bound)` as its pair. Any other - * `RangeBounds` is a function returning its `start_bound` and `end_bound`, called where + * `RangeBounds` is a `BoundsOf`, whose `start_bound` and `end_bound` are called where * std's calls them. */ export type Bounds = @@ -2097,22 +2097,33 @@ export type Bounds = | { readonly start: K } | { readonly end: K; readonly inclusive?: true } | BoundPair - | (() => BoundPair) + | BoundsOf | undefined; type BoundPair = readonly [RangeBound, RangeBound]; +/** Another `RangeBounds`: its `start_bound` and `end_bound`, each read as it is called. */ +interface BoundsOf { + readonly startBound: () => RangeBound; + readonly endBound: () => RangeBound; +} + const UNBOUNDED: RangeBound = { tag: "Unbounded" }; -/** Another `RangeBounds`, `r`, as `Bounds` reads it: by `start` and then `end`, once read. */ -export function boundsOf(r: R, start: (r: R) => RangeBound, end: (r: R) => RangeBound): () => BoundPair { - return () => [start(r), end(r)]; +/** Another `RangeBounds`, `r`, as `Bounds` reads it, by `start` and `end`. */ +export function boundsOf(r: R, start: (r: R) => RangeBound, end: (r: R) => RangeBound): BoundsOf { + return { startBound: () => start(r), endBound: () => end(r) }; +} + +/** Whether `r` is a `BoundsOf`: a field test, as `inclusive`'s is. */ +function other(r: Bounds): r is BoundsOf { + return typeof r === "object" && "endBound" in r; } /** `RangeBounds`: `start_bound`, then `end_bound`. */ function bounds(r: Bounds): BoundPair { if (r === undefined) return [UNBOUNDED, UNBOUNDED]; - if (typeof r === "function") return r(); + if (other(r)) return [r.startBound(), r.endBound()]; if (Array.isArray(r)) return r as BoundPair; if (inclusive(r)) return [rangeInclusiveStartBound(r), rangeInclusiveEndBound(r)]; const start: RangeBound = "start" in r ? { tag: "Included", _0: r.start } : UNBOUNDED; @@ -2163,6 +2174,132 @@ function edge(m: BTreeMap, key: K, past: boolean): [Leaf, numb return [l, i < 0 ? ~i : past ? i + 1 : i]; } +/** + * `extract_if` of a B-tree: from the first key `r` starts at, each entry `f` returns true + * for, taken out as it is read, in key order, as `[key, value]`, or of a set its key, up to + * the first key past `r`'s end, which `r` is read for at each key and found past by `le` and + * `lt`, a key of the crate's own's `PartialOrd`, or else by the map's order. `f` is given + * each key and value or, with `places`, the value's place, and drops with the iterator, + * then `r`, by `drop`; what it has not read stays. + */ +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; + i = 0; + private readonly r: Bounds; + private readonly f: (key: K, value: V | Place) => boolean; + private readonly places: boolean; + private readonly keys: boolean; + private readonly le: Test | undefined; + private readonly lt: Test | undefined; + private readonly drop: (() => void) | undefined; + + constructor(m: BTreeMap, r: Bounds, f: (key: K, value: V | Place) => boolean, places: boolean, keys: boolean, le?: Test, lt?: Test, drop?: () => void) { + super(); + this.m = m; + this.r = r; + this.f = f; + this.places = places; + this.keys = keys; + this.le = le; + this.lt = lt; + 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; + } + this.l = l; + this.i = i; + } + + /** Whether `key` is within the end of `r`. */ + private within(key: K): boolean { + const r = this.r; + const stop = other(r) ? r.endBound() : bounds(r)[1]; + if (stop.tag === "Unbounded") return true; + if (stop.tag === "Included") return this.le !== undefined ? this.le(key, stop._0) : this.m.c(key, stop._0) <= 0; + return this.lt !== undefined ? this.lt(key, stop._0) : this.m.c(key, stop._0) < 0; + } + + next(): T | Done { + const m = this.m; + for (let l = this.l; l !== undefined; l = 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]; + 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); + continue; + } + const value = l.v[i]; + // A leaf that takes from or merges with another moves what is left of it, which is + // then found again by the key it took out. + const settles = l.k.length <= MIN && m.h > 0; + removeAt(m, l, i); + if (settles) { + l = leafOf(m, key); + i = ~search(l.k, key, m.c); + } + this.at(l, i); + return (this.keys ? key : [key, value]) as T; + } + return DONE; + } + + /** std's, which counts what it read and kept, too. */ + override sizeHint(): SizeHint { + return [0, this.m.n]; + } + + [Symbol.dispose](): void { + dispose(this.f); + this.drop?.(); + } +} + +/** `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 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]); + } + return debugStructFinishNonExhaustive(debugStructField(fmtDebugStruct(f, "ExtractIf"), "peek", peek)); +} + +/** + * `BTreeMap::extract_if`, handing `f` each key and value, with what `BTreeExtractIf` takes: + * a key of the crate's own's `le` and `lt`, and what drops a range that drops. + */ +export function btreeExtractIf(m: BTreeMap, r: Bounds, f: (key: K, value: V) => boolean, le?: Test, lt?: Test, drop?: () => void): Iter<[K, V]> { + return new BTreeExtractIf(m, r, f as (key: K, value: V | Place) => boolean, false, false, le, lt, drop); +} + +/** `BTreeMap::extract_if` of values JS copies: `f` takes the place of each. */ +export function btreeExtractIfMut(m: BTreeMap, r: Bounds, f: (key: K, value: Place) => boolean, le?: Test, lt?: Test, drop?: () => void): Iter<[K, V]> { + return new BTreeExtractIf(m, r, f as (key: K, value: V | Place) => boolean, true, false, le, lt, drop); +} + +/** `BTreeSet::extract_if`, handing `f` each key. */ +export function btreeExtractKeysIf(m: BTreeSet, r: Bounds, f: (key: K) => boolean, le?: Test, lt?: Test, drop?: () => void): Iter { + return new BTreeExtractIf(m, r, f as (key: K) => boolean, false, true, le, lt, drop); +} + /** * `extend` of a map with `[key, value]`s, which keeps an equal key already there and drops * the one given, then the value it replaced, and takes `items` as `take` says. With diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 154680600..f0fc6a384 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -429,7 +429,10 @@ pub fn place_call<'tcx>(b: &Body<'_, '_, 'tcx>, call: &Call<'tcx>) -> bool { Std::HashMap => (&["get_mut", "iter_mut", "values_mut", "retain", "extract_if"], 1), Std::HashTable => (&["find_mut", "iter_mut", "retain"], 0), Std::TableEntry => (&["get_mut", "into_mut", "and_modify"], 0), - Std::BTreeMap => (&["get_mut", "iter_mut", "values_mut", "range_mut", "retain"], 1), + Std::BTreeMap => ( + &["get_mut", "iter_mut", "values_mut", "range_mut", "retain", "extract_if"], + 1, + ), Std::Entry if is_vacant(b, coll) => (&["insert"], 1), Std::Entry => ( &[ @@ -1335,6 +1338,25 @@ fn btree<'tcx>( values.push(test_fn(b, target, "gt", span)); } } + // It holds the range, reads its end at each key it reads, and checks each key against + // the end by `<=` or `<`, which a key of the crate's own does by its `PartialOrd`. + if name == "extract_if" { + let f = values.pop()?; + let range = bounds(b, call, values.pop()?); + let tests = match key.walk().filter_map(|arg| arg.as_type()).any(|t| local(b, t)) { + true => vec![test_fn(b, key, "le", span), test_fn(b, key, "lt", span)], + false => vec![Expr::undefined(), Expr::undefined()], + }; + let name = match set { + true => "btreeExtractKeysIf", + false if b.t.needs_place(arg(coll, 1)) => "btreeExtractIfMut", + false => "btreeExtractIf", + }; + return Some(range.held(|bounds, drop| { + values.extend([bounds, f]); + Expr::rt(name, super::iter::tail(values, [tests, vec![drop]].concat())) + })); + } let none = library::sentinel_option(b.t, call).is_some(); let last = |values: Vec| flagged(values, true, none); let first = |values: Vec| flagged(values, false, none); @@ -2656,6 +2678,17 @@ impl Ranged { Expr::rt("dropAfter", vec![range, call, drop]) } + /// `call` of the bounds and what drops the range, for a call whose result holds the + /// range and drops it as it drops: a range that drops, read once, and else `undefined`. + pub fn held(self, call: impl FnOnce(Expr, Expr) -> Expr) -> Expr { + let Some((range, name, drop)) = self.dropped else { + return call(self.bounds, Expr::undefined()); + }; + let dropper = Expr::Lambda(Vec::new(), Box::new(drop.call(vec![Expr::ident(&name)]))); + let e = call(self.bounds, dropper); + Expr::Lambda(vec![Param { name, ty: None }], Box::new(e)).call(vec![range]) + } + /// The bounds, for a call that checks more once `slice::range` has checked them within /// `len`, as `String`'s do its char boundaries: a range that drops drops between the two, /// as std's does, and the call takes the checked `{ start, end }`. diff --git a/ts/src/lower/fmt.rs b/ts/src/lower/fmt.rs index 20644b790..cf77db945 100644 --- a/ts/src/lower/fmt.rs +++ b/ts/src/lower/fmt.rs @@ -650,6 +650,16 @@ fn iter_debug<'tcx>( let debug = b.fmt_fn(Trait::Debug, args.types().next()?, span); return Some(call("fmtListExtractIf", vec![debug])); } + // The entry it reads next, of a set its key. + ["collections", "btree", map @ ("map" | "set"), "ExtractIf"] => { + let mut types = args.types(); + let debug = b.fmt_fn(Trait::Debug, types.next()?, span); + let value = match *map == "map" { + true => vec![b.fmt_fn(Trait::Debug, types.next()?, span)], + false => Vec::new(), + }; + return Some(call("fmtBTreeExtractIf", [vec![debug], value].concat())); + } ["iter", "sources", "from_fn", "FromFn"] => return write("FromFn".into()), ["iter", "sources", "once_with", "OnceWith"] => return Some(call("fmtOnceWith", Vec::new())), ["iter", "sources", "repeat", "Repeat"] | ["iter", "sources", "repeat_n", "RepeatN"] => { diff --git a/ts/src/lower/iter.rs b/ts/src/lower/iter.rs index 0daa3e282..dae604bec 100644 --- a/ts/src/lower/iter.rs +++ b/ts/src/lower/iter.rs @@ -1897,7 +1897,7 @@ pub(super) fn holds_closure(tcx: ty::TyCtxt<'_>, did: DefId) -> bool { | ["iter", "sources", "once_with", "OnceWith"] | ["iter", "sources", "successors", "Successors"] | ["collections", "linked_list", "ExtractIf"] - | ["collections", "hash", "map" | "set", "ExtractIf"] + | ["collections", "hash" | "btree", "map" | "set", "ExtractIf"] | [ "str", "iter", diff --git a/ts/tests/btree.ts b/ts/tests/btree.ts index cebed2e12..176a50821 100644 --- a/ts/tests/btree.ts +++ b/ts/tests/btree.ts @@ -1,6 +1,7 @@ // 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`, and `clone`. Prints what +// 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. // // usage: node btree.ts @@ -109,6 +110,21 @@ for (let step = 0; step < steps; step++) { out.push(`retain ${rt.btreeLen(m)} ${rt.btreeFirst(m)?.join(" ") ?? "-"} ${rt.btreeFirst(m, true)?.join(" ") ?? "-"}`); m = rand() & 1 ? copy : m; out.push(`kept ${rt.btreeLen(m)}`); + } else if (step % 211 === 0) { + const j = rand() % keys; + const [lo, hi] = k < j ? [k, j] : [j, k]; + const limit = rand() % 400; + const copy = rt.btreeClone(m); + const it = rt.btreeExtractIf(m, { start: lo, end: hi }, (key, value) => (key ^ value) % 3 !== 0); + const taken: number[] = []; + for (let i = 0; i < limit; i++) { + const entry = it.next(); + if (entry === rt.DONE) break; + taken.push(entry[0]); + } + out.push(`extract ${lo} ${hi} ${rt.btreeLen(m)} ${taken.join(",")}`); + check(m); + m = rand() & 1 ? copy : m; } if (step % 499 === 0) check(m); } diff --git a/ts/tests/runtime.rs b/ts/tests/runtime.rs index 6463c091f..1ece27258 100644 --- a/ts/tests/runtime.rs +++ b/ts/tests/runtime.rs @@ -151,6 +151,19 @@ fn btree_script() { m = copy; } want.push(format!("kept {}", 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)) + .take(limit as usize) + .map(|(k, _)| k.to_string()) + .collect(); + want.push(format!("extract {lo} {hi} {} {}", m.len(), taken.join(","))); + if rand() & 1 == 1 { + m = copy; + } } } let keys: Vec = m.keys().map(u32::to_string).collect(); From 862827a96cfa68912561481e4dec3739fa2e0d0d Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:02:34 -0800 Subject: [PATCH 3/5] docs(ts): note rt.boundsOf's record in MIGRATE Co-Authored-By: Claude Opus 5.5 --- ts/MIGRATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ts/MIGRATE.md b/ts/MIGRATE.md index 3a993fc5c..cd13631e9 100644 --- a/ts/MIGRATE.md +++ b/ts/MIGRATE.md @@ -2,6 +2,8 @@ ## Unreleased +- `rt.boundsOf` returns a record of the range's `startBound` and `endBound`, each read as + it is called, rather than a function reading both, which `rt.Bounds` takes as before. - A `dyn Error`'s or `dyn Buf`'s type and classes hold only the methods some call through one dispatches to: a TS caller can call no other, such as `getU16Le$Buf` where no Rust call reads one through a `dyn Buf`. From 5ca5a9be7e46045188d15291128e860fc4567eed Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:06:12 -0800 Subject: [PATCH 4/5] chore(quest): a B-tree's removals call no Ord where std's call none Co-Authored-By: Claude Opus 5.5 --- quest/m3/ts-btree-search-order.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quest/m3/ts-btree-search-order.md b/quest/m3/ts-btree-search-order.md index 2b88fc013..3052975f7 100644 --- a/quest/m3/ts-btree-search-order.md +++ b/quest/m3/ts-btree-search-order.md @@ -4,7 +4,8 @@ [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 answer the same where the tree is out of `Ord`'s order, as a +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`. From 7504ae849c5f70de86a6fc900ab2a376fa599f87 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:08:30 -0800 Subject: [PATCH 5/5] fix(ts): keep a B-tree's extract_if cursor through a rebalance without comparing keys Co-Authored-By: Claude Opus 5.5 --- ts/runtime/rt/collections.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index 1ca51daae..dc2be892d 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -2245,13 +2245,16 @@ class BTreeExtractIf extends Iter implements Disposable { continue; } const value = l.v[i]; - // A leaf that takes from or merges with another moves what is left of it, which is - // then found again by the key it took out. - const settles = l.k.length <= MIN && m.h > 0; + // 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 (settles) { - l = leafOf(m, key); - i = ~search(l.k, key, m.c); + if (prev !== undefined && prev.next !== l) { + i += before; + l = prev; + } else if (prev !== undefined) { + i += before - prev.k.length; } this.at(l, i); return (this.keys ? key : [key, value]) as T;