@fulcro/types 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,15 +1,15 @@
1
- ISC License
2
-
3
- Copyright (c) 2026 diguu <rodrigogeribola@hotmail.com>
4
-
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
8
-
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
- PERFORMANCE OF THIS SOFTWARE.
1
+ ISC License
2
+
3
+ Copyright (c) 2026 diguu <rodrigogeribola@hotmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,66 +1,86 @@
1
- # @fulcro/types
2
-
3
- Numeric types with a declared range and layout: fixed-width integers, half,
4
- single and double precision floats, an integer of any size, and a `Decimal` with
5
- the semantics of IEEE 754 decimal128 — with the JavaScript operators working on
6
- every one of them once the plugin is wired up.
7
-
8
- ```sh
9
- npm install @fulcro/types
10
- ```
11
-
12
- ```ts
13
- import { Decimal, SignedInteger, UnsignedInteger } from '@fulcro/types';
14
-
15
- const Int32 = SignedInteger(32);
16
-
17
- Int32.add(Int32.from(2), Int32.from(3)); // 5
18
- Int32.add(Int32.maximum, Int32.from(1)); // RangeError: outside [-2147483648, 2147483647]
19
- UnsignedInteger(8).wrap(-1); // 255, when modular arithmetic is what you mean
20
-
21
- Decimal.from('0.1').add(Decimal.from('0.2')).toString(); // '0.3'
22
- Decimal.from('19.99').multiply(Decimal.from(3)).toString(); // '59.97'
23
- ```
24
-
25
- | Type | What it is |
26
- | -------------------------------------------- | -------------------------------------------------- |
27
- | `SignedInteger<N>`, `UnsignedInteger<N>` | 8 to 128 bits, checked arithmetic, `wrap` |
28
- | `HalfPrecisionFloat`, `SinglePrecisionFloat` | binary16 and binary32, each operation rounded once |
29
- | `DoublePrecisionFloat` | binary64, the `number` named as a choice |
30
- | `BigInteger` | an integer of any size |
31
- | `Decimal` | 34 decimal digits, five rounding modes |
32
-
33
- Each type has a value of the same name that converts, recognises and computes,
34
- and every type but `BigInteger` reports its `minimum` and `maximum`. If you only
35
- need the types, `import type` them and no code is loaded.
36
-
37
- ## Operators
38
-
39
- With the plugin, the operators mean what the type means — checked on an
40
- integer, rounded on a float, decimal128 on a `Decimal` — and the result keeps
41
- its type. Both operands have to be the same type, or it is a type error at the
42
- line:
43
-
44
- ```ts
45
- const total = price * Decimal.from(3); // Decimal
46
- count++; // SignedInteger<32>.increment, checked
47
- a + 1; // type error: 'number' is not 'SignedInteger<32>'
48
- ```
49
-
50
- | Tool | Wire in |
51
- | ---------- | ---------------------------------------------------------------------------------------- |
52
- | `tsc` | `{ "transform": "@fulcro/types/transformer", "transformProgram": true }`, via `ts-patch` |
53
- | A bundler | `vite` (or `rollup`, `webpack`, `esbuild`, …) from `@fulcro/types/unplugin` |
54
- | The editor | `{ "name": "@fulcro/types/language-service" }` in the tsconfig `plugins` |
55
-
56
- Without it, the operators are the language's own: unchecked on the
57
- `number`-backed types, refused on a `Decimal`.
58
-
59
- Every type but `BigInteger` declares a size and alignment, which
60
- `sizeOf<T>()` and `alignOf<T>()` from `@fulcro/reflect` read at compile time.
61
-
62
- ---
63
-
64
- **Full guide:** [docs/types.md](../../docs/types.md) — ranges, rounding, the
65
- operators, the special values and the layout table.
66
- 🇧🇷 [Leia em português](../../docs/pt-BR/types.md).
1
+ # @fulcro/types
2
+
3
+ Numeric types with a declared range and layout: fixed-width integers, half,
4
+ single and double precision floats, an integer of any size, and a `Decimal` with
5
+ the semantics of IEEE 754 decimal128 — with the JavaScript operators working on
6
+ every one of them once the plugin is wired up.
7
+
8
+ ```sh
9
+ npm install @fulcro/types
10
+ ```
11
+
12
+ ```ts
13
+ import { Decimal, SignedInteger, UnsignedInteger } from '@fulcro/types';
14
+
15
+ const Int32 = SignedInteger(32);
16
+
17
+ Int32.add(Int32.from(2), Int32.from(3)); // 5
18
+ Int32.add(Int32.maximum, Int32.from(1)); // RangeError: outside [-2147483648, 2147483647]
19
+ UnsignedInteger(8).wrap(-1); // 255, when modular arithmetic is what you mean
20
+
21
+ Decimal.from('0.1').add(Decimal.from('0.2')).toString(); // '0.3'
22
+ Decimal.from('19.99').multiply(Decimal.from(3)).toString(); // '59.97'
23
+ ```
24
+
25
+ | Type | What it is |
26
+ | -------------------------------------------- | -------------------------------------------------- |
27
+ | `SignedInteger<N>`, `UnsignedInteger<N>` | 8 to 128 bits, checked arithmetic, `wrap` |
28
+ | `HalfPrecisionFloat`, `SinglePrecisionFloat` | binary16 and binary32, each operation rounded once |
29
+ | `DoublePrecisionFloat` | binary64, the `number` named as a choice |
30
+ | `BigInteger` | an integer of any size |
31
+ | `Decimal` | 34 decimal digits, five rounding modes |
32
+
33
+ Each type has a value of the same name that converts, recognises and computes,
34
+ and every type but `BigInteger` reports its `minimum` and `maximum`. If you only
35
+ need the types, `import type` them and no code is loaded.
36
+
37
+ ## Operators
38
+
39
+ With the plugin, the operators mean what the type means — checked on an
40
+ integer, rounded on a float, decimal128 on a `Decimal` — and the result keeps
41
+ its type. Both operands have to be the same type, or it is a type error at the
42
+ line:
43
+
44
+ ```ts
45
+ const total = price * Decimal.from(3); // Decimal
46
+ count++; // SignedInteger<32>.increment, checked
47
+ a + 1; // type error: 'number' is not 'SignedInteger<32>'
48
+ ```
49
+
50
+ | Tool | Wire in |
51
+ | ---------- | ---------------------------------------------------------------------------------------- |
52
+ | `tsc` | `{ "transform": "@fulcro/types/transformer", "transformProgram": true }`, via `ts-patch` |
53
+ | A bundler | `vite` (or `rollup`, `webpack`, `esbuild`, …) from `@fulcro/types/unplugin` |
54
+ | The editor | `{ "name": "@fulcro/types/language-service" }` in the tsconfig `plugins` |
55
+
56
+ Without it, the operators are the language's own: unchecked on the
57
+ `number`-backed types, refused on a `Decimal`.
58
+
59
+ Every type but `BigInteger` declares a size and alignment, which
60
+ `sizeOf<T>()` and `alignOf<T>()` from `@fulcro/reflect` read at compile time.
61
+
62
+ ## Structs
63
+
64
+ Value types with a fixed layout, built from the types above and from each
65
+ other — frozen, compared by field, and stored in a `DataView` without an object
66
+ per value:
67
+
68
+ ```ts
69
+ import { SinglePrecisionFloat, struct, type Struct } from '@fulcro/types';
70
+
71
+ const Vector3 = struct('Vector3', {
72
+ x: SinglePrecisionFloat,
73
+ y: SinglePrecisionFloat,
74
+ z: SinglePrecisionFloat,
75
+ });
76
+ type Vector3 = Struct<typeof Vector3>;
77
+
78
+ Vector3.layout.size; // 12, and sizeOf<Vector3>() at compile time
79
+ Vector3.write(view, 0, Vector3.from({ x: 0, y: 1, z: 0 }));
80
+ ```
81
+
82
+ ---
83
+
84
+ **Full guide:** [docs/types.md](../../docs/types.md) — ranges, rounding, the
85
+ operators, the special values, the layout table and structs.
86
+ 🇧🇷 [Leia em português](../../docs/pt-BR/types.md).
package/dist/index.d.ts CHANGED
@@ -19,4 +19,6 @@ export type { BoundedNumericType, NumericType } from './numericType/index.js';
19
19
  export type { RoundingMode } from './roundingMode/index.js';
