From 74e2a6ec3b6eff861117617a54d6e0c00983b925 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:32:59 -0800 Subject: [PATCH 1/6] chore(quest): claim quest/m3/ts-map-own-hasher.md Quest-Claim: 79aa32ce98cbf0be From ef3d302d9022b1a1e5d97189d2a4b7ffbd19ef3a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:46:55 -0800 Subject: [PATCH 2/6] feat(ts): hold a crate's own BuildHasher in a hash map or set Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 3 +- quest/m3/ts-build-hasher-default.md | 21 +++ quest/m3/ts-copied-hasher.md | 4 - quest/m3/ts-hash-map-eq.md | 19 +++ quest/m3/ts-map-own-hasher.md | 29 ---- ts/README.md | 6 +- ts/fixtures/lang/hashes/expected/harness.ts | 24 +++ .../lang/hashes/expected/hashes.Fnv.ts | 26 ++++ .../lang/hashes/expected/hashes.Key.ts | 41 +++++ .../lang/hashes/expected/hashes.Noisy.ts | 28 ++++ .../lang/hashes/expected/hashes.Seeded.ts | 41 +++++ ts/fixtures/lang/hashes/expected/hashes.ts | 116 ++++++++++++++ ts/fixtures/lang/hashes/expected/lib.ts | 15 +- ts/fixtures/lang/hashes/src/hashes.rs | 142 +++++++++++++++++- ts/fixtures/lang/hashes/src/lib.rs | 1 + ts/fixtures/stock/expected/core/option.ts | 7 + ts/fixtures/stock/expected/harness.ts | 4 +- ts/fixtures/stock/expected/lib.ts | 16 ++ ts/fixtures/stock/expected/maps.Seeded.ts | 36 +++++ ts/fixtures/stock/expected/maps.ts | 55 ++++++- ts/fixtures/stock/src/harness.rs | 1 + ts/fixtures/stock/src/maps.rs | 57 +++++++ ts/runtime/rt/collections.ts | 54 +++++-- ts/src/lower/collections.rs | 133 +++++++++++++--- ts/src/lower/drop.rs | 14 +- 25 files changed, 810 insertions(+), 83 deletions(-) create mode 100644 quest/m3/ts-build-hasher-default.md create mode 100644 quest/m3/ts-hash-map-eq.md delete mode 100644 quest/m3/ts-map-own-hasher.md create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Fnv.ts create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Key.ts create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Noisy.ts create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Seeded.ts create mode 100644 ts/fixtures/stock/expected/maps.Seeded.ts diff --git a/quest/m3/README.md b/quest/m3/README.md index b73b8aa75..3b3249655 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -41,5 +41,6 @@ generic std gap is planned here even when moq never calls it. - [Python's call cost](/quest/m3/ffi-python-calls.md) - a call on an object costs what BoltFFI's does, or the gap is explained - [No unused import in an export's generated crate](/quest/m3/ffi-unused-c-void.md) - the crate rust2-ffi generates builds without warnings when no export uses `void *` - [A box's clone_from, and a vector's AsRef and AsMut](/quest/m3/ts-box-vec-conversions.md) - `clone_from` of a `Box` and `AsRef<[T]>` of a `Vec` translate -- [A std map with a crate's own BuildHasher](/quest/m3/ts-map-own-hasher.md) - std's and hashbrown's maps built with a crate's own hasher translate for keys that are JS objects - [A crate's own hasher that JS copies](/quest/m3/ts-copied-hasher.md) - an enum `Hasher`, whose `&mut` is a place, hashes as a struct one does +- [A map built with BuildHasherDefault](/quest/m3/ts-build-hasher-default.md) - `FxHashMap`-style maps, of `BuildHasherDefault` over a crate's own `Hasher`, translate +- [Equality of hash maps and sets](/quest/m3/ts-hash-map-eq.md) - `==` of two hash maps or sets translates as std's diff --git a/quest/m3/ts-build-hasher-default.md b/quest/m3/ts-build-hasher-default.md new file mode 100644 index 000000000..59fe588b5 --- /dev/null +++ b/quest/m3/ts-build-hasher-default.md @@ -0,0 +1,21 @@ +# [S] A map built with BuildHasherDefault + +## Goal + +[rust2-ts](/ts/README.md) translates `BuildHasherDefault`, std's `BuildHasher` of a +`Hasher` that is `Default`, as a map's hasher, as `rustc-hash`'s `FxHashMap` and +`nohash-hasher`'s `IntMap` build theirs: making one, `Default`, `Clone`, `build_hasher`, +and `hash_one` through a crate's own `H`, proven by differential tests against native Rust. + +## Plan + +Found while landing maps that hold a crate's own `BuildHasher`, which refuse +`BuildHasherDefault` as "unsupported type `std::hash::BuildHasherDefault` from another +crate". + +- It holds nothing but a `PhantomData`, so it may be what such a type is, as `PhantomData` + is, and its `build_hasher` is `H::default()`. +- A map already holds any `BuildHasher` but `RandomState` and hashes keys around it. +- No consumer yet; planned here because rust2 aims to support all of std. + +Public API: the translator's output. diff --git a/quest/m3/ts-copied-hasher.md b/quest/m3/ts-copied-hasher.md index e4c615dc4..878472285 100644 --- a/quest/m3/ts-copied-hasher.md +++ b/quest/m3/ts-copied-hasher.md @@ -21,7 +21,3 @@ with "hashing into `_`, a `Hasher` JS copies, which is not supported yet". - No consumer yet; planned here because rust2 aims to support all of std. Public API: the translator's output. - -## Related - -- [A std map with a crate's own BuildHasher](/quest/m3/ts-map-own-hasher.md) - the other gap #424 left diff --git a/quest/m3/ts-hash-map-eq.md b/quest/m3/ts-hash-map-eq.md new file mode 100644 index 000000000..d82823dfd --- /dev/null +++ b/quest/m3/ts-hash-map-eq.md @@ -0,0 +1,19 @@ +# [S] Equality of hash maps and sets + +## Goal + +[rust2-ts](/ts/README.md) translates `==` and `!=` of two `HashMap`s or two `HashSet`s, +std's and hashbrown's, whatever their `BuildHasher`, as std's: equal lengths, then each +entry of one found in the other with an equal value, proven by differential tests against +native Rust. + +## Plan + +Found while landing maps that hold a crate's own `BuildHasher`: `PartialEq::eq` of two +maps is refused as an unsupported call, for a `RandomState` map too. + +- std's walks one map and looks each key up in the other, calling the value's + `PartialEq`; which map it walks decides which value's `eq` runs as `self`. +- No consumer yet; planned here because rust2 aims to support all of std. + +Public API: the translator's output. diff --git a/quest/m3/ts-map-own-hasher.md b/quest/m3/ts-map-own-hasher.md deleted file mode 100644 index 543a99e17..000000000 --- a/quest/m3/ts-map-own-hasher.md +++ /dev/null @@ -1,29 +0,0 @@ -# [M] A std map with a crate's own BuildHasher - -## Goal - -[rust2-ts](/ts/README.md) translates std's `HashMap` and `HashSet`, and hashbrown's, built -with a crate's own `BuildHasher`, for keys that are JS objects too, proven by differential -tests against native Rust with a hasher that logs what it is given. - -## Plan - -Found by [#424](https://github.com/moq-dev/rust2/pull/424), which translates hashing -through a crate's own `Hasher` but leaves this refused with "hashing by the hasher of -`_`". A key that is a JS primitive already translates, and never reaches the hasher. - -- The runtime finds a JS object key by its `Hash` into the collection's own `Mixer`. A - crate's own key `Hash` is generic over the hasher, so the map's instance takes the - crate's hasher type, not the runtime's. -- Open question: whether the runtime must call the crate's hasher at all. std leaves - how often a map hashes unspecified, and the README already treats it as the runtime's, - but native calls it at least once per insert or lookup, which a logging hasher sees. - Decide that before choosing between hashing through an `rt.OwnHasher` and hashing - around it. -- No consumer yet; planned here because rust2 aims to support all of std. - -Public API: the translator's output. - -## Related - -- [A crate's own hasher that JS copies](/quest/m3/ts-copied-hasher.md) - the other gap #424 left diff --git a/ts/README.md b/ts/README.md index d9e2b7f9b..1058d0916 100644 --- a/ts/README.md +++ b/ts/README.md @@ -436,7 +436,11 @@ such as a number, from `get_mut` or the entry API, is an `rt.Place` whose `v` re writes the map's slot. A hash map iterates in an order of the runtime's, mostly insertion order, which std leaves unspecified, and hashes a key with a hasher of its own, as std's keys its at random. So is how often the runtime -calls a key's `Hash`, `Eq`, or `Ord`. +calls a key's `Hash`, `Eq`, or `Ord`. A map or set built with any `BuildHasher` but +`RandomState` holds it, which `hasher` lends, `Default` and `collect` make, and `clone` +clones before what the map holds; it drops before the map's entries, and at once from +`into_iter`, `into_keys`, and `into_values`, as hashbrown's does. No key goes through it: +the runtime's own hasher finds them, so it builds no hasher for the map. A `DefaultHasher` is std's SipHash-1-3, and `Hash` writes into it what std's impls write, so `DefaultHasher::new`'s hash of a value is native's. `RandomState::new` keys it as std diff --git a/ts/fixtures/lang/hashes/expected/harness.ts b/ts/fixtures/lang/hashes/expected/harness.ts index 58ec1cf62..7c19e53ff 100644 --- a/ts/fixtures/lang/hashes/expected/harness.ts +++ b/ts/fixtures/lang/hashes/expected/harness.ts @@ -37,4 +37,28 @@ 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); +} + +/** A char of the alphabet for each byte, by its low bits. */ +export function text(data: rt.ByteSlice): string { + let s: string = ""; + const iterS: Uint8Array = rt.byteSliceView(data); + let iterI: number = 0; + while (true) { + const _5: rt.u8 | rt.Done = iterI < iterS.length ? iterS[iterI++] : rt.DONE; + if (_5 === rt.DONE) { + return s; + } + const _13: rt.usize = rt.u8Rem(_5, 16) as rt.usize; + if (!(_13 < 16)) { + rt.panic("index out of bounds: the len is " + 16 + " but the index is " + _13); + } + s = s + String.fromCodePoint([47, 92, 97, 48, 57, 127, 128, 233, 2047, 2048, 55295, 57344, 65535, 65536, 128512, 1114111][_13]); + } +} + //# sourceMappingURL=harness.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Fnv.ts b/ts/fixtures/lang/hashes/expected/hashes.Fnv.ts new file mode 100644 index 000000000..8815c1d5d --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Fnv.ts @@ -0,0 +1,26 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** + * FNV-1a, a `Hasher` of the crate's own that logs nothing, so a map may call it as often + * as it likes. + */ +export interface Fnv { + _0: rt.u64; +} + +export function finish$hash_Hasher(self: Fnv): rt.u64 { + return self._0; +} + +/** The methods of `hashes::Fnv`'s impl, which the runtime's hashing calls. */ +export const hasher$hash_Hasher: rt.HasherImpl = { write: write$hash_Hasher as (a0: Fnv, a1: rt.ByteSlice) => undefined, finish: finish$hash_Hasher }; + +export function write$hash_Hasher(self: Fnv, bytes: rt.ByteSlice): void { + for (const b of rt.byteSliceView(bytes)) { + self._0 = rt.u64WrappingMul(rt.u64Xor(self._0, b as rt.u64), 1099511628211); + } +} + +//# sourceMappingURL=hashes.Fnv.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Key.ts b/ts/fixtures/lang/hashes/expected/hashes.Key.ts new file mode 100644 index 000000000..01bd99c7f --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Key.ts @@ -0,0 +1,41 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import * as Fnv from "./hashes.Fnv.ts"; + +export function eq$cmp_PartialEq(self: Key, other: Key): boolean { + if (!(self._0 === other._0)) { + return false; + } + return self._1 === other._1; +} + +export function hash$Hash_hash_DefaultHasher(self: Key, state: rt.Hasher): void { + rt.hash16(self._0, state); + rt.hashStr(self._1, state); +} + +export function hash$Hash_hashes_Fnv(self: Key, state: Fnv.Fnv): void { + rt.hash16(self._0, new rt.OwnHasher(state, Fnv.hasher$hash_Hasher)); + rt.hashStr(self._1, new rt.OwnHasher(state, Fnv.hasher$hash_Hasher)); +} + +export function partialCmp$cmp_PartialOrd(self: Key, other: Key): rt.Ordering | undefined { + return cmp$cmp_Ord(self, other); +} + +export function cmp$cmp_Ord(self: Key, other: Key): rt.Ordering { + const cmp: rt.Ordering = rt.cmp(self._0, other._0); + if (cmp === 0) { + return rt.cmpStr(self._1, other._1); + } + return cmp; +} + +/** A key that is a JS object. */ +export interface Key { + _0: rt.u16; + _1: string; +} + +//# sourceMappingURL=hashes.Key.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Noisy.ts b/ts/fixtures/lang/hashes/expected/hashes.Noisy.ts new file mode 100644 index 000000000..2a9365ed5 --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Noisy.ts @@ -0,0 +1,28 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { event } from "./hashes.ts"; + +/** A value that logs its clone and its drop. */ +export class Noisy { + _0: rt.u8; + + constructor(_0: rt.u8) { + this._0 = _0; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function clone$Clone(self: Noisy): Noisy { + event(67, self._0); + return new Noisy(self._0); +} + +export function drop$ops_Drop(self: Noisy): void { + event(68, self._0); +} + +//# sourceMappingURL=hashes.Noisy.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Seeded.ts b/ts/fixtures/lang/hashes/expected/hashes.Seeded.ts new file mode 100644 index 000000000..a63c9fc3e --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Seeded.ts @@ -0,0 +1,41 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { event } from "./hashes.ts"; +import type * as Fnv from "./hashes.Fnv.ts"; + +/** + * A `BuildHasher` of the crate's own, numbered, which logs each one `Default` makes, each + * clone, and each drop, and seeds its [`Fnv`] by its number. + */ +export class Seeded { + _0: rt.u8; + + constructor(_0: rt.u8) { + this._0 = _0; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function buildHasher$hash_BuildHasher(self: Seeded): Fnv.Fnv { + return { _0: rt.u64Xor({ hi: 0xcbf29ce4, lo: 0x84222325 } as rt.u64, self._0 as rt.u64) }; +} + +export function default$Default(): Seeded { + event(110, 0); + return new Seeded(0); +} + +export function clone$Clone(self: Seeded): Seeded { + event(99, self._0); + return new Seeded(rt.u8WrappingAdd(self._0, 1)); +} + +export function drop$ops_Drop(self: Seeded): void { + event(120, self._0); +} + +//# sourceMappingURL=hashes.Seeded.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.ts b/ts/fixtures/lang/hashes/expected/hashes.ts index fc39c83fc..7dbbd0085 100644 --- a/ts/fixtures/lang/hashes/expected/hashes.ts +++ b/ts/fixtures/lang/hashes/expected/hashes.ts @@ -1,19 +1,25 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; +import { drop$Option_tuple2_u8_hashes_Noisy } from "./lib.ts"; import { thenSome$0n2gkz4 } from "./core/bool.ts"; import { unwrapOr$0k15iai as option_unwrapOr$0k15iai, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr } from "./core/option.ts"; import { unwrapOr$01jdcm7 as result_unwrapOr$01jdcm7 } from "./core/result.ts"; +import { put, text } from "./harness.ts"; import * as Bare from "./hashes.Bare.ts"; import * as Build from "./hashes.Build.ts"; import * as Counted from "./hashes.Counted.ts"; import * as Dir from "./hashes.Dir.ts"; +import * as Fnv from "./hashes.Fnv.ts"; import * as Full from "./hashes.Full.ts"; +import * as Key from "./hashes.Key.ts"; import * as Logged from "./hashes.Logged.ts"; import * as Nibble from "./hashes.Nibble.ts"; +import * as Noisy from "./hashes.Noisy.ts"; import * as Packet from "./hashes.Packet.ts"; import * as Partial from "./hashes.Partial.ts"; import * as Priorities$random from "./hashes.Priorities$random.ts"; +import * as Seeded from "./hashes.Seeded.ts"; import * as Shape from "./hashes.Shape.ts"; import * as Small from "./hashes.Small.ts"; @@ -871,4 +877,114 @@ export function own(data: rt.ByteSlice, out: rt.ByteVec): void { rt.byteVecExtendFromSlice(out, rt.byteVecSlice(log.value)); } +export const EVENTS: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(EVENTS$init, false); + +export function EVENTS$init(): rt.RefCell { + return new rt.RefCell(new rt.ByteVec()); +} + +/** Log `what` happened to the value numbered `n`. */ +export function event(what: rt.u8, n: rt.u8): void { + ((events: rt.RefCell): undefined => { + const _52: rt.RefCell = rt.refCellBorrowMut(events); + rt.iterExtendBytes(_52.value, rt.iterSlice(Uint8Array.of(what, n)), undefined, true); + rt.refMutRelease(_52); + })(EVENTS.get()); +} + +/** + * Hash maps and sets with a `BuildHasher` of the crate's own, keyed by JS objects and by + * numbers, which find their keys whatever it hashes them to. Each holds its own, which + * `hasher` lends, made by `Default` where std's makes one, cloned before what the map + * holds, and dropped before it, and at once by a consuming iterator. Only maps of one + * entry say what they drop or clone, and only sorted what they hold, as std's order is + * its own. + */ +export function maps(data: rt.ByteSlice, out: rt.ByteVec): void { + ((events: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(events); + rt.byteVecClear(_5.value); + rt.refMutRelease(_5); + })(EVENTS.get()); + const first: rt.u8 = option_unwrapOr$0r2bmpr(rt.byteSliceFirst(data), 7); + const keyed: rt.HashMap = rt.hashWithCapacity(data.length, 58720256, rt.hashing(Key.hash$Hash_hash_DefaultHasher as (a0: Key.Key, a1: rt.Hasher) => undefined, Key.eq$cmp_PartialEq), new Seeded.Seeded(first)) as rt.HashMap; + const seen: rt.HashSet = rt.hashNew(undefined, new Seeded.Seeded(1)) as rt.HashSet; + const iter: rt.Iter<[rt.usize, rt.u8]> = rt.iterEnumerate(rt.iterSlice(rt.byteSliceView(data))); + while (true) { + const _17: [rt.usize, rt.u8] | rt.Done = iter.next(); + if (_17 === rt.DONE) { + break; + } + const i: rt.usize = _17[0]; + const b: rt.u8 = _17[1]; + const _26: rt.u16 = rt.u8Rem(b, 8) as rt.u16; + const bounds = rt.sliceIndexBounds(new rt.RangeInclusive(i, i, rt.usizeStep), data.length); + const _25: Key.Key = { _0: _26, _1: text(rt.byteSliceRange(data, bounds[0], bounds[1])) }; + const _22: rt.Place = rt.entryOrInsertWithMut(rt.entry(keyed, _25), (): rt.u8 => { + return 0; + }); + _22.v = rt.u8Add(_22.v, 1); + rt.byteVecPush(out, rt.hashAdd(seen, rt.u8Rem(b, 16)) ? 1 : 0); + const _44: Key.Key = { _0: (rt.u8Rem(b, 8) ^ 1) as rt.u16, _1: text(rt.byteSliceFrom(Uint8Array.of(b ^ 1))) }; + rt.byteVecPush(out, rt.hashContains(keyed, _44) ? 1 : 0); + } + const held: [Key.Key, rt.u8][] = rt.iterToArray(rt.hashIter(keyed), "next", 8); + rt.sliceSort(held, rt.cmpLt(rt.cmpTuple<[Key.Key, rt.u8], rt.Ordering | undefined>([Key.partialCmp$cmp_PartialOrd, rt.cmp])), 8, true); + for (const _63 of held) { + rt.byteVecPush(out, _63[0]._0 & 0xff); + put(out, _63[0]._1); + rt.byteVecPush(out, _63[1]); + } + const probe: Key.Key = { _0: first as rt.u16, _1: text(rt.byteSliceFrom(Uint8Array.of(first))) }; + rt.byteVecPush(out, ((keyed.s as Seeded.Seeded))._0); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.hashOne(new rt.OwnHasher(Seeded.buildHasher$hash_BuildHasher(keyed.s as Seeded.Seeded), Fnv.hasher$hash_Hasher), probe, rt.hashOwn(Key.hash$Hash_hashes_Fnv as (a0: Key.Key, a1: Fnv.Fnv) => undefined))).reverse())); + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.hashOne(new rt.OwnHasher(Seeded.buildHasher$hash_BuildHasher(seen.s as Seeded.Seeded), Fnv.hasher$hash_Hasher), first, rt.hash8)).reverse())); + const noisy: rt.HashMap = rt.hashNew(undefined, new Seeded.Seeded(5)) as rt.HashMap; + rt.drop(rt.hashInsert(noisy, first, new Noisy.Noisy(first))); + const copy: rt.HashMap = rt.hashClone(noisy, undefined, Noisy.clone$Clone, Seeded.clone$Clone); + rt.byteVecPush(out, ((copy.s as Seeded.Seeded))._0); + rt.hashDrop(noisy, undefined, rt.drop, rt.drop); + const entries: rt.Iter<[rt.u8, Noisy.Noisy]> = rt.hashIter(copy, undefined, rt.drop, rt.drop); + event(105, 0); + drop$Option_tuple2_u8_hashes_Noisy(rt.iterToOption(entries.next())); + rt.drop(entries); + let made: rt.HashMap = rt.hashNew(undefined, Seeded.default$Default()) as rt.HashMap; + rt.drop(rt.hashInsert(made, 2, new Noisy.Noisy(2))); + const default_ = rt.hashNew(undefined, Seeded.default$Default()) as rt.HashMap; + const taken2 = made; + made = default_; + const taken: rt.HashMap = taken2; + rt.byteVecPush(out, rt.hashLen(taken) & 0xff); + rt.byteVecPush(out, rt.hashLen(made) & 0xff); + const _136: rt.Iter = rt.iterMap(rt.iterSlice(rt.byteSliceView(data)), (b2: rt.u8): rt.u8 => { + return rt.u8Rem(b2, 4); + }); + const small: rt.HashSet = rt.hashExtendKeys(rt.hashNew(undefined, Seeded.default$Default()) as rt.HashSet, _136, undefined, undefined, 469762048); + const union: rt.HashSet = rt.hashExtendKeys(rt.hashNew(undefined, Seeded.default$Default()) as rt.HashSet, rt.hashUnion(small, seen), undefined, undefined, 469762048); + rt.byteVecPush(out, rt.hashLen(union) & 0xff); + rt.byteVecPush(out, rt.hashIsSubset(small, seen) ? 1 : 0); + rt.hashExtendKeys(seen, rt.hashKeys(small, undefined, undefined, rt.drop), undefined, undefined, 469762048); + rt.byteVecPush(out, rt.hashLen(seen) & 0xff); + const keys: Key.Key[] = rt.iterToArray(rt.hashKeys(keyed, undefined, undefined, rt.drop), "next", 16); + rt.sliceSort(keys, rt.cmpLt(Key.partialCmp$cmp_PartialOrd), 16, true); + rt.byteVecPush(out, keys.length & 0xff); + const iter3: rt.Iter = rt.hashValues(taken, rt.drop, undefined, rt.drop); + while (true) { + const _169: Noisy.Noisy | rt.Done = iter3.next(); + if (_169 === rt.DONE) { + break; + } + const value: Noisy.Noisy = _169; + event(118, value._0); + value[Symbol.dispose](); + } + rt.drop(iter3); + rt.hashDrop(union, undefined, undefined, rt.drop); + rt.byteVecExtendFromSlice(out, rt.byteVecSlice(((events2: rt.RefCell): rt.ByteVec => { + return rt.refCellReplace(events2, new rt.ByteVec()); + })(EVENTS.get()))); + rt.hashDrop(made, undefined, rt.drop, rt.drop); + rt.hashDrop(seen, undefined, undefined, rt.drop); +} + //# sourceMappingURL=hashes.ts.map diff --git a/ts/fixtures/lang/hashes/expected/lib.ts b/ts/fixtures/lang/hashes/expected/lib.ts index c4f54d953..c6f28fa4a 100644 --- a/ts/fixtures/lang/hashes/expected/lib.ts +++ b/ts/fixtures/lang/hashes/expected/lib.ts @@ -1,7 +1,18 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { values, random, bytes, own } from "./hashes.ts"; +import { values, random, bytes, own, maps } from "./hashes.ts"; +import type * as Noisy from "./hashes.Noisy.ts"; + +export function drop$Option_tuple2_u8_hashes_Noisy(_1: [rt.u8, Noisy.Noisy] | undefined): void { + if (_1 !== undefined) { + drop$tuple2_u8_hashes_Noisy(_1); + } +} + +export function drop$tuple2_u8_hashes_Noisy(_1: [rt.u8, Noisy.Noisy]): void { + _1[1][Symbol.dispose](); +} /** * Each case's name and what it runs. The first two bytes, big-endian, pick one modulo @@ -14,6 +25,8 @@ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) bytes(data2, out2); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::own", (_24: rt.u8, data3: rt.ByteSlice, out3: rt.ByteVec): undefined => { own(data3, out3); + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::maps", (_25: rt.u8, data4: rt.ByteSlice, out4: rt.ByteVec): undefined => { + maps(data4, out4); }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined]]; } diff --git a/ts/fixtures/lang/hashes/src/hashes.rs b/ts/fixtures/lang/hashes/src/hashes.rs index 90b461dde..660b947e8 100644 --- a/ts/fixtures/lang/hashes/src/hashes.rs +++ b/ts/fixtures/lang/hashes/src/hashes.rs @@ -2,15 +2,19 @@ //! `DefaultHasher::new` keys it, whose values must be native's byte for byte, and //! `RandomState`, whose keys are random on both sides: its case prints only what holds //! whatever they are, such as one state hashing equal values alike, and never a hash. A -//! `Hasher` of the crate's own logs each call `Hash` makes of it. +//! `Hasher` of the crate's own logs each call `Hash` makes of it. Then hash maps and sets +//! that hold a `BuildHasher` of the crate's own. use std::cell::{Cell, RefCell}; use std::cmp::Reverse; +use std::collections::{HashMap, HashSet}; use std::hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}; use std::num::NonZero; use std::sync::Arc; use std::time::Duration; +use crate::harness::{put, text}; + #[derive(Hash)] struct Packet { id: u64, @@ -504,3 +508,139 @@ pub fn own(data: &[u8], out: &mut Vec) { } out.extend_from_slice(&log.into_inner()); } + +thread_local! { + /// What each [`Seeded`] and [`Noisy`] did, in order. + static EVENTS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Log `what` happened to the value numbered `n`. +fn event(what: u8, n: u8) { + EVENTS.with(|events| events.borrow_mut().extend([what, n])); +} + +/// FNV-1a, a `Hasher` of the crate's own that logs nothing, so a map may call it as often +/// as it likes. +struct Fnv(u64); + +impl Hasher for Fnv { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3); + } + } +} + +/// A `BuildHasher` of the crate's own, numbered, which logs each one `Default` makes, each +/// clone, and each drop, and seeds its [`Fnv`] by its number. +struct Seeded(u8); + +impl BuildHasher for Seeded { + type Hasher = Fnv; + + fn build_hasher(&self) -> Fnv { + Fnv(0xcbf2_9ce4_8422_2325 ^ u64::from(self.0)) + } +} + +impl Default for Seeded { + fn default() -> Self { + event(b'n', 0); + Seeded(0) + } +} + +impl Clone for Seeded { + fn clone(&self) -> Self { + event(b'c', self.0); + Seeded(self.0.wrapping_add(1)) + } +} + +impl Drop for Seeded { + fn drop(&mut self) { + event(b'x', self.0); + } +} + +/// A value that logs its clone and its drop. +struct Noisy(u8); + +impl Clone for Noisy { + fn clone(&self) -> Self { + event(b'C', self.0); + Noisy(self.0) + } +} + +impl Drop for Noisy { + fn drop(&mut self) { + event(b'D', self.0); + } +} + +/// A key that is a JS object. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct Key(u16, String); + +/// Hash maps and sets with a `BuildHasher` of the crate's own, keyed by JS objects and by +/// numbers, which find their keys whatever it hashes them to. Each holds its own, which +/// `hasher` lends, made by `Default` where std's makes one, cloned before what the map +/// holds, and dropped before it, and at once by a consuming iterator. Only maps of one +/// entry say what they drop or clone, and only sorted what they hold, as std's order is +/// its own. +pub fn maps(data: &[u8], out: &mut Vec) { + EVENTS.with(|events| events.borrow_mut().clear()); + let first = data.first().copied().unwrap_or(7); + let mut keyed: HashMap = HashMap::with_capacity_and_hasher(data.len(), Seeded(first)); + let mut seen: HashSet = HashSet::with_hasher(Seeded(1)); + for (i, &b) in data.iter().enumerate() { + *keyed.entry(Key(u16::from(b % 8), text(&data[i..=i]))).or_default() += 1; + out.push(seen.insert(b % 16) as u8); + out.push(keyed.contains_key(&Key(u16::from(b % 8 ^ 1), text(&[b ^ 1]))) as u8); + } + let mut held: Vec<(&Key, &u8)> = keyed.iter().collect(); + held.sort(); + for (Key(n, text), count) in held { + out.push(*n as u8); + put(out, text); + out.push(*count); + } + // What `hasher` lends is the map's own. + let probe = Key(u16::from(first), text(&[first])); + out.push(keyed.hasher().0); + out.extend_from_slice(&keyed.hasher().hash_one(&probe).to_le_bytes()); + out.extend_from_slice(&seen.hasher().hash_one(first).to_le_bytes()); + let mut noisy: HashMap = HashMap::with_hasher(Seeded(5)); + noisy.insert(first, Noisy(first)); + let copy = noisy.clone(); + out.push(copy.hasher().0); + drop(noisy); + let mut entries = copy.into_iter(); + event(b'i', 0); + drop(entries.next()); + drop(entries); + let mut made: HashMap = HashMap::default(); + made.insert(2, Noisy(2)); + let taken = std::mem::take(&mut made); + out.push(taken.len() as u8); + out.push(made.len() as u8); + let small: HashSet = data.iter().map(|b| b % 4).collect(); + let union = &small | &seen; + out.push(union.len() as u8); + out.push(small.is_subset(&seen) as u8); + seen.extend(small); + out.push(seen.len() as u8); + let mut keys: Vec = keyed.into_keys().collect(); + keys.sort(); + out.push(keys.len() as u8); + for value in taken.into_values() { + event(b'v', value.0); + } + drop(union); + out.extend_from_slice(&EVENTS.with(|events| events.take())); +} diff --git a/ts/fixtures/lang/hashes/src/lib.rs b/ts/fixtures/lang/hashes/src/lib.rs index 262d67509..771912c81 100644 --- a/ts/fixtures/lang/hashes/src/lib.rs +++ b/ts/fixtures/lang/hashes/src/lib.rs @@ -22,5 +22,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("hashes::random", |_, data, out| hashes::random(data, out)), ("hashes::bytes", |_, data, out| hashes::bytes(data, out)), ("hashes::own", |_, data, out| hashes::own(data, out)), + ("hashes::maps", |_, data, out| hashes::maps(data, out)), ] } diff --git a/ts/fixtures/stock/expected/core/option.ts b/ts/fixtures/stock/expected/core/option.ts index 308651a8c..8be4bd5d6 100644 --- a/ts/fixtures/stock/expected/core/option.ts +++ b/ts/fixtures/stock/expected/core/option.ts @@ -3,6 +3,13 @@ import * as rt from "../rt.ts"; import type * as Logged from "../maps.Logged.ts"; +export function unwrapOr$11c0we2(self: rt.u32 | undefined, default_: rt.u32): rt.u32 { + if (self === undefined) { + return default_; + } + return self; +} + export function unwrapOr$0r2bmpr(self: rt.u8 | undefined, default_: rt.u8): rt.u8 { if (self === undefined) { return default_; diff --git a/ts/fixtures/stock/expected/harness.ts b/ts/fixtures/stock/expected/harness.ts index 858aeb0a9..af286afb4 100644 --- a/ts/fixtures/stock/expected/harness.ts +++ b/ts/fixtures/stock/expected/harness.ts @@ -3,7 +3,7 @@ import * as rt from "./rt.ts"; import { bytesMut, reserves, reads, dyns, writes, edits, chains } from "./buf.ts"; import * as Owner from "./harness.Owner.ts"; -import { map as maps_map, table, capacity, linked, linkedIters, nested } from "./maps.ts"; +import { map as maps_map, table, capacity, linked, linkedIters, nested, ownHasher } from "./maps.ts"; /** * Each case's name and what it runs. The first byte picks one modulo their count, so a @@ -36,7 +36,7 @@ export function cases(): [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefine chains(a012.data, a012.start, a012.length, a112); }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::nested", (a013: rt.ByteSlice, a113: rt.ByteVec): undefined => { nested(a013.data, a013.start, a013.length, a113); - }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined]]; + }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::own_hasher", ownHasher as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined]]; } /** Run one case. */ diff --git a/ts/fixtures/stock/expected/lib.ts b/ts/fixtures/stock/expected/lib.ts index 619715e9f..122a9167b 100644 --- a/ts/fixtures/stock/expected/lib.ts +++ b/ts/fixtures/stock/expected/lib.ts @@ -63,6 +63,12 @@ export function drop$Option_tuple2_maps_Logged_maps_Logged(_1: [Logged.Logged, L } } +export function drop$Option_tuple2_tuple2_u8_u8_maps_Logged(_1: [[rt.u8, rt.u8], Logged.Logged] | undefined): void { + if (_1 !== undefined) { + drop$tuple2_tuple2_u8_u8_maps_Logged(_1); + } +} + export function drop$Option_tuple2_u16_maps_Logged(_1: [rt.u16, Logged.Logged] | undefined): void { if (_1 !== undefined) { drop$tuple2_u16_maps_Logged(_1); @@ -160,10 +166,20 @@ export function drop$tuple2_sync_Arc_str_u32(_1: [rt.Arc, rt.u32]): void _1[0][Symbol.dispose](); } +export function drop$tuple2_tuple2_u8_u8_maps_Logged(_1: [[rt.u8, rt.u8], Logged.Logged]): void { + _1[1][Symbol.dispose](); +} + export function drop$tuple2_u16_maps_Logged(_1: [rt.u16, Logged.Logged]): void { _1[1][Symbol.dispose](); } +export function drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded(_1: [rt.HashMap<[rt.u8, rt.u8], rt.u32>, rt.HashSet, rt.HashSet]): void { + rt.hashDrop(_1[0], undefined, undefined, rt.drop); + rt.hashDrop(_1[1], undefined, undefined, rt.drop); + rt.hashDrop(_1[2], undefined, undefined, rt.drop); +} + export class of$buf_Counted { v: buf_Counted.Counted; diff --git a/ts/fixtures/stock/expected/maps.Seeded.ts b/ts/fixtures/stock/expected/maps.Seeded.ts new file mode 100644 index 000000000..755176295 --- /dev/null +++ b/ts/fixtures/stock/expected/maps.Seeded.ts @@ -0,0 +1,36 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** + * A `BuildHasher` of the crate's own, numbered, which logs its clone and its drop, and + * builds std's `DefaultHasher`. + */ +export class Seeded { + _0: rt.u8; + _1: rt.RefCell; + + constructor(_0: rt.u8, _1: rt.RefCell) { + this._0 = _0; + this._1 = _1; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function clone$Clone(self: Seeded): Seeded { + const _5: rt.RefCell = rt.refCellBorrowMut(self._1); + rt.byteVecPush(_5.value, 99); + rt.refMutRelease(_5); + return new Seeded(rt.u8Add(self._0, 1), self._1); +} + +export function drop$ops_Drop(self: Seeded): void { + const _5: rt.RefCell = rt.refCellBorrowMut(self._1); + rt.iterExtendBytes(_5.value, rt.iterSlice(Uint8Array.of(120, self._0)), undefined, true); + rt.refMutRelease(_5); +} + +//# sourceMappingURL=maps.Seeded.ts.map diff --git a/ts/fixtures/stock/expected/maps.ts b/ts/fixtures/stock/expected/maps.ts index aa229ebe7..4f65ecea0 100644 --- a/ts/fixtures/stock/expected/maps.ts +++ b/ts/fixtures/stock/expected/maps.ts @@ -1,8 +1,8 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { copy$tuple2_u16_u32, drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged as lib_drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged, drop$hashlink_LinkedHashMap_sync_Arc_str_u32 as lib_drop$hashlink_LinkedHashMap_sync_Arc_str_u32, drop$hashlink_LinkedHashMap_u16_u32 as lib_drop$hashlink_LinkedHashMap_u16_u32, drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState as lib_drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState, drop$hashlink_LinkedHashSet_maps_Logged as lib_drop$hashlink_LinkedHashSet_maps_Logged, drop$hashlink_LruCache_maps_Logged_maps_Logged as lib_drop$hashlink_LruCache_maps_Logged_maps_Logged, drop$hashlink_LruCache_u16_maps_Logged as lib_drop$hashlink_LruCache_u16_maps_Logged, drop$hashlink_linked_hash_map_Drain_u16_u32 as lib_drop$hashlink_linked_hash_map_Drain_u16_u32, drop$hashlink_linked_hash_map_IntoIter_u16_u32 as lib_drop$hashlink_linked_hash_map_IntoIter_u16_u32, drop$maps_Held_maps_Logged as lib_drop$maps_Held_maps_Logged } from "./lib.ts"; -import { unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr, unwrapOr$0l7j3f6 as option_unwrapOr$0l7j3f6, mapOr$085zvfb as option_mapOr$085zvfb, mapOr$1787w44 as option_mapOr$1787w44, mapOr$0iaogpf as option_mapOr$0iaogpf, mapOr$0yixjb2 as option_mapOr$0yixjb2, mapOr$1vcgixw as option_mapOr$1vcgixw, mapOr$1noewr3 as option_mapOr$1noewr3 } from "./core/option.ts"; +import { copy$tuple2_u16_u32, drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged as lib_drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged, drop$Option_tuple2_tuple2_u8_u8_maps_Logged as lib_drop$Option_tuple2_tuple2_u8_u8_maps_Logged, drop$hashlink_LinkedHashMap_sync_Arc_str_u32 as lib_drop$hashlink_LinkedHashMap_sync_Arc_str_u32, drop$hashlink_LinkedHashMap_u16_u32 as lib_drop$hashlink_LinkedHashMap_u16_u32, drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState as lib_drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState, drop$hashlink_LinkedHashSet_maps_Logged as lib_drop$hashlink_LinkedHashSet_maps_Logged, drop$hashlink_LruCache_maps_Logged_maps_Logged as lib_drop$hashlink_LruCache_maps_Logged_maps_Logged, drop$hashlink_LruCache_u16_maps_Logged as lib_drop$hashlink_LruCache_u16_maps_Logged, drop$hashlink_linked_hash_map_Drain_u16_u32 as lib_drop$hashlink_linked_hash_map_Drain_u16_u32, drop$hashlink_linked_hash_map_IntoIter_u16_u32 as lib_drop$hashlink_linked_hash_map_IntoIter_u16_u32, drop$maps_Held_maps_Logged as lib_drop$maps_Held_maps_Logged, drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded as lib_drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded } from "./lib.ts"; +import { unwrapOr$11c0we2 as option_unwrapOr$11c0we2, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr, unwrapOr$0l7j3f6 as option_unwrapOr$0l7j3f6, mapOr$085zvfb as option_mapOr$085zvfb, mapOr$1787w44 as option_mapOr$1787w44, mapOr$0iaogpf as option_mapOr$0iaogpf, mapOr$0yixjb2 as option_mapOr$0yixjb2, mapOr$1vcgixw as option_mapOr$1vcgixw, mapOr$1noewr3 as option_mapOr$1noewr3 } from "./core/option.ts"; import * as linked_hash_map_Drain from "./hashlink/linked_hash_map.Drain.ts"; import * as linked_hash_map_IntoIter from "./hashlink/linked_hash_map.IntoIter.ts"; import * as linked_hash_map_Iter from "./hashlink/linked_hash_map.Iter.ts"; @@ -17,6 +17,7 @@ import type * as DefaultHashBuilder from "./hashlink.DefaultHashBuilder.ts"; import * as maps_Counted from "./maps.Counted.ts"; import * as Held from "./maps.Held.ts"; import * as Logged from "./maps.Logged.ts"; +import * as Seeded from "./maps.Seeded.ts"; /** A length, capacity, or value, as four bytes. */ export function put(out: rt.ByteVec, n: rt.usize): void { @@ -1159,4 +1160,54 @@ export function nested(dataData: Uint8Array, dataStart: number, dataLen: number, } } +/** + * `hashbrown::HashMap` and `HashSet` with a `BuildHasher` of the crate's own, of keys that + * are JS objects and keys that are not: each holds its own, which `hasher` lends, clones + * it before what it holds, and drops it before them, and at once from `into_iter`. Only + * maps of one entry say what they drop, as hashbrown's order is its own. + */ +export function ownHasher(data: rt.ByteSlice, out: rt.ByteVec): void { + let _36!: rt.u8; + let _43!: rt.u8; + const log: rt.RefCell = new rt.RefCell(new rt.ByteVec()); + const _6: Seeded.Seeded = new Seeded.Seeded(1, log); + const pairs: rt.HashMap<[rt.u8, rt.u8], rt.u32> = rt.hashNew(rt.hashing(rt.hashTuple([rt.hash8, rt.hash8]), rt.eqTuple([rt.same, rt.same])), _6) as rt.HashMap<[rt.u8, rt.u8], rt.u32>; + const set: rt.HashSet = rt.hashWithCapacity(data.length, 469762048, undefined, new Seeded.Seeded(10, log)) as rt.HashSet; + for (const b of rt.byteSliceView(data)) { + const _21: [rt.u8, rt.u8] = [rt.u8Rem(b, 4), rt.u8Rem(b, 3)]; + const _18: rt.Place = rt.entryOrInsertMut(rt.entry(pairs, _21), 0); + _18.v = rt.u32Add(_18.v, 1); + rt.byteVecPush(out, rt.hashAdd(set, rt.u8Rem(b, 8)) ? 1 : 0); + } + let iterStart: rt.u8 = 0; + const iterEnd: rt.u8 = 4; + l3: while ((_36 = iterStart++) < iterEnd) { + const a: rt.u8 = _36; + let iterStart2: rt.u8 = 0; + const iterEnd2: rt.u8 = 3; + while (true) { + if (!((_43 = iterStart2++) < iterEnd2)) { + continue l3; + } + put(out, option_unwrapOr$11c0we2(rt.hashGet(pairs, [a, _43] as [rt.u8, rt.u8]), 0) as rt.usize); + } + } + rt.byteVecPush(out, ((pairs.s as Seeded.Seeded))._0); + rt.byteVecPush(out, ((set.s as Seeded.Seeded))._0); + const _64: Seeded.Seeded = new Seeded.Seeded(20, log); + const one: rt.HashMap<[rt.u8, rt.u8], Logged.Logged> = rt.hashNew(rt.hashing(rt.hashTuple([rt.hash8, rt.hash8]), rt.eqTuple([rt.same, rt.same])), _64) as rt.HashMap<[rt.u8, rt.u8], Logged.Logged>; + const first: rt.u8 = option_unwrapOr$0r2bmpr(rt.byteSliceFirst(data), 0); + rt.drop(rt.hashInsert(one, [first, 0] as [rt.u8, rt.u8], new Logged.Logged(first, log))); + const copy: rt.HashSet = rt.hashClone(set, undefined, undefined, Seeded.clone$Clone); + rt.byteVecPush(out, ((copy.s as Seeded.Seeded))._0); + const entries: rt.Iter<[[rt.u8, rt.u8], Logged.Logged]> = rt.hashIter(one, undefined, rt.drop, rt.drop); + const _85: rt.RefCell = rt.refCellBorrowMut(log); + rt.byteVecPush(_85.value, 105); + rt.refMutRelease(_85); + lib_drop$Option_tuple2_tuple2_u8_u8_maps_Logged(rt.iterToOption(entries.next())); + rt.drop(entries); + lib_drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded([pairs, set, copy] as [rt.HashMap<[rt.u8, rt.u8], rt.u32>, rt.HashSet, rt.HashSet]); + rt.byteVecExtendFromSlice(out, rt.byteVecSlice(rt.refCellReplace(log, new rt.ByteVec()))); +} + //# sourceMappingURL=maps.ts.map diff --git a/ts/fixtures/stock/src/harness.rs b/ts/fixtures/stock/src/harness.rs index 50c2ddf9c..81a2f76fb 100644 --- a/ts/fixtures/stock/src/harness.rs +++ b/ts/fixtures/stock/src/harness.rs @@ -28,6 +28,7 @@ fn cases() -> Vec<(&'static str, Case)> { ("maps::linked_iters", crate::maps::linked_iters), ("buf::chains", crate::buf::chains), ("maps::nested", crate::maps::nested), + ("maps::own_hasher", crate::maps::own_hasher), ] } diff --git a/ts/fixtures/stock/src/maps.rs b/ts/fixtures/stock/src/maps.rs index 2a2d8d722..b4f6b7b42 100644 --- a/ts/fixtures/stock/src/maps.rs +++ b/ts/fixtures/stock/src/maps.rs @@ -608,3 +608,60 @@ pub fn nested(data: &[u8], out: &mut Vec) { } out.extend(log.borrow().iter()); } + +/// A `BuildHasher` of the crate's own, numbered, which logs its clone and its drop, and +/// builds std's `DefaultHasher`. +struct Seeded<'a>(u8, &'a RefCell>); + +impl BuildHasher for Seeded<'_> { + type Hasher = std::hash::DefaultHasher; + + fn build_hasher(&self) -> Self::Hasher { + std::hash::DefaultHasher::new() + } +} + +impl Clone for Seeded<'_> { + fn clone(&self) -> Self { + self.1.borrow_mut().push(b'c'); + Seeded(self.0 + 1, self.1) + } +} + +impl Drop for Seeded<'_> { + fn drop(&mut self) { + self.1.borrow_mut().extend([b'x', self.0]); + } +} + +/// `hashbrown::HashMap` and `HashSet` with a `BuildHasher` of the crate's own, of keys that +/// are JS objects and keys that are not: each holds its own, which `hasher` lends, clones +/// it before what it holds, and drops it before them, and at once from `into_iter`. Only +/// maps of one entry say what they drop, as hashbrown's order is its own. +pub fn own_hasher(data: &[u8], out: &mut Vec) { + let log = RefCell::new(Vec::new()); + let mut pairs: HashMap<(u8, u8), u32, Seeded> = HashMap::with_hasher(Seeded(1, &log)); + let mut set: HashSet = HashSet::with_capacity_and_hasher(data.len(), Seeded(10, &log)); + for &b in data { + *pairs.entry((b % 4, b % 3)).or_insert(0) += 1; + out.push(u8::from(set.insert(b % 8))); + } + for a in 0..4 { + for b in 0..3 { + put(out, pairs.get(&(a, b)).copied().unwrap_or(0) as usize); + } + } + out.push(pairs.hasher().0); + out.push(set.hasher().0); + let mut one: HashMap<(u8, u8), Logged, Seeded> = HashMap::with_hasher(Seeded(20, &log)); + let first = data.first().copied().unwrap_or(0); + one.insert((first, 0), Logged(first, &log)); + let copy = HashSet::::clone(&set); + out.push(copy.hasher().0); + let mut entries = one.into_iter(); + log.borrow_mut().push(b'i'); + drop(entries.next()); + drop(entries); + drop((pairs, set, copy)); + out.extend_from_slice(&log.take()); +} diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index 8f1c22c6d..294428907 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -441,6 +441,11 @@ export interface HashMap { n: number; /** How to find a key that is an object, or `undefined` if no key is one. */ readonly h: Hashing | undefined; + /** + * Its own `BuildHasher`, which `hasher` lends, or `undefined` for a `RandomState`. No + * key goes through it: `h` finds them by the runtime's own hasher. + */ + readonly s: unknown; /** The capacity an operation last set it to, if any, which `hashCapacity` reads. */ cap?: number; } @@ -449,11 +454,12 @@ export interface HashMap { export type HashSet = HashMap; /** - * `HashMap::new` and `HashSet::new`, with how to find a key that is an object. The type - * `K` is asserted, not inferred from `h`: a slice's hashing takes any `ArrayLike`. + * `HashMap::new` and `HashSet::new`, with how to find a key that is an object, and + * `with_hasher` of its own `BuildHasher` `s`. The type `K` is asserted, not inferred from + * `h`: a slice's hashing takes any `ArrayLike`. */ -export function hashNew(h?: Hashing>): HashMap { - return { p: new Map(), o: undefined, n: 0, h }; +export function hashNew(h?: Hashing>, s?: unknown): HashMap { + return { p: new Map(), o: undefined, n: 0, h, s }; } /** Whether `key` goes by its hash rather than as its own JS map key. */ @@ -495,9 +501,9 @@ function hold(m: HashMap): void { } /** `with_capacity`, of at most `max` entries, past which it panics, as hashbrown's does. */ -export function hashWithCapacity(n: number, max: number, h?: Hashing>): HashMap { +export function hashWithCapacity(n: number, max: number, h?: Hashing>, s?: unknown): HashMap { if (n > max) panic("Hash table capacity overflow"); - const m = hashNew(h); + const m = hashNew(h, s); if (n > 0) m.cap = n; return m; } @@ -725,8 +731,12 @@ export function hashKey(m: HashSet, key: K, take = false, none?: None): K return key; } -/** Drop `m`: each key by `k` and each value by `v`, of those that drop, entry by entry. */ -export function hashDrop(m: HashMap, k: ((key: K) => void) | undefined, v?: (value: V) => void): void { +/** + * Drop `m`: its own `BuildHasher` by `s`, as hashbrown's field before its table, then each + * key by `k` and each value by `v`, of those that drop, entry by entry. + */ +export function hashDrop(m: HashMap, k: ((key: K) => void) | undefined, v?: (value: V) => void, s?: (state: never) => void): void { + s?.(m.s as never); for (const [key, value] of m.p) dropPair(key, value, k, v); for (const first of m.o?.values() ?? []) { for (let s: Slot | undefined = first; s !== undefined; s = s.next) dropPair(s.k, s.v, k, v); @@ -927,19 +937,23 @@ export class HashIter extends Iter implements Disposable { /** * `iter` and `iter_mut` of a map, and with what drops each key and value of those that - * drop, `into_iter`: its entries, as `[key, value]`. + * drop, `into_iter`: its entries, as `[key, value]`. Each consuming one drops the map's own + * `BuildHasher` by `s` at once, as hashbrown's does. */ -export function hashIter(m: HashMap, k?: (key: K) => void, v?: (value: V) => void): Iter<[K, V]> { +export function hashIter(m: HashMap, k?: (key: K) => void, v?: (value: V) => void, s?: (state: never) => void): Iter<[K, V]> { + s?.(m.s as never); return new HashIter(m, ENTRIES, k, v); } /** `keys` of a map and `iter` of a set, and with what drops each key and value, `into_keys` and `into_iter`. */ -export function hashKeys(m: HashMap, k?: (key: K) => void, v?: (value: V) => void): Iter { +export function hashKeys(m: HashMap, k?: (key: K) => void, v?: (value: V) => void, s?: (state: never) => void): Iter { + s?.(m.s as never); return new HashIter(m, KEYS, k, v); } /** `values` and `values_mut`, and with what drops each value and key, `into_values`. */ -export function hashValues(m: HashMap, v?: (value: V) => void, k?: (key: K) => void): Iter { +export function hashValues(m: HashMap, v?: (value: V) => void, k?: (key: K) => void, s?: (state: never) => void): Iter { + s?.(m.s as never); return new HashIter(m, VALUES, k, v); } @@ -966,7 +980,7 @@ export function hashDrainKeys(m: HashMap, k?: (key: K) => void): Ite /** What `m` held, which it no longer does, with the room it had. */ function taken(m: HashMap): HashMap { hold(m); - const out: HashMap = { p: m.p, o: m.o, n: m.n, h: m.h }; + const out: HashMap = { p: m.p, o: m.o, n: m.n, h: m.h, s: undefined }; m.p = new Map(); hashClear(m); return out; @@ -1037,9 +1051,17 @@ export function hashExtendKeys(m: HashSet, keys: Iter, k?: (key: K) => return extending(m, keys, (key) => hashAdd(m, key, k), take); } -/** `clone`, with each key and value through its `Clone`, or as itself without one. */ -export function hashClone(m: HashMap, key: (k: K) => K = itself, value: (v: V) => V = itself): HashMap { - const out = hashNew(m.h); +/** + * `clone`, with its own `BuildHasher` through `state`, first, as hashbrown's, then each key + * and value through its `Clone`, or each as itself without one. + */ +export function hashClone( + m: HashMap, + key: (k: K) => K = itself, + value: (v: V) => V = itself, + state?: (s: never) => unknown, +): HashMap { + const out = hashNew(m.h, state === undefined ? m.s : state(m.s as never)); for (const [k, v] of m.p) out.p.set(key(k), value(v)); // Each clone keeps its original's hash, as hashbrown's copies its table without // rehashing, and a table has no hasher to rehash by. diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index a40ae86e7..0d0f52a01 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -559,17 +559,33 @@ fn hash<'tcx>( let none = library::sentinel_option(b.t, call).is_some(); let mut values = args; let e = match (owner, name) { - (_, "new" | "with_hasher") | ("Default", "default") => { + (_, "new") | ("Default", "default") => { + discard(values, out); + let state = new_state(b, coll, span); + hash_new(b, coll, state, span)? + } + // Its own `BuildHasher`, which it keeps, or a `RandomState`, which nothing reads. + (_, "with_hasher") => { + let state = held_state(b, coll).and_then(|_| values.pop()); discard(values, out); - hash_new(b, coll, span)? + hash_new(b, coll, state, span)? } (_, "with_capacity" | "with_capacity_and_hasher") => { let n = values.remove(0); + let state = held_state(b, coll).and_then(|_| values.pop()); discard(values, out); let mut values = vec![n, hash_max(b, coll, span)]; values.extend(hashing(b, coll, key, span)?); + if let Some(state) = state { + values.resize(3, Expr::undefined()); + values.push(state); + } empty(b, Expr::rt("hashWithCapacity", values), coll, span) } + (_, "hasher") if let Some(state) = held_state(b, coll) => { + let ty = b.t.ty(state, span); + Expr::As(Box::new(values.pop()?.member("s")), ty) + } (_, "len") => Expr::rt("hashLen", values), (_, "is_empty") => Expr::rt("hashLen", values).binary("===", Expr::lit("0")), (_, "clear") => Expr::rt("hashClear", values), @@ -596,9 +612,13 @@ fn hash<'tcx>( // does: before `next` returns, and after `fold` has the item. (_, "into_keys" | "into_values") => { let (kept, other) = if name == "into_keys" { (0, 1) } else { (1, 0) }; - let Some(droppers) = b.t.droppers([arg(coll, kept), arg(coll, other)], span) else { + let Some(mut droppers) = b.t.droppers([arg(coll, kept), arg(coll, other)], span) else { return Some(Expr::undefined()); }; + if let Some(state) = state_dropper(b, coll, span) { + droppers.resize(2, Expr::undefined()); + droppers.push(state); + } let f = if name == "into_keys" { "hashKeys" } else { "hashValues" }; Expr::rt(f, super::iter::tail(values, droppers)) } @@ -613,7 +633,8 @@ fn hash<'tcx>( (_, name) if let Some(f) = set_op("hash", name) => Expr::rt(f, values), ("BitOr" | "BitAnd" | "Sub" | "BitXor", _) => { let keys = set_op_keys(b, coll, "hash", owner, values, span)?; - let target = hash_new(b, coll, span)?; + let state = new_state(b, coll, span); + let target = hash_new(b, coll, state, span)?; let max = hash_max(b, coll, span); Expr::rt( "hashExtendKeys", @@ -630,13 +651,19 @@ fn hash<'tcx>( Some(v) => clone_fn(b, v, span), None => Expr::undefined(), }; - values.extend([k, v]); - Expr::rt("hashClone", values) + let s = held_state(b, coll).map(|s| clone_fn(b, s, span)); + Expr::rt( + "hashClone", + super::iter::tail(values, [k, v].into_iter().chain(s).collect()), + ) } ("Extend", "extend") | ("From", "from") => { let (source, iter) = extend_source(b, coll, call, owner, values.pop()?)?; let target = match owner { - "From" => hash_new(b, coll, span)?, + "From" => { + let state = new_state(b, coll, span); + hash_new(b, coll, state, span)? + } _ => values.pop()?, }; hash_extend(b, coll, target, source, iter, span) @@ -890,13 +917,58 @@ fn hash_max<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, span: Span) -> Exp Expr::lit(lo.to_string()) } -/// An empty hash collection of type `coll`, which finds its keys as their type needs. -fn hash_new<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, span: Span) -> Option { +/// An empty hash collection of type `coll`, which finds its keys as their type needs and +/// holds `state`, its own `BuildHasher`, if given. +fn hash_new<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, state: Option, span: Span) -> Option { let key = arg(coll, 0); - let values = hashing(b, coll, key, span)?.into_iter().collect(); + let mut values: Vec = hashing(b, coll, key, span)?.into_iter().collect(); + if let Some(state) = state { + values.resize(1, Expr::undefined()); + values.push(state); + } Some(empty(b, Expr::rt("hashNew", values), coll, span)) } +/// The `BuildHasher` of a `HashMap` or `HashSet` of type `coll`. +fn state_of<'tcx>(b: &Body<'_, '_, 'tcx>, coll: Ty<'tcx>) -> Option> { + let n = match b.t.std_of(coll)? { + Std::HashMap => 2, + Std::HashSet => 1, + _ => return None, + }; + let ty::Adt(_, args) = coll.kind() else { + return None; + }; + args.types().nth(n) +} + +/// The `BuildHasher` a hash collection of type `coll` holds, which `hasher` lends: any but +/// `RandomState`, whose keys nothing reads. +fn held_state<'tcx>(b: &Body<'_, '_, 'tcx>, coll: Ty<'tcx>) -> Option> { + state_of(b, coll).filter(|&state| b.t.std_of(state) != Some(Std::RandomState)) +} + +/// `Default::default` of the `BuildHasher` a new hash collection of type `coll` holds, if +/// it holds one. +fn new_state<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, span: Span) -> Option { + let state = held_state(b, coll)?; + let tcx = b.t.tcx; + let default = trait_fn(tcx, "Default", "default"); + let mut body = Vec::new(); + let value = b.call_def(default, tcx.mk_args(&[state.into()]), Vec::new(), span, &mut body); + if body.is_empty() { + return Some(value); + } + body.push(StmtKind::Return(Some(value.typed())).at(None)); + Some(Expr::Arrow(Vec::new(), b.t.ty(state, span), body).call(Vec::new())) +} + +/// What drops the `BuildHasher` a hash collection of type `coll` holds, if it drops. +fn state_dropper<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, span: Span) -> Option { + let state = held_state(b, coll)?; + b.t.drops(state).then(|| b.t.dropper(state, span)) +} + /// `new`, as an empty collection of type `coll`: TS infers a generic key's type, as of /// `rt.cmp`, from no argument, only from the type it is asserted to be. fn empty<'tcx>(b: &mut Body<'_, '_, 'tcx>, new: Expr, coll: Ty<'tcx>, span: Span) -> Expr { @@ -1039,16 +1111,24 @@ fn derived_eq<'tcx>(b: &Body<'_, '_, 'tcx>, ty: Ty<'tcx>) -> bool { .is_some_and(|imp| tcx.is_automatically_derived(imp)) } -/// The `Hasher` a hash collection of type `coll` hashes with: its `BuildHasher`'s. +/// The `Hasher` a hash collection of type `coll` hashes a key with, as the runtime's own +/// hasher stands in for it: its `RandomState`'s, or std's `DefaultHasher` in place of any +/// other `BuildHasher`'s, which no key goes through. fn hasher_ty<'tcx>(b: &Body<'_, '_, 'tcx>, coll: Ty<'tcx>) -> Option> { - let ty::Adt(_, args) = coll.kind() else { - return None; - }; - // `HashMap` and `HashSet`. - let state = args - .types() - .nth(if b.t.std_of(coll) == Some(Std::HashSet) { 1 } else { 2 })?; - built_hasher(b, state).filter(|&hasher| b.t.std_of(hasher) == Some(Std::Hasher)) + let tcx = b.t.tcx; + if held_state(b, coll).is_some() { + let std = tcx + .crates(()) + .iter() + .find(|&&c| tcx.crate_name(c) == rustc_span::sym::std)?; + let child = |module: DefId, name: &str| { + let child = tcx.module_children(module).iter().find(|c| c.ident.as_str() == name)?; + child.res.opt_def_id() + }; + let hasher = child(child(std.as_def_id(), "hash")?, "DefaultHasher")?; + return Some(tcx.type_of(hasher).instantiate_identity().skip_norm_wip()); + } + built_hasher(b, state_of(b, coll)?).filter(|&hasher| b.t.std_of(hasher) == Some(Std::Hasher)) } /// The `Hasher` a `BuildHasher` of type `state` builds. @@ -2145,7 +2225,10 @@ fn default<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, span: Span) -> Opti Std::VecDeque => deque_new(b, coll, span), Std::LinkedList => list_new(b, coll, span), Std::BinaryHeap => empty(b, Expr::rt("heapNew", Vec::new()), coll, span), - Std::HashMap | Std::HashSet => hash_new(b, coll, span)?, + Std::HashMap | Std::HashSet => { + let state = new_state(b, coll, span); + hash_new(b, coll, state, span)? + } Std::BTreeMap | Std::BTreeSet => btree_new(b, coll, span), _ => return None, }) @@ -3043,7 +3126,12 @@ pub fn into_iter<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, value: Expr, sp if !ty.is_ref() { // A deque's is the deque itself, which it pops from. let name = if name == "dequeIter" { "dequeIntoIter" } else { name }; - let droppers = entry_droppers(b, coll, span); + let mut droppers = entry_droppers(b, coll, span); + // A map's own `BuildHasher` drops at once, after what drops each entry. + if let Some(state) = state_dropper(b, coll, span) { + droppers.resize(2, Expr::undefined()); + droppers.push(state); + } return Some(Expr::rt(name, super::iter::tail(vec![value], droppers))); } // What it holds, borrowed. @@ -3067,7 +3155,8 @@ pub fn collect<'tcx>( let std = b.t.std_of(ty)?; Some(match std { Std::HashMap | Std::HashSet => { - let target = hash_new(b, ty, span)?; + let state = new_state(b, ty, span); + let target = hash_new(b, ty, state, span)?; hash_extend(b, ty, target, iter, from, span) } Std::BTreeMap | Std::BTreeSet => btree_collect(b, ty, iter, from, trusted, span), diff --git a/ts/src/lower/drop.rs b/ts/src/lower/drop.rs index b38cea08e..7516451cb 100644 --- a/ts/src/lower/drop.rs +++ b/ts/src/lower/drop.rs @@ -297,7 +297,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Std::Entry) if self.tcx.item_name(adt.did()).as_str() == "OccupiedEntry" => true, Some(Std::OnceCell | Std::OnceLock) => self.uniform(Ty::new_option(self.tcx, args.type_at(0))), Some(Std::Cow) => library::cow_slice(self.tcx, ty).is_none_or(|elem| self.uniform(elem)), - Some(Std::LazyCell | Std::LazyLock | Std::HashMap | Std::BTreeMap) => held(self, 2), + // A hash map's and set's own `BuildHasher` too. + Some(Std::HashMap) => held(self, 3), + Some(Std::HashSet) => held(self, 2), + Some(Std::LazyCell | Std::LazyLock | Std::BTreeMap) => held(self, 2), Some( Std::Arc | Std::Mutex @@ -309,7 +312,6 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { | Std::Wrapping | Std::Saturating | Std::PoisonError - | Std::HashSet | Std::HashTable | Std::BTreeSet | Std::VecDeque @@ -415,9 +417,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } // Its value, or the function that never made one. Std::LazyCell | Std::LazyLock => ("lazyDrop", vec![value, each(self, 0)?, each(self, 1)?]), - Std::HashMap => ("hashDrop", vec![value, each(self, 0)?, each(self, 1)?]), + // Its own `BuildHasher` last, which the runtime drops first, as hashbrown's field. + Std::HashMap => ("hashDrop", vec![value, each(self, 0)?, each(self, 1)?, each(self, 2)?]), Std::BTreeMap => ("btreeDrop", vec![value, each(self, 0)?, each(self, 1)?]), - Std::HashSet => ("hashDrop", vec![value, each(self, 0)?]), + Std::HashSet => ( + "hashDrop", + vec![value, each(self, 0)?, Expr::undefined(), each(self, 1)?], + ), // Its items are the values of a map whose keys are nothing. Std::HashTable => ("hashDrop", vec![value, Expr::undefined(), each(self, 0)?]), Std::BTreeSet => ("btreeDrop", vec![value, each(self, 0)?]), From 26a6fc4498a4fb462193fb372eacebb7f687a9c2 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:50:43 -0800 Subject: [PATCH 3/6] fix(ts): keep hashClone's arguments as they were for a map of no own BuildHasher Co-Authored-By: Claude Opus 5.5 --- ts/src/lower/collections.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 0d0f52a01..20809a1e3 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -651,11 +651,9 @@ fn hash<'tcx>( Some(v) => clone_fn(b, v, span), None => Expr::undefined(), }; + values.extend([k, v]); let s = held_state(b, coll).map(|s| clone_fn(b, s, span)); - Expr::rt( - "hashClone", - super::iter::tail(values, [k, v].into_iter().chain(s).collect()), - ) + Expr::rt("hashClone", super::iter::tail(values, s.into_iter().collect())) } ("Extend", "extend") | ("From", "from") => { let (source, iter) = extend_source(b, coll, call, owner, values.pop()?)?; From 65f0b824255dde502cdeb78e570398f3b3cd1d4b Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:05:18 -0800 Subject: [PATCH 4/6] fix(ts): take hashbrown's map's items before making its own BuildHasher in from_iter Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/stock/expected/harness.ts | 4 +- ts/fixtures/stock/expected/lib.ts | 5 +++ ts/fixtures/stock/expected/maps.Made.ts | 39 ++++++++++++++++++++ ts/fixtures/stock/expected/maps.ts | 40 +++++++++++++++++++- ts/fixtures/stock/src/harness.rs | 1 + ts/fixtures/stock/src/maps.rs | 49 +++++++++++++++++++++++++ ts/src/lower/collections.rs | 17 +++++++++ 7 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 ts/fixtures/stock/expected/maps.Made.ts diff --git a/ts/fixtures/stock/expected/harness.ts b/ts/fixtures/stock/expected/harness.ts index af286afb4..b50b9e1f0 100644 --- a/ts/fixtures/stock/expected/harness.ts +++ b/ts/fixtures/stock/expected/harness.ts @@ -3,7 +3,7 @@ import * as rt from "./rt.ts"; import { bytesMut, reserves, reads, dyns, writes, edits, chains } from "./buf.ts"; import * as Owner from "./harness.Owner.ts"; -import { map as maps_map, table, capacity, linked, linkedIters, nested, ownHasher } from "./maps.ts"; +import { map as maps_map, table, capacity, linked, linkedIters, nested, ownHasher, ownFromIter } from "./maps.ts"; /** * Each case's name and what it runs. The first byte picks one modulo their count, so a @@ -36,7 +36,7 @@ export function cases(): [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefine chains(a012.data, a012.start, a012.length, a112); }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::nested", (a013: rt.ByteSlice, a113: rt.ByteVec): undefined => { nested(a013.data, a013.start, a013.length, a113); - }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::own_hasher", ownHasher as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined]]; + }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::own_hasher", ownHasher as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["maps::own_from_iter", ownFromIter as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined]]; } /** Run one case. */ diff --git a/ts/fixtures/stock/expected/lib.ts b/ts/fixtures/stock/expected/lib.ts index 122a9167b..7d0185826 100644 --- a/ts/fixtures/stock/expected/lib.ts +++ b/ts/fixtures/stock/expected/lib.ts @@ -148,6 +148,11 @@ export function drop$maps_Held_maps_Logged(_1: Held.Held): void { _1.last[Symbol.dispose](); } +export function drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made(_1: [rt.HashMap, rt.HashSet]): void { + rt.hashDrop(_1[0], undefined, undefined, rt.drop); + rt.hashDrop(_1[1], undefined, undefined, rt.drop); +} + export function drop$tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged(_1: [LinkedHashSet.LinkedHashSet, Logged.Logged]): void { drop$hashlink_LinkedHashSet_maps_Logged(_1[0]); _1[1][Symbol.dispose](); diff --git a/ts/fixtures/stock/expected/maps.Made.ts b/ts/fixtures/stock/expected/maps.Made.ts new file mode 100644 index 000000000..d379fcca5 --- /dev/null +++ b/ts/fixtures/stock/expected/maps.Made.ts @@ -0,0 +1,39 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { MADE } from "./maps.ts"; + +/** + * A `BuildHasher` of the crate's own, numbered, which logs each one `Default` makes and + * each drop. + */ +export class Made { + _0: rt.u8; + + constructor(_0: rt.u8) { + this._0 = _0; + } + + [Symbol.dispose](): void { + drop$ops_Drop(this); + } +} + +export function default$Default(): Made { + ((made: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(made); + rt.byteVecPush(_5.value, 110); + rt.refMutRelease(_5); + })(MADE.get()); + return new Made(0); +} + +export function drop$ops_Drop(self: Made): void { + ((made: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(made); + rt.iterExtendBytes(_5.value, rt.iterSlice(Uint8Array.of(120, self._0)), undefined, true); + rt.refMutRelease(_5); + })(MADE.get()); +} + +//# sourceMappingURL=maps.Made.ts.map diff --git a/ts/fixtures/stock/expected/maps.ts b/ts/fixtures/stock/expected/maps.ts index 4f65ecea0..5d96ed8c9 100644 --- a/ts/fixtures/stock/expected/maps.ts +++ b/ts/fixtures/stock/expected/maps.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { copy$tuple2_u16_u32, drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged as lib_drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged, drop$Option_tuple2_tuple2_u8_u8_maps_Logged as lib_drop$Option_tuple2_tuple2_u8_u8_maps_Logged, drop$hashlink_LinkedHashMap_sync_Arc_str_u32 as lib_drop$hashlink_LinkedHashMap_sync_Arc_str_u32, drop$hashlink_LinkedHashMap_u16_u32 as lib_drop$hashlink_LinkedHashMap_u16_u32, drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState as lib_drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState, drop$hashlink_LinkedHashSet_maps_Logged as lib_drop$hashlink_LinkedHashSet_maps_Logged, drop$hashlink_LruCache_maps_Logged_maps_Logged as lib_drop$hashlink_LruCache_maps_Logged_maps_Logged, drop$hashlink_LruCache_u16_maps_Logged as lib_drop$hashlink_LruCache_u16_maps_Logged, drop$hashlink_linked_hash_map_Drain_u16_u32 as lib_drop$hashlink_linked_hash_map_Drain_u16_u32, drop$hashlink_linked_hash_map_IntoIter_u16_u32 as lib_drop$hashlink_linked_hash_map_IntoIter_u16_u32, drop$maps_Held_maps_Logged as lib_drop$maps_Held_maps_Logged, drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded as lib_drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded } from "./lib.ts"; +import { copy$tuple2_u16_u32, drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged as lib_drop$Option_tuple2_hashlink_LinkedHashSet_maps_Logged_maps_Logged, drop$Option_tuple2_tuple2_u8_u8_maps_Logged as lib_drop$Option_tuple2_tuple2_u8_u8_maps_Logged, drop$hashlink_LinkedHashMap_sync_Arc_str_u32 as lib_drop$hashlink_LinkedHashMap_sync_Arc_str_u32, drop$hashlink_LinkedHashMap_u16_u32 as lib_drop$hashlink_LinkedHashMap_u16_u32, drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState as lib_drop$hashlink_LinkedHashMap_u16_u32_hash_RandomState, drop$hashlink_LinkedHashSet_maps_Logged as lib_drop$hashlink_LinkedHashSet_maps_Logged, drop$hashlink_LruCache_maps_Logged_maps_Logged as lib_drop$hashlink_LruCache_maps_Logged_maps_Logged, drop$hashlink_LruCache_u16_maps_Logged as lib_drop$hashlink_LruCache_u16_maps_Logged, drop$hashlink_linked_hash_map_Drain_u16_u32 as lib_drop$hashlink_linked_hash_map_Drain_u16_u32, drop$hashlink_linked_hash_map_IntoIter_u16_u32 as lib_drop$hashlink_linked_hash_map_IntoIter_u16_u32, drop$maps_Held_maps_Logged as lib_drop$maps_Held_maps_Logged, drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made as lib_drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made, drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded as lib_drop$tuple3_hashbrown_HashMap_tuple2_u8_u8_u32_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded_hashbrown_HashSet_u8_maps_Seeded } from "./lib.ts"; import { unwrapOr$11c0we2 as option_unwrapOr$11c0we2, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr, unwrapOr$0l7j3f6 as option_unwrapOr$0l7j3f6, mapOr$085zvfb as option_mapOr$085zvfb, mapOr$1787w44 as option_mapOr$1787w44, mapOr$0iaogpf as option_mapOr$0iaogpf, mapOr$0yixjb2 as option_mapOr$0yixjb2, mapOr$1vcgixw as option_mapOr$1vcgixw, mapOr$1noewr3 as option_mapOr$1noewr3 } from "./core/option.ts"; import * as linked_hash_map_Drain from "./hashlink/linked_hash_map.Drain.ts"; import * as linked_hash_map_IntoIter from "./hashlink/linked_hash_map.IntoIter.ts"; @@ -17,6 +17,7 @@ import type * as DefaultHashBuilder from "./hashlink.DefaultHashBuilder.ts"; import * as maps_Counted from "./maps.Counted.ts"; import * as Held from "./maps.Held.ts"; import * as Logged from "./maps.Logged.ts"; +import * as Made from "./maps.Made.ts"; import * as Seeded from "./maps.Seeded.ts"; /** A length, capacity, or value, as four bytes. */ @@ -1210,4 +1211,41 @@ export function ownHasher(data: rt.ByteSlice, out: rt.ByteVec): void { rt.byteVecExtendFromSlice(out, rt.byteVecSlice(rt.refCellReplace(log, new rt.ByteVec()))); } +export const MADE: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(MADE$init, false); + +export function MADE$init(): rt.RefCell { + return new rt.RefCell(new rt.ByteVec()); +} + +/** + * `hashbrown`'s `from_iter` of a map or set that holds a `BuildHasher` of the crate's own: + * a map's takes the source's items, which drops its hasher, before it makes its own, and a + * set's after. + */ +export function ownFromIter(data: rt.ByteSlice, out: rt.ByteVec): void { + ((made: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(made); + rt.byteVecClear(_5.value); + rt.refMutRelease(_5); + })(MADE.get()); + const map: rt.HashMap = rt.hashNew(undefined, new Made.Made(1)) as rt.HashMap; + const set: rt.HashSet = rt.hashNew(undefined, new Made.Made(2)) as rt.HashSet; + for (const b of rt.byteSliceView(data)) { + rt.hashInsert(map, rt.u8Rem(b, 8), b); + rt.hashAdd(set, rt.u8Rem(b, 8)); + } + const _24: rt.HashMap = map; + const map2: rt.HashMap = ((items: rt.Iter<[rt.u8, rt.u8]>): rt.HashMap => { + return rt.hashExtend(rt.hashNew(undefined, Made.default$Default()) as rt.HashMap, items, undefined, undefined, undefined, 469762048); + })(rt.hashIter(_24, undefined, undefined, rt.drop)); + const _26: rt.HashSet = set; + const set2: rt.HashSet = rt.hashExtendKeys(rt.hashNew(undefined, Made.default$Default()) as rt.HashSet, rt.hashKeys(_26, undefined, undefined, rt.drop), undefined, undefined, 469762048); + put(out, rt.hashLen(map2)); + put(out, rt.hashLen(set2)); + lib_drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made([map2, set2] as [rt.HashMap, rt.HashSet]); + rt.byteVecExtendFromSlice(out, rt.byteVecSlice(((made2: rt.RefCell): rt.ByteVec => { + return rt.refCellReplace(made2, new rt.ByteVec()); + })(MADE.get()))); +} + //# sourceMappingURL=maps.ts.map diff --git a/ts/fixtures/stock/src/harness.rs b/ts/fixtures/stock/src/harness.rs index 81a2f76fb..4d9f71cd1 100644 --- a/ts/fixtures/stock/src/harness.rs +++ b/ts/fixtures/stock/src/harness.rs @@ -29,6 +29,7 @@ fn cases() -> Vec<(&'static str, Case)> { ("buf::chains", crate::buf::chains), ("maps::nested", crate::maps::nested), ("maps::own_hasher", crate::maps::own_hasher), + ("maps::own_from_iter", crate::maps::own_from_iter), ] } diff --git a/ts/fixtures/stock/src/maps.rs b/ts/fixtures/stock/src/maps.rs index b4f6b7b42..27cb85381 100644 --- a/ts/fixtures/stock/src/maps.rs +++ b/ts/fixtures/stock/src/maps.rs @@ -665,3 +665,52 @@ pub fn own_hasher(data: &[u8], out: &mut Vec) { drop((pairs, set, copy)); out.extend_from_slice(&log.take()); } + +thread_local! { + /// What each [`Made`] did, in order. + static MADE: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// A `BuildHasher` of the crate's own, numbered, which logs each one `Default` makes and +/// each drop. +struct Made(u8); + +impl BuildHasher for Made { + type Hasher = std::hash::DefaultHasher; + + fn build_hasher(&self) -> Self::Hasher { + std::hash::DefaultHasher::new() + } +} + +impl Default for Made { + fn default() -> Self { + MADE.with(|made| made.borrow_mut().push(b'n')); + Made(0) + } +} + +impl Drop for Made { + fn drop(&mut self) { + MADE.with(|made| made.borrow_mut().extend([b'x', self.0])); + } +} + +/// `hashbrown`'s `from_iter` of a map or set that holds a `BuildHasher` of the crate's own: +/// a map's takes the source's items, which drops its hasher, before it makes its own, and a +/// set's after. +pub fn own_from_iter(data: &[u8], out: &mut Vec) { + MADE.with(|made| made.borrow_mut().clear()); + let mut map: HashMap = HashMap::with_hasher(Made(1)); + let mut set: HashSet = HashSet::with_hasher(Made(2)); + for &b in data { + map.insert(b % 8, b); + set.insert(b % 8); + } + let map: HashMap = HashMap::from_iter(map); + let set: HashSet = HashSet::from_iter(set); + put(out, map.len()); + put(out, set.len()); + drop((map, set)); + out.extend_from_slice(&MADE.with(|made| made.take())); +} diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 5b30b2958..3c9cb5092 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -3168,6 +3168,23 @@ pub fn collect<'tcx>( Some(match std { Std::HashMap | Std::HashSet => { let state = new_state(b, ty, span); + // hashbrown's map takes its items before it makes its hasher, where std's and + // hashbrown's set make it first. + let first = std == Std::HashMap + && state.is_some() + && !iter.is_path() + && matches!(ty.kind(), ty::Adt(adt, _) if super::stock::as_std(b.t.tcx, adt.did())); + if first { + let name = b.fresh("items"); + let target = hash_new(b, ty, state, span)?; + let made = hash_extend(b, ty, target, Expr::ident(&name), from, span); + let param = Param { + name, + ty: Some(b.t.ty(from, span)), + }; + let body = vec![StmtKind::Return(Some(made)).at(None)]; + return Some(Expr::Arrow(vec![param], b.t.ty(ty, span), body).call(vec![iter])); + } let target = hash_new(b, ty, state, span)?; hash_extend(b, ty, target, iter, from, span) } From bd15089a6aacc896713d817a7b8e44f1dec88261 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:11:05 -0800 Subject: [PATCH 5/6] fix(ts): read the size hint before making hashbrown's map's own BuildHasher in from_iter Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/stock/expected/maps.Hinted.ts | 41 +++++++++++++++++++++ ts/fixtures/stock/expected/maps.ts | 17 +++++---- ts/fixtures/stock/src/maps.rs | 26 +++++++++++-- ts/runtime/rt/collections.ts | 23 ++++++++++++ ts/src/lower/collections.rs | 45 ++++++++++++----------- 5 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 ts/fixtures/stock/expected/maps.Hinted.ts diff --git a/ts/fixtures/stock/expected/maps.Hinted.ts b/ts/fixtures/stock/expected/maps.Hinted.ts new file mode 100644 index 000000000..80ff5f09e --- /dev/null +++ b/ts/fixtures/stock/expected/maps.Hinted.ts @@ -0,0 +1,41 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { MADE } from "./maps.ts"; + +/** An iterator of the crate's own that logs each `size_hint`. */ +export class Hinted { + _0: I; + + constructor(_0: I) { + this._0 = _0; + } +} + +export function next$iter_Iterator_hashbrown_hash_map_IntoIter_u8_u8(self: Hinted>): [rt.u8, rt.u8] | rt.Done { + return self._0.next(); +} + +export function next$iter_Iterator_hashbrown_hash_set_IntoIter_u8(self: Hinted>): rt.u8 | rt.Done { + return self._0.next(); +} + +export function sizeHint$iter_Iterator_hashbrown_hash_map_IntoIter_u8_u8(self: Hinted>): [rt.usize, rt.usize | undefined] { + ((made: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(made); + rt.byteVecPush(_5.value, 104); + rt.refMutRelease(_5); + })(MADE.get()); + return self._0.sizeHint(); +} + +export function sizeHint$iter_Iterator_hashbrown_hash_set_IntoIter_u8(self: Hinted>): [rt.usize, rt.usize | undefined] { + ((made: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(made); + rt.byteVecPush(_5.value, 104); + rt.refMutRelease(_5); + })(MADE.get()); + return self._0.sizeHint(); +} + +//# sourceMappingURL=maps.Hinted.ts.map diff --git a/ts/fixtures/stock/expected/maps.ts b/ts/fixtures/stock/expected/maps.ts index 5d96ed8c9..14f19555d 100644 --- a/ts/fixtures/stock/expected/maps.ts +++ b/ts/fixtures/stock/expected/maps.ts @@ -16,6 +16,7 @@ import * as LruCache from "./hashlink/lru_cache.LruCache.ts"; import type * as DefaultHashBuilder from "./hashlink.DefaultHashBuilder.ts"; import * as maps_Counted from "./maps.Counted.ts"; import * as Held from "./maps.Held.ts"; +import * as Hinted from "./maps.Hinted.ts"; import * as Logged from "./maps.Logged.ts"; import * as Made from "./maps.Made.ts"; import * as Seeded from "./maps.Seeded.ts"; @@ -1219,8 +1220,8 @@ export function MADE$init(): rt.RefCell { /** * `hashbrown`'s `from_iter` of a map or set that holds a `BuildHasher` of the crate's own: - * a map's takes the source's items, which drops its hasher, before it makes its own, and a - * set's after. + * a map's takes the source's items, which drops its hasher, and reads their size hint + * before it makes its own, and a set's after. */ export function ownFromIter(data: rt.ByteSlice, out: rt.ByteVec): void { ((made: rt.RefCell): undefined => { @@ -1234,15 +1235,17 @@ export function ownFromIter(data: rt.ByteSlice, out: rt.ByteVec): void { rt.hashInsert(map, rt.u8Rem(b, 8), b); rt.hashAdd(set, rt.u8Rem(b, 8)); } - const _24: rt.HashMap = map; - const map2: rt.HashMap = ((items: rt.Iter<[rt.u8, rt.u8]>): rt.HashMap => { - return rt.hashExtend(rt.hashNew(undefined, Made.default$Default()) as rt.HashMap, items, undefined, undefined, undefined, 469762048); - })(rt.hashIter(_24, undefined, undefined, rt.drop)); + const map2: rt.HashMap = rt.hashFromIter(rt.hashIter(map, undefined, undefined, rt.drop), () => rt.hashNew(undefined, Made.default$Default()) as rt.HashMap, undefined, undefined, undefined, 469762048); const _26: rt.HashSet = set; const set2: rt.HashSet = rt.hashExtendKeys(rt.hashNew(undefined, Made.default$Default()) as rt.HashSet, rt.hashKeys(_26, undefined, undefined, rt.drop), undefined, undefined, 469762048); put(out, rt.hashLen(map2)); put(out, rt.hashLen(set2)); - lib_drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made([map2, set2] as [rt.HashMap, rt.HashSet]); + const hinted: rt.HashMap = rt.hashFromIter(rt.iterFrom(new Hinted.Hinted(rt.hashIter(map2, undefined, undefined, rt.drop)), Hinted.next$iter_Iterator_hashbrown_hash_map_IntoIter_u8_u8, undefined, { hint: Hinted.sizeHint$iter_Iterator_hashbrown_hash_map_IntoIter_u8_u8 }), () => rt.hashNew(undefined, Made.default$Default()) as rt.HashMap, undefined, undefined, undefined, 469762048); + const _37: Hinted.Hinted> = new Hinted.Hinted(rt.hashKeys(set2, undefined, undefined, rt.drop)); + const keys: rt.HashSet = rt.hashExtendKeys(rt.hashNew(undefined, Made.default$Default()) as rt.HashSet, rt.iterFrom(_37, Hinted.next$iter_Iterator_hashbrown_hash_set_IntoIter_u8, undefined, { hint: Hinted.sizeHint$iter_Iterator_hashbrown_hash_set_IntoIter_u8 }), undefined, undefined, 469762048); + put(out, rt.hashLen(hinted)); + put(out, rt.hashLen(keys)); + lib_drop$tuple2_hashbrown_HashMap_u8_u8_maps_Made_hashbrown_HashSet_u8_maps_Made([hinted, keys] as [rt.HashMap, rt.HashSet]); rt.byteVecExtendFromSlice(out, rt.byteVecSlice(((made2: rt.RefCell): rt.ByteVec => { return rt.refCellReplace(made2, new rt.ByteVec()); })(MADE.get()))); diff --git a/ts/fixtures/stock/src/maps.rs b/ts/fixtures/stock/src/maps.rs index 27cb85381..a741927b4 100644 --- a/ts/fixtures/stock/src/maps.rs +++ b/ts/fixtures/stock/src/maps.rs @@ -696,9 +696,25 @@ impl Drop for Made { } } +/// An iterator of the crate's own that logs each `size_hint`. +struct Hinted(I); + +impl Iterator for Hinted { + type Item = I::Item; + + fn next(&mut self) -> Option { + self.0.next() + } + + fn size_hint(&self) -> (usize, Option) { + MADE.with(|made| made.borrow_mut().push(b'h')); + self.0.size_hint() + } +} + /// `hashbrown`'s `from_iter` of a map or set that holds a `BuildHasher` of the crate's own: -/// a map's takes the source's items, which drops its hasher, before it makes its own, and a -/// set's after. +/// a map's takes the source's items, which drops its hasher, and reads their size hint +/// before it makes its own, and a set's after. pub fn own_from_iter(data: &[u8], out: &mut Vec) { MADE.with(|made| made.borrow_mut().clear()); let mut map: HashMap = HashMap::with_hasher(Made(1)); @@ -711,6 +727,10 @@ pub fn own_from_iter(data: &[u8], out: &mut Vec) { let set: HashSet = HashSet::from_iter(set); put(out, map.len()); put(out, set.len()); - drop((map, set)); + let hinted: HashMap = HashMap::from_iter(Hinted(map.into_iter())); + let keys: HashSet = HashSet::from_iter(Hinted(set.into_iter())); + put(out, hinted.len()); + put(out, keys.len()); + drop((hinted, keys)); out.extend_from_slice(&MADE.with(|made| made.take())); } diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index 37053981b..56c713cab 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -1039,6 +1039,29 @@ function extendReserve(m: HashMap, additional: number, max: number | */ export function hashExtend(m: HashMap, items: Iter<[K, V]>, k?: (key: K) => void, v?: (value: V) => void, take?: Take, max?: number): HashMap { extendReserve(m, items.sizeHint()[0], max); + return hashFill(m, items, k, v, take); +} + +/** + * hashbrown's `from_iter` of a map, as `hashExtend` into what `make` makes, but which reads + * `items`' size hint first, as hashbrown's does before it makes its `BuildHasher`. + */ +export function hashFromIter( + items: Iter<[K, V]>, + make: () => HashMap, + k?: (key: K) => void, + v?: (value: V) => void, + take?: Take, + max?: number, +): HashMap { + const n = items.sizeHint()[0]; + const m = make(); + extendReserve(m, n, max); + return hashFill(m, items, k, v, take); +} + +/** Insert each of `items` into `m`, as `hashExtend` does once it has reserved. */ +function hashFill(m: HashMap, items: Iter<[K, V]>, k?: (key: K) => void, v?: (value: V) => void, take?: Take): HashMap { const add = (item: [K, V]) => { const old = hashInsert(m, item[0], item[1], k); if (old !== undefined) v?.(old); diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 3c9cb5092..c3ede760d 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -859,15 +859,22 @@ fn hash_extend<'tcx>( iter: Ty<'tcx>, span: Span, ) -> Expr { - let mut droppers = entry_droppers(b, coll, span); - // By `for_each`. - droppers.push(super::iter::iter_take(b, iter, true, span)); - droppers.push(hash_max(b, coll, span)); let f = match b.t.std_of(coll) { Some(Std::HashSet) => "hashExtendKeys", _ => "hashExtend", }; - Expr::rt(f, super::iter::tail(vec![target, source], droppers)) + let tail = hash_extend_tail(b, coll, iter, span); + Expr::rt(f, super::iter::tail(vec![target, source], tail)) +} + +/// What `hashExtend` and its kin take after the collection and the items, of an iterator of +/// type `iter`: what drops each entry, how it reads them, and the most entries it holds. +fn hash_extend_tail<'tcx>(b: &mut Body<'_, '_, 'tcx>, coll: Ty<'tcx>, iter: Ty<'tcx>, span: Span) -> Vec { + let mut tail = entry_droppers(b, coll, span); + // By `for_each`. + tail.push(super::iter::iter_take(b, iter, true, span)); + tail.push(hash_max(b, coll, span)); + tail } /// The most entries a hash collection of type `coll` holds on wasm32, past which hashbrown's @@ -3168,24 +3175,18 @@ pub fn collect<'tcx>( Some(match std { Std::HashMap | Std::HashSet => { let state = new_state(b, ty, span); - // hashbrown's map takes its items before it makes its hasher, where std's and - // hashbrown's set make it first. - let first = std == Std::HashMap - && state.is_some() - && !iter.is_path() - && matches!(ty.kind(), ty::Adt(adt, _) if super::stock::as_std(b.t.tcx, adt.did())); - if first { - let name = b.fresh("items"); - let target = hash_new(b, ty, state, span)?; - let made = hash_extend(b, ty, target, Expr::ident(&name), from, span); - let param = Param { - name, - ty: Some(b.t.ty(from, span)), - }; - let body = vec![StmtKind::Return(Some(made)).at(None)]; - return Some(Expr::Arrow(vec![param], b.t.ty(ty, span), body).call(vec![iter])); - } + let held = state.is_some(); let target = hash_new(b, ty, state, span)?; + // hashbrown's map takes its items and reads their size hint before it makes its + // hasher, where std's and hashbrown's set make it first. + if std == Std::HashMap + && held + && matches!(ty.kind(), ty::Adt(adt, _) if super::stock::as_std(b.t.tcx, adt.did())) + { + let make = Expr::Lambda(Vec::new(), Box::new(target)); + let tail = hash_extend_tail(b, ty, from, span); + return Some(Expr::rt("hashFromIter", super::iter::tail(vec![iter, make], tail))); + } hash_extend(b, ty, target, iter, from, span) } Std::BTreeMap | Std::BTreeSet => btree_collect(b, ty, iter, from, trusted, span), From a8a49a64396fec7ba8c1045119f290facae3ec98 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 06:53:54 -0800 Subject: [PATCH 6/6] chore(ts): update the hashes and stock snapshots for main's parentheses Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/hashes/expected/hashes.ts | 4 ++-- ts/fixtures/stock/expected/maps.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ts/fixtures/lang/hashes/expected/hashes.ts b/ts/fixtures/lang/hashes/expected/hashes.ts index 203ab39ae..d5bce2449 100644 --- a/ts/fixtures/lang/hashes/expected/hashes.ts +++ b/ts/fixtures/lang/hashes/expected/hashes.ts @@ -1063,13 +1063,13 @@ export function maps(data: rt.ByteSlice, out: rt.ByteVec): void { rt.byteVecPush(out, _63[1]); } const probe: Key.Key = { _0: first as rt.u16, _1: text(rt.byteSliceFrom(Uint8Array.of(first))) }; - rt.byteVecPush(out, ((keyed.s as Numbered.Numbered))._0); + rt.byteVecPush(out, (keyed.s as Numbered.Numbered)._0); rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.hashOne(new rt.OwnHasher(Numbered.buildHasher$hash_BuildHasher(keyed.s as Numbered.Numbered), Fnv.hasher$hash_Hasher), probe, rt.hashOwn(Key.hash$Hash_hashes_Fnv as (a0: Key.Key, a1: Fnv.Fnv) => undefined))).reverse())); rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u64ToBeBytes(rt.hashOne(new rt.OwnHasher(Numbered.buildHasher$hash_BuildHasher(seen.s as Numbered.Numbered), Fnv.hasher$hash_Hasher), first, rt.hash8)).reverse())); const noisy: rt.HashMap = rt.hashNew(undefined, new Numbered.Numbered(5)) as rt.HashMap; rt.drop(rt.hashInsert(noisy, first, new Noisy.Noisy(first))); const copy: rt.HashMap = rt.hashClone(noisy, undefined, Noisy.clone$Clone, Numbered.clone$Clone); - rt.byteVecPush(out, ((copy.s as Numbered.Numbered))._0); + rt.byteVecPush(out, (copy.s as Numbered.Numbered)._0); rt.hashDrop(noisy, undefined, rt.drop, rt.drop); const entries: rt.Iter<[rt.u8, Noisy.Noisy]> = rt.hashIter(copy, undefined, rt.drop, rt.drop); event(105, 0); diff --git a/ts/fixtures/stock/expected/maps.ts b/ts/fixtures/stock/expected/maps.ts index 128d9df85..c5ccf6f21 100644 --- a/ts/fixtures/stock/expected/maps.ts +++ b/ts/fixtures/stock/expected/maps.ts @@ -1194,14 +1194,14 @@ export function ownHasher(data: rt.ByteSlice, out: rt.ByteVec): void { put(out, option_unwrapOr$11c0we2(rt.hashGet(pairs, [a, _43] as [rt.u8, rt.u8]), 0) as rt.usize); } } - rt.byteVecPush(out, ((pairs.s as Seeded.Seeded))._0); - rt.byteVecPush(out, ((set.s as Seeded.Seeded))._0); + rt.byteVecPush(out, (pairs.s as Seeded.Seeded)._0); + rt.byteVecPush(out, (set.s as Seeded.Seeded)._0); const _64: Seeded.Seeded = new Seeded.Seeded(20, log); const one: rt.HashMap<[rt.u8, rt.u8], Logged.Logged> = rt.hashNew(rt.hashing(rt.hashTuple([rt.hash8, rt.hash8]), rt.eqTuple([rt.same, rt.same])), _64) as rt.HashMap<[rt.u8, rt.u8], Logged.Logged>; const first: rt.u8 = option_unwrapOr$0r2bmpr(rt.byteSliceFirst(data), 0); rt.drop(rt.hashInsert(one, [first, 0] as [rt.u8, rt.u8], new Logged.Logged(first, log))); const copy: rt.HashSet = rt.hashClone(set, undefined, undefined, Seeded.clone$Clone); - rt.byteVecPush(out, ((copy.s as Seeded.Seeded))._0); + rt.byteVecPush(out, (copy.s as Seeded.Seeded)._0); const entries: rt.Iter<[[rt.u8, rt.u8], Logged.Logged]> = rt.hashIter(one, undefined, rt.drop, rt.drop); const _85: rt.RefCell = rt.refCellBorrowMut(log); rt.byteVecPush(_85.value, 105);