2 // ====================================================================
4 // Copyright © 2022 Lady [@ Lady’s Computer].
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
11 const MAX_SAFE_INTEGER
= Number((1n
<< 53n
) - 1n
);
14 * An object which can be built up in an arraylike fashion.
16 * This class copies the `from` and `of` static methods of Array to
17 * dodge Array limitations on `length`.
19 class MockArray
extends Array
{
20 /** Constructs a new MockArray as an ordinary (non·exotic) object. */
21 constuctor(...values
) {
22 const array
= Object
.create(MockArray
.prototype, {
30 const numberOfArgs
= values
.length
;
31 if (numberOfArgs
== 0) {
32 // No arguments were supplied.
34 } else if (numberOfArgs
== 1) {
35 // One argument was supplied.
36 const len
= values
[0];
37 if (typeof len
!= "number") {
38 // The argument was not a number.
39 return Object
.assign(array
, {
44 // The argument was a number, and needs to be treated as a
46 const intLen
= toLength(len
); // allow larger length than Array
48 // The provided length was invalid.
49 throw new RangeError("ハブ: Invalid length.");
51 // The provided length was valid.
52 return Object
.assign(array
, { length
: len
});
56 // More than one argument was supplied.
57 return Object
.assign(array
, values
, { length
: numberOfArgs
});
63 * A generic container for lists of binary data of defined size.
65 * Values can be anywhere from 1 to 64 bits wide, although this class
66 * is not advantageous for widths larger than 21. Regardless of size,
67 * values will be represented as big·ints.
69 * This class trades computational efficiency getting and setting
70 * values for storage efficiency (ability to compact many values in a
71 * tight space); it is subsequently targeted at larger datasets which
72 * need to be available in memory but which don’t often need to be
75 * A note on limits: The upper limit for the length of an ordinary
76 * Ecmascript array of integers is 2^32 − 1. Each integer has 32 bytes
77 * which are readily accessible via bitwise operations, so that makes
78 * for 137 438 953 440 total bits accessible in an ordinary array.
79 * In contrast, an array buffer has a higher maximum byte length of
80 * 2^53 − 1, but obviously only allows for the storage of 8 bits per
81 * byte, for 72 057 594 037 927 928 total accessible bits. For
82 * technical reasons, the maximum total length of the ハブ instance is
83 * also capped to 2^53 − 1 (relevant for bit widths of less than 8).
85 * While this class uses an array buffer for its storage, and is
86 * conceptually more similar to Ecmascript typed arrays, it is an array
87 * exotic object and inherits from the ordinary Array class.
89 export default class ハブ extends Array
{
91 * Constructs a new ハブ with items of the provided size (in bits).
93 * The first argument must be the number of bits per item. If the
94 * second argument is an array buffer, it is used as the underlying
95 * store, and additional byte offset and length arguments may be
96 * supplied. Otherwise, if the second argument is an object, it is
97 * iterated over in a manner similar to `Array.from`. Otherwise, it
98 * is used as the length.
100 constructor(bitWidth
, ...args
) {
101 super(); // sets `length` (unused)
102 const bitsPerItem
= validSize(bitWidth
);
103 const wordSize
= wordSizeByBits
[bitsPerItem
];
104 const scale
= wordSize
* 8 / bitsPerItem
>> 0;
105 Object
.defineProperties(
108 wordScale
: { ...unlisted
, value
: scale
},
109 wordSize
: { ...unlisted
, value
: wordSize
},
110 bitWidth
: { ...unlisted
, value
: bitsPerItem
},
113 const length
= (() => {
114 if (args
.length
== 0) {
115 // No additional arguments provided; initialize with zero
117 defineNewBuffer
.call(this, 0);
120 // An additional argument is present.
121 const firstArg
= args
[0];
122 const bufferByteLength
= (() => {
125 ArrayBuffer
.prototype,
128 ); // will throw if not an array buffer
133 if (bufferByteLength
!= null) {
134 // The first additional argument is an array buffer.
135 const offset
= toIndex(args
[1]);
136 const length
= args
[2];
137 if (offset
% wordSize
|| offset
> bufferByteLength
) {
138 // The provided offset exceeds the length of the buffer or is
139 // not divisible by the word size.
140 throw new RangeError("ハブ: Improper byte offset.");
141 } else if (length
=== undefined) {
142 // There is no provided length.
143 if (bufferByteLength
% wordSize
) {
144 // The provided buffer is not divisible by the word size.
145 throw new RangeError("ハブ: Improperly sized buffer.");
147 // The entire buffer after the offset should be used.
148 const newByteLength
= bufferByteLength
- offset
;
149 const itemLength
= BigInt(newByteLength
/ wordSize
) *
151 if (itemLength
> MAX_SAFE_INTEGER
) {
152 // If the entire buffer is used, it will result in a
153 // length greater than `MAX_SAFE_INTEGER`.
154 throw new RangeError("ハブ: Buffer too large.");
156 // The buffer is sized properly.
157 Object
.defineProperties(this, {
158 buffer
: { ...unlisted
, value
: firstArg
},
159 byteLength
: { ...unlisted
, value
: newByteLength
},
160 byteOffset
: { ...unlisted
, value
: offset
},
162 return Number(itemLength
);
166 // A length was provided.
167 const newByteLength
= toIndex(Math
.ceil(length
/ scale
)) *
169 const itemLength
= toIndex(length
);
170 if (offset
+ newByteLength
> bufferByteLength
) {
171 // The resulting byte length combined with the offset
172 // exceeds the length of the buffer.
173 throw new RangeError("ハブ: Improper length.");
175 // All of the provided values check out and can be used.
176 Object
.defineProperties(this, {
177 buffer
: { ...unlisted
, value
: firstArg
},
178 byteLength
: { ...unlisted
, value
: newByteLength
},
179 byteOffset
: { ...unlisted
, value
: offset
},
185 typeof firstArg
== "function" ||
186 typeof firstArg
== "object" && firstArg
!= null
188 // The first additional argument is an object, but not an
190 const data
= MockArray
.from(firstArg
);
191 const { length
} = data
;
192 defineNewBuffer
.call(
194 Math
.ceil(length
/ scale
) * wordSize
,
196 for (const [index
, datum
] of data
.entries()) {
197 setItem
.call(this, index
, datum
);
201 // The first additional argument is a primitive.
202 const length
= Math
.floor(firstArg
);
203 if (isNaN(length
) || length
== 0) {
204 // The provided argument is zero·like.
205 defineNewBuffer
.call(this, 0);
208 // The provided argument can’t be treated as zero.
209 const neededBytes
= Math
.ceil(length
/ scale
) * wordSize
;
210 if (neededBytes
< 0 || neededBytes
> MAX_SAFE_INTEGER
) {
211 // The provided argument is not a valid length.
212 throw new RangeError(`ハブ: Invalid length: ${firstArg}`);
214 // The provided argument can be treated like a length.
215 defineNewBuffer
.call(this, neededBytes
);
222 return new Proxy(this, new ハブ·ProxyHandler(length
));
226 * Returns a new ハブ instance of the provided bit width, generated
227 * in a manner akin to `Array.from()`.
235 return new (this ?? ハブ)(
237 MockArray
.from(items
, mapFn
, thisArg
),
241 /** `isArray` should not be inherited, so is set to undefined. */
242 static isArray
= undefined;
245 * Returns a new ハブ instance of the provided bit width, generated
246 * in a manner akin to `Array.of()`.
248 static of(bitWidth
, ...items
) {
249 return new (this ?? ハブ)(bitWidth
, MockArray
.of(...items
));
253 * Returns a new ハブ instance of the provided bit width, generated
254 * in a manner akin to `Array.prototype.concat()`.
257 const Species
= arraySpecies(this);
258 return Species
== null ? [].concat(...items
) : new Species(
259 Object
.assign([], { constructor: MockArray
}).concat(...items
),
264 * Returns the ハブ constructor, bound to the bit width of this ハブ
267 //deno-lint-ignore adjacent-overload-signatures
268 get ["constructor"]() {
269 return ハブ.bind(undefined, this.bitWidth
);
272 /** `copyWithin` should not be inherited, so is set to undefined. */
278 * Returns a new ハブ instance of the provided bit width, generated
279 * in a manner akin to `Array.prototype.filter()`.
281 filter(callbackfn
, thisArg
= undefined) {
282 const O
= new Object(this);
283 const len
= toLength(this.length
);
284 if (typeof callbackfn
!= "function") {
285 // The callback is not callable.
286 throw new TypeError("ハブ: Callback must be callable.");
288 // The callback is callable.
289 const Species
= arraySpecies(this);
290 const iterator
= function* () {
291 for (let k
= 0; k
< len
; ++k
) {
293 // The current index is in the provided value.
295 if (callbackfn
.call(thisArg
, kValue
, k
, O
)) {
296 // The callback returned true.
299 // The callback returned false.
303 // The current index is not in the provided value.
308 return Species
== null
309 ? Array
.from(iterator
)
310 : new Species(iterator
);
314 /** `flat` should not be inherited, so is set to undefined. */
319 /** `flatMap` should not be inherited, so is set to undefined. */
324 /** `pop` should not be inherited, so is set to undefined. */
329 /** `push` should not be inherited, so is set to undefined. */
334 /** `shift` should not be inherited, so is set to undefined. */
339 /** `splice` should not be inherited, so is set to undefined. */
344 /** `unshift` should not be inherited, so is set to undefined. */
351 * A proxy handler for ハブ instances.
353 * Although ハブ instances are array exotic objects, this handler
354 * overrides the handling of both numeric indices and `length` so that
355 * the exotic array behaviour never actually takes place. Instead, data
356 * is pulled from the object’s associated buffer.
358 class ハブ·ProxyHandler
extends Object
.assign(
360 { prototype: Reflect
},
363 * The actual number which should be returned as the length of the
369 * Constructs a new ハブ·ProxyHandler with the provided numeric
372 constructor(numericLength
) {
374 this.#length
= Math
.min(Number(numericLength
), MAX_SAFE_INTEGER
);
378 * Defines P on O based on the provided Desc.
380 * If P is "length", then its value for `writable`, if present, must
381 * be `true`—despite the fact that ハブ lengths are not writable.
382 * This is because the proxied value of `length` does not necessarily
383 * match that of the underlying object—any attempt to actually
384 * change this value, however, will fail.
386 * If P is a numeric index, then this function will return false
387 * if Desc defines a nonconfigurable, nonwritable, non·enumerable, or
388 * accessor property or if it is not a valid integer index for O.
390 defineProperty(O
, P
, Desc
) {
391 const length
= this.#length
;
393 // The provided property is "length".
395 "get" in Desc
|| "set" in Desc
|| Desc
.writable
=== false ||
396 Desc
.value
!== length
398 // An attempt to change the value or writability of `length`
402 // The provided description for `length` does not attempt to
403 // change value or writability.
405 // This will still throw an error if `Desc.configurable` or
406 // `Desc.enumerable` is `true`, because this proxy wraps
408 return Reflect
.defineProperty(
413 if ("configurable" in Desc
) {
414 yield ["configurable", Desc
.configurable
];
416 if ("enumerable" in Desc
) {
417 yield ["enumerable", Desc
.enumerable
];
424 // The provided property is not "length".
425 const numericIndex
= canonicalNumericIndexString(P
);
426 if (numericIndex
=== undefined) {
427 // The provided property is not a numeric index.
428 return Reflect
.definePropety(O
, P
, Desc
);
429 } else if (isValidIntegerIndex
.call({ length
}, numericIndex
)) {
430 // The provided property is a valid numeric index for the
433 Desc
.configurable
=== false || Desc
.enumerable
=== false ||
434 "get" in Desc
|| "set" in Desc
|| Desc
.writable
=== false
436 // An attempt to change immutable attributes of the index was
439 } else if (!("value" in Desc
)) {
440 // The provided descriptor is compatible, but didn’t provide
445 // The provided descriptor is compatible and provides a
446 // value, so the value can be set.
447 setItem
.call(O
, numericIndex
, Desc
.value
);
451 // The provided property is a numeric index, but is not a valid
452 // integer index for the provided object.
461 * If P is "length", this function will return false.
463 * If P is a numeric index, this function will return false if it is
464 * a valid integer index for O and true otherwise.
466 deleteProperty(O
, P
) {
467 const length
= this.#length
;
469 // The provided property is "length".
472 // The provided property is not "length".
473 const numericIndex
= canonicalNumericIndexString(P
);
474 return numericIndex
=== undefined
475 ? Reflect
.deleteProperty(O
, P
)
476 : !isValidIntegerIndex
.call({ length
}, numericIndex
);
481 * Gets P on O using the provided Receiver.
483 * "length" returns the length as a number, capped to
484 * `MAX_SAFE_INTEGER`.
486 * Valid integer indices return the item at that index. Other numeric
487 * indices return undefined.
489 get(O
, P
, Receiver
) {
490 const length
= this.#length
;
492 // The provided property is "length".
495 // The provided property is not "length".
496 const numericIndex
= canonicalNumericIndexString(P
);
497 return numericIndex
=== undefined
498 ? Reflect
.get(O
, P
, Receiver
)
499 : isValidIntegerIndex
.call({ length
}, numericIndex
)
500 ? getItem
.call(O
, numericIndex
)
506 * Gets a property descriptor for P on O.
508 * "length" returns a descriptor describing a nonconfigurable,
509 * non·enumerable, writable length, as a number capped to
510 * `MAX_SAFE_INTEGER`.
512 * Valid integer indices return a descriptor describing a
513 * configurable, enumerable, writable item at that index. Other
514 * numeric indices return undefined.
516 getOwnPropertyDescriptor(O
, P
) {
517 const length
= this.#length
;
519 // The provided property is "length".
527 // The provided property is not "length".
528 const numericIndex
= canonicalNumericIndexString(P
);
529 return numericIndex
=== undefined
530 ? Reflect
.getOwnPropertyDescriptor(O
, P
)
531 : isValidIntegerIndex
.call({ length
}, numericIndex
)
535 value
: getItem
.call(O
, numericIndex
),
543 * Determines whether P exists on O .
545 * "length" always returns true.
547 * Valid integer indices return true. Other numeric indices return
551 const length
= this.#length
;
553 // The provided property is "length".
556 // The provided property is not "length".
557 const numericIndex
= canonicalNumericIndexString(P
);
558 return numericIndex
=== undefined
560 : isValidIntegerIndex
.call({ length
}, numericIndex
);
565 * Returns the own properties available on O .
567 * Valid integer indices are included.
570 const length
= this.#length
;
573 for (let i
= 0; i
< length
; ++i
) {
579 for (const P
of Object
.getOwnPropertyNames(O
)) {
581 // The current property name is "length".
584 const numericIndex
= canonicalNumericIndexString(P
);
586 numericIndex
=== undefined ||
587 !(Object
.is(numericIndex
, 0) ||
588 numericIndex
> 0 && numericIndex
<= MAX_SAFE_INTEGER
)
590 // The current property name is not "length" or an
591 // integer index. Note that there is no way to set or
592 // access numeric indices which are not integer indices,
593 // even though such a property would be listed here.
596 // The current property name is an integer index. In
597 // practice, this will only be present if the object has
598 // been made non·extensible.
604 ...Object
.getOwnPropertySymbols(O
),
609 * Prevents extensions on O.
611 * Even though they won’t ever be accessed, proxy invariants mandate
612 * that indices for a nonextensible O exist as own properties, so
613 * they are defined here as configurable, writable, enumerable
614 * properties with a value of undefined.
616 preventExtensions(O
) {
617 const length
= this.#length
;
618 if (Object
.isExtensible(O
)) {
619 // The provided object is currently extensible. Properties
620 // corresponding to its valid integer indices need to be defined
622 for (let i
= 0; i
< length
; ++i
) {
623 Object
.defineProperty(O
, index
, {
631 // The provided object is already not extensible.
634 return Reflect
.preventExtensions(O
);
638 * Sets P on O to V using the provided Receiver.
640 * Attempting to set "length" will always fail silently.
642 * Valid integer indices set the item at that index. Other numeric
643 * indices fail silently.
645 set(O
, P
, V
, Receiver
) {
646 const length
= this.#length
;
648 // The provided property is "length".
651 // The provided property is not "length".
652 const numericIndex
= canonicalNumericIndexString(P
);
653 if (numericIndex
=== undefined) {
654 // The provided propety is not a numeric index.
655 return Reflect
.set(O
, P
, V
, Receiver
);
656 } else if (isValidIntegerIndex
.call({ length
}, numericIndex
)) {
657 // The provided property is a valid integer index for the
659 setItem
.call(O
, numericIndex
, V
);
662 // The provided property in a numeric index, but not a valid
663 // integer index. This always fails silently.
671 * Returns the array species, or `null` if no species was specified.
673 * If the provided value is not an array, this function always returns
676 const arraySpecies
= (originalArray
) => {
677 if (!Array
.isArray(originalArray
)) {
680 const C
= originalArray
.constructor;
681 if (C
=== undefined || isObject(C
)) {
682 const species
= C
?.[Symbol
.species
] ?? undefined;
683 if (species
=== undefined) {
685 } else if (!isConstructor(species
)) {
686 throw new TypeError("ハブ: Species not constructable.");
692 "ハブ: Constructor must be an object or undefined.",
699 * Returns -0 if the provided argument is "-0"; returns a number
700 * representing the index if the provided argument is a canonical
701 * numeric index string; otherwise, returns undefined.
703 * There is no clamping of the numeric index, but note that numbers
704 * above 2^53 − 1 are not safe nor valid integer indices.
706 const canonicalNumericIndexString
= ($) => {
707 if (typeof $ != "string") {
709 } else if ($ === "-0") {
713 return $ === `${n}` ? n
: undefined;
718 * Defines a new array buffer on this which is the provided byte
721 const defineNewBuffer
= function defineNewBuffer(byteLength
) {
722 Object
.defineProperties(this, {
723 buffer
: { ...unlisted
, value
: new ArrayBuffer(byteLength
) },
724 byteLength
: { ...unlisted
, value
: byteLength
},
725 byteOffset
: { ...unlisted
, value
: 0 },
730 * Gets an item of the provided size and at the provided index from
733 * The returned value will always be a big·int (not a number).
735 const getItem
= function getItem(index
) {
736 const bitsPerItem
= BigInt(validSize(this.bitWidth
));
737 const bitIndex
= BigInt(index
);
738 const bytesPerElement
= wordSizeByBits
[bitsPerItem
];
739 const wordSize
= BigInt(bytesPerElement
);
740 const typedArray
= new typedArrayConstructors
[bytesPerElement
](
743 this.byteLength
/ bytesPerElement
,
745 const scale
= wordSize
* 8n
/ bitsPerItem
;
746 const offset
= Number(bitIndex
/ scale
);
747 if (offset
>= typedArray
.length
) {
748 // The offset exceeds the length of the typed array. This case
749 // ought to be unreachable.
750 throw RangeError("ハブ: Index out of range.");
752 // The offset is within the bounds of the typed array.
753 const fill
= (2n
<< (bitsPerItem
- 1n
)) - 1n
;
754 const shift
= bitsPerItem
* (bitIndex
% scale
);
755 return BigInt(typedArray
[offset
]) >> shift
& fill
;
759 /** Returns whether the provided value is a constructor. */
760 const isConstructor
= (C
) => {
762 // The provided value is not an object.
765 // The provided value is an object.
771 ); // will throw if C is not a constructor
779 /** Returns whether the provided value is an object. */
780 const isObject
= (O
) => {
782 (typeof O
== "function" || typeof O
== "object");
786 * Returns whether the provided number is a valid integer index for
789 * Note that integer indices must be numbers, not big·ints, and the
790 * latter will throw an error.
792 const isValidIntegerIndex
= function isValidIntegerIndex($) {
793 return !(Object
.is($, -0) || !Number
.isInteger($) || $ < 0 ||
798 * Sets an item of the provided size and at the provided index to the
799 * provided value in this buffer.
801 * The value must be convertable to a big·int (not a number).
803 const setItem
= function setItem(index
, value
) {
804 const bitsPerItem
= BigInt(validSize(this.bitWidth
));
805 const bitIndex
= BigInt(index
);
806 const bytesPerElement
= wordSizeByBits
[bitsPerItem
];
807 const wordSize
= BigInt(bytesPerElement
);
808 const typedArray
= new typedArrayConstructors
[bytesPerElement
](
811 this.byteLength
/ bytesPerElement
,
813 const scale
= wordSize
* 8n
/ bitsPerItem
;
814 const offset
= Number(bitIndex
/ scale
);
815 if (offset
>= typedArray
.length
) {
816 // The offset exceeds the length of the typed array. This case
817 // ought to be unreachable.
818 throw RangeError("ハブ: Index out of range.");
820 // The offset is within the bounds of the typed array.
821 const fill
= (2n
<< (bitsPerItem
- 1n
)) - 1n
;
822 const shift
= bitsPerItem
* (bitIndex
% scale
);
823 typedArray
[offset
] = (wordSize
> 4 ? BigInt
: Number
)(
824 BigInt(typedArray
[offset
]) & ~(fill
<< shift
) |
825 BigInt
.asUintN(Number(bitsPerItem
), value
) << shift
,
831 * Converts the provided value to an array index, or throws an error if
832 * it is out of range.
834 const toIndex
= ($) => {
835 const integer
= Math
.floor($);
836 if (isNaN(integer
) || integer
== 0) {
837 // The value is zero·like.
840 // The value is not zero·like.
841 const clamped
= toLength(integer
);
842 if (clamped
!== integer
) {
843 // Clamping the value changes it.
844 throw new RangeError(`ハブ: Index out of range: ${$}`);
846 // The value is within appropriate bounds.
853 * Converts the provided value to a length.
855 const toLength
= ($) => {
856 const len
= Math
.floor($);
857 return isNaN(len
) || len
== 0
859 : Math
.max(Math
.min(len
, MAX_SAFE_INTEGER
), 0);
862 /** Maps bit widths to the corresponding typed array constructor. */
863 const typedArrayConstructors
= Object
.assign(Object
.create(null), {
871 * Definitions for non·enumerable nonconfigurable readonly properties.
880 * Returns the provided argument as an integer if it is a valid bit
881 * width for a ハブ, and throws otherwise.
883 const validSize
= ($) => {
885 if (!Number
.isInteger(n
) || n
<= 0 || n
> 64 || `${n}` != `${$}`) {
886 // The provided argument is not a valid bit width.
887 throw new TypeError(`ハブ: Invalid bit width: ${$}`);
889 // The provided argument is a valid bit width.
895 * An array which maps sizes to the number of bytes which can fit them
898 const wordSizeByBits
= [
909 4, // 10×3 (2 spares)
910 2, // 11×1 (5 spares)
911 2, // 12×1 (4 spares)
912 2, // 13×1 (3 spares)
913 2, // 14×1 (2 spares)
915 2, // 16×1 (0 spares)
916 8, // 17×3 (13 spares)
917 8, // 18×3 (10 spares)
918 8, // 19×3 (7 spares)
919 8, // 20×3 (4 spares)
921 ...new Array(32 - 21).fill(4), // n×1 (32−n spares)
922 ...new Array(64 - 32).fill(8), // n×1 (64−n spares)