From 643e459f544c3da3c166ea4806aa142fd4b09096 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:32:34 -0800 Subject: [PATCH 1/2] chore(quest): claim quest/m2/ts-model/main-std.md Quest-Claim: 11c17c157fc9a81f From f40dc5e8fc9dd92eff177ed70b2c60b40959b02a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:58:48 -0800 Subject: [PATCH 2/2] feat(ts): translate VecDeque::range, str::rsplit_once, and split_terminator Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-model/README.md | 1 - quest/m2/ts-model/main-std.md | 17 ---- quest/m3/README.md | 1 + quest/m3/ts-str-searches.md | 24 +++++ quest/m3/ts-vec-rest.md | 2 + ts/fixtures/lang/caller.ts | 24 ++++- ts/fixtures/lang/expected/collections.ts | 80 ++++++++++++++++ ts/fixtures/lang/expected/harness.ts | 4 +- ts/fixtures/lang/expected/strs.ts | 116 ++++++++++++++--------- ts/fixtures/lang/src/collections.rs | 44 +++++++++ ts/fixtures/lang/src/harness.rs | 1 + ts/fixtures/lang/src/strs.rs | 29 ++++++ ts/runtime/rt/collections.ts | 6 ++ ts/runtime/rt/str.ts | 29 +++++- ts/src/lower/collections.rs | 5 + ts/src/lower/iter.rs | 2 +- ts/src/lower/library.rs | 4 +- 17 files changed, 318 insertions(+), 71 deletions(-) delete mode 100644 quest/m2/ts-model/main-std.md create mode 100644 quest/m3/ts-str-searches.md diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index bbabea6b8..54b1df567 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -49,6 +49,5 @@ Public API: the translator's output for moq-net's model. ## Required -- [The std calls moq's main added to the model](/quest/m2/ts-model/main-std.md) - `VecDeque::range`, and `str::rsplit_once` and `split_terminator` by a `char`, translate - [A crate's own hasher](/quest/m2/ts-model/own-hasher.md) - hashlink's default hasher, a crate's own `Hasher`, hashes keys as native's does - [A crate's own iterator traits](/quest/m2/ts-model/iter-traits.md) - hashlink's double-ended, exact-size, and fused iterators, and their `size_hint` diff --git a/quest/m2/ts-model/main-std.md b/quest/m2/ts-model/main-std.md deleted file mode 100644 index 91d4bfe67..000000000 --- a/quest/m2/ts-model/main-std.md +++ /dev/null @@ -1,17 +0,0 @@ -# [S] The std calls moq's main added to the model - -## Goal - -rust2-ts translates the std calls that moq-net's model reaches at moq's `main` and refused -there first, proven by differential tests against native Rust: `VecDeque::range`, and -`str::rsplit_once` and `str::split_terminator` by a `char`. - -## Plan - -Found moving the translator's moq pin past moq's WASM removal: each is one refusal in the -census of `ts/moq/model.toml` (`model/group.rs`, `model/origin.rs`, `path/mod.rs`) that the -previous pin's census did not have. Translate each as std's others of its kind are, with -no config. The model's new `dyn DoubleEndedIterator` in `model/track.rs` is -[trait objects](/quest/m3/ts-dyn.md)' work. - -Public API: the translator's output. diff --git a/quest/m3/README.md b/quest/m3/README.md index 5c3286027..3d32fd352 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -31,6 +31,7 @@ generic std gap is planned here even when moq never calls it. - [std's sorts call their tests in std's order](/quest/m3/ts-sort-order.md) - `slice::sort` and a B-tree's `collect` call `lt` or `cmp` as std's driftsort does past 16 elements - [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 - [Rc and Arc of a slice](/quest/m3/ts-shared-slices.md) - `Rc<[T]>` and `Arc<[T]>` from a `Vec`, a slice, or an iterator translate +- [The rest of str's searches](/quest/m3/ts-str-searches.md) - `find`, `strip_prefix`, `replace`, `rsplit`, `splitn`, `matches`, `lines`, and the rest of `str`'s searches translate - [The rest of a vector's methods](/quest/m3/ts-vec-rest.md) - `append`, `resize`, `split_off`, `try_reserve`, and `Vec::from(VecDeque)` translate - [A reversed zip's per-pair cost](/quest/m3/ts-zip-rev-overhead.md) - a reversed by-index zip costs per pair what a forward one does, or the gap is explained - [Whole writes into a lazy cell or thread-local key](/quest/m3/ts-lazy-writes.md) - a whole write through a `&mut` to a `LazyCell`, `LazyLock`, or `LocalKey` keeps its state diff --git a/quest/m3/ts-str-searches.md b/quest/m3/ts-str-searches.md new file mode 100644 index 000000000..380ea2524 --- /dev/null +++ b/quest/m3/ts-str-searches.md @@ -0,0 +1,24 @@ +# [M] The rest of str's searches + +## Goal + +[rust2-ts](/ts/README.md) translates `str`'s pattern searches and text iterators still +refused, each by every pattern std takes, searching in std's order from each end, proven by +differential tests against native Rust. + +## Plan + +Found translating the [std calls moq's main added to the model](https://github.com/moq-dev/rust2/pull/434), +by a census of a crate calling each by a `char`. These refuse as unsupported calls: + +- `find`, `strip_prefix`, `strip_suffix`, `trim_matches`, and `replace`; +- the iterators `rsplit`, `rsplit_terminator`, `splitn`, `rsplitn`, `split_inclusive`, + `matches`, `rmatches`, and `rmatch_indices`; +- `char_indices`, `lines`, and `split_whitespace`. + +The runtime's `Searcher` in `rt/str.ts` already searches from both ends and `Split` holds +std's `allow_trailing_empty`, so most map onto them as `split`, `split_terminator`, and +`rsplit_once` do. A predicate's calls are visible, so each must call it on the chars std's +does, in order. No consumer calls them yet; they are planned here as part of std. + +Public API: the translator's output. diff --git a/quest/m3/ts-vec-rest.md b/quest/m3/ts-vec-rest.md index 56f4ea908..ae73cd266 100644 --- a/quest/m3/ts-vec-rest.md +++ b/quest/m3/ts-vec-rest.md @@ -14,6 +14,8 @@ elements through raw pointers, so each refuses with its span: - `Vec::append`, `resize`, `resize_with`, `split_off`, `extend_from_within`, and `try_reserve`, and `VecDeque`'s `append`, `resize`, and `split_off`. - `From>` for `Vec`. +- `VecDeque::range_mut` of items JS copies, which `range` and `iter_mut` translate but its + places do not. A vector's capacity follows only std's guarantees, at least its length and what `with_capacity` and `reserve` ask for, and doubles as it grows (decided with the diff --git a/ts/fixtures/lang/caller.ts b/ts/fixtures/lang/caller.ts index 5e12e45c1..79c15b495 100644 --- a/ts/fixtures/lang/caller.ts +++ b/ts/fixtures/lang/caller.ts @@ -601,6 +601,28 @@ const drains = Array.from({ length: 32 * 13 * 13 }, (_, i) => { /** `strs::iters` of each kind, on no text, text of each UTF-8 width, and parts between `/`s, empty ones too. */ const iters = Array.from({ length: 12 }, (_, kind) => [[], [2, 7, 9, 14], [0, 2, 0, 0, 7, 9], [14, 0]].map((data) => Uint8Array.of(...op("strs::iters"), kind, ...data))).flat(); +/** + * `strs::strs`' splits and `rsplit_once` by `/`, and `strs::slices`' by an empty and a + * one-char pattern, on no text, a separator alone, twice, and at either end, read from + * either end. + */ +const separatorEnds = [[], [0], [0, 0], [0, 2], [2, 0], [0, 2, 0], [0x80, 2, 0], [2, 0x80, 0x80], [0, 0x82, 0x80, 7]]; +const splitEnds = separatorEnds.flatMap((data) => [ + ...[1, 3].map((kind) => Uint8Array.of(...op("strs::strs"), kind, ...data)), + ...[2, 2 | 8].map((kind) => Uint8Array.of(...op("strs::slices"), kind, ...data)), +]); + +/** + * `collections::deque_ranges` of each form of range, over no items and over items whose + * front wraps past the buffer's end, at bounds empty, at either end, past it, and backwards. + */ +const dequeRangesOp = op("collections::deque_ranges"); +const dequeRanges = [0, 0x18, 0xa8].flatMap((high) => + Array.from({ length: 8 }, (_, form) => + [[0, 0], [0, 3], [2, 2], [1, 4], [5, 5], [3, 1], [6, 7]].flatMap(([a, b]) => [[], [7, 8, 9]].map((items) => Uint8Array.of(...dequeRangesOp, high | form, a, b, ...items))), + ).flat(), +); + /** `strs::hinted` of each adaptor and each pair of range lengths, each way, on `aé` alone. */ const hintedOp = op("strs::hinted"); const hinted = Array.from({ length: 10 * 7 * 21 }, (_, i) => Uint8Array.of(...hintedOp, i % 10, Math.floor(i / 10) % 7, Math.floor(i / 70))); @@ -864,4 +886,4 @@ const capacityCases = [ ...Array.from({ length: 8 * 6 }, (_, i) => Uint8Array.of(...op("capacity::collects"), i % 8, [0, 1, 2, 5, 9, 23][Math.floor(i / 8)], 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), ]; -export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...singlesCases, ...singlesFormats, ...singlesParsed, ...dyns, ...cells, ...shared, ...locks, ...guards, ...maps, ...mappedLocks, ...relocks, ...tries, ...mutexMaps, ...printed, ...downgrades, ...leaks, ...rwlocks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...elements, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...hinted, ...peekedOthers, ...hints, ...reserves, ...setOps, ...zeros, ...writes, ...floatsMulAdds, ...singlesMulAdds, ...unlikeEqs, ...ownNes, ...rangeFroms, ...compares, ...unlikeCompares, ...rangeMethods, ...wraps(), ...refOrders, ...saturates(), ...methods(), ...separated, ...asciiCases, ...gets, ...capacityCases]; +export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...singlesCases, ...singlesFormats, ...singlesParsed, ...dyns, ...cells, ...shared, ...locks, ...guards, ...maps, ...mappedLocks, ...relocks, ...tries, ...mutexMaps, ...printed, ...downgrades, ...leaks, ...rwlocks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...elements, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...splitEnds, ...dequeRanges, ...hinted, ...peekedOthers, ...hints, ...reserves, ...setOps, ...zeros, ...writes, ...floatsMulAdds, ...singlesMulAdds, ...unlikeEqs, ...ownNes, ...rangeFroms, ...compares, ...unlikeCompares, ...rangeMethods, ...wraps(), ...refOrders, ...saturates(), ...methods(), ...separated, ...asciiCases, ...gets, ...capacityCases]; diff --git a/ts/fixtures/lang/expected/collections.ts b/ts/fixtures/lang/expected/collections.ts index a32b5ee7c..207e814dc 100644 --- a/ts/fixtures/lang/expected/collections.ts +++ b/ts/fixtures/lang/expected/collections.ts @@ -1043,6 +1043,86 @@ export function deques(data: rt.ByteSlice, out: rt.ByteVec): void { rt.byteVecPush(out, rt.dequeIndex(q2, b2)); } +/** + * `VecDeque::range` by each form of range, over a ring buffer whose front items wrap past + * its end, read from both ends and cloned midway; a range past the length or backwards + * panics as std's does. + */ +export function dequeRanges(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { + let _31!: rt.u8; + let it!: rt.Iter; + let item!: rt.u8 | undefined; + let _7_0!: rt.usize; + let _7_1!: rt.usize; + let _7_2Data!: Uint8Array; + let _7_2Start!: number; + let _7_2Len!: number; + if (!(data.length >= 2)) { + _7_0 = 0; + _7_1 = 0; + _7_2Data = data.data; + _7_2Start = data.start; + _7_2Len = data.length; + } else { + const _13: rt.usize = rt.usizeRem(data.data[data.start] as rt.usize, 8); + const _17: rt.usize = rt.usizeRem(data.data[data.start + 1] as rt.usize, 8); + _7_0 = _13; + _7_1 = _17; + _7_2Data = data.data; + _7_2Start = data.start + 2; + _7_2Len = data.length - 2; + } + const a: rt.usize = _7_0; + const b: rt.usize = _7_1; + const q: rt.VecDeque = rt.dequeFromVec(rt.iterToArray(rt.iterSlice(rt.byteSliceView(new rt.ByteSlice(_7_2Data, _7_2Start, _7_2Len))), undefined, 1, true)) as rt.VecDeque; + const _26: rt.u8 = rt.u8Rem(rt.u8Div(kind, 8), 4); + let iterStart: rt.u8 = 0; + const iterEnd: rt.u8 = _26; + while ((_31 = iterStart++) < iterEnd) { + rt.dequePushFront(q, 240 | _31); + } + const _39: rt.u8 = rt.u8Rem(kind, 8); + if (_39 === 0) { + it = rt.dequeRange(q, new rt.Range(a, b)); + } else if (_39 === 1) { + const _44: rt.RangeInclusive = new rt.RangeInclusive(a, b, rt.usizeStep); + it = rt.dequeRange(q, _44); + } else if (_39 === 2) { + it = rt.dequeRange(q, { end: b }); + } else if (_39 === 3) { + it = rt.dequeRange(q, new rt.RangeFrom(a, rt.usizeStep)); + } else if (_39 === 4) { + it = rt.dequeRange(q, undefined); + } else if (_39 === 5) { + it = rt.dequeRange(q, [{ tag: "Excluded", _0: a } as rt.RangeBound, { tag: "Included", _0: b } as rt.RangeBound] as [rt.RangeBound, rt.RangeBound]); + } else if (_39 === 6) { + it = rt.dequeRange(q, { end: b, inclusive: true }); + } else { + it = rt.dequeRange(q, new rt.Range(a, a)); + } + rt.byteVecPush(out, it.len() & 0xff); + let i2: rt.i32 = 0; + while (true) { + if ((rt.u8Shr(kind, rt.i32Add(5, rt.i32Rem(i2, 3))) & 1) === 1) { + item = rt.iterToOption(it.nextBack()); + } else { + item = rt.iterToOption(it.next()); + } + if (item !== undefined) { + rt.byteVecPush(out, item); + if (i2 === 1) { + rt.iterExtendBytes(out, rt.iterRev(it.clone()), undefined, true); + } + i2 = rt.i32Add(i2, 1); + } else { + rt.byteVecPush(out, 253); + const _93: rt.RangeFrom = new rt.RangeFrom(rt.min(a, q.n), rt.usizeStep); + rt.iterExtendBytes(out, rt.iterRev(rt.dequeRange(q, _93)), undefined, true); + return; + } + } +} + /** `vec![a, b]` and `vec![x; n]`, of bytes, numbers, `Copy` tuples, and types that clone. */ export function vecs(data: rt.ByteSlice, out: rt.ByteVec): void { let name!: string; diff --git a/ts/fixtures/lang/expected/harness.ts b/ts/fixtures/lang/expected/harness.ts index a4b115d32..67bb526c2 100644 --- a/ts/fixtures/lang/expected/harness.ts +++ b/ts/fixtures/lang/expected/harness.ts @@ -14,7 +14,7 @@ import { cells as cells_cells } from "./cells.ts"; import { chars as chars_chars } from "./chars.ts"; import { chunks } from "./chunks.ts"; import { iters as clones_iters } from "./clones.ts"; -import { hashMaps, sets, btreeMaps, deques as collections_deques, vecs as collections_vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders } from "./collections.ts"; +import { hashMaps, sets, btreeMaps, deques as collections_deques, dequeRanges, vecs as collections_vecs, stores, optionals, setOps, unlikeEqs, ownNes, cellEqs, unlikeCompares, refOrders } from "./collections.ts"; import { combinators } from "./combinators.ts"; import { conversions } from "./conversions.ts"; import { arcs as counts_arcs, clonedArcs, cyclicArcs, uniqueArcs, clonedRcs, cyclicRcs, rcs, uniqueRcs, copies as counts_copies } from "./counts.ts"; @@ -280,7 +280,7 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) downgraded(data97, out98); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["borrows::leaks", (_2198: rt.u8, data98: rt.ByteSlice, out99: rt.ByteVec): undefined => { leaks(data98, out99); - }] 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::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]]; } /** Run one case. */ diff --git a/ts/fixtures/lang/expected/strs.ts b/ts/fixtures/lang/expected/strs.ts index f9aa54a2d..a3e6f98f0 100644 --- a/ts/fixtures/lang/expected/strs.ts +++ b/ts/fixtures/lang/expected/strs.ts @@ -67,8 +67,9 @@ export function strs(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { let c!: rt.char | undefined; let part!: string | undefined; let part3!: string | undefined; + let part5!: string | undefined; let item!: [rt.usize, string] | undefined; - let _236!: rt.char; + let _260!: rt.char; const s: string = text(data); const _7: rt.usize = rt.u8Rem(rt.u8Div(kind, 8), 16) as rt.usize; if (!(_7 < 16)) { @@ -122,29 +123,44 @@ export function strs(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { } else { rt.byteVecPush(out, i2 & 0xff); fmt_put(out, option_unwrapOr$1iemzn8(rt.iterToOption(rt.strSplit(s, String.fromCodePoint(sep)).next()), "none")); - break b0; + const parts2: rt.Iter = rt.strSplitTerminator(s, String.fromCodePoint(sep)); + let i3: rt.usize = 0; + while (true) { + if (!fromBack(data, i3)) { + part3 = rt.iterToOption(parts2.next()); + } else { + part3 = rt.iterToOption(parts2.nextBack()); + } + if (part3 !== undefined) { + fmt_put(out, part3); + i3 = rt.usizeAdd(i3, 1); + } else { + rt.byteVecPush(out, i3 & 0xff); + break b0; + } + } } } } if (_13 === 2) { const seen: rt.char[] = []; - const parts2: rt.Iter = rt.strSplit(s, (c9: rt.char): boolean => { + const parts3: rt.Iter = rt.strSplit(s, (c9: rt.char): boolean => { seen.push(c9); if (!(c9 === sep)) { return rt.isAsciiDigit(c9); } return true; }); - let i3: rt.usize = 0; + let i4: rt.usize = 0; while (true) { - if (!fromBack(data, i3)) { - part3 = rt.iterToOption(parts2.next()); + if (!fromBack(data, i4)) { + part5 = rt.iterToOption(parts3.next()); } else { - part3 = rt.iterToOption(parts2.nextBack()); + part5 = rt.iterToOption(parts3.nextBack()); } - if (part3 !== undefined) { - fmt_put(out, part3); - i3 = rt.usizeAdd(i3, 1); + if (part5 !== undefined) { + fmt_put(out, part5); + i4 = rt.usizeAdd(i4, 1); } else { putSeen(out, seen); break b0; @@ -157,6 +173,13 @@ export function strs(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { fmt_put(out, rt.strTrimEndMatches(rt.strTrimStartMatches(s, String.fromCodePoint(sep)), String.fromCodePoint(sep))); rt.byteVecPush(out, rt.strStartsWith(s, String.fromCodePoint(sep)) ? 1 : 0); putOffset(out, rt.strRfind(s, String.fromCodePoint(sep))); + const _143: [string, string] | undefined = rt.strRsplitOnce(s, String.fromCodePoint(sep)); + if (_143 === undefined) { + rt.byteVecPush(out, 253); + } else { + fmt_put(out, _143[0]); + fmt_put(out, _143[1]); + } } else if (_13 === 4) { const seen2: rt.char[] = []; fmt_put(out, rt.strTrimStartMatches(s, (c5: rt.char): boolean => { @@ -181,9 +204,9 @@ export function strs(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { } else { if (_13 === 5) { const found2: rt.Iter<[rt.usize, string]> = rt.strMatchIndices(s, String.fromCodePoint(sep)); - let i4: rt.usize = 0; + let i5: rt.usize = 0; while (true) { - if (!fromBack(data, i4)) { + if (!fromBack(data, i5)) { item = rt.iterToOption(found2.next()); } else { item = rt.iterToOption(found2.nextBack()); @@ -193,48 +216,48 @@ export function strs(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { const matched: string = item[1]; putOffset(out, at); fmt_put(out, matched); - i4 = rt.usizeAdd(i4, 1); + i5 = rt.usizeAdd(i5, 1); } else { - for (const _190 of rt.iterToArray(rt.iterMap(rt.strMatchIndices(s, "/"), (_2: [rt.usize, string]): rt.usize => { + for (const _214 of rt.iterToArray(rt.iterMap(rt.strMatchIndices(s, "/"), (_2: [rt.usize, string]): rt.usize => { return _2[0]; }), "next", 4)) { - putOffset(out, _190); + putOffset(out, _214); } break b0; } } } if (_13 === 6) { - const parts3: string[] = unescape(s); - for (const _202 of parts3) { - fmt_put(out, _202); + const parts4: string[] = unescape(s); + for (const _226 of parts4) { + fmt_put(out, _226); } - fmt_put(out, escape(parts3)); + fmt_put(out, escape(parts4)); } else { let built: string = ""; const named: strs_Named.Named = { name: "" }; const each: string[] = ["", ""]; const iter4: rt.Iter<[rt.usize, rt.char]> = rt.iterEnumerate(rt.strChars(s)); while (true) { - const _227: [rt.usize, rt.char] | rt.Done = iter4.next(); - if (_227 === rt.DONE) { + const _251: [rt.usize, rt.char] | rt.Done = iter4.next(); + if (_251 === rt.DONE) { break; } - const i5: rt.usize = _227[0]; - const c3: rt.char = _227[1]; + const i6: rt.usize = _251[0]; + const c3: rt.char = _251[1]; built = built + String.fromCodePoint(c3); - if (!(rt.usizeRem(i5, 2) === 0)) { - _236 = sep; + if (!(rt.usizeRem(i6, 2) === 0)) { + _260 = sep; } else { - _236 = c3; + _260 = c3; } - named.name = named.name + String.fromCodePoint(_236); + named.name = named.name + String.fromCodePoint(_260); strs_Named.add(named, c3); - const at2 = rt.idx(each, rt.usizeRem(i5, 2)); + const at2 = rt.idx(each, rt.usizeRem(i6, 2)); each[at2] = each[at2] + String.fromCodePoint(c3); - const _247: rt.char | undefined = rt.charFromU32(rt.u32Add(c3 as rt.u32, 1)); - if (_247 !== undefined) { - built = built + String.fromCodePoint(_247); + const _271: rt.char | undefined = rt.charFromU32(rt.u32Add(c3 as rt.u32, 1)); + if (_271 !== undefined) { + built = built + String.fromCodePoint(_271); } } named.name = named.name + "􏿿"; @@ -279,7 +302,7 @@ export function pattern(kind: rt.u8, s: string): string { * one matches before each char and after the last. */ export function slices(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { - let _209!: [rt.usize, rt.usize]; + let _219!: [rt.usize, rt.usize]; let sliced!: string; const s: string = text(data); const p: string = pattern(kind, s); @@ -328,6 +351,13 @@ export function slices(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { return rt.isAsciiDigit(c); }) ? 1 : 0); putOffset(out, rt.strRfind(s, p)); + const _123: [string, string] | undefined = rt.strRsplitOnce(s, p); + if (_123 === undefined) { + rt.byteVecPush(out, 253); + } else { + fmt_put(out, _123[0]); + fmt_put(out, _123[1]); + } } else { if (_8 === 3) { const iter3: rt.Iter = rt.iterSuccessors(s, (path: string): string | undefined => { @@ -339,11 +369,11 @@ export function slices(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { }); }); while (true) { - const _129: string | rt.Done = iter3.next(); - if (_129 === rt.DONE) { + const _139: string | rt.Done = iter3.next(); + if (_139 === rt.DONE) { break b0; } - fmt_put(out, _129); + fmt_put(out, _139); } } if (_8 === 4) { @@ -370,18 +400,18 @@ export function slices(kind: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): void { return rt.usizeSaturatingSub(rt.usizeAdd(rt.stringLen(s), 1), rt.usizeRem(b2 as rt.usize, 4)); }; if (!(data.length >= 2)) { - _209 = [0, 0] as [rt.usize, rt.usize]; + _219 = [0, 0] as [rt.usize, rt.usize]; } else { - _209 = [at2(data.data[data.start]), at2(data.data[data.start + 1])] as [rt.usize, rt.usize]; + _219 = [at2(data.data[data.start]), at2(data.data[data.start + 1])] as [rt.usize, rt.usize]; } - const a: rt.usize = _209[0]; - const b: rt.usize = _209[1]; - const _223: rt.u8 = rt.u8Rem(rt.u8Div(kind, 8), 4); - if (_223 === 0) { + const a: rt.usize = _219[0]; + const b: rt.usize = _219[1]; + const _233: rt.u8 = rt.u8Rem(rt.u8Div(kind, 8), 4); + if (_233 === 0) { sliced = rt.strSlice(s, a, b); - } else if (_223 === 1) { + } else if (_233 === 1) { sliced = rt.strSlice(s, a); - } else if (_223 === 2) { + } else if (_233 === 2) { sliced = rt.strSlice(s, 0, b); } else { sliced = rt.strSlice(s, 0); diff --git a/ts/fixtures/lang/src/collections.rs b/ts/fixtures/lang/src/collections.rs index bdb8993bd..43ce1bbb2 100644 --- a/ts/fixtures/lang/src/collections.rs +++ b/ts/fixtures/lang/src/collections.rs @@ -712,6 +712,50 @@ pub fn deques(data: &[u8], out: &mut Vec) { out.push(q[b]); } +/// `VecDeque::range` by each form of range, over a ring buffer whose front items wrap past +/// its end, read from both ends and cloned midway; a range past the length or backwards +/// panics as std's does. +pub fn deque_ranges(kind: u8, data: &[u8], out: &mut Vec) { + let (a, b, items) = match data { + [a, b, items @ ..] => (*a as usize % 8, *b as usize % 8, items), + _ => (0, 0, data), + }; + let mut q: VecDeque = items.iter().copied().collect(); + // Pushed before the first item, which starts the buffer, they wrap to its end. + for i in 0..kind / 8 % 4 { + q.push_front(0xf0 | i); + } + let mut it = match kind % 8 { + 0 => q.range(a..b), + 1 => q.range(a..=b), + 2 => q.range(..b), + 3 => q.range(a..), + 4 => q.range(..), + 5 => q.range((Bound::Excluded(a), Bound::Included(b))), + 6 => q.range(..=b), + _ => q.range(a..a), + }; + out.push(it.len() as u8); + // Each step reads from the back by the kind's high bits, in turn. + let mut i = 0; + loop { + let item = match kind >> (5 + i % 3) & 1 { + 1 => it.next_back(), + _ => it.next(), + }; + let Some(&item) = item else { + break; + }; + out.push(item); + if i == 1 { + out.extend(it.clone().rev()); + } + i += 1; + } + out.push(0xfd); + out.extend(q.range(a.min(q.len())..).rev()); +} + /// A value that counts how far it is cloned from the one `vec!` was given, which /// `vec![x; n]` clones for every element but the last, which is `x` itself. struct Cloned { diff --git a/ts/fixtures/lang/src/harness.rs b/ts/fixtures/lang/src/harness.rs index 0a4253ba2..72c4d8828 100644 --- a/ts/fixtures/lang/src/harness.rs +++ b/ts/fixtures/lang/src/harness.rs @@ -288,6 +288,7 @@ fn cases() -> Vec<(&'static str, Case)> { ("capacity::maps", capacity::maps), ("locks::downgraded", |_, data, out| locks::downgraded(data, out)), ("borrows::leaks", |_, data, out| borrows::leaks(data, out)), + ("collections::deque_ranges", collections::deque_ranges), ] } diff --git a/ts/fixtures/lang/src/strs.rs b/ts/fixtures/lang/src/strs.rs index 36495bb5e..978597a31 100644 --- a/ts/fixtures/lang/src/strs.rs +++ b/ts/fixtures/lang/src/strs.rs @@ -109,6 +109,21 @@ pub fn strs(kind: u8, data: &[u8], out: &mut Vec) { } out.push(i as u8); put(out, s.split(sep).next().unwrap_or("none")); + // `split_terminator`, which drops an empty last part, from both ends. + let mut parts = s.split_terminator(sep); + let mut i = 0; + loop { + let part = match from_back(data, i) { + true => parts.next_back(), + false => parts.next(), + }; + let Some(part) = part else { + break; + }; + put(out, part); + i += 1; + } + out.push(i as u8); } 2 => { // `split` by a predicate, which sees each char once, in the order it is read. @@ -138,6 +153,13 @@ pub fn strs(kind: u8, data: &[u8], out: &mut Vec) { put(out, s.trim_start_matches(sep).trim_end_matches(sep)); out.push(s.starts_with(sep) as u8); put_offset(out, s.rfind(sep)); + match s.rsplit_once(sep) { + Some((head, tail)) => { + put(out, head); + put(out, tail); + } + None => out.push(0xfd), + } } 4 => { // Trimmed, tested, and found by a predicate. @@ -275,6 +297,13 @@ pub fn slices(kind: u8, data: &[u8], out: &mut Vec) { out.push(s.contains('/') as u8); out.push(s.contains(|c: char| c.is_ascii_digit()) as u8); put_offset(out, s.rfind(p.as_str())); + match s.rsplit_once(p.as_str()) { + Some((head, tail)) => { + put(out, head); + put(out, tail); + } + None => out.push(0xfd), + } } 3 => { // A path and each parent of it, as moq-net's discovery walks them. diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index cc891e3d0..154b95e7c 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -2866,6 +2866,12 @@ export function dequeIter(q: VecDeque, drop?: (item: T) => void): Iter return iterSlice(items, drop); } +/** `range`: `iter` of the items within `r`, which panics as std's does past the length. */ +export function dequeRange(q: VecDeque, r: Bounds): Iter { + const [start, end] = boundsRange(r, q.n); + return new DequeIter(q, start, end); +} + /** `iter_mut` of items JS copies: the place of each. */ export function dequeIterMut(q: VecDeque): Iter> { return new DequePlaces(q); diff --git a/ts/runtime/rt/str.ts b/ts/runtime/rt/str.ts index 0351cd7a9..6672a1836 100644 --- a/ts/runtime/rt/str.ts +++ b/ts/runtime/rt/str.ts @@ -324,23 +324,31 @@ class Searcher { /** `str::split`. */ export function strSplit(s: string, p: Pattern): Iter { - return new Split(new Searcher(s, p)); + return new Split(new Searcher(s, p), true); +} + +/** `str::split_terminator`: `split`, without the empty part after a last match. */ +export function strSplitTerminator(s: string, p: Pattern): Iter { + return new Split(new Searcher(s, p), false); } class Split extends Iter { readonly #m: Searcher; #start = 0; #end: number; + // Whether an empty last part is yielded, which `nextBack` sets once it has passed it. + #trailing: boolean; #finished = false; - constructor(m: Searcher) { + constructor(m: Searcher, trailing: boolean) { super(); this.#m = m; this.#end = m.s.length; + this.#trailing = trailing; } override clone(h?: Clones): Iter { - const it = new Split(this.#m.clone(h)); + const it = new Split(this.#m.clone(h), this.#trailing); it.#start = this.#start; it.#end = this.#end; it.#finished = this.#finished; @@ -356,11 +364,18 @@ class Split extends Iter { return part; } this.#finished = true; - return m.s.slice(this.#start, this.#end); + return this.#trailing || this.#start < this.#end ? m.s.slice(this.#start, this.#end) : DONE; } override nextBack(): string | Done { if (this.#finished) return DONE; + if (!this.#trailing) { + // The last part, unless it is empty, as std's does. + this.#trailing = true; + const part = this.nextBack(); + if (part !== DONE && part !== "") return part; + if (this.#finished) return DONE; + } const m = this.#m; if (m.nextMatchBack()) { const part = m.s.slice(m.end, this.#end); @@ -490,6 +505,12 @@ export function strSplitOnce(s: string, p: Pattern): [string, string] | undefine return m.nextMatch() ? [s.slice(0, m.start), s.slice(m.end)] : undefined; } +/** `str::rsplit_once`: the text before the last match and after it. */ +export function strRsplitOnce(s: string, p: Pattern): [string, string] | undefined { + const m = new Searcher(s, p); + return m.nextMatchBack() ? [s.slice(0, m.start), s.slice(m.end)] : undefined; +} + /** `str::is_char_boundary`: whether byte `i` starts a char, or is the end. */ export function strIsCharBoundary(s: string, i: number): boolean { return unitAt(s, 0, 0, i) >= 0; diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 75dbfc627..ce077844b 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -1334,6 +1334,11 @@ fn deque<'tcx>( } (_, "iter_mut") if b.t.needs_place(item) => Expr::rt("dequeIterMut", values), (_, "iter" | "iter_mut") => Expr::rt("dequeIter", values), + (_, "range") => { + let ranged = bounds(b, call, values.pop()?); + values.push(ranged.bounds.clone()); + ranged.after(Expr::rt("dequeRange", values)) + } (_, "drain") => { let ranged = bounds(b, call, values.pop()?); values.push(ranged.bounds.clone()); diff --git a/ts/src/lower/iter.rs b/ts/src/lower/iter.rs index 73205ac10..a91314e57 100644 --- a/ts/src/lower/iter.rs +++ b/ts/src/lower/iter.rs @@ -1007,7 +1007,7 @@ fn clones<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Option< | ["collections", "linked_list", "IntoIter"] | ["collections", "binary_heap", "IntoIter"] => parts(vec![("v", value(b, arg(0)?))]), // A closure's searcher clones the closure; another holds what it searches for. - ["str", "iter", "Split" | "MatchIndices"] => match arg(0)?.kind() { + ["str", "iter", "Split" | "SplitTerminator" | "MatchIndices"] => match arg(0)?.kind() { ty::Closure(..) => parts(vec![("f", value(b, arg(0)?))]), _ => Expr::undefined(), }, diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index 7bb8a1e28..ebebdc1b9 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -2248,8 +2248,8 @@ fn mapping<'tcx>( ("str", "bytes") => Expr::rt("strBytes", args), ("str", "is_char_boundary") => Expr::rt("strIsCharBoundary", args), // Searches by a pattern. - ("str", "split" | "match_indices" | "contains" | "starts_with" | "ends_with" | "rfind" | "split_once") - | ("str", "trim_start_matches" | "trim_end_matches") => { + ("str", "split" | "split_terminator" | "match_indices" | "contains" | "starts_with" | "ends_with") + | ("str", "rfind" | "split_once" | "rsplit_once" | "trim_start_matches" | "trim_end_matches") => { let [s, p] = <[Expr; 2]>::try_from(args).ok()?; Expr::rt_of("str", &super::camel(&name)).call(vec![s, pattern(b.t, targ(gargs, 0), p)?]) }