├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── LICENSE ├── README.md ├── package.json ├── polyfill ├── collection-array.test.ts ├── collection-array.ts ├── collection-map.test.ts ├── collection-map.ts ├── collection-set.test.ts ├── collection-set.ts ├── composite.test.ts ├── composite.ts ├── index.test.ts ├── index.ts └── internal │ ├── composite-class.ts │ ├── hash.test.ts │ ├── hash.ts │ ├── hashmap.test.ts │ ├── hashmap.ts │ ├── key-lookup.ts │ ├── murmur.ts │ ├── originals.ts │ ├── utils.test.ts │ └── utils.ts ├── spec.emu └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build spec 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ljharb/actions/node/install@main 12 | name: "nvm install lts/* && npm install" 13 | with: 14 | node-version: lts/* 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: ljharb/actions/node/install@main 15 | name: "nvm install lts/* && npm install" 16 | with: 17 | node-version: lts/* 18 | - run: npm run build 19 | - uses: JamesIves/github-pages-deploy-action@v4 20 | with: 21 | branch: gh-pages 22 | folder: build 23 | clean: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | pnpm-lock.yaml 44 | 45 | # Build directory 46 | build 47 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ECMA TC39 and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proposal Composites 2 | 3 | Keys for Maps and Sets that represent a structured group of values. 4 | 5 | ## Status 6 | 7 | Stage: [1](https://tc39.es/process-document/) 8 | 9 | Champion(s): [Ashley Claymore](https://github.com/acutmore) 10 | 11 | ## The _issue_ 12 | 13 | Right now `Map` and `Set` always use [SameValueZero](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero) for their internal equality predicate answering "Is this value in this collection?". 14 | 15 | ```js 16 | new Set([42, 42]).size; // 1 17 | const m = new Map(); 18 | m.set("hello", "world"); 19 | m.get("hello"); // "world"; 20 | ``` 21 | 22 | This means that when it comes to objects, all objects are only equal to themselves. There is no capability to override this behavior and allow two different objects to be treated equal within the collection. 23 | 24 | ```js 25 | const position1 = Object.freeze({ x: 1, y: 4 }); 26 | const position2 = Object.freeze({ x: 1, y: 4 }); 27 | 28 | const positions = new Set([position1, position2]); 29 | positions.size; // 2 30 | ``` 31 | 32 | ### Current workaround 33 | 34 | One way to work around this limitation in JavaScript is to flatten the value to a string representation. 35 | 36 | ```js 37 | const positions = new Set([JSON.stringify(position1), JSON.stringify(position2)]); 38 | positions.size; // 1 39 | ``` 40 | 41 | The downsides of this are: 42 | 43 | - It can be easy to construct incorrect strings, for example `JSON.stringify` will produce a different string if the object keys are enumerated in a different order or throw if the value does not have a built-in JSON representation. 44 | - The collection now contains strings and not structured objects. To read the values back out they would need to be parsed. 45 | 46 | Alternatively two collections can be used, one to track uniqueness and another to track values: 47 | 48 | ```js 49 | const positions = []; 50 | const positionKeys = new Set(); 51 | function add(position) { 52 | const asString = JSON.stringify(position); 53 | if (positionKeys.has(asString)) return; 54 | positions.push(position); 55 | positionKeys.add(asString); 56 | } 57 | ``` 58 | 59 | The downsides of this are: 60 | 61 | - Code needs to ensure the two collections are kept in-sync with each other. 62 | - Extra noise/boilerplate to follow this pattern 63 | - Same risk as above of flattening a value to a string 64 | 65 | ## The proposal 66 | 67 | Introduce built-in 'composite values' with well defined equality. 68 | 69 | > [!IMPORTANT] 70 | > Expect changes. The design below is a starting point to evolve from as discussion continues. 71 | 72 | ```js 73 | const pos1 = Composite({ x: 1, y: 4 }); 74 | const pos2 = Composite({ x: 1, y: 4 }); 75 | Composite.equal(pos1, pos2); // true 76 | 77 | const positions = new Set(); // the standard ES Set 78 | positions.add(pos1); 79 | positions.has(pos2); // true 80 | ``` 81 | 82 | ### What is a 'composite' 83 | 84 | It is an object. 85 | 86 | ```js 87 | typeof Composite({}); // "object" 88 | ``` 89 | 90 | Each call to `Composite(...)` returns a new object. 91 | 92 | ```js 93 | Composite({}) !== Composite({}); // true 94 | ``` 95 | 96 | It does not modify the argument. 97 | 98 | ```js 99 | const template = { x: 1 }; 100 | Composite(template) !== template; // true 101 | ``` 102 | 103 | The argument must be an object. 104 | 105 | ```js 106 | Composite(null); // throws TypeError 107 | ``` 108 | 109 | They are not a class. 110 | 111 | ```js 112 | Object.getPrototypeOf(Composite({})); // Object.prototype 113 | new Composite({}); // throws TypeError 114 | ``` 115 | 116 | They are frozen. 117 | 118 | ```js 119 | Object.isFrozen(Composite({})); // true 120 | ``` 121 | 122 | They expose the data they were constructed from. 123 | 124 | ```js 125 | const c = Composite({ x: 1, y: 2 }); 126 | Object.keys(c); // ["x", "y"] 127 | c.x; // 1 128 | c.y; // 2 129 | ``` 130 | 131 | They can contain any value. 132 | 133 | ```js 134 | const d = new Date(); 135 | const c = Composite({ d, zero: -0 }); 136 | c.d === d; // true 137 | Object.is(c.zero, -0); // true 138 | ``` 139 | 140 | ### What are the equality semantics? 141 | 142 | Two composites are equal only if they have the same prototype ([#5](https://github.com/acutmore/proposal-composites/issues/5)) and their properties form the same set of key-value pairs. 143 | 144 | The values of each property are considered equal if 145 | 146 | - they are considered equal by `SameValueZero` 147 | - or if they are both composites and considered as equal composites (deeply recursive). 148 | 149 | As composites are immutable from birth checking their equality never leads to a cycle. 150 | 151 | Checking if two composites are equal always terminates and never throws. 152 | 153 | The equality of two composites never changes. 154 | 155 | The equality of two composites is symmetric. 156 | 157 | ```js 158 | const eq = Composite.equal; 159 | const C = Composite; 160 | 161 | eq(C({}), C({})); // true 162 | eq(C({ a: 1 }), C({ a: 1 })); // true 163 | !eq(C({ a: 1 }), C({ a: 1 , b: 2 })); // true 164 | 165 | eq(C({ 166 | z: 0 167 | c: C({}) 168 | }), 169 | C({ 170 | z: -0, 171 | c: C({}) 172 | })); // true 173 | 174 | !eq(C({ obj: {} }), C({ obj: {} })); // true 175 | eq(C({ obj: globalThis }), C({ obj: globalThis })); // true 176 | ``` 177 | 178 | Composite equality would be used by: 179 | 180 | - `Composite.equal` 181 | - `Map` 182 | - `Set` 183 | - `Array.prototype.includes` 184 | - `Array.prototype.indexOf` \* 185 | - `Array.prototype.lastIndexOf` \* 186 | 187 | > \* `indexOf` and `lastIndexOf` remain strict equality (`===`) when the argument is not a composite. When the argument is a composite they use the same equality as `includes` i.e. `Composite.equal` 188 | 189 | And future proposals such as https://github.com/tc39/proposal-iterator-unique could also use it. 190 | 191 | ```js 192 | someIterator.uniqueBy((obj) => Composite({ name: obj.name, company: obj.company })); 193 | // or if the iterator already contains composites: 194 | someIterator.uniqueBy(); 195 | ``` 196 | 197 | ## Other languages 198 | 199 | Python: 200 | 201 | ```py 202 | position1 = (1, 4) 203 | position2 = (1, 4) 204 | 205 | positions = set() 206 | positions.add(position1) 207 | positions.add(position2) 208 | 209 | print(len(positions)) # 1 210 | ``` 211 | 212 | Clojure: 213 | 214 | ```clj 215 | (def position1 '(1 4)) 216 | (def position2 '(1 4)) 217 | (count (set [position1 position2])) ; 1 218 | ``` 219 | 220 | ## FAQ 221 | 222 | ### How to check if something is a composite? 223 | 224 | `Composite.isComposite(arg)` only returns true for composites. A proxy with a composite as its target is not considered a composite. 225 | 226 | ### Can this be polyfilled? 227 | 228 | Yes ["./polyfill"](./polyfill/). 229 | 230 | Though like all JS polyfills it can only emulate internal slots with a local WeakMap. So a composite created by one instance of the polyfill would not be considered as being a composite by a separate instance of the polyfill, and would thus also not be equal. 231 | 232 | ### Should a composite's keys be sorted 233 | 234 | Let's discuss in [#1](https://github.com/acutmore/proposal-composites/issues/1). 235 | 236 | ### Performance expectations 237 | 238 | Creation of Composites should be similar to regular objects. The values do not need to be validated, or hashed eagerly. Composites would also need one extra flag stored to mark that they are composites, and potentially an additional hash value, so there is potential that they would consume more memory than a regular object. Storing data in composites avoids the need to flatten them into strings when wanting a map key, which may offset the additional memory consumption. 239 | 240 | Comparison of two composites would be linear time. Comparing two composites that contain the same components is the worst case as we only know they are equal once everything has been compared. When two composites are not equal the equality finishes earlier as soon as the first difference is found. If two composites have different keys the equality can stop without needing to recurse into the values. It would be expected that composites store a hash value so that comparisons are more likely to find differences immediately and reduce collisions in Maps. 241 | 242 | ### Are composites deeply immutable? 243 | 244 | Not necessarily. Composites are generic containers, so can contain any values. They are only deeply immutable if everything they contain are deeply immutable. 245 | 246 | ### Are keys enumerable? 247 | 248 | Yes, all keys are 249 | 250 | - enumerable: true 251 | - configurable: false 252 | - writable: false 253 | 254 | ### What about _Tuples_, or _ordinal_ rather than _nominal_ keys 255 | 256 | The simplest thing we could do here (beyond nothing) is provide a convenience API for ordinal composites. 257 | 258 | ```js 259 | Composite.of("a", "b", "c"); 260 | // Convenience (maybe more efficient) API for: 261 | Composite({ 0: "a", 1: "b", 2: "c", length: 3 }); 262 | ``` 263 | 264 | Or more advanced would be that these ordinal composites come with a prototype to allow list-like methods. 265 | 266 | ```js 267 | const c = Composite.of("a", "b", "c"); 268 | Object.getPrototypeOf(c) !== Object.prototype; // true, some other prototype 269 | Iterator.from(c); // implements Symbol.iterator 270 | c.forEach((v) => console.log(v)); 271 | ``` 272 | 273 | One idea is that it would be possible to create composites that are also array exotic objects. 274 | 275 | ```js 276 | const c = Composite(["a", "b", "c"]); 277 | Composite.isComposite(c); // true 278 | Array.isArray(c); // true 279 | Object.getPrototypeOf(c); // Array.prototype 280 | ``` 281 | 282 | This would have the advantage of being able to re-use the existing `Array.prototype` rather than creating more built-in methods. But overloading the concept of arrays (that can be mutable) with immutable composites may make the language harder to follow. 283 | 284 | Let's discuss in [#2](https://github.com/acutmore/proposal-composites/issues/2). 285 | 286 | ### What about WeakMaps and WeakSets? 287 | 288 | Composites act like regular objects in a `WeakMap` or `WeakSet`. 289 | 290 | ```js 291 | const objs = new WeakSet(); 292 | const c1 = Composite({}); 293 | const c2 = Composite({}); 294 | objs.add(c1); 295 | objs.has(c1); // true 296 | objs.has(c2); // false 297 | ``` 298 | 299 | This is for a variety of reasons: 300 | 301 | - Existing code that is putting objects in a `Weak{Map,Set}` is more likely to be expecting object referential keying. 302 | - Composites are not guaranteed to contain lifetime bearing values such as regular objects or unique symbols 303 | - It provides a way to still create a lookup that uses the object's reference as the key 304 | - It's possible to create a custom `Weak{Map,Set}` that has special handling for composites in user-land. 305 | - A follow-on proposal could propose a configurable `Weak{Map,Set}` with opt-in support composite weak-keys. 306 | 307 | ### Are composites new 'primitives'? 308 | 309 | No. A composite is an object. It's `typeof` is `"object"`. 310 | 311 | ### Can composites be compared with `===` or `Object.is`? 312 | 313 | Discussion continues in [issue 15](https://github.com/tc39/proposal-composites/issues/15). 314 | 315 | ### Why modify existing `Map` and `Set` semantics 316 | 317 | Instead of changing the semantics of `Map` there could be a new special `CompositeMap`. The reason the proposal does not do this is that JS developers already can choose between using objects as dictionaries or `Map`. Giving a third option prompts the question "when should I use the old `Map`, and when should I use the new one?", and the answer would be "safest bet is to always use the new `Map`". If code has a composite and it's being put in a `Map` the code is more likely to have wanted composite key lookup so it would become a foot gun to use the wrong map. 318 | 319 | It also is less clear how other APIs would provide the opt-in, such as https://github.com/tc39/proposal-iterator-unique. 320 | 321 | By replacing all the existing places in the language that currently use `SameValueZero` to take composites into account almost avoids adding a 5th form of equality to the language. It does technically still add a 5th by updating `Array.prototype.indexOf` so that it aligns with `Array.prototype.includes` when the argument is a composite. 322 | 323 | ### Symbols keys? 324 | 325 | Symbols keys are supported. 326 | 327 | ### Custom prototypes? 328 | 329 | Let's discuss in [#4](https://github.com/acutmore/proposal-composites/issues/4). 330 | 331 | ### Why not a new protocol? 332 | 333 | Why limit equality to only these composites values rather than let any object implement a new symbol protocol? The reason is reliability. To be able to participate as a `Map` key the equality must be pure, stable, and reliable, these guarantees would not come from a protocol that can execute arbitrary code. For example: an object having the symbol protocol added to it while it's in the map. 334 | 335 | ### Syntax? 336 | 337 | There could be syntax to make creating composites more ergonomic and cleaner to read. This would most likely be a separate, follow-on, proposal - after the Composites API has had time on its own in the ecosystem. 338 | 339 | ```js 340 | #{ x: 1 }; 341 | // Syntax for: 342 | Composite({ x: 1 }); 343 | ``` 344 | 345 | Syntax may also make the creation of composites more efficient, due to the engine being able to create the composite directly instead of needing to create the object argument for `Composite(arg)`. 346 | 347 | If there were [ordinal composites](#what-about-tuples-or-ordinal-rather-than-nominal-keys) they could also have syntax: 348 | 349 | ```js 350 | #[1, 2, 3]; 351 | // Syntax for: 352 | Composite.of(1, 2, 3); 353 | ``` 354 | 355 | ### Why named properties instead of an ordered key? 356 | 357 | On one hand it sounds simpler to start with a proposal where keys are lists instead of dictionaries, it could just be: 358 | 359 | ```js 360 | const c = Composite(1, 4); 361 | c[0]; // 1 362 | c[1]; // 4 363 | ``` 364 | 365 | We instead encourage the constituents of the composite to be named to make the code easier to follow and avoid bugs where the indices are mixed up. We can see that this is how JavaScript is most commonly written today - code passes around objects with named properties rather than indexed lists. 366 | 367 | ### Why implement natively in the language? 368 | 369 | Being able to create multi-value `Map` and `Set` keys is a common need across many application domains. 370 | 371 | Additionally, engines will have an advantage when it comes to implementing composites compared to user-land. Engines can access the existing hash value of objects and strings, and they can access the internals of `Map` and `Set`. 372 | 373 | ### How does this compare to [proposal-richer-keys](https://github.com/tc39/proposal-richer-keys)? 374 | 375 | That proposal: 376 | 377 | - Work as `Map` keys 378 | - `compositeKey` takes an ordered list, not named properties 379 | - The returned key is opaque with no properties and no prototype 380 | - At least one of the values must be an object 381 | - keys are strictly equal `compositeKey(Object) === compositeKey(Object)` (relies on GC for cleanup) 382 | - Also has `compositeSymbol` to create `symbol` keys 383 | 384 | This proposal: 385 | 386 | - Work as `Map` keys 387 | - Keys are made of named properties 388 | - The returned key exposes the data and has a prototype 389 | - No restriction on what the values must be 390 | - keys are not `===` equal, they create fresh objects (no reliance on GC) 391 | - All keys are objects, no 'symbol' keys 392 | 393 | ### How does this compare to [proposal-record-tuple](https://github.com/tc39/proposal-record-tuple)? 394 | 395 | That proposal: 396 | 397 | - Records work as `Map` keys 398 | - Records can be compared using `===` 399 | - Records are primitives with custom `typeof` 400 | - Records have no prototype (`null`) 401 | - Records cannot have symbol keys 402 | - Records can only contain primitives (deeply immutable) 403 | 404 | This proposal: 405 | 406 | - Composites work as `Map` keys 407 | - Composites are compared using `Composite.equal` 408 | - Composites are objects 409 | - Composites have a prototype 410 | - Composites can have symbol keys 411 | - Composites can contain any value (shallowly immutable) 412 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "proposal-composites", 4 | "description": "ECMAScript proposal for immutable values with structural equality", 5 | "type": "module", 6 | "scripts": { 7 | "test": "node --test --experimental-strip-types", 8 | "build-polyfill": "npx --yes esbuild@0.25.9 ./polyfill/index.ts --bundle --global-name=compositePolyfill --outfile=composite.js --drop-labels=DEV", 9 | "format": "prettier --write .", 10 | "start": "npm run build-loose -- --watch", 11 | "build": "npm run build-loose -- --strict", 12 | "build-loose": "node -e 'fs.mkdirSync(\"build\", { recursive: true })' && ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec.emu build/index.html --lint-spec" 13 | }, 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@tc39/ecma262-biblio": "2.1.2862", 17 | "ecmarkup": "^21.2.0", 18 | "@types/node": "^22.13.14", 19 | "prettier": "^3.5.3" 20 | }, 21 | "engines": { 22 | "node": ">= 18" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /polyfill/collection-array.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { Composite } from "./composite.ts"; 4 | import { 5 | arrayPrototypeIncludes, 6 | arrayPrototypeIndexOf, 7 | arrayPrototypeLastIndexOf, 8 | arrayPrototypeMethods, 9 | } from "./collection-array.ts"; 10 | 11 | await test("exports the replacements", () => { 12 | assert.deepStrictEqual(arrayPrototypeMethods, { 13 | includes: arrayPrototypeIncludes, 14 | indexOf: arrayPrototypeIndexOf, 15 | lastIndexOf: arrayPrototypeLastIndexOf, 16 | }); 17 | }); 18 | 19 | function makeArray(...args: unknown[]): unknown[] { 20 | Object.setPrototypeOf(args, { 21 | __proto__: Array.prototype, 22 | ...arrayPrototypeMethods, 23 | }); 24 | return args; 25 | } 26 | 27 | await test("includes", () => { 28 | const arr = makeArray(2, NaN, Composite({ a: 1 })); 29 | assert(arr.includes(2)); 30 | assert(!arr.includes(4)); 31 | assert(arr.includes(NaN)); 32 | assert(arr.includes(Composite({ a: 1 }))); 33 | assert(!arr.includes(Composite({ b: 1 }))); 34 | }); 35 | 36 | await test("indexOf", () => { 37 | const arr = makeArray(2, NaN, Composite({ a: 1 }), 2, Composite({ a: 1 })); 38 | assert.strictEqual(arr.indexOf(2), 0); 39 | assert.strictEqual(arr.indexOf(4), -1); 40 | assert.strictEqual(arr.indexOf(NaN), -1); 41 | assert.strictEqual(arr.indexOf(Composite({ a: 1 })), 2); 42 | assert.strictEqual(arr.indexOf(Composite({ b: 1 })), -1); 43 | }); 44 | 45 | await test("lastIndexOf", () => { 46 | const arr = makeArray(2, NaN, Composite({ a: 1 }), 2, Composite({ a: 1 })); 47 | assert.strictEqual(arr.lastIndexOf(2), 3); 48 | assert.strictEqual(arr.lastIndexOf(4), -1); 49 | assert.strictEqual(arr.lastIndexOf(NaN), -1); 50 | assert.strictEqual(arr.lastIndexOf(Composite({ a: 1 })), 4); 51 | assert.strictEqual(arr.lastIndexOf(Composite({ b: 1 })), -1); 52 | }); 53 | -------------------------------------------------------------------------------- /polyfill/collection-array.ts: -------------------------------------------------------------------------------- 1 | import { isComposite, compositeEqual, type Composite } from "./composite.ts"; 2 | import { hashComposite } from "./internal/hash.ts"; 3 | import { apply, includes, indexOf, lastIndexOf, freeze } from "./internal/originals.ts"; 4 | 5 | export function arrayPrototypeIncludes(this: T[], value: T): boolean { 6 | if (isComposite(value)) { 7 | return arrayPrototypeCompositeIndexOf(this, value, /* reverse: */ false) !== -1; 8 | } 9 | 10 | return apply(includes, this, [value]); 11 | } 12 | 13 | function arrayPrototypeCompositeIndexOf(arr: T[], value: Composite, reverse: boolean): number { 14 | let triggeredHash = false; 15 | for (let i = reverse ? arr.length - 1 : 0; reverse ? i >= 0 : i < arr.length; i += reverse ? -1 : 1) { 16 | const item = arr[i]; 17 | if (!triggeredHash && isComposite(item)) { 18 | triggeredHash = true; 19 | hashComposite(value); 20 | } 21 | if (compositeEqual(item, value)) { 22 | return i; 23 | } 24 | } 25 | return -1; 26 | } 27 | 28 | export function arrayPrototypeIndexOf(this: T[], value: T): number { 29 | if (isComposite(value)) { 30 | return arrayPrototypeCompositeIndexOf(this, value, /* reverse:*/ false); 31 | } 32 | return apply(indexOf, this, [value]); 33 | } 34 | 35 | export function arrayPrototypeLastIndexOf(this: T[], value: T): number { 36 | if (isComposite(value)) { 37 | return arrayPrototypeCompositeIndexOf(this, value, /* reverse:*/ true); 38 | } 39 | return apply(lastIndexOf, this, [value]); 40 | } 41 | 42 | export const arrayPrototypeMethods = freeze({ 43 | includes: arrayPrototypeIncludes, 44 | indexOf: arrayPrototypeIndexOf, 45 | lastIndexOf: arrayPrototypeLastIndexOf, 46 | }); 47 | -------------------------------------------------------------------------------- /polyfill/collection-map.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { Composite } from "./composite.ts"; 4 | import { Map as OGMap } from "./internal/originals.ts"; 5 | import { mapPrototypeMethods } from "./collection-map.ts"; 6 | 7 | class Map extends OGMap {} 8 | for (const [key, method] of Object.entries(mapPrototypeMethods)) { 9 | (Map.prototype as any)[key] = method; 10 | } 11 | 12 | await test("Map", () => { 13 | const c1 = Composite({ a: 1 }); 14 | const c2 = Composite({ a: 1 }); 15 | const m = new Map([ 16 | [c1, 1], 17 | [42, 99], 18 | [c2, 2], 19 | ]); 20 | assert.strictEqual(m.size, 2); 21 | assert.strictEqual(m.get(Composite({ a: 1 })), 2); 22 | assert.strictEqual(m.get(Composite({ b: 1 })), undefined); 23 | assert.strictEqual(m.get(c1), 2); 24 | assert.strictEqual(m.get(c2), 2); 25 | assert.strictEqual(m.get(42), 99); 26 | 27 | assert.deepStrictEqual( 28 | [...m], 29 | [ 30 | [c2, 2], 31 | [42, 99], 32 | ], 33 | ); 34 | assert.deepStrictEqual( 35 | [...m.entries()], 36 | [ 37 | [c2, 2], 38 | [42, 99], 39 | ], 40 | ); 41 | assert.deepStrictEqual([...m.keys()], [c2, 42]); 42 | assert.deepStrictEqual([...m.values()], [2, 99]); 43 | assert([...m.keys()][0] === c1, "c2 should not replace the reference to c1"); 44 | 45 | assert(m.has(Composite({ a: 1 }))); 46 | assert(!m.has(Composite({ a: 2 }))); 47 | assert(!m.delete(Composite({ b: 1 }))); 48 | assert(m.delete(Composite({ a: 1 }))); 49 | assert.strictEqual(m.size, 1); 50 | m.clear(); 51 | assert.strictEqual(m.size, 0); 52 | }); 53 | -------------------------------------------------------------------------------- /polyfill/collection-map.ts: -------------------------------------------------------------------------------- 1 | import { apply, mapSet, mapGet, mapHas, mapDelete, mapClear, freeze, mapSize } from "./internal/originals.ts"; 2 | import { isComposite } from "./composite.ts"; 3 | import { resolveKey, missing, clearCompMap, deleteKey } from "./internal/key-lookup.ts"; 4 | import { EMPTY } from "./internal/utils.ts"; 5 | 6 | function requireInternalSlot(that: unknown): void { 7 | apply(mapSize, that, EMPTY); 8 | } 9 | 10 | export function mapPrototypeSet(this: Map, key: K, value: V): globalThis.Map { 11 | requireInternalSlot(this); 12 | const keyToUse = resolveKey(this, key, /* create */ true); 13 | apply(mapSet, this, [keyToUse, value]); 14 | return this; 15 | } 16 | 17 | export function mapPrototypeDelete(this: Map, key: K): boolean { 18 | requireInternalSlot(this); 19 | if (!isComposite(key)) { 20 | return apply(mapDelete, this, [key]); 21 | } 22 | const existingKey = deleteKey(this, key); 23 | if (!existingKey) { 24 | return false; 25 | } 26 | apply(mapDelete, this, [existingKey]); 27 | return true; 28 | } 29 | 30 | export function mapPrototypeHas(this: Map, key: K): boolean { 31 | requireInternalSlot(this); 32 | const keyToUse = resolveKey(this, key, /* create */ false); 33 | if (keyToUse === missing) return false; 34 | return apply(mapHas, this, [keyToUse]); 35 | } 36 | 37 | export function mapPrototypeGet(this: Map, key: K): any { 38 | requireInternalSlot(this); 39 | const keyToUse = resolveKey(this, key, /* create */ false); 40 | if (keyToUse === missing) return undefined; 41 | return apply(mapGet, this, [keyToUse]); 42 | } 43 | 44 | export function mapPrototypeClear(this: Map): void { 45 | apply(mapClear, this, EMPTY); 46 | clearCompMap(this); 47 | } 48 | 49 | export const mapPrototypeMethods = freeze({ 50 | set: mapPrototypeSet, 51 | delete: mapPrototypeDelete, 52 | has: mapPrototypeHas, 53 | get: mapPrototypeGet, 54 | clear: mapPrototypeClear, 55 | }); 56 | -------------------------------------------------------------------------------- /polyfill/collection-set.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { Composite } from "./composite.ts"; 4 | import { Set as OGSet } from "./internal/originals.ts"; 5 | import { setPrototypeMethods } from "./collection-set.ts"; 6 | 7 | class Set extends OGSet {} 8 | for (const [key, method] of Object.entries(setPrototypeMethods)) { 9 | (Set.prototype as any)[key] = method; 10 | } 11 | 12 | await test("Set", () => { 13 | const c1 = Composite({ a: 1 }); 14 | const c2 = Composite({ a: 1 }); 15 | const s = new Set([c1, 42, c2]); 16 | assert.strictEqual(s.size, 2); 17 | assert(s.has(Composite({ a: 1 }))); 18 | assert(s.has(c1)); 19 | assert(s.has(c2)); 20 | assert(!s.has(Composite({ b: 1 }))); 21 | assert(s.has(42)); 22 | 23 | assert.deepStrictEqual([...s], [c2, 42]); 24 | assert.deepStrictEqual( 25 | [...s.entries()], 26 | [ 27 | [c2, c2], 28 | [42, 42], 29 | ], 30 | ); 31 | assert.deepStrictEqual([...s.keys()], [c2, 42]); 32 | assert.deepStrictEqual([...s.values()], [c2, 42]); 33 | assert([...s.keys()][0] === c1, "c1 should not be replaced by c2"); 34 | 35 | assert(!s.delete(Composite({ b: 1 }))); 36 | assert(s.delete(Composite({ a: 1 }))); 37 | assert.strictEqual(s.size, 1); 38 | s.clear(); 39 | assert.strictEqual(s.size, 0); 40 | }); 41 | 42 | await test("Set union", () => { 43 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 44 | 45 | const s2 = new Set([Composite({ b: 1 }), Composite({ c: 1 })]); 46 | 47 | const union = [...s1.union(s2)]; 48 | assert.deepStrictEqual(union, [Composite({ a: 1 }), Composite({ b: 1 }), Composite({ c: 1 })]); 49 | }); 50 | 51 | await test("Set intersection", () => { 52 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 53 | 54 | const s2 = new Set([Composite({ b: 1 }), Composite({ c: 1 })]); 55 | 56 | const intersection = [...s1.intersection(s2)]; 57 | assert.deepStrictEqual(intersection, [Composite({ b: 1 })]); 58 | }); 59 | 60 | await test("Set difference", () => { 61 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 62 | 63 | const s2 = new Set([Composite({ b: 1 }), Composite({ c: 1 })]); 64 | 65 | const difference = [...s1.difference(s2)]; 66 | assert.deepStrictEqual(difference, [Composite({ a: 1 })]); 67 | }); 68 | 69 | await test("Set symmetricDifference", () => { 70 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 71 | 72 | const s2 = new Set([Composite({ b: 1 }), Composite({ c: 1 })]); 73 | 74 | const symmetricDifference = [...s1.symmetricDifference(s2)]; 75 | assert.deepStrictEqual(symmetricDifference, [Composite({ a: 1 }), Composite({ c: 1 })]); 76 | }); 77 | 78 | await test("Set isSubsetOf", () => { 79 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 80 | 81 | const s2 = new Set([Composite({ a: 1 }), Composite({ b: 1 }), Composite({ c: 1 })]); 82 | 83 | assert(s1.isSubsetOf(s2)); 84 | assert(!s2.isSubsetOf(s1)); 85 | }); 86 | 87 | await test("Set isSupersetOf", () => { 88 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 89 | 90 | const s2 = new Set([Composite({ a: 1 }), Composite({ b: 1 }), Composite({ c: 1 })]); 91 | 92 | assert(!s1.isSupersetOf(s2)); 93 | assert(s2.isSupersetOf(s1)); 94 | }); 95 | 96 | await test("Set isDisjointFrom", () => { 97 | const s1 = new Set([Composite({ a: 1 }), Composite({ b: 1 })]); 98 | 99 | const s2 = new Set([Composite({ c: 1 })]); 100 | 101 | const s3 = new Set([Composite({ b: 1 })]); 102 | 103 | assert(s1.isDisjointFrom(s2)); 104 | assert(!s1.isDisjointFrom(s3)); 105 | }); 106 | -------------------------------------------------------------------------------- /polyfill/collection-set.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apply, 3 | min as toNumber, 4 | isNaN, 5 | abs, 6 | floor, 7 | NEGATIVE_INFINITY, 8 | POSITIVE_INFINITY, 9 | iterator, 10 | setAdd, 11 | setClear, 12 | setHas, 13 | setSize, 14 | setDelete, 15 | Set, 16 | freeze, 17 | setValues, 18 | setNext, 19 | } from "./internal/originals.ts"; 20 | import { isComposite } from "./composite.ts"; 21 | import { resolveKey, missing, clearCompMap, deleteKey } from "./internal/key-lookup.ts"; 22 | import { EMPTY } from "./internal/utils.ts"; 23 | 24 | function requireInternalSlot(that: unknown): void { 25 | apply(setSize, that, EMPTY); 26 | } 27 | 28 | function setPrototypeAdd(this: Set, value: T): Set { 29 | requireInternalSlot(this); 30 | const valueToUse = resolveKey(this, value, /* create */ true); 31 | apply(setAdd, this, [valueToUse]); 32 | return this; 33 | } 34 | 35 | function setPrototypeClear(this: Set): void { 36 | requireInternalSlot(this); 37 | apply(setClear, this, EMPTY); 38 | clearCompMap(this); 39 | } 40 | 41 | function setPrototypeDelete(this: Set, value: T): boolean { 42 | requireInternalSlot(this); 43 | if (!isComposite(value)) { 44 | return apply(setDelete, this, [value]); 45 | } 46 | const existingKey = deleteKey(this, value); 47 | if (!existingKey) { 48 | return false; 49 | } 50 | apply(setDelete, this, [existingKey]); 51 | return true; 52 | } 53 | 54 | function setPrototypeHas(this: Set, value: any): boolean { 55 | requireInternalSlot(this); 56 | const valueToUse = resolveKey(this, value, /* create */ false); 57 | if (valueToUse === missing) { 58 | return false; 59 | } 60 | return apply(setHas, this, [valueToUse]); 61 | } 62 | 63 | function setPrototypeUnion(this: Set, other: ReadonlySetLike): Set { 64 | requireInternalSlot(this); 65 | const otherSet = getSetRecord(other); 66 | const result = new Set(); 67 | for (const value of setIterator(this)) { 68 | apply(setPrototypeAdd, result, [value]); 69 | } 70 | for (const value of otherSet.keys()) { 71 | apply(setPrototypeAdd, result, [value]); 72 | } 73 | return result; 74 | } 75 | 76 | function setPrototypeIntersection(this: Set, other: ReadonlySetLike): Set { 77 | requireInternalSlot(this); 78 | const otherSet = getSetRecord(other); 79 | const result = new Set(); 80 | if (apply(setSize, this, EMPTY) <= otherSet.size) { 81 | for (const value of setIterator(this)) { 82 | if (otherSet.has(value)) { 83 | apply(setPrototypeAdd, result, [value]); 84 | } 85 | } 86 | } else { 87 | for (const value of otherSet.keys()) { 88 | if (apply(setPrototypeHas, this, [value])) { 89 | apply(setPrototypeAdd, result, [value]); 90 | } 91 | } 92 | } 93 | return result; 94 | } 95 | 96 | function setPrototypeDifference(this: Set, other: ReadonlySetLike): Set { 97 | requireInternalSlot(this); 98 | const otherSet = getSetRecord(other); 99 | const result = new Set(); 100 | for (const value of setIterator(this)) { 101 | apply(setPrototypeAdd, result, [value]); 102 | } 103 | if (result.size <= otherSet.size) { 104 | for (const value of result) { 105 | if (otherSet.has(value)) { 106 | apply(setPrototypeDelete, result, [value]); 107 | } 108 | } 109 | } else { 110 | for (const value of otherSet.keys()) { 111 | apply(setPrototypeDelete, result, [value]); 112 | } 113 | } 114 | return result; 115 | } 116 | 117 | function setPrototypeSymmetricDifference(this: Set, other: ReadonlySetLike): Set { 118 | requireInternalSlot(this); 119 | const otherSet = getSetRecord(other); 120 | const result = new Set(); 121 | for (const value of setIterator(this)) { 122 | if (!otherSet.has(value)) { 123 | apply(setPrototypeAdd, result, [value]); 124 | } 125 | } 126 | for (const value of otherSet.keys()) { 127 | if (!apply(setPrototypeHas, this, [value])) { 128 | apply(setPrototypeAdd, result, [value]); 129 | } 130 | } 131 | return result; 132 | } 133 | 134 | function setPrototypeIsSubsetOf(this: Set, other: ReadonlySetLike): boolean { 135 | requireInternalSlot(this); 136 | const otherSet = getSetRecord(other); 137 | if (apply(setSize, this, EMPTY) > otherSet.size) return false; 138 | for (const value of setIterator(this)) { 139 | if (!otherSet.has(value)) { 140 | return false; 141 | } 142 | } 143 | return true; 144 | } 145 | 146 | function setPrototypeIsSupersetOf(this: Set, other: ReadonlySetLike): boolean { 147 | requireInternalSlot(this); 148 | const otherSet = getSetRecord(other); 149 | if (apply(setSize, this, EMPTY) < otherSet.size) return false; 150 | for (const value of otherSet.keys()) { 151 | if (!apply(setPrototypeHas, this, [value])) { 152 | return false; 153 | } 154 | } 155 | return true; 156 | } 157 | 158 | function setPrototypeIsDisjointFrom(this: Set, other: ReadonlySetLike): boolean { 159 | requireInternalSlot(this); 160 | const otherSet = getSetRecord(other); 161 | 162 | if (apply(setSize, this, EMPTY) <= otherSet.size) { 163 | for (const value of setIterator(this)) { 164 | if (otherSet.has(value)) { 165 | return false; 166 | } 167 | } 168 | } else { 169 | for (const value of otherSet.keys()) { 170 | if (apply(setPrototypeHas, this, [value])) { 171 | return false; 172 | } 173 | } 174 | } 175 | 176 | return true; 177 | } 178 | 179 | const setIteratorProto = { 180 | __proto__: null, 181 | nextFn: undefined as any, 182 | it: undefined as unknown as Iterator, 183 | [iterator]() { 184 | return this; 185 | }, 186 | next() { 187 | return apply(this.nextFn, this.it, EMPTY); 188 | }, 189 | return(value: any) { 190 | const ret = this.it.return; 191 | if (ret) { 192 | return apply(ret, this.it, [value]); 193 | } 194 | return { 195 | value: undefined, 196 | done: true, 197 | }; 198 | }, 199 | }; 200 | 201 | function setIterator(set: Set): Iterable { 202 | const it = apply(setValues, set, EMPTY); 203 | return { 204 | __proto__: setIteratorProto, 205 | nextFn: setNext, 206 | it, 207 | } as {} as Iterable; 208 | } 209 | 210 | export const setPrototypeMethods = freeze({ 211 | add: setPrototypeAdd, 212 | clear: setPrototypeClear, 213 | delete: setPrototypeDelete, 214 | has: setPrototypeHas, 215 | union: setPrototypeUnion, 216 | intersection: setPrototypeIntersection, 217 | difference: setPrototypeDifference, 218 | symmetricDifference: setPrototypeSymmetricDifference, 219 | isSubsetOf: setPrototypeIsSubsetOf, 220 | isSupersetOf: setPrototypeIsSupersetOf, 221 | isDisjointFrom: setPrototypeIsDisjointFrom, 222 | }); 223 | 224 | function getSetRecord(other: ReadonlySetLike) { 225 | const size = toNumber(other.size); 226 | if (isNaN(size)) { 227 | throw new TypeError("invalid size"); 228 | } 229 | const intSize = toIntegerOrInfinity(size); 230 | if (intSize < 0) { 231 | throw new RangeError("invalid size"); 232 | } 233 | const has = other.has; 234 | if (typeof has !== "function") { 235 | throw new TypeError("invalid has"); 236 | } 237 | const keys = other.keys; 238 | if (typeof keys !== "function") { 239 | throw new TypeError("invalid keys"); 240 | } 241 | return { 242 | obj: other, 243 | size: intSize, 244 | has(v: any): boolean { 245 | return Boolean(apply(has, other, [v])); 246 | }, 247 | keys(): Iterable { 248 | const it = apply(keys, other, EMPTY); 249 | if (it === null || typeof it !== "object") { 250 | throw new TypeError("invalid keys"); 251 | } 252 | const next = it.next; 253 | 254 | return { 255 | __proto__: setIteratorProto, 256 | nextFn: next, 257 | it, 258 | } as {} as Iterable; 259 | }, 260 | }; 261 | } 262 | 263 | function toIntegerOrInfinity(arg: unknown): number { 264 | const n = toNumber(arg as number); 265 | if (isNaN(n) || n === 0) { 266 | return 0; 267 | } 268 | if (n === POSITIVE_INFINITY) { 269 | return POSITIVE_INFINITY; 270 | } 271 | if (n === NEGATIVE_INFINITY) { 272 | return NEGATIVE_INFINITY; 273 | } 274 | let i = floor(abs(n)); 275 | if (n < 0) { 276 | i = -i; 277 | } 278 | return i; 279 | } 280 | -------------------------------------------------------------------------------- /polyfill/composite.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { Composite } from "./composite.ts"; 4 | 5 | await test("should throw an error when constructed with 'new'", () => { 6 | assert.throws( 7 | () => { 8 | // @ts-expect-error 9 | new Composite({}); 10 | }, 11 | { 12 | message: "Composite should not be constructed with 'new'", 13 | }, 14 | ); 15 | }); 16 | await test("should throw an error when constructed with a non-object", () => { 17 | assert.throws( 18 | () => { 19 | // @ts-expect-error 20 | Composite(null); 21 | }, 22 | { 23 | message: "Composite should be constructed with an object", 24 | }, 25 | ); 26 | }); 27 | await test("creation", () => { 28 | assert.strictEqual(typeof Composite({}), "object"); 29 | assert.strictEqual(Object.getPrototypeOf(Composite({})), Object.prototype); 30 | assert.deepStrictEqual(Reflect.ownKeys(Composite({ a: 1 })), ["a"]); 31 | }); 32 | await test(".isComposite", () => { 33 | assert(Composite.isComposite(Composite({}))); 34 | assert(!Composite.isComposite({})); 35 | }); 36 | await test("key order", () => { 37 | const s1 = Symbol(); 38 | const s2 = Symbol(); 39 | const s3 = Symbol(); 40 | const sA = Symbol.for("a"); 41 | const sB = Symbol.for("b"); 42 | const c = Composite({ 43 | b: 0, 44 | a: 0, 45 | [0]: 0, 46 | [10]: 0, 47 | [s1]: 0, 48 | [sB]: 0, 49 | [s2]: 0, 50 | [sA]: 0, 51 | [s3]: 0, 52 | }); 53 | const keys = Reflect.ownKeys(c); 54 | assert.deepStrictEqual(keys, ["0", "10", "a", "b", s1, sB, s2, sA, s3]); 55 | }); 56 | await test(".equal non-composite equal", () => { 57 | assert(Composite.equal(-0, 0)); 58 | assert(Composite.equal(Function, Function)); 59 | assert(Composite.equal(globalThis, globalThis)); 60 | assert(Composite.equal("abc", "abc")); 61 | }); 62 | await test(".equal non-composite not-equal", () => { 63 | assert(!Composite.equal(1, 2)); 64 | assert(!Composite.equal({}, {})); 65 | assert( 66 | !Composite.equal( 67 | () => {}, 68 | () => {}, 69 | ), 70 | ); 71 | }); 72 | await test(".equal composites", () => { 73 | const c1 = Composite({ a: 1 }); 74 | const c2 = Composite({ a: 1 }); 75 | assert(c1 !== c2, "c1 and c2 should not be the same object"); 76 | assert(Composite.equal(c1, c2), "c1 and c2 should be equal"); 77 | const c3 = Composite({ a: 2 }); 78 | assert(!Composite.equal(c1, c3), "c1 and c3 should not be equal"); 79 | }); 80 | await test(".equal composites symbol props equal", () => { 81 | const s1 = Symbol(); 82 | const s2 = Symbol(); 83 | const c1 = Composite({ [s1]: 1, [s2]: 2 }); 84 | assert(Composite.equal(c1, Composite({ [s1]: 1, [s2]: 2 }))); 85 | 86 | const c2 = Composite({ [s2]: 2, [s1]: 1 }); 87 | assert(Composite.equal(c1, c2)); 88 | 89 | assert.deepStrictEqual(Reflect.ownKeys(c1), [s1, s2]); 90 | assert.deepStrictEqual(Reflect.ownKeys(c2), [s2, s1]); 91 | }); 92 | await test(".equal composites symbol props not-equal", () => { 93 | const s1 = Symbol(); 94 | const s2 = Symbol(); 95 | const c1 = Composite({ [s1]: 1 }); 96 | assert(!Composite.equal(c1, Composite({ [s1]: 2 })), "value under symbol is different"); 97 | assert(!Composite.equal(c1, Composite({ [s2]: 1 })), "symbol key is different"); 98 | }); 99 | await test(".equal deep", () => { 100 | const C = Composite; 101 | const c1 = C({ a: C({ b: C({ c: 1 }) }) }); 102 | const c2 = C({ a: C({ b: C({ c: 1 }) }) }); 103 | assert(Composite.equal(c1, c2), "Deeply nested composites c1 and c2 should be equal"); 104 | 105 | const c3 = C({ a: C({ b: C({ c: 2 }) }) }); 106 | assert(!Composite.equal(c1, c3), "Deeply nested composites c1 and c3 should not be equal"); 107 | 108 | const c4 = C({ 109 | a: C({ b: C({ c: 1, d: 2 }) }), 110 | }); 111 | assert(!Composite.equal(c1, c4), "Deeply nested composites c1 and c4 should not be equal due to extra property"); 112 | 113 | const c5 = C({ 114 | a: C({ b: C({ c: 1 }) }), 115 | e: 3, 116 | }); 117 | const c6 = C({ 118 | a: C({ b: C({ c: 1 }) }), 119 | e: 3, 120 | }); 121 | assert(Composite.equal(c5, c6), "Deeply nested composites c5 and c6 with additional properties should be equal"); 122 | 123 | const c7 = C({ 124 | a: C({ b: C({ c: 1 }) }), 125 | e: 4, 126 | }); 127 | assert( 128 | !Composite.equal(c5, c7), 129 | "Deeply nested composites c5 and c7 should not be equal due to differing additional properties", 130 | ); 131 | }); 132 | await test(".equal composites decimal numbers", () => { 133 | const c1 = Composite({ a: 2.0 }); 134 | const c2 = Composite({ a: 2.5 }); 135 | assert(c1 !== c2, "c1 and c2 should not be the same object"); 136 | assert(!Composite.equal(c1, c2), "c1 and c2 should not be equal"); 137 | const c3 = Composite({ a: 2.5 }); 138 | assert(c2 !== c3, "c2 and c3 should not be the same object"); 139 | assert(Composite.equal(c2, c3), "c2 and c3 should be equal"); 140 | }); 141 | await test(".equal composites interesting decimal numbers", () => { 142 | const c1 = Composite({ a: 1 + Number.EPSILON }); 143 | const c2 = Composite({ a: 1 + 2 * Number.EPSILON }); 144 | assert(c1 !== c2, "c1 and c2 should not be the same object"); 145 | assert(!Composite.equal(c1, c2), "c1 and c2 should not be equal"); 146 | const c3 = Composite({ a: 1 + 2 * Number.EPSILON }); 147 | assert(c2 !== c3, "c2 and c3 should not be the same object"); 148 | assert(Composite.equal(c2, c3), "c2 and c3 should be equal"); 149 | }); 150 | await test(".equal composites with polluted Object.prototype", () => { 151 | (Object.prototype as any)["pollution"] = true; 152 | try { 153 | const c1 = Composite({ pollution: true }); 154 | const c2 = Composite({ other: true }); 155 | assert(!Composite.equal(c1, c2), "c1 and c2 should not be equal"); 156 | } finally { 157 | delete (Object.prototype as any)["pollution"]; 158 | } 159 | }); 160 | 161 | await test(".equal composites with different key order", () => { 162 | const c1 = Composite({ a: true, b: true }); 163 | const c2 = Composite({ b: true, a: true }); 164 | assert(Composite.equal(c1, c2), "c1 and c2 should not be equal"); 165 | }); 166 | -------------------------------------------------------------------------------- /polyfill/composite.ts: -------------------------------------------------------------------------------- 1 | import { assert, EMPTY, sameValueZero } from "./internal/utils.ts"; 2 | import { 3 | ownKeys, 4 | apply, 5 | freeze, 6 | setAdd, 7 | setHas, 8 | Set, 9 | setPrototypeOf, 10 | objectPrototype, 11 | sort, 12 | } from "./internal/originals.ts"; 13 | import { __Composite__, objectIsComposite, maybeGetCompositeHash, setHash } from "./internal/composite-class.ts"; 14 | 15 | export type Composite = __Composite__; 16 | 17 | export function Composite(arg: object): Composite { 18 | if (new.target) { 19 | throw new TypeError("Composite should not be constructed with 'new'"); 20 | } 21 | if (typeof arg !== "object" || arg === null) { 22 | throw new TypeError("Composite should be constructed with an object"); 23 | } 24 | const argKeys = ownKeys(arg); 25 | const c = new __Composite__(); 26 | const stringKeys: string[] = []; 27 | for (let i = 0; i < argKeys.length; i++) { 28 | let k = argKeys[i]; 29 | if (typeof k === "string") { 30 | stringKeys[stringKeys.length] = k; 31 | } else { 32 | DEV: assert(typeof k === "symbol"); 33 | (c as any)[k] = (arg as any)[k]; 34 | } 35 | } 36 | apply(sort, stringKeys, EMPTY); 37 | for (let i = 0; i < stringKeys.length; i++) { 38 | let k = stringKeys[i]; 39 | (c as any)[k] = (arg as any)[k]; 40 | } 41 | setPrototypeOf(c, objectPrototype); 42 | freeze(c); 43 | return c; 44 | } 45 | 46 | export function isComposite(arg: unknown): arg is Composite { 47 | return typeof arg === "object" && arg !== null && objectIsComposite(arg); 48 | } 49 | Composite.isComposite = isComposite; 50 | 51 | export function compositeEqual(a: unknown, b: unknown): boolean { 52 | if (a === b) return true; 53 | 54 | const maybeHashA = typeof a === "object" && a !== null ? maybeGetCompositeHash(a) : undefined; 55 | 56 | const maybeHashB = 57 | maybeHashA !== undefined && typeof b === "object" && b !== null ? maybeGetCompositeHash(b) : undefined; 58 | 59 | if (maybeHashB === undefined) { 60 | return sameValueZero(a, b); 61 | } 62 | 63 | DEV: assert(maybeHashA !== undefined); 64 | DEV: assert(isComposite(a)); 65 | DEV: assert(isComposite(b)); 66 | if (maybeHashA !== 0 && maybeHashB !== 0 && maybeHashA !== maybeHashB) return false; 67 | 68 | const aKeys = ownKeys(a); 69 | const bKeys = ownKeys(b); 70 | if (aKeys.length !== bKeys.length) { 71 | return false; 72 | } 73 | let symbolKeysB: Set | undefined; 74 | let firstSymbolIndex: number | undefined; 75 | for (let i = 0; i < aKeys.length; i++) { 76 | const aKey = aKeys[i]; 77 | const bKey = bKeys[i]; 78 | if (typeof aKey !== typeof bKey) { 79 | // Different ratios of strings and symbols 80 | return false; 81 | } 82 | if (typeof aKey === "symbol") { 83 | if (symbolKeysB === undefined) { 84 | symbolKeysB = new Set(); 85 | firstSymbolIndex = i; 86 | } 87 | apply(setAdd, symbolKeysB, [bKey]); 88 | continue; 89 | } 90 | if (aKey !== bKey) { 91 | return false; 92 | } 93 | } 94 | if (firstSymbolIndex !== undefined) { 95 | DEV: assert(symbolKeysB !== undefined); 96 | for (let i = firstSymbolIndex; i < aKeys.length; i++) { 97 | if (!apply(setHas, symbolKeysB, [aKeys[i]])) { 98 | return false; 99 | } 100 | } 101 | } 102 | for (let i = 0; i < aKeys.length; i++) { 103 | const k = aKeys[i]; 104 | const aV = (a as any)[k]; 105 | const bV = (b as any)[k]; 106 | if (!compositeEqual(aV, bV)) { 107 | return false; 108 | } 109 | } 110 | 111 | if (maybeHashA === 0 && maybeHashB !== 0) { 112 | setHash(a, maybeHashB); 113 | } else if (maybeHashB === 0 && maybeHashA !== 0) { 114 | setHash(b, maybeHashA); 115 | } 116 | return true; 117 | } 118 | Composite.equal = compositeEqual; 119 | -------------------------------------------------------------------------------- /polyfill/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import VM from "node:vm"; 4 | import * as index from "./index.ts"; 5 | 6 | await test("index", () => { 7 | assert.deepStrictEqual(Object.keys(index).sort(), [ 8 | "Composite", 9 | "arrayPrototypeMethods", 10 | "install", 11 | "mapPrototypeMethods", 12 | "setPrototypeMethods", 13 | ]); 14 | }); 15 | 16 | await test("install", () => { 17 | // Given: 18 | const ctx = VM.createContext(); 19 | const globalThat = VM.runInContext("globalThis", ctx); 20 | globalThat.assert = assert; 21 | assert.deepStrictEqual(VM.runInContext("typeof Composite", ctx), "undefined", "not already available"); 22 | 23 | // When: 24 | index.install(globalThat); 25 | 26 | // Then: 27 | assert.deepStrictEqual(VM.runInContext("typeof Composite", ctx), "function"); 28 | VM.runInContext( 29 | ` 30 | const c1 = Composite({ x: 1 }); 31 | const c2 = Composite({ x: 1 }); 32 | const m = new Map(); 33 | m.set(c1, 42); 34 | assert.equal(m.get(c1), 42); 35 | 36 | const s = new Set(); 37 | s.add(c1); 38 | assert(s.has(c2)); 39 | `, 40 | ctx, 41 | ); 42 | ctx; 43 | }); 44 | -------------------------------------------------------------------------------- /polyfill/index.ts: -------------------------------------------------------------------------------- 1 | import { Composite } from "./composite.ts"; 2 | import { mapPrototypeMethods } from "./collection-map.ts"; 3 | import { setPrototypeMethods } from "./collection-set.ts"; 4 | import { arrayPrototypeMethods } from "./collection-array.ts"; 5 | import { ownKeys } from "./internal/originals.ts"; 6 | 7 | export { Composite, arrayPrototypeMethods, mapPrototypeMethods, setPrototypeMethods }; 8 | 9 | export function install(global: Record) { 10 | global["Composite"] = Composite; 11 | const arrayMethods = ownKeys(arrayPrototypeMethods); 12 | for (let i = 0; i < arrayMethods.length; i++) { 13 | const method = arrayMethods[i] as keyof typeof arrayPrototypeMethods; 14 | const impl = arrayPrototypeMethods[method]; 15 | global["Array"].prototype[method] = impl; 16 | } 17 | 18 | const mapMethods = ownKeys(mapPrototypeMethods); 19 | for (let i = 0; i < mapMethods.length; i++) { 20 | const method = mapMethods[i] as keyof typeof mapPrototypeMethods; 21 | const impl = mapPrototypeMethods[method]; 22 | global["Map"].prototype[method] = impl; 23 | } 24 | 25 | const setMethods = ownKeys(setPrototypeMethods); 26 | for (let i = 0; i < setMethods.length; i++) { 27 | const method = setMethods[i] as keyof typeof setPrototypeMethods; 28 | const impl = setPrototypeMethods[method]; 29 | global["Set"].prototype[method] = impl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /polyfill/internal/composite-class.ts: -------------------------------------------------------------------------------- 1 | import { setPrototypeOf } from "./originals.ts"; 2 | 3 | export class __Composite__ { 4 | // 0 == lazy hash 5 | #hash = 0; 6 | static maybeGetCompositeHash(c: object): number | undefined { 7 | if (#hash in c) return c.#hash; 8 | return undefined; 9 | } 10 | static getCompositeHash(c: __Composite__) { 11 | return c.#hash; 12 | } 13 | static objectIsComposite(c: object): c is __Composite__ { 14 | return #hash in c; 15 | } 16 | static setHash(c: __Composite__, hash: number): void { 17 | // 0 == lazy hash 18 | if (hash === 0) hash = 1; 19 | c.#hash = hash; 20 | } 21 | } 22 | 23 | // Ensure setting properties during construction doesn't trigger Object.prototype setters. 24 | setPrototypeOf(__Composite__.prototype, null); 25 | 26 | export const { getCompositeHash, maybeGetCompositeHash, objectIsComposite, setHash } = __Composite__; 27 | -------------------------------------------------------------------------------- /polyfill/internal/hash.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { Composite } from "../composite.ts"; 4 | import { hashComposite } from "./hash.ts"; 5 | import { Set } from "./originals.ts"; 6 | import { setPrototypeMethods } from "../collection-set.ts"; 7 | 8 | class CompositeSet extends Set {} 9 | for (const [key, method] of Object.entries(setPrototypeMethods)) { 10 | (CompositeSet.prototype as any)[key] = method; 11 | } 12 | 13 | await test("unique symbol key order does not impact hash", () => { 14 | const s1 = Symbol(); 15 | const s2 = Symbol(); 16 | const c1 = Composite({ [s1]: 1, [s2]: 2 }); 17 | const c2 = Composite({ [s2]: 2, [s1]: 1 }); 18 | assert(Composite.equal(c1, c2)); 19 | assert.strictEqual(hashComposite(c1), hashComposite(c2)); 20 | }); 21 | 22 | await test("hash is same value zero", () => { 23 | const c1 = Composite({ x: 0 }); 24 | const c2 = Composite({ x: -0 }); 25 | assert(Composite.equal(c1, c2)); 26 | assert.strictEqual(hashComposite(c1), hashComposite(c2)); 27 | }); 28 | 29 | await test("non-composite objects have different hash values", () => { 30 | const c1 = Composite({ a: 1 }); 31 | const c2 = Composite({ a: 2 }); 32 | assert(!Composite.equal(c1, c2)); 33 | assert.notStrictEqual(hashComposite(c1), hashComposite(c2)); 34 | }); 35 | 36 | function flip() { 37 | return Math.random() < 0.5; 38 | } 39 | 40 | function randomString() { 41 | if (Math.random() < 0.95) { 42 | return Math.random().toString(36).substring(2, 15); 43 | } else { 44 | return flip() ? "hello" : "world"; 45 | } 46 | } 47 | 48 | function randomSymbol() { 49 | return flip() ? Symbol() : Symbol.for(randomString()); 50 | } 51 | 52 | function randomKey() { 53 | return flip() ? randomString() : randomSymbol(); 54 | } 55 | 56 | const preMade: object[] = [new Date(), Object.prototype]; 57 | 58 | function randomValue(): unknown { 59 | const types = [ 60 | "number", 61 | "number", 62 | "number", 63 | "bigint", 64 | "bigint", 65 | "string", 66 | "string", 67 | "string", 68 | "boolean", 69 | "boolean", 70 | "null", 71 | "undefined", 72 | "object", 73 | "function", 74 | ]; 75 | const type = types[Math.floor(Math.random() * types.length)]; 76 | switch (type) { 77 | case "number": 78 | return Math.random() * 1000; 79 | case "bigint": 80 | return BigInt(Math.floor(Math.random() * 1000)); 81 | case "string": 82 | return randomString(); 83 | case "boolean": 84 | return Math.random() < 0.5; 85 | case "null": 86 | return null; 87 | case "undefined": 88 | return undefined; 89 | case "object": 90 | if (flip()) { 91 | return preMade[Math.floor(Math.random() * preMade.length)]; 92 | } else { 93 | const newObject = flip() ? randomComposite() : new Date(); 94 | preMade.push(newObject); 95 | return newObject; 96 | } 97 | case "function": 98 | return flip() ? () => {} : Function; 99 | default: 100 | throw new TypeError(`Unsupported type: ${type}`); 101 | } 102 | } 103 | 104 | function randomComposite(): Composite { 105 | const template: Record = {}; 106 | const numKeys = Math.floor(Math.random() * 10) + 1; 107 | for (let i = 0; i < numKeys; i++) { 108 | template[randomKey()] = randomValue(); 109 | } 110 | return Composite(template); 111 | } 112 | 113 | await test("fuzz test for hash collisions", () => { 114 | const hashes = new Set(); 115 | const created = new CompositeSet(); 116 | const total = 100_000; 117 | let collisions = 0; 118 | while (created.size < total) { 119 | const c = randomComposite(); 120 | if (created.has(c)) { 121 | continue; 122 | } 123 | created.add(c); 124 | const hash = hashComposite(c); 125 | if (hashes.has(hash)) { 126 | collisions++; 127 | } else { 128 | hashes.add(hash); 129 | } 130 | } 131 | const limit = total * 0.0001; // 0.01% collision rate limit 132 | console.log(`Collisions: ${(collisions / total) * 100}%`); 133 | assert(collisions < limit, `Collisions exceeded limit: ${collisions} > ${limit}`); 134 | }); 135 | -------------------------------------------------------------------------------- /polyfill/internal/hash.ts: -------------------------------------------------------------------------------- 1 | import { isComposite, type Composite } from "../composite.ts"; 2 | import { isNaN, NaN, apply, ownKeys, keyFor, weakMapGet, weakMapSet, sort, localeCompare } from "./originals.ts"; 3 | import { assert } from "./utils.ts"; 4 | import { randomHash, MurmurHashStream, type Hasher } from "./murmur.ts"; 5 | import { getCompositeHash, maybeGetCompositeHash, setHash } from "./composite-class.ts"; 6 | 7 | const TRUE = randomHash(); 8 | const FALSE = randomHash(); 9 | const NULL = randomHash(); 10 | const UNDEFINED = randomHash(); 11 | const SYMBOLS = randomHash(); 12 | const KEY = randomHash(); 13 | const OBJECTS = randomHash(); 14 | 15 | const hashCache = new WeakMap(); 16 | const symbolsInWeakMap = (() => { 17 | try { 18 | hashCache.set(Symbol(), 0); 19 | return true; 20 | } catch { 21 | return false; 22 | } 23 | })(); 24 | 25 | const keySortArgs = [keySort]; 26 | export function hashComposite(input: Composite): number { 27 | const cachedHash = getCompositeHash(input); 28 | if (cachedHash !== 0) { 29 | return cachedHash; 30 | } 31 | const hasher = new MurmurHashStream(); 32 | const keys = ownKeys(input); 33 | apply(sort, keys, keySortArgs); 34 | for (let i = 0; i < keys.length; i++) { 35 | const key = keys[i]; 36 | if (typeof key === "string") { 37 | hasher.update(KEY); 38 | hasher.update(key); 39 | updateHasher(hasher, input[key as keyof typeof input]); 40 | continue; 41 | } 42 | DEV: assert(typeof key === "symbol"); 43 | if (!symbolsInWeakMap && keyFor(key) === undefined) { 44 | // Remaining keys can't be hashed in this JS engine 45 | break; 46 | } 47 | hasher.update(KEY); 48 | symbolUpdateHasher(hasher, key); 49 | updateHasher(hasher, input[key as keyof typeof input]); 50 | } 51 | DEV: assert(getCompositeHash(input) === 0); 52 | const hash = hasher.digest(); 53 | setHash(input, hash); 54 | return hash; 55 | } 56 | 57 | function updateHasher(hasher: Hasher, input: unknown): void { 58 | if (input === null) { 59 | hasher.update(NULL); 60 | return; 61 | } 62 | switch (typeof input) { 63 | case "undefined": 64 | hasher.update(UNDEFINED); 65 | return; 66 | case "boolean": 67 | hasher.update(input ? TRUE : FALSE); 68 | return; 69 | case "number": 70 | // Normalize NaNs and -0 71 | hasher.update(isNaN(input) ? NaN : input === 0 ? 0 : input); 72 | return; 73 | case "bigint": 74 | case "string": 75 | hasher.update(input); 76 | return; 77 | case "symbol": 78 | symbolUpdateHasher(hasher, input); 79 | return; 80 | case "object": 81 | case "function": 82 | hasher.update(cachedHash(input)); 83 | return; 84 | default: 85 | throw new TypeError(`Unsupported input type: ${typeof input}`); 86 | } 87 | } 88 | 89 | function symbolUpdateHasher(hasher: Hasher, input: symbol): void { 90 | const regA = Symbol.keyFor(input); 91 | if (regA !== undefined) { 92 | hasher.update(SYMBOLS); 93 | hasher.update(regA); 94 | return; 95 | } 96 | if (!symbolsInWeakMap) { 97 | hasher.update(SYMBOLS); 98 | return; 99 | } else { 100 | hasher.update(cachedHash(input)); 101 | } 102 | } 103 | 104 | let nextObjectId = 1; 105 | function cachedHash(input: object | symbol): number { 106 | let maybeCompHash = typeof input === "object" ? maybeGetCompositeHash(input) : undefined; 107 | if (maybeCompHash !== undefined) { 108 | DEV: assert(isComposite(input)); 109 | return maybeCompHash !== 0 ? maybeCompHash : hashComposite(input); 110 | } 111 | let hash = apply(weakMapGet, hashCache, [input]); 112 | if (hash === undefined) { 113 | hash = nextObjectId ^ OBJECTS; 114 | nextObjectId++; 115 | apply(weakMapSet, hashCache, [input, hash]); 116 | return hash; 117 | } 118 | return hash; 119 | } 120 | 121 | /** 122 | * Strings before symbols. 123 | * Strings sorted lexicographically. 124 | * Symbols sorted by {@link symbolSort} 125 | */ 126 | function keySort(a: string | symbol, b: string | symbol): number { 127 | if (typeof a !== typeof b) { 128 | return typeof a === "string" ? 1 : -1; 129 | } 130 | if (typeof a === "string") { 131 | return apply(localeCompare, a, [b]); 132 | } 133 | DEV: assert(typeof b === "symbol"); 134 | return symbolSort(a, b); 135 | } 136 | 137 | /** 138 | * Registered symbols are sorted by their string key. 139 | * Registered symbols come before non-registered symbols. 140 | * Non-registered symbols are not sorted (stable order preserved). 141 | */ 142 | function symbolSort(a: symbol, b: symbol): number { 143 | const regA = keyFor(a); 144 | const regB = keyFor(b); 145 | if (regA !== undefined && regB !== undefined) { 146 | return apply(localeCompare, regA, [regB]); 147 | } 148 | if (regA === undefined && regB === undefined) { 149 | return symbolsInWeakMap ? secretSymbolSort(a, b) : 0; 150 | } 151 | return regA === undefined ? 1 : -1; 152 | } 153 | 154 | const secretSymbolOrder = new WeakMap(); 155 | let nextOrder = 0; 156 | function getSymbolOrder(input: symbol): number { 157 | let order = secretSymbolOrder.get(input); 158 | if (order === undefined) { 159 | order = nextOrder++; 160 | secretSymbolOrder.set(input, order); 161 | } 162 | return order; 163 | } 164 | function secretSymbolSort(a: symbol, b: symbol): number { 165 | return getSymbolOrder(a) - getSymbolOrder(b); 166 | } 167 | -------------------------------------------------------------------------------- /polyfill/internal/hashmap.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { HashStore } from "./hashmap.ts"; 4 | 5 | await test("HashStore", () => { 6 | const h = new HashStore( 7 | (key) => key.length, 8 | (a, b) => a === b, 9 | ); 10 | h.set("a"); 11 | h.set("b"); 12 | h.set("aa"); 13 | assert.strictEqual(h.get("a"), "a"); 14 | assert.strictEqual(h.get("b"), "b"); 15 | assert.strictEqual(h.get("aa"), "aa"); 16 | assert.strictEqual(h.get("aaa"), undefined); 17 | assert.strictEqual(h.has("a"), true); 18 | assert.strictEqual(h.has("b"), true); 19 | assert.strictEqual(h.has("aa"), true); 20 | assert.strictEqual(h.has("aaa"), false); 21 | h.delete("a"); 22 | assert.strictEqual(h.get("a"), undefined); 23 | assert.strictEqual(h.has("a"), false); 24 | assert.strictEqual(h.get("b"), "b"); 25 | h.clear(); 26 | assert.strictEqual(h.get("b"), undefined); 27 | }); 28 | -------------------------------------------------------------------------------- /polyfill/internal/hashmap.ts: -------------------------------------------------------------------------------- 1 | import { Map, mapSet, mapGet, apply, splice, mapDelete, mapClear, freeze } from "./originals.ts"; 2 | 3 | const missing = Symbol("missing"); 4 | 5 | function replaced(): never { 6 | throw new Error("implementation replaced"); 7 | } 8 | 9 | class SafeMap extends Map { 10 | declare get: never; 11 | safeGet(k: K): V | undefined { 12 | replaced(); 13 | } 14 | declare set: never; 15 | safeSet(k: K, v: V) { 16 | replaced(); 17 | } 18 | declare delete: never; 19 | safeDelete(k: K) { 20 | replaced(); 21 | } 22 | declare clear: never; 23 | safeClear() { 24 | replaced(); 25 | } 26 | } 27 | SafeMap.prototype.safeGet = mapGet; 28 | SafeMap.prototype.safeSet = mapSet; 29 | SafeMap.prototype.safeDelete = mapDelete; 30 | SafeMap.prototype.safeClear = mapClear; 31 | 32 | export class HashStore { 33 | #hasher: (key: K) => number; 34 | #equals: (a: K, b: K) => boolean; 35 | #map = new SafeMap>(); 36 | constructor(hasher: (key: K) => number, equals: (a: K, b: K) => boolean) { 37 | this.#hasher = hasher; 38 | this.#equals = equals; 39 | } 40 | clear(): void { 41 | this.#map.safeClear(); 42 | } 43 | #get(key: K): K | typeof missing { 44 | const hash = this.#hasher(key); 45 | const bucket = this.#map.safeGet(hash); 46 | if (bucket === undefined) { 47 | return missing; 48 | } 49 | var eq; 50 | for (let i = 0; i < bucket.length; i++) { 51 | eq ??= this.#equals; 52 | const b = bucket[i]; 53 | if (eq(b, key)) { 54 | return b; 55 | } 56 | } 57 | return missing; 58 | } 59 | has(key: K): boolean { 60 | return this.#get(key) !== missing; 61 | } 62 | get(key: K): K | undefined { 63 | const value = this.#get(key); 64 | if (value === missing) { 65 | return undefined; 66 | } 67 | return value; 68 | } 69 | set(key: K): void { 70 | const hash = this.#hasher(key); 71 | let bucket = this.#map.safeGet(hash); 72 | if (bucket === undefined) { 73 | bucket = []; 74 | this.#map.safeSet(hash, bucket); 75 | } 76 | for (let i = 0; i < bucket.length; i++) { 77 | const k = bucket[i]; 78 | if (this.#equals(k, key)) { 79 | bucket[i] = key; 80 | return; 81 | } 82 | } 83 | bucket[bucket.length] = key; 84 | } 85 | delete(key: K): boolean { 86 | const hash = this.#hasher(key); 87 | const bucket = this.#map.safeGet(hash); 88 | if (bucket === undefined) { 89 | return false; 90 | } 91 | for (let i = 0; i < bucket.length; i++) { 92 | const k = bucket[i]; 93 | if (this.#equals(k, key)) { 94 | if (bucket.length === 1) { 95 | this.#map.safeDelete(hash); 96 | } else { 97 | apply(splice, bucket, [i, 1]); 98 | } 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | } 105 | freeze(HashStore.prototype); 106 | freeze(HashStore); 107 | -------------------------------------------------------------------------------- /polyfill/internal/key-lookup.ts: -------------------------------------------------------------------------------- 1 | import { weakMapGet, weakMapSet, WeakMap } from "./originals.ts"; 2 | import { HashStore } from "./hashmap.ts"; 3 | import { Composite, compositeEqual, isComposite } from "../composite.ts"; 4 | import { hashComposite } from "./hash.ts"; 5 | 6 | type CompMap = HashStore; 7 | const CompMap = HashStore; 8 | type Maps = Map; 9 | type Sets = Set; 10 | 11 | function replaced(): never { 12 | throw new Error("function replaced"); 13 | } 14 | 15 | class SafeWeakMap extends WeakMap { 16 | declare get: never; 17 | safeGet(k: K): V | undefined { 18 | replaced(); 19 | } 20 | declare set: never; 21 | safeSet(k: K, v: unknown) { 22 | replaced(); 23 | } 24 | } 25 | SafeWeakMap.prototype.safeGet = weakMapGet; 26 | SafeWeakMap.prototype.safeSet = weakMapSet; 27 | 28 | const compositeKeyLookups = new SafeWeakMap(); 29 | 30 | export const missing = Symbol("missing"); 31 | 32 | export function resolveKey(collection: Maps | Sets, key: unknown, create: boolean): unknown { 33 | if (!isComposite(key)) { 34 | return key; 35 | } 36 | let compMap = compositeKeyLookups.safeGet(collection); 37 | if (!compMap) { 38 | if (!create) return missing; 39 | compMap = new CompMap(hashComposite, compositeEqual); 40 | compositeKeyLookups.safeSet(collection, compMap); 41 | } 42 | 43 | let keyToUse = compMap.get(key); 44 | if (!keyToUse) { 45 | if (!create) return missing; 46 | keyToUse = key; 47 | compMap.set(key); 48 | } 49 | return keyToUse; 50 | } 51 | 52 | export function clearCompMap(map: Maps | Sets) { 53 | compositeKeyLookups.safeGet(map)?.clear(); 54 | } 55 | 56 | export function deleteKey(collection: Maps | Sets, key: Composite): Composite | undefined { 57 | const compMap = compositeKeyLookups.safeGet(collection); 58 | if (!compMap) { 59 | return undefined; 60 | } 61 | const existingKey = compMap.get(key); 62 | if (!existingKey) { 63 | return undefined; 64 | } 65 | compMap.delete(key); 66 | return existingKey; 67 | } 68 | -------------------------------------------------------------------------------- /polyfill/internal/murmur.ts: -------------------------------------------------------------------------------- 1 | import { apply, imul, charCodeAt, Number } from "./originals.ts"; 2 | 3 | const RANDOM_SEED = randomHash(); 4 | const STRING_MARKER = randomHash(); 5 | const BIG_INT_MARKER = randomHash(); 6 | const NEG_BIG_INT_MARKER = randomHash(); 7 | 8 | export function randomHash() { 9 | return (Math.random() * (2 ** 31 - 1)) >>> 0; 10 | } 11 | 12 | export interface Hasher { 13 | update(val: string | number | bigint): void; 14 | digest(): number; 15 | } 16 | 17 | export class MurmurHashStream implements Hasher { 18 | private hash: number = RANDOM_SEED; 19 | private length: number = 0; 20 | private carry: number = 0; 21 | private carryBytes: number = 0; 22 | 23 | private _mix(k1: number): void { 24 | k1 = imul(k1, 0xcc9e2d51); 25 | k1 = (k1 << 15) | (k1 >>> 17); 26 | k1 = imul(k1, 0x1b873593); 27 | this.hash ^= k1; 28 | this.hash = (this.hash << 13) | (this.hash >>> 19); 29 | this.hash = imul(this.hash, 5) + 0xe6546b64; 30 | } 31 | 32 | private _writeByte(byte: number): void { 33 | this.carry |= (byte & 0xff) << (8 * this.carryBytes); 34 | this.carryBytes++; 35 | this.length++; 36 | 37 | if (this.carryBytes === 4) { 38 | this._mix(this.carry >>> 0); 39 | this.carry = 0; 40 | this.carryBytes = 0; 41 | } 42 | } 43 | 44 | update(chunk: string | number | bigint): void { 45 | switch (typeof chunk) { 46 | case "string": 47 | this.update(STRING_MARKER); 48 | for (let i = 0; i < chunk.length; i++) { 49 | const code = apply(charCodeAt, chunk, [i]); 50 | this._writeByte(code & 0xff); 51 | this._writeByte((code >>> 8) & 0xff); 52 | } 53 | return; 54 | case "number": 55 | this._writeByte(chunk & 0xff); 56 | this._writeByte((chunk >>> 8) & 0xff); 57 | this._writeByte((chunk >>> 16) & 0xff); 58 | this._writeByte((chunk >>> 24) & 0xff); 59 | return; 60 | case "bigint": { 61 | let value = chunk; 62 | if (value < 0n) { 63 | value = -value; 64 | this.update(NEG_BIG_INT_MARKER); 65 | } else { 66 | this.update(BIG_INT_MARKER); 67 | } 68 | while (value > 0n) { 69 | this._writeByte(Number(value & 0xffn)); 70 | value >>= 8n; 71 | } 72 | if (chunk === 0n) this._writeByte(0); 73 | return; 74 | } 75 | default: 76 | throw new TypeError(`Unsupported input type: ${typeof chunk}`); 77 | } 78 | } 79 | 80 | digest(): number { 81 | if (this.carryBytes > 0) { 82 | let k1 = this.carry >>> 0; 83 | k1 = imul(k1, 0xcc9e2d51); 84 | k1 = (k1 << 15) | (k1 >>> 17); 85 | k1 = imul(k1, 0x1b873593); 86 | this.hash ^= k1; 87 | } 88 | 89 | this.hash ^= this.length; 90 | this.hash ^= this.hash >>> 16; 91 | this.hash = imul(this.hash, 0x85ebca6b); 92 | this.hash ^= this.hash >>> 13; 93 | this.hash = imul(this.hash, 0xc2b2ae35); 94 | this.hash ^= this.hash >>> 16; 95 | 96 | return this.hash >>> 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /polyfill/internal/originals.ts: -------------------------------------------------------------------------------- 1 | export const Number = globalThis.Number; 2 | export const { isNaN, NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY } = Number; 3 | export const { abs, floor, min, imul } = Math; 4 | export const { apply, ownKeys, getOwnPropertyDescriptor, setPrototypeOf } = Reflect; 5 | export const { is, freeze, prototype: objectPrototype } = Object; 6 | export const { sort, splice, includes, indexOf, lastIndexOf } = Array.prototype; 7 | export const { keyFor, iterator } = Symbol; 8 | export const { localeCompare, charCodeAt } = String.prototype; 9 | export const Map = globalThis.Map; 10 | export const { has: mapHas, set: mapSet, get: mapGet, delete: mapDelete, clear: mapClear } = Map.prototype; 11 | export const mapSize = getOwnPropertyDescriptor(Map.prototype, "size")!.get!; 12 | export const Set = globalThis.Set; 13 | export const { has: setHas, add: setAdd, clear: setClear, delete: setDelete, values: setValues } = Set.prototype; 14 | export const setSize = getOwnPropertyDescriptor(Set.prototype, "size")!.get!; 15 | export const setNext = new Set().values().next; 16 | export const WeakMap = globalThis.WeakMap; 17 | export const { set: weakMapSet, get: weakMapGet } = WeakMap.prototype; 18 | -------------------------------------------------------------------------------- /polyfill/internal/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import assert from "node:assert"; 3 | import { sameValueZero } from "./utils.ts"; 4 | 5 | await test("sameValueZero", () => { 6 | assert(sameValueZero(0, 0)); 7 | assert(sameValueZero(0, -0)); 8 | assert(sameValueZero(-0, 0)); 9 | assert(sameValueZero(-0, -0)); 10 | assert(sameValueZero("abc", "abc")); 11 | assert(sameValueZero(Function, Function)); 12 | assert(sameValueZero(globalThis, globalThis)); 13 | assert(!sameValueZero(0, 1)); 14 | assert(!sameValueZero(0, {})); 15 | assert(!sameValueZero(0, () => {})); 16 | }); 17 | -------------------------------------------------------------------------------- /polyfill/internal/utils.ts: -------------------------------------------------------------------------------- 1 | import { is, freeze } from "./originals.ts"; 2 | 3 | export function assert(v: unknown): asserts v { 4 | if (!v) { 5 | const err = new Error("Assertion failed"); 6 | if (Error.captureStackTrace) { 7 | Error.captureStackTrace(err, assert); 8 | } 9 | throw err; 10 | } 11 | } 12 | 13 | export function sameValueZero(a: unknown, b: unknown): boolean { 14 | return is(a === 0 ? 0 : a, b === 0 ? 0 : b); 15 | } 16 | 17 | export const EMPTY = freeze([]); 18 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 |

This is an emu-clause

14 |

This is an algorithm:

15 | 16 | 1. Let _proposal_ be *undefined*. 17 | 1. If IsAccepted(_proposal_) is *true*, then 18 | 1. Let _stage_ be *0*. 19 | 1. Else, 20 | 1. Let _stage_ be *-1*. 21 | 1. Return ? ToString(_stage_). 22 | 23 |
24 | 25 | 26 |

27 | IsAccepted ( 28 | _proposal_: an ECMAScript language value 29 | ): a Boolean 30 |

31 |
32 |
description
33 |
Tells you if the proposal was accepted
34 |
35 | 36 | 1. If _proposal_ is not a String, or is not accepted, return *false*. 37 | 1. Return *true*. 38 | 39 |
40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "Preserve", 6 | "moduleDetection": "force", 7 | "allowImportingTsExtensions": true, 8 | "verbatimModuleSyntax": true, 9 | "noEmit": true 10 | }, 11 | "include": ["polyfill/**/*.ts"] 12 | } 13 | --------------------------------------------------------------------------------