From 79995a6c7c1549a20bbb37f00225ef831d2d1aa0 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:59:52 -0800 Subject: [PATCH 1/2] chore(quest): claim quest/m3/ts-hash-map-eq.md Quest-Claim: 6d48ccdf3947cf4f From 8a32ee10d551546d8a5e330a4911e7c7719336f6 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:10:39 -0800 Subject: [PATCH 2/2] feat(ts): translate == of hash maps and sets as std's Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-hash-map-eq.md | 19 -- ts/README.md | 5 +- .../lang/hashes/expected/hashes.Index.ts | 18 ++ .../lang/hashes/expected/hashes.Sided.ts | 25 ++ .../lang/hashes/expected/hashes.Spread.ts | 14 ++ ts/fixtures/lang/hashes/expected/hashes.ts | 235 ++++++++++++++++++ ts/fixtures/lang/hashes/expected/lib.ts | 14 +- ts/fixtures/lang/hashes/src/hashes.rs | 169 ++++++++++++- ts/fixtures/lang/hashes/src/lib.rs | 1 + ts/fixtures/stock/expected/harness.ts | 4 +- ts/fixtures/stock/expected/maps.ts | 74 ++++++ ts/fixtures/stock/src/harness.rs | 1 + ts/fixtures/stock/src/maps.rs | 44 ++++ ts/runtime/rt/collections.ts | 23 +- ts/src/lower/collections.rs | 7 +- 16 files changed, 620 insertions(+), 34 deletions(-) delete mode 100644 quest/m3/ts-hash-map-eq.md create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Index.ts create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Sided.ts create mode 100644 ts/fixtures/lang/hashes/expected/hashes.Spread.ts diff --git a/quest/m3/README.md b/quest/m3/README.md index 5933a7569..7a9a7185a 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -37,5 +37,4 @@ generic std gap is planned here even when moq never calls it. - [Python's wake on the loop's own thread](/quest/m3/ffi-python-wake.md) - a wake on the event loop's thread resumes the future with no socket pair - [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 - [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 - [A name or an operator argument at its own Rust span](/quest/m3/ts-argument-spans.md) - a `tsc` error on a plain-name or operator argument names that argument's Rust, not the method it is passed to diff --git a/quest/m3/ts-hash-map-eq.md b/quest/m3/ts-hash-map-eq.md deleted file mode 100644 index d82823dfd..000000000 --- a/quest/m3/ts-hash-map-eq.md +++ /dev/null @@ -1,19 +0,0 @@ -# [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/ts/README.md b/ts/README.md index 0d2472593..d7c7bdfb5 100644 --- a/ts/README.md +++ b/ts/README.md @@ -472,7 +472,10 @@ calls a key's `Hash`, `Eq`, or `Ord`. A map or set built with any `BuildHasher` `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. +the runtime's own hasher finds them, so it builds no hasher for the map. A lookup calls +`eq` of the key looked up by, given the key held, as std's does. Two maps or sets compare +as std's: their lengths, then each entry of the left looked up in the right, and its +value's `==` of the one found. 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/hashes.Index.ts b/ts/fixtures/lang/hashes/expected/hashes.Index.ts new file mode 100644 index 000000000..eba57d8a8 --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Index.ts @@ -0,0 +1,18 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +export function eq$cmp_PartialEq(self: Index, other: Index): boolean { + if (!rt.hashEq(rt.same)(self.names, other.names)) { + return false; + } + return rt.hashEq()(self.seen, other.seen); +} + +/** A type whose derived `PartialEq` compares the maps it holds. */ +export interface Index { + names: rt.HashMap; + seen: rt.HashSet<[rt.u8, boolean]>; +} + +//# sourceMappingURL=hashes.Index.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Sided.ts b/ts/fixtures/lang/hashes/expected/hashes.Sided.ts new file mode 100644 index 000000000..c379f202a --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Sided.ts @@ -0,0 +1,25 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { call } from "./hashes.ts"; + +/** + * A key or value of a map's side that logs each `Hash` and `eq` of it, so the log says + * whose key std hashes, and whose `eq` it calls with the other's as its argument. + */ +export interface Sided { + n: rt.u8; + side: rt.u8; +} + +export function hash$Hash_hash_DefaultHasher(self: Sided, state: rt.Hasher): void { + call(104, self.side, 0, self.n); + rt.hash8(self.n, state); +} + +export function eq$cmp_PartialEq(self: Sided, other: Sided): boolean { + call(101, self.side, other.side, self.n); + return self.n === other.n; +} + +//# sourceMappingURL=hashes.Sided.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.Spread.ts b/ts/fixtures/lang/hashes/expected/hashes.Spread.ts new file mode 100644 index 000000000..2a0c2af7c --- /dev/null +++ b/ts/fixtures/lang/hashes/expected/hashes.Spread.ts @@ -0,0 +1,14 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +export function default$Default(): Spread { + return { _0: 0 }; +} + +/** What builds a [`Spreader`] of its seed. */ +export interface Spread { + _0: rt.u64; +} + +//# sourceMappingURL=hashes.Spread.ts.map diff --git a/ts/fixtures/lang/hashes/expected/hashes.ts b/ts/fixtures/lang/hashes/expected/hashes.ts index d5bce2449..eef4da387 100644 --- a/ts/fixtures/lang/hashes/expected/hashes.ts +++ b/ts/fixtures/lang/hashes/expected/hashes.ts @@ -13,6 +13,7 @@ import * as Dir from "./hashes.Dir.ts"; import * as Fnv from "./hashes.Fnv.ts"; import * as Full from "./hashes.Full.ts"; import * as Holder from "./hashes.Holder.ts"; +import * as hashes_Index from "./hashes.Index.ts"; import * as Key from "./hashes.Key.ts"; import * as Logged from "./hashes.Logged.ts"; import * as Nibble from "./hashes.Nibble.ts"; @@ -23,7 +24,9 @@ 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 Sided from "./hashes.Sided.ts"; import * as Small from "./hashes.Small.ts"; +import * as Spread from "./hashes.Spread.ts"; import * as Switch from "./hashes.Switch.ts"; /** `DefaultHasher::new`'s hash of `value`, little-endian. */ @@ -1114,4 +1117,236 @@ export function maps(data: rt.ByteSlice, out: rt.ByteVec): void { rt.hashDrop(seen, undefined, undefined, rt.drop); } +export const CALLS: rt.LocalKey> = /* @__PURE__ */ new rt.LocalKey(CALLS$init, false); + +export function CALLS$init(): rt.RefCell { + const _1: rt.u32[] = []; + return new rt.RefCell(_1); +} + +/** Log a call of `what` on the value of `side` holding `n`, given one of `other`. */ +export function call(what: rt.u8, side: rt.u8, other: rt.u8, n: rt.u8): void { + ((calls: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(calls); + _5.value.push(((what << 24) | (side << 16) | (other << 8) | n) >>> 0); + rt.refMutRelease(_5); + })(CALLS.get()); +} + +/** + * Whether `a == b`, and what it logged: in the order it called each if it did so for one + * entry at most, else sorted if it visited every entry, else nothing, as which entries it + * visits before one differs follows std's order. + */ +export function compared$collections_HashMap_hashes_Sided_hashes_Sided_hashes_Spread(a: rt.HashMap, b: rt.HashMap, len: rt.usize, out: rt.ByteVec): void { + ((calls2: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(calls2); + rt.vecTruncate(_5.value, 0); + rt.refMutRelease(_5); + })(CALLS.get()); + const equal: boolean = rt.hashEq(Sided.eq$cmp_PartialEq)(a, b); + rt.byteVecPush(out, equal ? 1 : 0); + rt.byteVecPush(out, !rt.hashEq(Sided.eq$cmp_PartialEq)(a, b) ? 1 : 0); + const calls: rt.u32[] = ((calls3: rt.RefCell): rt.u32[] => { + return rt.refCellReplace(calls3, []); + })(CALLS.get()); + if (len > 1) { + if (!equal) { + rt.vecTruncate(calls, 0); + } + } + rt.vecTruncate(calls, rt.usizeDiv(calls.length, 2)); + if (len > 1) { + rt.sliceSort(calls, rt.lt, 4, true); + } + rt.byteVecPush(out, calls.length & 0xff); + for (const _39 of calls) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(_39))); + } +} + +/** + * Whether `a == b`, and what it logged: in the order it called each if it did so for one + * entry at most, else sorted if it visited every entry, else nothing, as which entries it + * visits before one differs follows std's order. + */ +export function compared$collections_HashSet_hashes_Sided_hashes_Spread(a: rt.HashSet, b: rt.HashSet, len: rt.usize, out: rt.ByteVec): void { + ((calls2: rt.RefCell): undefined => { + const _5: rt.RefCell = rt.refCellBorrowMut(calls2); + rt.vecTruncate(_5.value, 0); + rt.refMutRelease(_5); + })(CALLS.get()); + const equal: boolean = rt.hashEq()(a, b); + rt.byteVecPush(out, equal ? 1 : 0); + rt.byteVecPush(out, !rt.hashEq()(a, b) ? 1 : 0); + const calls: rt.u32[] = ((calls3: rt.RefCell): rt.u32[] => { + return rt.refCellReplace(calls3, []); + })(CALLS.get()); + if (len > 1) { + if (!equal) { + rt.vecTruncate(calls, 0); + } + } + rt.vecTruncate(calls, rt.usizeDiv(calls.length, 2)); + if (len > 1) { + rt.sliceSort(calls, rt.lt, 4, true); + } + rt.byteVecPush(out, calls.length & 0xff); + for (const _39 of calls) { + rt.byteVecExtendFromSlice(out, rt.byteSliceFrom(rt.u32ToBeBytes(_39))); + } +} + +/** + * `==` and `!=` of hash maps and sets, as std's: equal lengths, then each entry of the + * left looked up in the right, which hashes the left's key by its own `BuildHasher` and + * calls the left key's `eq` on the right's, then the left value's `eq` on the right's. + * Each side holds its own `BuildHasher`, seeded apart, and is built of the same bytes in + * another order, so a key given twice keeps the value it was given last; or of all but + * the last, or of one byte flipped. Then maps of a `RandomState`, which compare their keys + * as JS does, and maps in a `Vec` and in a type that derives `PartialEq`. + */ +export function equal(data: rt.ByteSlice, out: rt.ByteVec): void { + const _8: (a0: rt.u8, a1: rt.ByteSlice) => rt.HashSet = (side: rt.u8, bytes: rt.ByteSlice): rt.HashSet => { + const _52: Spread.Spread = { _0: side as rt.u64 }; + const s: rt.HashSet = rt.hashNew(rt.hashing(Sided.hash$Hash_hash_DefaultHasher as (a0: Sided.Sided, a1: rt.Hasher) => undefined, Sided.eq$cmp_PartialEq), _52) as rt.HashSet; + const iterS: Uint8Array = rt.byteSliceView(bytes); + let iterI: number = 0; + while (true) { + const _9: rt.u8 | rt.Done = iterI < iterS.length ? iterS[iterI++] : rt.DONE; + if (_9 === rt.DONE) { + return s; + } + rt.hashAdd(s, { n: _9 & 127, side }); + } + }; + const _96: (a0: rt.ByteSlice) => rt.HashMap = (bytes2: rt.ByteSlice): rt.HashMap => { + const m: rt.HashMap = rt.hashNew() as rt.HashMap; + const iterS2: Uint8Array = rt.byteSliceView(bytes2); + let iterI2: number = 0; + while (true) { + const _64: rt.u8 | rt.Done = iterI2 < iterS2.length ? iterS2[iterI2++] : rt.DONE; + if (_64 === rt.DONE) { + return m; + } + const b2: rt.u8 = _64; + const _102: rt.u16 = b2 as rt.u16; + const _113: rt.Place = rt.entryOrInsertWithMut(rt.entry(m, rt.u8Rem(b2, 8)), (): rt.u16 => { + return 0; + }); + _113.v = rt.u16Add(_113.v, _102); + } + }; + const _123: (a0: rt.ByteSlice) => rt.HashSet = (bytes3: rt.ByteSlice): rt.HashSet => { + const _3: rt.Iter = rt.iterMap(rt.iterSlice(rt.byteSliceView(bytes3)), (_2: rt.u8): string => { + return text(rt.byteSliceFrom(Uint8Array.of(rt.u8Rem(_2, 4)))); + }); + return rt.hashExtendKeys(rt.hashNew() as rt.HashSet, _3, undefined, undefined, 117440512); + }; + const _175: (a0: rt.ByteSlice) => hashes_Index.Index = (bytes4: rt.ByteSlice): hashes_Index.Index => { + const _46: rt.Iter<[rt.u8, string]> = rt.iterMap(rt.iterSlice(rt.byteSliceView(bytes4)), (_22: rt.u8): [rt.u8, string] => { + const b4: rt.u8 = _22; + return [rt.u8Rem(b4, 8), text(rt.byteSliceFrom(Uint8Array.of(rt.u8Rem(b4, 3))))] as [rt.u8, string]; + }); + const _33: rt.HashMap = rt.hashExtend(rt.hashNew() as rt.HashMap, _46, undefined, undefined, undefined, 58720256); + const _75: rt.Iter<[rt.u8, boolean]> = rt.iterMap(rt.iterSlice(rt.byteSliceView(bytes4)), (_23: rt.u8): [rt.u8, boolean] => { + const b5: rt.u8 = _23; + return [rt.u8Rem(b5, 5), rt.u8Rem(b5, 2) === 0] as [rt.u8, boolean]; + }); + return { names: _33, seen: rt.hashExtendKeys(rt.hashNew(rt.hashing(rt.hashTuple([rt.hash8, rt.hash8]), rt.eqTuple([rt.same, rt.same]))) as rt.HashSet<[rt.u8, boolean]>, _75, undefined, undefined, 469762048) }; + }; + const first: rt.u8 = option_unwrapOr$0r2bmpr(rt.byteSliceFirst(data), 0); + const map: (a0: rt.u8, a1: rt.ByteSlice) => rt.HashMap = (side2: rt.u8, bytes5: rt.ByteSlice): rt.HashMap => { + const _58: Spread.Spread = { _0: rt.u64Add(rt.u64Mul(side2 as rt.u64, 65537), first as rt.u64) }; + const m2: rt.HashMap = rt.hashNew(rt.hashing(Sided.hash$Hash_hash_DefaultHasher as (a0: Sided.Sided, a1: rt.Hasher) => undefined, Sided.eq$cmp_PartialEq), _58) as rt.HashMap; + const iterS3: Uint8Array = rt.byteSliceView(bytes5); + let iterI3: number = 0; + while (true) { + const _1510: rt.u8 | rt.Done = iterI3 < iterS3.length ? iterS3[iterI3++] : rt.DONE; + if (_1510 === rt.DONE) { + return m2; + } + const b6: rt.u8 = _1510; + const _212: Sided.Sided = { n: b6 & 127, side: side2 }; + const _232: Sided.Sided = { n: b6 >> 6, side: rt.u8Add(side2, 2) }; + rt.hashInsert(m2, _212, _232); + } + }; + const reversed: rt.ByteVec = rt.iterToByteVec(rt.iterRev(rt.iterSlice(rt.byteSliceView(data))), undefined, true); + const flipped: rt.ByteVec = rt.iterToByteVec(rt.iterMap(rt.iterEnumerate(rt.iterSlice(rt.byteSliceView(data))), (_210: [rt.usize, rt.u8]): rt.u8 => { + const i: rt.usize = _210[0]; + const b7: rt.u8 = _210[1]; + if (!(i === rt.usizeDiv(data.length, 2))) { + return b7; + } + return b7 ^ 65; + }), undefined, true); + const shorter: rt.ByteSlice = rt.byteSliceRange(data, 0, rt.usizeSaturatingSub(data.length, 1)); + const left: rt.HashMap = map(1, data); + const part2 = rt.byteVecSlice(reversed); + const _27: rt.HashMap = map(2, part2); + const _32: rt.HashMap = map(2, data); + const _35: rt.HashMap = map(2, shorter); + const part3 = rt.byteVecSlice(flipped); + const iter: rt.Iter> = rt.iterSlice([_27, _32, _35, map(2, part3)]); + while (true) { + const _44: rt.HashMap | rt.Done = iter.next(); + if (_44 === rt.DONE) { + break; + } + const right: rt.HashMap = _44; + const _51: rt.usize = rt.hashLen(left); + compared$collections_HashMap_hashes_Sided_hashes_Sided_hashes_Spread(left, right, _51, out); + const _56: rt.usize = rt.hashLen(left); + compared$collections_HashMap_hashes_Sided_hashes_Sided_hashes_Spread(right, left, _56, out); + } + const left2: rt.HashSet = _8(1, data); + const part4 = rt.byteVecSlice(reversed); + const _63: rt.HashSet = _8(2, part4); + const _68: rt.HashSet = _8(2, shorter); + const part5 = rt.byteVecSlice(flipped); + const iter2: rt.Iter> = rt.iterSlice([_63, _68, _8(2, part5)]); + while (true) { + const _77: rt.HashSet | rt.Done = iter2.next(); + if (_77 === rt.DONE) { + break; + } + const right2: rt.HashSet = _77; + const _84: rt.usize = rt.hashLen(left2); + compared$collections_HashSet_hashes_Sided_hashes_Spread(left2, right2, _84, out); + const _89: rt.usize = rt.hashLen(left2); + compared$collections_HashSet_hashes_Sided_hashes_Spread(right2, left2, _89, out); + } + compared$collections_HashMap_hashes_Sided_hashes_Sided_hashes_Spread(rt.hashNew(rt.hashing(Sided.hash$Hash_hash_DefaultHasher as (a0: Sided.Sided, a1: rt.Hasher) => undefined, Sided.eq$cmp_PartialEq), Spread.default$Default()) as rt.HashMap, rt.hashNew(rt.hashing(Sided.hash$Hash_hash_DefaultHasher as (a0: Sided.Sided, a1: rt.Hasher) => undefined, Sided.eq$cmp_PartialEq), Spread.default$Default()) as rt.HashMap, 0, out); + const _101: rt.HashMap = _96(data); + const part6 = rt.byteVecSlice(reversed); + const _105: rt.HashMap = _96(part6); + rt.byteVecPush(out, rt.hashEq(rt.same)(_101, _105) ? 1 : 0); + const _114: rt.HashMap = _96(data); + const part7 = rt.byteVecSlice(flipped); + const _118: rt.HashMap = _96(part7); + rt.byteVecPush(out, !rt.hashEq(rt.same)(_114, _118) ? 1 : 0); + const _128: rt.HashSet = _123(data); + const part8 = rt.byteVecSlice(reversed); + const _132: rt.HashSet = _123(part8); + rt.byteVecPush(out, rt.hashEq()(_128, _132) ? 1 : 0); + const _141: rt.HashSet = _123(data); + const _145: rt.HashSet = _123(shorter); + rt.byteVecPush(out, rt.hashEq()(_141, _145) ? 1 : 0); + const _154: rt.HashSet = _123(data); + const part9 = rt.byteVecSlice(flipped); + const _152: rt.HashSet[] = [_154, _123(part9)]; + const part10 = rt.byteVecSlice(reversed); + const _165: rt.HashSet = _123(part10); + const part11 = rt.byteVecSlice(flipped); + const _163: rt.HashSet[] = [_165, _123(part11)]; + rt.byteVecPush(out, rt.eqSeq(rt.hashEq())(_152, _163) ? 1 : 0); + const _180: hashes_Index.Index = _175(data); + const part12 = rt.byteVecSlice(reversed); + rt.byteVecPush(out, hashes_Index.eq$cmp_PartialEq(_180, _175(part12)) ? 1 : 0); + const _193: hashes_Index.Index = _175(data); + const part13 = rt.byteVecSlice(flipped); + rt.byteVecPush(out, !hashes_Index.eq$cmp_PartialEq(_193, _175(part13)) ? 1 : 0); +} + //# sourceMappingURL=hashes.ts.map diff --git a/ts/fixtures/lang/hashes/expected/lib.ts b/ts/fixtures/lang/hashes/expected/lib.ts index 8de3149d7..14fd1ab8e 100644 --- a/ts/fixtures/lang/hashes/expected/lib.ts +++ b/ts/fixtures/lang/hashes/expected/lib.ts @@ -1,7 +1,7 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { values, random, bytes, own, copied as hashes_copied, maps } from "./hashes.ts"; +import { values, random, bytes, own, copied as hashes_copied, maps, equal } from "./hashes.ts"; import type * as Noisy from "./hashes.Noisy.ts"; import type * as Seeded from "./hashes.Seeded.ts"; import * as Switch from "./hashes.Switch.ts"; @@ -30,16 +30,18 @@ export function drop$tuple2_u8_hashes_Noisy(_1: [rt.u8, Noisy.Noisy]): void { * their count, so a case added at the end moves no other. */ export function cases(): [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined][] { - return [["hashes::values", values as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::random", (_22: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): undefined => { + return [["hashes::values", values as (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::random", (_24: rt.u8, data: rt.ByteSlice, out: rt.ByteVec): undefined => { random(data, out); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::bytes", (_23: rt.u8, data2: rt.ByteSlice, out2: rt.ByteVec): undefined => { + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::bytes", (_25: rt.u8, data2: rt.ByteSlice, out2: rt.ByteVec): undefined => { 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 => { + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::own", (_26: 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::copied", (_25: rt.u8, data4: rt.ByteSlice, out4: rt.ByteVec): undefined => { + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::copied", (_27: rt.u8, data4: rt.ByteSlice, out4: rt.ByteVec): undefined => { hashes_copied(data4, out4); - }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::maps", (_26: rt.u8, data5: rt.ByteSlice, out5: rt.ByteVec): undefined => { + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::maps", (_28: rt.u8, data5: rt.ByteSlice, out5: rt.ByteVec): undefined => { maps(data5, out5); + }] as [string, (a0: rt.u8, a1: rt.ByteSlice, a2: rt.ByteVec) => undefined], ["hashes::equal", (_29: rt.u8, data6: rt.ByteSlice, out6: rt.ByteVec): undefined => { + equal(data6, out6); }] 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 86acbe978..d5e73b8ad 100644 --- a/ts/fixtures/lang/hashes/src/hashes.rs +++ b/ts/fixtures/lang/hashes/src/hashes.rs @@ -3,7 +3,7 @@ //! `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. Then hash maps and sets -//! that hold a `BuildHasher` of the crate's own. +//! that hold a `BuildHasher` of the crate's own, and their `==`. use std::cell::{Cell, RefCell}; use std::cmp::Reverse; @@ -751,3 +751,170 @@ pub fn maps(data: &[u8], out: &mut Vec) { drop(union); out.extend_from_slice(&EVENTS.with(|events| events.take())); } + +thread_local! { + /// Each `Hash` and `eq` of a [`Sided`], in order. + static CALLS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Log a call of `what` on the value of `side` holding `n`, given one of `other`. +fn call(what: u8, side: u8, other: u8, n: u8) { + CALLS.with(|calls| calls.borrow_mut().push(u32::from_be_bytes([what, side, other, n]))); +} + +/// A key or value of a map's side that logs each `Hash` and `eq` of it, so the log says +/// whose key std hashes, and whose `eq` it calls with the other's as its argument. +#[derive(Clone)] +struct Sided { + n: u8, + side: u8, +} + +impl Hash for Sided { + fn hash(&self, state: &mut H) { + call(b'h', self.side, 0, self.n); + self.n.hash(state); + } +} + +impl PartialEq for Sided { + fn eq(&self, other: &Self) -> bool { + call(b'e', self.side, other.side, self.n); + self.n == other.n + } +} + +impl Eq for Sided {} + +/// A `Hasher` of the crate's own whose hash puts the low 7 bits of the last byte written +/// where hashbrown reads each slot's tag, on 32- and 64-bit targets, so a lookup among keys +/// that differ there calls `eq` only on the one it finds, as the runtime's does. The seed +/// moves the rest, and so where each key goes. +struct Spreader { + seed: u64, + last: u64, +} + +impl Hasher for Spreader { + fn finish(&self) -> u64 { + let tag = self.last & 0x7f; + (tag << 57) | (tag << 25) | ((self.last ^ self.seed).wrapping_mul(0x9e37_79b9) & 0x1ff_ffff) + } + + fn write(&mut self, bytes: &[u8]) { + if let Some(&b) = bytes.last() { + self.last = u64::from(b); + } + } +} + +/// What builds a [`Spreader`] of its seed. +#[derive(Clone, Default)] +struct Spread(u64); + +impl BuildHasher for Spread { + type Hasher = Spreader; + + fn build_hasher(&self) -> Spreader { + Spreader { seed: self.0, last: 0 } + } +} + +/// A type whose derived `PartialEq` compares the maps it holds. +#[derive(PartialEq)] +struct Index { + names: HashMap, + seen: HashSet<(u8, bool)>, +} + +/// Whether `a == b`, and what it logged: in the order it called each if it did so for one +/// entry at most, else sorted if it visited every entry, else nothing, as which entries it +/// visits before one differs follows std's order. +fn compared(a: &T, b: &T, len: usize, out: &mut Vec) { + CALLS.with(|calls| calls.borrow_mut().clear()); + let equal = a == b; + out.push(equal as u8); + out.push((a != b) as u8); + let mut calls = CALLS.with(|calls| calls.take()); + if len > 1 && !equal { + calls.clear(); + } + // `!=` repeats `==`'s calls. + calls.truncate(calls.len() / 2); + if len > 1 { + calls.sort(); + } + out.push(calls.len() as u8); + for c in calls { + out.extend_from_slice(&c.to_be_bytes()); + } +} + +/// `==` and `!=` of hash maps and sets, as std's: equal lengths, then each entry of the +/// left looked up in the right, which hashes the left's key by its own `BuildHasher` and +/// calls the left key's `eq` on the right's, then the left value's `eq` on the right's. +/// Each side holds its own `BuildHasher`, seeded apart, and is built of the same bytes in +/// another order, so a key given twice keeps the value it was given last; or of all but +/// the last, or of one byte flipped. Then maps of a `RandomState`, which compare their keys +/// as JS does, and maps in a `Vec` and in a type that derives `PartialEq`. +pub fn equal(data: &[u8], out: &mut Vec) { + let first = data.first().copied().unwrap_or(0); + let map = |side: u8, bytes: &[u8]| { + let mut m = HashMap::with_hasher(Spread(u64::from(side) * 0x1_0001 + u64::from(first))); + for &b in bytes { + m.insert( + Sided { n: b & 0x7f, side }, + Sided { + n: b >> 6, + side: side + 2, + }, + ); + } + m + }; + let set = |side: u8, bytes: &[u8]| { + let mut s = HashSet::with_hasher(Spread(u64::from(side))); + for &b in bytes { + s.insert(Sided { n: b & 0x7f, side }); + } + s + }; + let reversed: Vec = data.iter().rev().copied().collect(); + let flipped: Vec = data + .iter() + .enumerate() + .map(|(i, &b)| if i == data.len() / 2 { b ^ 0x41 } else { b }) + .collect(); + let shorter = &data[..data.len().saturating_sub(1)]; + let left = map(1, data); + for right in [map(2, &reversed), map(2, data), map(2, shorter), map(2, &flipped)] { + compared(&left, &right, left.len(), out); + compared(&right, &left, left.len(), out); + } + let left = set(1, data); + for right in [set(2, &reversed), set(2, shorter), set(2, &flipped)] { + compared(&left, &right, left.len(), out); + compared(&right, &left, left.len(), out); + } + let empty: HashMap = HashMap::default(); + compared(&empty, &HashMap::default(), 0, out); + let counts = |bytes: &[u8]| { + let mut m: HashMap = HashMap::new(); + for &b in bytes { + *m.entry(b % 8).or_default() += u16::from(b); + } + m + }; + out.push((counts(data) == counts(&reversed)) as u8); + out.push((counts(data) != counts(&flipped)) as u8); + let strings = |bytes: &[u8]| bytes.iter().map(|&b| text(&[b % 4])).collect::>(); + out.push((strings(data) == strings(&reversed)) as u8); + out.push((strings(data) == strings(shorter)) as u8); + out.push((vec![strings(data), strings(&flipped)] == vec![strings(&reversed), strings(&flipped)]) as u8); + let index = |bytes: &[u8]| Index { + names: bytes.iter().map(|&b| (b % 8, text(&[b % 3]))).collect(), + seen: bytes.iter().map(|&b| (b % 5, b % 2 == 0)).collect(), + }; + out.push((index(data) == index(&reversed)) as u8); + out.push((index(data) != index(&flipped)) as u8); +} diff --git a/ts/fixtures/lang/hashes/src/lib.rs b/ts/fixtures/lang/hashes/src/lib.rs index 60ab0ca46..02878d1bd 100644 --- a/ts/fixtures/lang/hashes/src/lib.rs +++ b/ts/fixtures/lang/hashes/src/lib.rs @@ -24,5 +24,6 @@ fn cases() -> Vec<(&'static str, Case)> { ("hashes::own", |_, data, out| hashes::own(data, out)), ("hashes::copied", |_, data, out| hashes::copied(data, out)), ("hashes::maps", |_, data, out| hashes::maps(data, out)), + ("hashes::equal", |_, data, out| hashes::equal(data, out)), ] } diff --git a/ts/fixtures/stock/expected/harness.ts b/ts/fixtures/stock/expected/harness.ts index 0ca27b03f..5d87f8c3b 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, late, writes, edits, chains } from "./buf.ts"; import * as Owner from "./harness.Owner.ts"; -import { map as maps_map, table, capacity, linked, linkedIters, nested, ownHasher, ownFromIter } from "./maps.ts"; +import { map as maps_map, table, capacity, linked, linkedIters, nested, ownHasher, ownFromIter, equal } 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], ["buf::late", late as (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]]; + }] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["buf::late", late as (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], ["maps::equal", equal 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/maps.ts b/ts/fixtures/stock/expected/maps.ts index c5ccf6f21..106095120 100644 --- a/ts/fixtures/stock/expected/maps.ts +++ b/ts/fixtures/stock/expected/maps.ts @@ -1251,4 +1251,78 @@ export function ownFromIter(data: rt.ByteSlice, out: rt.ByteVec): void { })(MADE.get()))); } +/** + * `==` and `!=` of `hashbrown`'s maps and sets, as std's: equal lengths, then each entry + * of the left found in the right with an equal value, whatever the `BuildHasher` of each, + * its own seeded apart on each side, or the default. The right is of the same bytes in + * another order, so a key given twice keeps the value it was given last; or of all but the + * last, or of one byte flipped. + */ +export function equal(data: rt.ByteSlice, out: rt.ByteVec): void { + const _22: (a0: rt.ByteSlice) => rt.HashMap = (bytes: rt.ByteSlice): rt.HashMap => { + const _3: rt.Iter<[rt.u16, rt.u32]> = rt.iterMap(rt.iterSlice(rt.byteSliceView(bytes)), (_2: rt.u8): [rt.u16, rt.u32] => { + const b: rt.u8 = _2; + return [rt.u8Rem(b, 8) as rt.u16, b as rt.u32] as [rt.u16, rt.u32]; + }); + return rt.hashExtend(rt.hashNew() as rt.HashMap, _3, undefined, undefined, undefined, 117440512); + }; + const log: rt.RefCell = new rt.RefCell(new rt.ByteVec()); + const reversed: rt.ByteVec = rt.iterToByteVec(rt.iterRev(rt.iterSlice(rt.byteSliceView(data))), undefined, true); + const flipped: rt.ByteVec = rt.iterToByteVec(rt.iterMap(rt.iterEnumerate(rt.iterSlice(rt.byteSliceView(data))), (_23: [rt.usize, rt.u8]): rt.u8 => { + const i: rt.usize = _23[0]; + const b2: rt.u8 = _23[1]; + if (!(i === rt.usizeDiv(data.length, 2))) { + return b2; + } + return b2 ^ 33; + }), undefined, true); + const shorter: rt.ByteSlice = rt.byteSliceRange(data, 0, rt.usizeSaturatingSub(data.length, 1)); + const pairs: (a0: rt.u8, a1: rt.ByteSlice) => rt.HashMap<[rt.u8, boolean], rt.u8> = (seed: rt.u8, bytes2: rt.ByteSlice): rt.HashMap<[rt.u8, boolean], rt.u8> => { + const _54: Seeded.Seeded = new Seeded.Seeded(seed, log); + const m: rt.HashMap<[rt.u8, boolean], rt.u8> = rt.hashNew(rt.hashing(rt.hashTuple([rt.hash8, rt.hash8]), rt.eqTuple([rt.same, rt.same])), _54) as rt.HashMap<[rt.u8, boolean], rt.u8>; + const iterS: Uint8Array = rt.byteSliceView(bytes2); + let iterI: number = 0; + while (true) { + const _83: rt.u8 | rt.Done = iterI < iterS.length ? iterS[iterI++] : rt.DONE; + if (_83 === rt.DONE) { + return m; + } + const b3: rt.u8 = _83; + const _142: [rt.u8, boolean] = [rt.u8Rem(b3, 8), rt.u8Rem(b3, 3) === 0]; + rt.hashInsert(m, _142, b3 >> 5); + } + }; + const keys: (a0: rt.u8, a1: rt.ByteSlice) => rt.HashSet = (seed2: rt.u8, bytes3: rt.ByteSlice): rt.HashSet => { + const s: rt.HashSet = rt.hashNew(undefined, new Seeded.Seeded(seed2, log)) as rt.HashSet; + const _84: rt.Iter = rt.iterMap(rt.iterSlice(rt.byteSliceView(bytes3)), (b4: rt.u8): rt.u8 => { + return rt.u8Rem(b4, 16); + }); + rt.hashExtendKeys(s, _84, undefined, undefined, 469762048); + return s; + }; + const _26: [rt.HashMap<[rt.u8, boolean], rt.u8>, rt.HashSet, rt.HashMap] = [pairs(1, data), keys(1, data), _22(data)]; + const left: rt.HashMap<[rt.u8, boolean], rt.u8> = _26[0]; + const set: rt.HashSet = _26[1]; + const counts: rt.HashMap = _26[2]; + const iter: rt.Iter = rt.iterSlice([rt.byteSliceRange(rt.byteVecSlice(reversed), 0), data, shorter, rt.byteVecSlice(flipped)]); + while (true) { + const _43: rt.ByteSlice | rt.Done = iter.next(); + if (_43 === rt.DONE) { + break; + } + const right: rt.HashMap<[rt.u8, boolean], rt.u8> = pairs(2, _43); + rt.byteVecPush(out, rt.hashEq(rt.same)(left, right) ? 1 : 0); + rt.byteVecPush(out, !rt.hashEq(rt.same)(right, left) ? 1 : 0); + const other: rt.HashSet = keys(2, _43); + rt.byteVecPush(out, rt.hashEq()(set, other) ? 1 : 0); + rt.byteVecPush(out, !rt.hashEq()(other, set) ? 1 : 0); + const _78: rt.HashMap = _22(_43); + rt.byteVecPush(out, rt.hashEq(rt.same)(counts, _78) ? 1 : 0); + rt.hashDrop(other, undefined, undefined, rt.drop); + rt.hashDrop(right, undefined, undefined, rt.drop); + } + rt.hashDrop(set, undefined, undefined, rt.drop); + rt.hashDrop(left, undefined, undefined, rt.drop); +} + //# sourceMappingURL=maps.ts.map diff --git a/ts/fixtures/stock/src/harness.rs b/ts/fixtures/stock/src/harness.rs index cdc5591a6..d9ca28494 100644 --- a/ts/fixtures/stock/src/harness.rs +++ b/ts/fixtures/stock/src/harness.rs @@ -31,6 +31,7 @@ fn cases() -> Vec<(&'static str, Case)> { ("buf::late", crate::buf::late), ("maps::own_hasher", crate::maps::own_hasher), ("maps::own_from_iter", crate::maps::own_from_iter), + ("maps::equal", crate::maps::equal), ] } diff --git a/ts/fixtures/stock/src/maps.rs b/ts/fixtures/stock/src/maps.rs index a741927b4..a14f8eea9 100644 --- a/ts/fixtures/stock/src/maps.rs +++ b/ts/fixtures/stock/src/maps.rs @@ -734,3 +734,47 @@ pub fn own_from_iter(data: &[u8], out: &mut Vec) { drop((hinted, keys)); out.extend_from_slice(&MADE.with(|made| made.take())); } + +/// `==` and `!=` of `hashbrown`'s maps and sets, as std's: equal lengths, then each entry +/// of the left found in the right with an equal value, whatever the `BuildHasher` of each, +/// its own seeded apart on each side, or the default. The right is of the same bytes in +/// another order, so a key given twice keeps the value it was given last; or of all but the +/// last, or of one byte flipped. +pub fn equal(data: &[u8], out: &mut Vec) { + let log = RefCell::new(Vec::new()); + let reversed: Vec = data.iter().rev().copied().collect(); + let flipped: Vec = data + .iter() + .enumerate() + .map(|(i, &b)| if i == data.len() / 2 { b ^ 0x21 } else { b }) + .collect(); + let shorter = &data[..data.len().saturating_sub(1)]; + let pairs = |seed: u8, bytes: &[u8]| { + let mut m = HashMap::with_hasher(Seeded(seed, &log)); + for &b in bytes { + m.insert((b % 8, b % 3 == 0), b >> 5); + } + m + }; + let keys = |seed: u8, bytes: &[u8]| { + let mut s = HashSet::with_hasher(Seeded(seed, &log)); + s.extend(bytes.iter().map(|b| b % 16)); + s + }; + let numbers = |bytes: &[u8]| { + bytes + .iter() + .map(|&b| (u16::from(b % 8), u32::from(b))) + .collect::>() + }; + let (left, set, counts) = (pairs(1, data), keys(1, data), numbers(data)); + for bytes in [&reversed[..], data, shorter, &flipped] { + let right = pairs(2, bytes); + out.push(u8::from(left == right)); + out.push(u8::from(right != left)); + let other = keys(2, bytes); + out.push(u8::from(set == other)); + out.push(u8::from(other != set)); + out.push(u8::from(counts == numbers(bytes))); + } +} diff --git a/ts/runtime/rt/collections.ts b/ts/runtime/rt/collections.ts index 3d9d09b7f..f43e89c8e 100644 --- a/ts/runtime/rt/collections.ts +++ b/ts/runtime/rt/collections.ts @@ -468,10 +468,13 @@ function hashed(m: HashMap, key: K): boolean { return m.h !== undefined && (m.h.all || typeof key === "object"); } -/** The slot holding a key equal to `key`, a hashed key whose hash is `hash` if given. */ +/** + * The slot holding a key equal to `key`, a hashed key whose hash is `hash` if given, which + * compares by `key`'s `eq`, as std's lookups do. + */ function slot(m: HashMap, key: K, hash?: number): Slot | undefined { const h = m.h as Hashing; - for (let s = m.o?.get(hash ?? h.hash(key)); s !== undefined; s = s.next) if (h.eq(s.k, key)) return s; + for (let s = m.o?.get(hash ?? h.hash(key)); s !== undefined; s = s.next) if (h.eq(key, s.k)) return s; return undefined; } @@ -1095,6 +1098,22 @@ export function hashClone( return out; } +/** + * `PartialEq::eq` of hash maps, by their values' `eq`, or of sets, without one: as std's, + * equal lengths, then each entry of `a` found in `b`, by the key and then the value of + * `a`'s `eq`. The runtime's order decides which entries it compares before one differs. + */ +export function hashEq(eq?: (a: V, b: V) => boolean): (a: HashMap, b: HashMap) => boolean { + return (a: HashMap, b: HashMap) => { + if (hashLen(a) !== hashLen(b)) return false; + for (const [key, value] of hashIter(a)) { + const found = hashGetKeyValue(b, key); + if (found === undefined || (eq !== undefined && !eq(value, found[1]))) return false; + } + return true; + }; +} + // Set operations of hash sets, as std's: each walks one set's keys in the runtime's // order, which picks which set's key of two equal ones it yields. diff --git a/ts/src/lower/collections.rs b/ts/src/lower/collections.rs index 65ca57671..d53c54a01 100644 --- a/ts/src/lower/collections.rs +++ b/ts/src/lower/collections.rs @@ -133,11 +133,11 @@ pub fn call<'tcx>( let eq = eq.call(args); return Ok(Some(if name == "eq" { eq } else { eq.not() })); } - let kind = b.t.std_of(t) == Some(Std::ErrorKind); + let comparable = matches!(b.t.std_of(t), Some(Std::ErrorKind | Std::HashMap | Std::HashSet)); if matches!(t.kind(), ty::Bool | ty::Int(_) | ty::Uint(_)) || library::nothing(tcx, t) || bytes(b, t) - || !(std_key(b, t) || kind) + || !(std_key(b, t) || comparable) || !alike(b, t, rhs) { return Err(args); @@ -2825,6 +2825,9 @@ pub fn eq_fn<'tcx>(b: &mut Body<'_, '_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Expr ty::Adt(..) if let Some(item) = deque_of(b, ty) => Expr::rt("eqSeq", vec![not_ne_fn(b, item, span)]), // A list's iterator compares its items by `==`. ty::Adt(..) if let Some(item) = list_of(b, ty) => Expr::rt("eqList", vec![eq_fn(b, item, span)]), + // A map compares its values by `==`, and a set holds none. + ty::Adt(..) if b.t.std_of(ty) == Some(Std::HashMap) => Expr::rt("hashEq", vec![eq_fn(b, arg(ty, 1), span)]), + ty::Adt(..) if b.t.std_of(ty) == Some(Std::HashSet) => Expr::rt("hashEq", vec![]), _ if bytes(b, ty) => Expr::Rt("eqBytes".into()), ty::Adt(..) if let Some(inner) = arc_of(b, ty) => { let eq = eq_fn(b, inner, span);