20
20
  export { SignedInteger } from './signedInteger/index.js';
21
21
  export { SinglePrecisionFloat } from './singlePrecisionFloat/index.js';
22
+ export type { Struct, StructType } from './struct/index.js';
23
+ export { struct } from './struct/index.js';
22
24
  export { UnsignedInteger } from './unsignedInteger/index.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UnsignedInteger = exports.SinglePrecisionFloat = exports.SignedInteger = exports.HalfPrecisionFloat = exports.DoublePrecisionFloat = exports.Decimal = exports.BigInteger = void 0;
3
+ exports.UnsignedInteger = exports.struct = exports.SinglePrecisionFloat = exports.SignedInteger = exports.HalfPrecisionFloat = exports.DoublePrecisionFloat = exports.Decimal = exports.BigInteger = void 0;
4
4
  /**
5
5
  * Public entry point of the package, matching the `main` and `types` fields of
6
6
  * `package.json`.
@@ -25,5 +25,7 @@ var signedInteger_1 = require("./signedInteger/index.js");
25
25
  Object.defineProperty(exports, "SignedInteger", { enumerable: true, get: function () { return signedInteger_1.SignedInteger; } });
26
26
  var singlePrecisionFloat_1 = require("./singlePrecisionFloat/index.js");
27
27
  Object.defineProperty(exports, "SinglePrecisionFloat", { enumerable: true, get: function () { return singlePrecisionFloat_1.SinglePrecisionFloat; } });
28
+ var struct_1 = require("./struct/index.js");
29
+ Object.defineProperty(exports, "struct", { enumerable: true, get: function () { return struct_1.struct; } });
28
30
  var unsignedInteger_1 = require("./unsignedInteger/index.js");
29
31
  Object.defineProperty(exports, "UnsignedInteger", { enumerable: true, get: function () { return unsignedInteger_1.UnsignedInteger; } });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Arithmetic on number literals, at the type level, for the layout a struct
3
+ * type declares.
4
+ *
5
+ * `sizeOf<T>()` answers from the literal in `T['~layout']`, so a struct type
6
+ * has to carry its size as a literal, and that literal is a sum of its fields
7
+ * rounded up to its alignment. TypeScript has no arithmetic on literals; this
8
+ * module is the smallest amount of it the layout needs.
9
+ *
10
+ * Addition works digit by digit on the decimal text of a number, so it is not
11
+ * bounded by the recursion limit the way counting a tuple up to the sum would
12
+ * be. Rounding needs a remainder, and the remainder by an alignment — a power
13
+ * of two up to sixteen — depends only on the last four digits, since sixteen
14
+ * divides ten thousand; so no tuple built here is ever longer than 9,999.
15
+ *
16
+ * Wherever an input is `number` rather than a literal, the answer is `number`:
17
+ * `sizeOf` then refuses the type rather than report a wrong size.
18
+ */
19
+ /** A decimal digit, as text. */
20
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
21
+ /** The value of each digit. */
22
+ interface DigitValues {
23
+ readonly '0': 0;
24
+ readonly '1': 1;
25
+ readonly '2': 2;
26
+ readonly '3': 3;
27
+ readonly '4': 4;
28
+ readonly '5': 5;
29
+ readonly '6': 6;
30
+ readonly '7': 7;
31
+ readonly '8': 8;
32
+ readonly '9': 9;
33
+ }
34
+ /**
35
+ * A tuple of `N` elements, for the small `N` a digit or an alignment needs.
36
+ *
37
+ * @template N Length, from 0 to 19.
38
+ */
39
+ type Units<N extends number, TAccumulated extends unknown[] = []> = TAccumulated['length'] extends N ? TAccumulated : Units<N, [...TAccumulated, unknown]>;
40
+ /** Ten elements, the carry out of a digit. */
41
+ type Ten = Units<10>;
42
+ /**
43
+ * Adds two digits and a carry.
44
+ *
45
+ * @template TLeft First digit.
46
+ * @template TRight Second digit.
47
+ * @template TCarry Carry into this digit.
48
+ * @returns `[digit, carry]` of the sum.
49
+ */
50
+ type AddDigits<TLeft extends Digit, TRight extends Digit, TCarry extends 0 | 1> = [
51
+ ...Units<DigitValues[TLeft]>,
52
+ ...Units<DigitValues[TRight]>,
53
+ ...Units<TCarry>
54
+ ] extends infer TAll extends unknown[] ? TAll extends [...Ten, ...infer TRest] ? [`${TRest['length']}`, 1] : [`${TAll['length']}`, 0] : never;
55
+ /** First character of a text, `'0'` once it is empty. */
56
+ type Head<TText extends string> = TText extends `${infer THead extends Digit}${string}` ? THead : '0';
57
+ /** A text without its first character. */
58
+ type Tail<TText extends string> = TText extends `${Digit}${infer TTail}` ? TTail : '';
59
+ /**
60
+ * Adds two numbers written least significant digit first.
61
+ *
62
+ * @returns The sum, least significant digit first.
63
+ */
64
+ type AddReversed<TLeft extends string, TRight extends string, TCarry extends 0 | 1 = 0, TSum extends string = ''> = TLeft extends '' ? TRight extends '' ? TCarry extends 1 ? `${TSum}1` : TSum : AddStep<TLeft, TRight, TCarry, TSum> : AddStep<TLeft, TRight, TCarry, TSum>;
65
+ /** One digit of {@link AddReversed}. */
66
+ type AddStep<TLeft extends string, TRight extends string, TCarry extends 0 | 1, TSum extends string> = AddDigits<Head<TLeft>, Head<TRight>, TCarry> extends [
67
+ infer TDigit extends string,
68
+ infer TNext extends 0 | 1
69
+ ] ? AddReversed<Tail<TLeft>, Tail<TRight>, TNext, `${TSum}${TDigit}`> : never;
70
+ /** A text, backwards. */
71
+ type Reverse<TText extends string, TReversed extends string = ''> = TText extends `${infer THead}${infer TTail}` ? Reverse<TTail, `${THead}${TReversed}`> : TReversed;
72
+ /** The number a text of digits spells. */
73
+ type ToNumber<TText extends string> = TText extends `${infer N extends number}` ? N : never;
74
+ /**
75
+ * The sum of two number literals; `number` when either is not a literal.
76
+ *
77
+ * @template TLeft First operand, a non-negative integer.
78
+ * @template TRight Second operand, a non-negative integer.
79
+ */
80
+ export type Add<TLeft extends number, TRight extends number> = number extends TLeft | TRight ? number : ToNumber<Reverse<AddReversed<Reverse<`${TLeft}`>, Reverse<`${TRight}`>>>>;
81
+ /**
82
+ * A tuple as long as a number of up to four digits, built one digit at a time
83
+ * — ten copies of what came before, and the digit — so its depth is the number
84
+ * of digits rather than the number itself.
85
+ */
86
+ type TupleOf<TDigits extends string, TAccumulated extends unknown[] = []> = TDigits extends `${infer THead extends Digit}${infer TTail}` ? TupleOf<TTail, [
87
+ ...TAccumulated,
88
+ ...TAccumulated,
89
+ ...TAccumulated,
90
+ ...TAccumulated,
91
+ ...TAccumulated,
92
+ ...TAccumulated,
93
+ ...TAccumulated,
94
+ ...TAccumulated,
95
+ ...TAccumulated,
96
+ ...TAccumulated,
97
+ ...Units<DigitValues[THead]>
98
+ ]> : TAccumulated;
99
+ /** The last `N` characters of a text written least significant first. */
100
+ type Take<TText extends string, N extends number, TTaken extends string = '', TCount extends unknown[] = []> = TCount['length'] extends N ? TTaken : TText extends `${infer THead}${infer TTail}` ? Take<TTail, N, `${THead}${TTaken}`, [...TCount, unknown]> : TTaken;
101
+ /** Digits of a number that decide its remainder by each alignment. */
102
+ interface RemainderDigits {
103
+ readonly 2: 1;
104
+ readonly 4: 2;
105
+ readonly 8: 3;
106
+ readonly 16: 4;
107
+ }
108
+ /** How many elements are left of a tuple once whole chunks are taken off it. */
109
+ type Leftover<TTuple extends unknown[], TChunk extends unknown[]> = TTuple extends [...TChunk, ...infer TRest] ? Leftover<TRest, TChunk> : TTuple['length'];
110
+ /**
111
+ * The remainder of a number by an alignment.
112
+ *
113
+ * @template N A non-negative integer literal.
114
+ * @template TAlignment 2, 4, 8 or 16.
115
+ */
116
+ type Remainder<N extends number, TAlignment extends keyof RemainderDigits> = Leftover<TupleOf<Take<Reverse<`${N}`>, RemainderDigits[TAlignment]>>, Units<TAlignment>>;
117
+ /**
118
+ * A number rounded up to a multiple of an alignment: the size of a struct once
119
+ * its tail padding is added.
120
+ *
121
+ * @template N Size before padding.
122
+ * @template TAlignment Alignment of the struct, a power of two up to 16.
123
+ */
124
+ export type RoundUp<N extends number, TAlignment extends number> = number extends N | TAlignment ? number : TAlignment extends keyof RemainderDigits ? Remainder<N, TAlignment> extends infer TRemainder extends number ? TRemainder extends 0 ? N : Units<TAlignment> extends [...Units<TRemainder>, ...infer TPadding] ? Add<N, TPadding['length']> : never : never : N;
125
+ /**
126
+ * The largest of a union of alignments.
127
+ *
128
+ * @template TAlignments Alignments of the fields, each a power of two up to 16.
129
+ */
130
+ export type LargestAlignment<TAlignments extends number> = number extends TAlignments ? number : 16 extends TAlignments ? 16 : 8 extends TAlignments ? 8 : 4 extends TAlignments ? 4 : 2 extends TAlignments ? 2 : 1;
131
+ export {};
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * Arithmetic on number literals, at the type level, for the layout a struct
4
+ * type declares.
5
+ *
6
+ * `sizeOf<T>()` answers from the literal in `T['~layout']`, so a struct type
7
+ * has to carry its size as a literal, and that literal is a sum of its fields
8
+ * rounded up to its alignment. TypeScript has no arithmetic on literals; this
9
+ * module is the smallest amount of it the layout needs.
10
+ *
11
+ * Addition works digit by digit on the decimal text of a number, so it is not
12
+ * bounded by the recursion limit the way counting a tuple up to the sum would
13
+ * be. Rounding needs a remainder, and the remainder by an alignment — a power
14
+ * of two up to sixteen — depends only on the last four digits, since sixteen
15
+ * divides ten thousand; so no tuple built here is ever longer than 9,999.
16
+ *
17
+ * Wherever an input is `number` rather than a literal, the answer is `number`:
18
+ * `sizeOf` then refuses the type rather than report a wrong size.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * How a field of a struct is laid out in bytes, and how it is compared.
3
+ *
4
+ * Internal. A struct is built from descriptors, and a descriptor says how a
5
+ * value is checked but not how it is stored — `SinglePrecisionFloat` has no
6
+ * byte order. The codec is what a struct looks up for each field, by the
7
+ * identity of its descriptor, so the numeric types did not have to grow a
8
+ * public byte encoding for this.
9
+ *
10
+ * Every multi-byte value is little-endian, whatever the platform: bytes a
11
+ * struct writes on one machine read back the same on any other, and it is the
12
+ * order of every platform a JavaScript engine runs on in practice.
13
+ */
14
+ export interface FieldCodec {
15
+ /** Size, in bytes. */
16
+ readonly size: number;
17
+ /** Alignment, in bytes. */
18
+ readonly alignment: number;
19
+ /**
20
+ * Reads a value.
21
+ *
22
+ * @param view Bytes to read from.
23
+ * @param offset Where the value starts.
24
+ * @returns The value.
25
+ */
26
+ read(view: DataView, offset: number): unknown;
27
+ /**
28
+ * Writes a value.
29
+ *
30
+ * @param view Bytes to write into.
31
+ * @param offset Where the value starts.
32
+ * @param value The value, already of the field's type.
33
+ */
34
+ write(view: DataView, offset: number, value: unknown): void;
35
+ /**
36
+ * Compares two values as the field's own type does.
37
+ *
38
+ * @param left First value.
39
+ * @param right Second value.
40
+ * @returns `true` when they are equal.
41
+ */
42
+ equals(left: unknown, right: unknown): boolean;
43
+ }
44
+ /**
45
+ * Registers the codec of a struct, so that another struct can hold it as a
46
+ * field.
47
+ *
48
+ * @param descriptor Descriptor of the struct.
49
+ * @param codec How it is stored.
50
+ */
51
+ export declare const registerStructCodec: (descriptor: object, codec: FieldCodec) => void;
52
+ /**
53
+ * The codec of a field's descriptor.
54
+ *
55
+ * @param descriptor What the field was declared with.
56
+ * @returns Its codec, or `undefined` for a descriptor with no fixed layout.
57
+ */
58
+ export declare const codecOf: (descriptor: unknown) => FieldCodec | undefined;
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codecOf = exports.registerStructCodec = void 0;
4
+ const decimal_1 = require("../decimal/index.js");
5
+ const format_1 = require("../decimal/format.js");
6
+ const parse_1 = require("../decimal/parse.js");
7
+ const parts_1 = require("../decimal/parts.js");
8
+ const doublePrecisionFloat_1 = require("../doublePrecisionFloat/index.js");
9
+ const halfPrecisionFloat_1 = require("../halfPrecisionFloat/index.js");
10
+ const signedInteger_1 = require("../signedInteger/index.js");
11
+ const singlePrecisionFloat_1 = require("../singlePrecisionFloat/index.js");
12
+ const unsignedInteger_1 = require("../unsignedInteger/index.js");
13
+ /** Bits of a 64-bit half of a 128-bit value. */
14
+ const HALF_WIDTH = 64n;
15
+ /**
16
+ * The codec of a 128-bit integer, as two 64-bit halves, low half first.
17
+ *
18
+ * @param descriptor Descriptor of the width.
19
+ * @returns The codec.
20
+ */
21
+ const integer128Codec = (descriptor) => ({
22
+ size: 16,
23
+ alignment: 16,
24
+ read: (view, offset) => {
25
+ const low = view.getBigUint64(offset, true);
26
+ const high = descriptor.signed
27
+ ? view.getBigInt64(offset + 8, true)
28
+ : view.getBigUint64(offset + 8, true);
29
+ return (high << HALF_WIDTH) | low;
30
+ },
31
+ write: (view, offset, value) => {
32
+ view.setBigUint64(offset, BigInt.asUintN(64, value), true);
33
+ view.setBigUint64(offset + 8, BigInt.asUintN(64, value >> HALF_WIDTH), true);
34
+ },
35
+ equals: (left, right) => descriptor.equals(left, right),
36
+ });
37
+ /**
38
+ * The codec of a fixed-width integer, through the `DataView` accessor of its
39
+ * width, or two of them at 128 bits.
40
+ *
41
+ * @param descriptor Descriptor of the width.
42
+ * @returns The codec.
43
+ */
44
+ const integerCodec = (descriptor) => {
45
+ const { signed, width } = descriptor;
46
+ if (width === 128)
47
+ return integer128Codec(descriptor);
48
+ const size = width / 8;
49
+ const equals = (left, right) => descriptor.equals(left, right);
50
+ switch (width) {
51
+ case 8:
52
+ return {
53
+ size,
54
+ alignment: size,
55
+ read: (view, offset) => signed ? view.getInt8(offset) : view.getUint8(offset),
56
+ write: (view, offset, value) => signed
57
+ ? view.setInt8(offset, value)
58
+ : view.setUint8(offset, value),
59
+ equals,
60
+ };
61
+ case 16:
62
+ return {
63
+ size,
64
+ alignment: size,
65
+ read: (view, offset) => signed ? view.getInt16(offset, true) : view.getUint16(offset, true),
66
+ write: (view, offset, value) => signed
67
+ ? view.setInt16(offset, value, true)
68
+ : view.setUint16(offset, value, true),
69
+ equals,
70
+ };
71
+ case 32:
72
+ return {
73
+ size,
74
+ alignment: size,
75
+ read: (view, offset) => signed ? view.getInt32(offset, true) : view.getUint32(offset, true),
76
+ write: (view, offset, value) => signed
77
+ ? view.setInt32(offset, value, true)
78
+ : view.setUint32(offset, value, true),
79
+ equals,
80
+ };
81
+ default:
82
+ return {
83
+ size,
84
+ alignment: size,
85
+ read: (view, offset) => signed
86
+ ? view.getBigInt64(offset, true)
87
+ : view.getBigUint64(offset, true),
88
+ write: (view, offset, value) => signed
89
+ ? view.setBigInt64(offset, value, true)
90
+ : view.setBigUint64(offset, value, true),
91
+ equals,
92
+ };
93
+ }
94
+ };
95
+ /** Exponent of the smallest normal half precision value. */
96
+ const HALF_MINIMUM_NORMAL_EXPONENT = -14;
97
+ /** Spacing of the half precision subnormals, 2^-24. */
98
+ const HALF_SUBNORMAL_SPACING = 2 ** -24;
99
+ /**
100
+ * The binary16 bit pattern of a half precision value.
101
+ *
102
+ * Written here rather than delegated to `DataView.prototype.setFloat16`, which
103
+ * Node 22 — the oldest line this package supports — does not have. The value
104
+ * is already exactly representable, so every division below is exact and
105
+ * nothing is rounded: this only rearranges bits.
106
+ *
107
+ * @param value A half precision value.
108
+ * @returns Its sixteen bits.
109
+ */
110
+ const encodeHalf = (value) => {
111
+ if (Number.isNaN(value))
112
+ return 0x7e00;
113
+ const sign = value < 0 || Object.is(value, -0) ? 0x8000 : 0;
114
+ const magnitude = Math.abs(value);
115
+ if (magnitude === Infinity)
116
+ return sign | 0x7c00;
117
+ if (magnitude < 2 ** HALF_MINIMUM_NORMAL_EXPONENT) {
118
+ return sign | (magnitude / HALF_SUBNORMAL_SPACING);
119
+ }
120
+ // `Math.log2` is not exact next to a power of two, so the estimate is
121
+ // corrected against the power itself.
122
+ let exponent = Math.floor(Math.log2(magnitude));
123
+ if (2 ** exponent > magnitude)
124
+ exponent--;
125
+ else if (2 ** (exponent + 1) <= magnitude)
126
+ exponent++;
127
+ return (sign | ((exponent + 15) << 10) | ((magnitude / 2 ** exponent - 1) * 1024));
128
+ };
129
+ /**
130
+ * The half precision value of a binary16 bit pattern.
131
+ *
132
+ * @param bits Sixteen bits.
133
+ * @returns The value they encode.
134
+ */
135
+ const decodeHalf = (bits) => {
136
+ const sign = bits & 0x8000 ? -1 : 1;
137
+ const exponent = (bits >> 10) & 0x1f;
138
+ const fraction = bits & 0x3ff;
139
+ if (exponent === 0)
140
+ return sign * fraction * HALF_SUBNORMAL_SPACING;
141
+ if (exponent === 0x1f)
142
+ return fraction === 0 ? sign * Infinity : NaN;
143
+ return sign * (1 + fraction / 1024) * 2 ** (exponent - 15);
144
+ };
145
+ /** The codecs of the three binary float formats, by descriptor. */
146
+ const floatCodecs = new Map([
147
+ [
148
+ halfPrecisionFloat_1.HalfPrecisionFloat,
149
+ {
150
+ size: 2,
151
+ alignment: 2,
152
+ read: (view, offset) => decodeHalf(view.getUint16(offset, true)),
153
+ write: (view, offset, value) => view.setUint16(offset, encodeHalf(value), true),
154
+ equals: (left, right) => halfPrecisionFloat_1.HalfPrecisionFloat.equals(left, right),
155
+ },
156
+ ],
157
+ [
158
+ singlePrecisionFloat_1.SinglePrecisionFloat,
159
+ {
160
+ size: 4,
161
+ alignment: 4,
162
+ read: (view, offset) => view.getFloat32(offset, true),
163
+ write: (view, offset, value) => view.setFloat32(offset, value, true),
164
+ equals: (left, right) => singlePrecisionFloat_1.SinglePrecisionFloat.equals(left, right),
165
+ },
166
+ ],
167
+ [
168
+ doublePrecisionFloat_1.DoublePrecisionFloat,
169
+ {
170
+ size: 8,
171
+ alignment: 8,
172
+ read: (view, offset) => view.getFloat64(offset, true),
173
+ write: (view, offset, value) => view.setFloat64(offset, value, true),
174
+ equals: (left, right) => doublePrecisionFloat_1.DoublePrecisionFloat.equals(left, right),
175
+ },
176
+ ],
177
+ ]);
178
+ /** Bias of the decimal128 exponent: the stored field is `exponent + 6176`. */
179
+ const DECIMAL_EXPONENT_BIAS = 6176;
180
+ /**
181
+ * Largest exponent a decimal128 coefficient can be scaled by: the largest
182
+ * value, thirty-four nines, has its last digit at 10^6111.
183
+ */
184
+ const DECIMAL_MAXIMUM_EXPONENT = 6111;
185
+ /** Bits of the decimal128 coefficient field. */
186
+ const DECIMAL_COEFFICIENT_BITS = 113n;
187
+ /** The coefficients a decimal128 holds are below this; above it is non-canonical. */
188
+ const DECIMAL_COEFFICIENT_LIMIT = (0, parts_1.powerOfTen)(34);
189
+ /**
190
+ * The decimal128 bits of a decimal, in the binary integer decimal encoding
191
+ * IEEE 754-2008 defines: a sign bit, fourteen bits of biased exponent and a
192
+ * 113-bit binary coefficient.
193
+ *
194
+ * The parts are normalised, which can leave a large value with a short
195
+ * coefficient and an exponent past the one the format stores — 1 × 10^6144.
196
+ * Those are scaled back into range by moving digits into the coefficient,
197
+ * which always has room for them because the value is within range.
198
+ *
199
+ * @param parts The value.
200
+ * @returns Its 128 bits.
201
+ */
202
+ const encodeDecimal = (parts) => {
203
+ const sign = parts.negative ? 1n << 127n : 0n;
204
+ if (parts.kind === 'nan')
205
+ return 0x7c00000000000000n << HALF_WIDTH;
206
+ if (parts.kind === 'infinity') {
207
+ return sign | (0x7800000000000000n << HALF_WIDTH);
208
+ }
209
+ let { coefficient, exponent } = parts;
210
+ if (exponent > DECIMAL_MAXIMUM_EXPONENT) {
211
+ coefficient *= (0, parts_1.powerOfTen)(exponent - DECIMAL_MAXIMUM_EXPONENT);
212
+ exponent = DECIMAL_MAXIMUM_EXPONENT;
213
+ }
214
+ return (sign |
215
+ (BigInt(exponent + DECIMAL_EXPONENT_BIAS) << DECIMAL_COEFFICIENT_BITS) |
216
+ coefficient);
217
+ };
218
+ /**
219
+ * The decimal of decimal128 bits.
220
+ *
221
+ * A coefficient past thirty-four digits — including every one written in the
222
+ * encoding's second form, whose leading bits are `11` — is non-canonical, and
223
+ * IEEE 754 reads it as zero. Trailing zeros are moved into the exponent, which
224
+ * is the normalised form every `Decimal` holds.
225
+ *
226
+ * @param bits 128 bits.
227
+ * @returns The parts they encode.
228
+ */
229
+ const decodeDecimal = (bits) => {
230
+ const negative = bits >> 127n === 1n;
231
+ const combination = (bits >> 122n) & 0x1fn;
232
+ if (combination === 0x1fn) {
233
+ return { kind: 'nan', negative: false, coefficient: 0n, exponent: 0 };
234
+ }
235
+ if (combination === 0x1en) {
236
+ return { kind: 'infinity', negative, coefficient: 0n, exponent: 0 };
237
+ }
238
+ let coefficient = ((bits >> 125n) & 0x3n) === 0x3n
239
+ ? 0n
240
+ : bits & ((1n << DECIMAL_COEFFICIENT_BITS) - 1n);
241
+ if (coefficient >= DECIMAL_COEFFICIENT_LIMIT)
242
+ coefficient = 0n;
243
+ if (coefficient === 0n) {
244
+ return { kind: 'finite', negative, coefficient: 0n, exponent: 0 };
245
+ }
246
+ let exponent = Number((bits >> DECIMAL_COEFFICIENT_BITS) & 0x3fffn) -
247
+ DECIMAL_EXPONENT_BIAS;
248
+ while (coefficient % 10n === 0n) {
249
+ coefficient /= 10n;
250
+ exponent++;
251
+ }
252
+ return { kind: 'finite', negative, coefficient, exponent };
253
+ };
254
+ /**
255
+ * The codec of `Decimal`, as decimal128.
256
+ *
257
+ * Its parts are private to the class, so they travel through its text, which
258
+ * reads back exactly: the shortest text of a normalised value names exactly
259
+ * that value.
260
+ */
261
+ const decimalCodec = {
262
+ size: 16,
263
+ alignment: 16,
264
+ read: (view, offset) => {
265
+ const bits = (view.getBigUint64(offset + 8, true) << HALF_WIDTH) |
266
+ view.getBigUint64(offset, true);
267
+ return decimal_1.Decimal.from((0, format_1.formatDecimal)(decodeDecimal(bits)));
268
+ },
269
+ write: (view, offset, value) => {
270
+ const bits = encodeDecimal((0, parse_1.parseDecimal)(String(value)));
271
+ view.setBigUint64(offset, BigInt.asUintN(64, bits), true);
272
+ view.setBigUint64(offset + 8, bits >> HALF_WIDTH, true);
273
+ },
274
+ equals: (left, right) => left.equals(right),
275
+ };
276
+ /** Codecs registered by the structs themselves, so one can nest in another. */
277
+ const structCodecs = new WeakMap();
278
+ /** Widths a fixed-width integer comes in. */
279
+ const INTEGER_WIDTHS = [8, 16, 32, 64, 128];
280
+ /**
281
+ * Tells whether a value is the descriptor `SignedInteger` or `UnsignedInteger`
282
+ * returns for its width — the same object, not one shaped like it.
283
+ *
284
+ * @param descriptor Value to inspect.
285
+ * @returns `true` for an integer descriptor of this package.
286
+ */
287
+ const isIntegerDescriptor = (descriptor) => {
288
+ const { signed, width } = descriptor;
289
+ if (typeof signed !== 'boolean' || !INTEGER_WIDTHS.includes(width)) {
290
+ return false;
291
+ }
292
+ const factory = signed ? signedInteger_1.SignedInteger : unsignedInteger_1.UnsignedInteger;
293
+ return factory(width) === descriptor;
294
+ };
295
+ /**
296
+ * Registers the codec of a struct, so that another struct can hold it as a
297
+ * field.
298
+ *
299
+ * @param descriptor Descriptor of the struct.
300
+ * @param codec How it is stored.
301
+ */
302
+ const registerStructCodec = (descriptor, codec) => {
303
+ structCodecs.set(descriptor, codec);
304
+ };
305
+ exports.registerStructCodec = registerStructCodec;
306
+ /**
307
+ * The codec of a field's descriptor.
308
+ *
309
+ * @param descriptor What the field was declared with.
310
+ * @returns Its codec, or `undefined` for a descriptor with no fixed layout.
311
+ */
312
+ const codecOf = (descriptor) => {
313
+ if (typeof descriptor !== 'object' && typeof descriptor !== 'function') {
314
+ return undefined;
315
+ }
316
+ if (descriptor === null)
317
+ return undefined;
318
+ if (descriptor === decimal_1.Decimal)
319
+ return decimalCodec;
320
+ const known = floatCodecs.get(descriptor) ?? structCodecs.get(descriptor);
321
+ if (known !== undefined)
322
+ return known;
323
+ return isIntegerDescriptor(descriptor) ? integerCodec(descriptor) : undefined;
324
+ };
325
+ exports.codecOf = codecOf;
@@ -0,0 +1,174 @@
1
+ import type { Layout } from '../layout/index.js';
2
+ import type { Add, LargestAlignment, RoundUp } from './arithmetic.js';
3
+ /**
4
+ * What a field of a struct may be declared with: the descriptor of a type that
5
+ * declares a fixed layout — a numeric type of this package other than
6
+ * `BigInteger`, or another struct.
7
+ */
8
+ interface FieldDescriptor {
9
+ is(value: unknown): value is Layout<number, number>;
10
+ }
11
+ /** The fields of a struct, by name, in declaration order. */
12
+ type StructFields = {
13
+ readonly [field: string]: FieldDescriptor;
14
+ };
15
+ /** The type of the values a descriptor recognises. */
16
+ type ValueOf<TDescriptor> = TDescriptor extends {
17
+ is(value: unknown): value is infer T;
18
+ } ? T : never;
19
+ /** What a descriptor's `from` accepts. */
20
+ type SourceOf<TDescriptor> = TDescriptor extends {
21
+ from(value: infer TSource): unknown;
22
+ } ? TSource : never;
23
+ /** The layout a field's type declares. */
24
+ type FieldLayout<TDescriptor> = ValueOf<TDescriptor> extends Layout<infer TSize, infer TAlignment> ? {
25
+ readonly size: TSize;
26
+ readonly alignment: TAlignment;
27
+ } : never;
28
+ /** A union, as a tuple in no particular order — enough to sum over it. */
29
+ type UnionToTuple<TUnion, TTuple extends unknown[] = []> = [TUnion] extends [
30
+ never
31
+ ] ? TTuple : UnionToTuple<Exclude<TUnion, LastOf<TUnion>>, [LastOf<TUnion>, ...TTuple]>;
32
+ /** One member of a union, picked by how TypeScript orders overloads. */
33
+ type LastOf<TUnion> = (TUnion extends unknown ? (member: () => TUnion) => void : never) extends (member: infer TIntersection) => void ? TIntersection extends () => infer TMember ? TMember : never : never;
34
+ /** The sizes of some fields, added up. */
35
+ type SumOfSizes<TFields extends StructFields, TKeys extends unknown[], TSum extends number = 0> = TKeys extends [infer TKey extends keyof TFields, ...infer TRest] ? SumOfSizes<TFields, TRest, Add<TSum, FieldLayout<TFields[TKey]>['size']>> : TSum;
36
+ /** Alignment of a struct: the largest alignment of its fields. */
37
+ type StructAlignment<TFields extends StructFields> = LargestAlignment<{
38
+ [TKey in keyof TFields]: FieldLayout<TFields[TKey]>['alignment'];
39
+ }[keyof TFields]>;
40
+ /**
41
+ * Size of a struct: its fields, which pack without padding once ordered by
42
+ * alignment, rounded up to its alignment. Independent of the order the fields
43
+ * were declared in, which is why it can be computed here at all — a type does
44
+ * not promise an order for its keys.
45
+ */
46
+ type StructSize<TFields extends StructFields> = RoundUp<SumOfSizes<TFields, UnionToTuple<keyof TFields>>, StructAlignment<TFields>>;
47
+ /** A value of a struct. */
48
+ type StructValue<TFields extends StructFields> = {
49
+ readonly [TKey in keyof TFields]: ValueOf<TFields[TKey]>;
50
+ } & Layout<StructSize<TFields>, StructAlignment<TFields>>;
51
+ /** What a struct's `from` accepts: each field in what its own `from` accepts. */
52
+ type StructSource<TFields extends StructFields> = {
53
+ readonly [TKey in keyof TFields]: SourceOf<TFields[TKey]>;
54
+ };
55
+ /**
56
+ * The descriptor of a struct: its layout, and how its values are made,
57
+ * recognised, compared and stored.
58
+ *
59
+ * @template TFields Descriptors of the fields, by name.
60
+ */
61
+ export interface StructType<TFields extends StructFields> {
62
+ /** Name of the struct, as it reads in an error message. */
63
+ readonly name: string;
64
+ /**
65
+ * Where each field sits and how much room the struct takes.
66
+ *
67
+ * Fields are placed by alignment, largest first, and in declaration order
68
+ * among equals — so no field needs padding before it, and only the end of
69
+ * the struct is padded, up to its alignment, so that the next one in an
70
+ * array starts aligned. `size` and `alignment` are the numbers
71
+ * `sizeOf<T>()` and `alignOf<T>()` report.
72
+ */
73
+ readonly layout: {
74
+ readonly size: StructSize<TFields>;
75
+ readonly alignment: StructAlignment<TFields>;
76
+ readonly fields: {
77
+ readonly [TKey in keyof TFields]: {
78
+ readonly offset: number;
79
+ readonly size: FieldLayout<TFields[TKey]>['size'];
80
+ readonly alignment: FieldLayout<TFields[TKey]>['alignment'];
81
+ };
82
+ };
83
+ };
84
+ /**
85
+ * Makes a value, converting each field with its own type's `from`.
86
+ *
87
+ * @param source One entry per field, and nothing else.
88
+ * @returns The value, frozen.
89
+ * @throws {TypeError} When a field is missing or not a field of the struct.
90
+ * @throws {RangeError} When a field's own conversion refuses its value; the
91
+ * message names the field, and `cause` is the original error.
92
+ */
93
+ from(source: StructSource<TFields>): StructValue<TFields>;
94
+ /**
95
+ * Tells whether a value is one of this struct's: frozen, with exactly its
96
+ * fields, each of its type.
97
+ *
98
+ * @param value Value to inspect.
99
+ * @returns `true` when it is.
100
+ */
101
+ is(value: unknown): value is StructValue<TFields>;
102
+ /**
103
+ * Compares two values field by field, each as its own type compares — so a
104
+ * `NaN` field makes a value unequal to itself, as it does in C#.
105
+ *
106
+ * @param left First value.
107
+ * @param right Second value.
108
+ * @returns `true` when every field is equal.
109
+ */
110
+ equals(left: StructValue<TFields>, right: StructValue<TFields>): boolean;
111
+ /**
112
+ * Reads a value from bytes, in the layout of {@link StructType.layout},
113
+ * little-endian.
114
+ *
115
+ * @param view Bytes to read from.
116
+ * @param offset Where the value starts.
117
+ * @returns The value, frozen.
118
+ * @throws {RangeError} When the struct does not fit in the view at that
119
+ * offset.
120
+ */
121
+ read(view: DataView, offset: number): StructValue<TFields>;
122
+ /**
123
+ * Writes a value into bytes, in the layout of {@link StructType.layout},
124
+ * little-endian. Padding bytes are left as they were.
125
+ *
126
+ * @param view Bytes to write into.
127
+ * @param offset Where the value starts.
128
+ * @param value Value to write.
129
+ * @throws {RangeError} When the struct does not fit in the view at that
130
+ * offset.
131
+ */
132
+ write(view: DataView, offset: number, value: StructValue<TFields>): void;
133
+ }
134
+ /**
135
+ * The type of the values of a struct, named from its descriptor.
136
+ *
137
+ * ```ts
138
+ * export const Vector3 = struct('Vector3', { x: SinglePrecisionFloat, … });
139
+ * export type Vector3 = Struct<typeof Vector3>;
140
+ * ```
141
+ *
142
+ * @template TDescriptor Type of the descriptor `struct` returned.
143
+ */
144
+ export type Struct<TDescriptor extends StructType<StructFields>> = TDescriptor extends StructType<infer TFields> ? StructValue<TFields> : never;
145
+ /**
146
+ * Declares a struct: a value type with a fixed layout.
147
+ *
148
+ * ```ts
149
+ * export const Vector3 = struct('Vector3', {
150
+ * x: SinglePrecisionFloat,
151
+ * y: SinglePrecisionFloat,
152
+ * z: SinglePrecisionFloat,
153
+ * });
154
+ * export type Vector3 = Struct<typeof Vector3>;
155
+ *
156
+ * const up: Vector3 = Vector3.from({ x: 0, y: 1, z: 0 });
157
+ *
158
+ * Vector3.layout.size; // 12
159
+ * Vector3.write(new DataView(buffer), 0, up);
160
+ * ```
161
+ *
162
+ * A value has no identity: it is frozen, two values with the same fields are
163
+ * equal by {@link StructType.equals}, and it can be written into bytes and
164
+ * read back as the same value. It is still a JavaScript object while it is
165
+ * held as one; the layout is what it occupies when it is stored.
166
+ *
167
+ * @param name Name of the struct, for error messages.
168
+ * @param fields Descriptor of each field, by name.
169
+ * @returns The descriptor of the struct.
170
+ * @throws {TypeError} When there is no field, a field's type has no fixed
171
+ * layout, or a field is named like an array index or `~layout`.
172
+ */
173
+ export declare const struct: <TFields extends StructFields>(name: string, fields: TFields) => StructType<TFields>;
174
+ export {};
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.struct = void 0;
4
+ const codec_1 = require("./codec.js");
5
+ /**
6
+ * Keys an object lists before every other, in numeric order, whatever order
7
+ * they were written in. A field named like one would silently move, so it is
8
+ * refused.
9
+ */
10
+ const ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
11
+ /**
12
+ * Describes a value for an error message.
13
+ *
14
+ * @param value Value being reported.
15
+ * @returns Its kind.
16
+ */
17
+ const describeKind = (value) => value === null ? 'null' : typeof value;
18
+ /**
19
+ * Re-throws the error a field's own conversion raised, naming the field.
20
+ *
21
+ * @param name Name of the struct.
22
+ * @param key Field being converted.
23
+ * @param error What the conversion threw.
24
+ * @returns Never.
25
+ */
26
+ const rethrowForField = (name, key, error) => {
27
+ const message = (original) => `${name}.from: field '${key}': ${original.message}`;
28
+ if (error instanceof RangeError) {
29
+ throw new RangeError(message(error), { cause: error });
30
+ }
31
+ if (error instanceof TypeError) {
32
+ throw new TypeError(message(error), { cause: error });
33
+ }
34
+ if (error instanceof SyntaxError) {
35
+ throw new SyntaxError(message(error), { cause: error });
36
+ }
37
+ throw error;
38
+ };
39
+ /**
40
+ * Declares a struct: a value type with a fixed layout.
41
+ *
42
+ * ```ts
43
+ * export const Vector3 = struct('Vector3', {
44
+ * x: SinglePrecisionFloat,
45
+ * y: SinglePrecisionFloat,
46
+ * z: SinglePrecisionFloat,
47
+ * });
48
+ * export type Vector3 = Struct<typeof Vector3>;
49
+ *
50
+ * const up: Vector3 = Vector3.from({ x: 0, y: 1, z: 0 });
51
+ *
52
+ * Vector3.layout.size; // 12
53
+ * Vector3.write(new DataView(buffer), 0, up);
54
+ * ```
55
+ *
56
+ * A value has no identity: it is frozen, two values with the same fields are
57
+ * equal by {@link StructType.equals}, and it can be written into bytes and
58
+ * read back as the same value. It is still a JavaScript object while it is
59
+ * held as one; the layout is what it occupies when it is stored.
60
+ *
61
+ * @param name Name of the struct, for error messages.
62
+ * @param fields Descriptor of each field, by name.
63
+ * @returns The descriptor of the struct.
64
+ * @throws {TypeError} When there is no field, a field's type has no fixed
65
+ * layout, or a field is named like an array index or `~layout`.
66
+ */
67
+ const struct = (name, fields) => {
68
+ if (typeof name !== 'string' || name === '') {
69
+ throw new TypeError(`struct: expected a name, received ${describeKind(name)}.`);
70
+ }
71
+ if (typeof fields !== 'object' || fields === null) {
72
+ throw new TypeError(`struct ${name}: expected an object of fields, received ${describeKind(fields)}.`);
73
+ }
74
+ const keys = Object.keys(fields);
75
+ if (keys.length === 0) {
76
+ throw new TypeError(`struct ${name}: expected at least one field.`);
77
+ }
78
+ const declared = keys.map((key) => {
79
+ if (ARRAY_INDEX.test(key) || key === '~layout') {
80
+ throw new TypeError(`struct ${name}: '${key}' cannot name a field; an array index would be reordered, and '~layout' is the layout itself.`);
81
+ }
82
+ const codec = (0, codec_1.codecOf)(fields[key]);
83
+ if (codec === undefined) {
84
+ throw new TypeError(`struct ${name}: field '${key}' has no fixed layout. Declare it with a numeric type of @fulcro/types other than BigInteger, or with another struct.`);
85
+ }
86
+ return { key, descriptor: fields[key], codec };
87
+ });
88
+ // Largest alignment first, so that every field lands aligned without padding
89
+ // before it: each size is a multiple of its own alignment. `sort` is stable,
90
+ // which keeps declaration order among equals.
91
+ const offsets = new Map();
92
+ let end = 0;
93
+ for (const field of [...declared].sort((left, right) => right.codec.alignment - left.codec.alignment)) {
94
+ offsets.set(field.key, end);
95
+ end += field.codec.size;
96
+ }
97
+ const alignment = Math.max(...declared.map((field) => field.codec.alignment));
98
+ const size = Math.ceil(end / alignment) * alignment;
99
+ const plan = declared.map((field) => ({
100
+ ...field,
101
+ descriptor: field.descriptor,
102
+ offset: offsets.get(field.key),
103
+ }));
104
+ const layout = Object.freeze({
105
+ size,
106
+ alignment,
107
+ fields: Object.freeze(Object.fromEntries(plan.map((field) => [
108
+ field.key,
109
+ Object.freeze({
110
+ offset: field.offset,
111
+ size: field.codec.size,
112
+ alignment: field.codec.alignment,
113
+ }),
114
+ ]))),
115
+ });
116
+ /**
117
+ * Refuses a view and an offset the struct does not fit in, before a byte is
118
+ * touched — so a failed write leaves the view as it was.
119
+ *
120
+ * @param operation Operation being performed.
121
+ * @param view View handed in.
122
+ * @param offset Offset handed in.
123
+ */
124
+ const requireRoom = (operation, view, offset) => {
125
+ if (!(view instanceof DataView)) {
126
+ throw new TypeError(`${name}.${operation}: expected a DataView, received ${describeKind(view)}.`);
127
+ }
128
+ if (!Number.isInteger(offset) ||
129
+ offset < 0 ||
130
+ offset + size > view.byteLength) {
131
+ throw new RangeError(`${name}.${operation}: ${size} bytes at offset ${offset} do not fit in a view of ${view.byteLength} bytes.`);
132
+ }
133
+ };
134
+ const readFields = (view, offset) => {
135
+ const value = {};
136
+ for (const field of plan) {
137
+ value[field.key] = field.codec.read(view, offset + field.offset);
138
+ }
139
+ return Object.freeze(value);
140
+ };
141
+ const writeFields = (view, offset, value) => {
142
+ for (const field of plan) {
143
+ field.codec.write(view, offset + field.offset, value[field.key]);
144
+ }
145
+ };
146
+ const equals = (left, right) => plan.every((field) => field.codec.equals(left[field.key], right[field.key]));
147
+ const descriptor = {
148
+ name,
149
+ layout: layout,
150
+ from: (source) => {
151
+ if (typeof source !== 'object' || source === null) {
152
+ throw new TypeError(`${name}.from: expected an object, received ${describeKind(source)}.`);
153
+ }
154
+ for (const key of Object.keys(source)) {
155
+ if (!Object.hasOwn(fields, key)) {
156
+ throw new TypeError(`${name}.from: '${key}' is not a field; the fields are ${keys.join(', ')}.`);
157
+ }
158
+ }
159
+ const value = {};
160
+ for (const field of plan) {
161
+ if (!Object.hasOwn(source, field.key)) {
162
+ throw new TypeError(`${name}.from: missing field '${field.key}'.`);
163
+ }
164
+ try {
165
+ value[field.key] = field.descriptor.from(source[field.key]);
166
+ }
167
+ catch (error) {
168
+ rethrowForField(name, field.key, error);
169
+ }
170
+ }
171
+ return Object.freeze(value);
172
+ },
173
+ is: (value) => typeof value === 'object' &&
174
+ value !== null &&
175
+ Object.isFrozen(value) &&
176
+ Object.keys(value).length === plan.length &&
177
+ plan.every((field) => Object.hasOwn(value, field.key) &&
178
+ field.descriptor.is(value[field.key])),
179
+ equals,
180
+ read: (view, offset) => {
181
+ requireRoom('read', view, offset);
182
+ return readFields(view, offset);
183
+ },
184
+ write: (view, offset, value) => {
185
+ requireRoom('write', view, offset);
186
+ writeFields(view, offset, value);
187
+ },
188
+ };
189
+ (0, codec_1.registerStructCodec)(descriptor, {
190
+ size,
191
+ alignment,
192
+ read: readFields,
193
+ write: writeFields,
194
+ equals,
195
+ });
196
+ return descriptor;
197
+ };
198
+ exports.struct = struct;
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "@fulcro/types",
3
- "version": "0.1.0",
4
- "description": "Numeric types with a defined range and layout: fixed-width integers, half, single and double precision floats, and a decimal128 Decimal.",
5
- "keywords": [
6
- "integer",
7
- "float",
8
- "decimal",
9
- "decimal128",
10
- "numeric",
11
- "typescript"
12
- ],
13
- "license": "ISC",
14
- "author": "diguu <rodrigogeribola@hotmail.com>",
15
- "main": "./dist/index.js",
16
- "types": "./dist/index.d.ts",
17
- "exports": {
18
- ".": {
19
- "types": "./dist/index.d.ts",
20
- "default": "./dist/index.js"
21
- },
22
- "./transformer": {
23
- "types": "./dist/transformer/index.d.ts",
24
- "default": "./dist/transformer/index.js"
25
- },
26
- "./unplugin": {
27
- "types": "./dist/unplugin/index.d.mts",
28
- "default": "./dist/unplugin/index.mjs"
29
- },
30
- "./language-service": {
31
- "types": "./dist/languageService/index.d.ts",
32
- "default": "./dist/languageService/index.js"
33
- },
34
- "./package.json": "./package.json"
35
- },
36
- "files": [
37
- "dist"
38
- ],
39
- "sideEffects": false,
40
- "dependencies": {
41
- "@fulcro/transform-core": "^0.10.0"
42
- },
43
- "peerDependencies": {
44
- "typescript": ">=5.3.3 <7"
45
- },
46
- "peerDependenciesMeta": {
47
- "typescript": {
48
- "optional": true
49
- }
50
- },
51
- "engines": {
52
- "node": ">=22"
53
- },
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "repository": {
58
- "type": "git",
59
- "url": "git+https://github.com/DigUu-RL/fulcro.git",
60
- "directory": "packages/types"
61
- },
62
- "homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/types#readme",
63
- "bugs": {
64
- "url": "https://github.com/DigUu-RL/fulcro/issues"
65
- },
66
- "scripts": {
67
- "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
68
- "typecheck": "tsc --noEmit -p tsconfig.json",
69
- "prepublishOnly": "npm run build"
70
- }
71
- }
1
+ {
2
+ "name": "@fulcro/types",
3
+ "version": "0.2.0",
4
+ "description": "Numeric types with a defined range and layout: fixed-width integers, half, single and double precision floats, and a decimal128 Decimal.",
5
+ "keywords": [
6
+ "integer",
7
+ "float",
8
+ "decimal",
9
+ "decimal128",
10
+ "numeric",
11
+ "typescript"
12
+ ],
13
+ "license": "ISC",
14
+ "author": "diguu <rodrigogeribola@hotmail.com>",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./transformer": {
23
+ "types": "./dist/transformer/index.d.ts",
24
+ "default": "./dist/transformer/index.js"
25
+ },
26
+ "./unplugin": {
27
+ "types": "./dist/unplugin/index.d.mts",
28
+ "default": "./dist/unplugin/index.mjs"
29
+ },
30
+ "./language-service": {
31
+ "types": "./dist/languageService/index.d.ts",
32
+ "default": "./dist/languageService/index.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "sideEffects": false,
40
+ "dependencies": {
41
+ "@fulcro/transform-core": "^0.10.0"
42
+ },
43
+ "peerDependencies": {
44
+ "typescript": ">=5.3.3 <7"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "typescript": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/DigUu-RL/fulcro.git",
60
+ "directory": "packages/types"
61
+ },
62
+ "homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/types#readme",
63
+ "bugs": {
64
+ "url": "https://github.com/DigUu-RL/fulcro/issues"
65
+ },
66
+ "scripts": {
67
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
68
+ "typecheck": "tsc --noEmit -p tsconfig.json",
69
+ "prepublishOnly": "npm run build"
70
+ }
71
+ }