@formaui-net/nexo 0.0.0 → 1.0.0-preview.1

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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +148 -6
  4. package/package.json +23 -3
  5. package/src/common/ArrayUtils.mjs +16 -0
  6. package/src/common/ObjectUtils.mjs +33 -0
  7. package/src/language/DefinitionLocations.mjs +41 -0
  8. package/src/language/DefinitionValidator.mjs +1265 -0
  9. package/src/language/DefinitionWriter.mjs +475 -0
  10. package/src/language/Dictionary.mjs +52 -0
  11. package/src/language/Evaluator.mjs +3480 -0
  12. package/src/language/ImportedVocabulary.mjs +137 -0
  13. package/src/language/Imports.mjs +141 -0
  14. package/src/language/Index.mjs +29 -0
  15. package/src/language/LanguageResolver.mjs +4068 -0
  16. package/src/language/Limits.mjs +78 -0
  17. package/src/language/LineStructure.mjs +385 -0
  18. package/src/language/Lowering.mjs +453 -0
  19. package/src/language/MemberSelector.mjs +89 -0
  20. package/src/language/Names.mjs +65 -0
  21. package/src/language/NexoError.mjs +145 -0
  22. package/src/language/Pair.mjs +21 -0
  23. package/src/language/ParameterBindings.mjs +361 -0
  24. package/src/language/ParsedTexts.mjs +62 -0
  25. package/src/language/Parser.mjs +1817 -0
  26. package/src/language/Range.mjs +51 -0
  27. package/src/language/RecentlyUsed.mjs +50 -0
  28. package/src/language/ResolvedDefinition.mjs +219 -0
  29. package/src/language/ResultPath.mjs +101 -0
  30. package/src/language/ScopeLocals.mjs +102 -0
  31. package/src/language/StructureTypes.mjs +48 -0
  32. package/src/language/SyntaxTree.mjs +203 -0
  33. package/src/language/TextLocations.mjs +185 -0
  34. package/src/language/TextPositions.mjs +23 -0
  35. package/src/language/Tokenizer.mjs +917 -0
  36. package/src/language/Type.mjs +401 -0
  37. package/src/language/TypeChecker.mjs +449 -0
  38. package/src/language/ValueCapture.mjs +315 -0
  39. package/src/language/Versions.mjs +14 -0
  40. package/src/language/vocabulary/Declarations.mjs +64 -0
  41. package/src/language/vocabulary/ItemPositions.mjs +48 -0
  42. package/src/language/vocabulary/ListOrdering.mjs +145 -0
  43. package/src/language/vocabulary/MemberFailure.mjs +21 -0
  44. package/src/language/vocabulary/NexoModule.mjs +1119 -0
  45. package/src/language/vocabulary/NexoRegistry.mjs +1540 -0
  46. package/src/language/vocabulary/TypeNameScope.mjs +63 -0
  47. package/src/language/vocabulary/Vocabulary.mjs +640 -0
  48. package/src/nexo.d.ts +743 -0
  49. package/src/nexo.js +338 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * ValueCapture — a value from outside a script captured as the type declared for it, as a value a host supplies is: a value its type admits taken, a list from an array or a set, a structure as its data shape alone,
3
+ * and a reference of another type null where a nullable reference type is declared, each list, object, structure, dictionary and pair it makes frozen as it is made
4
+ */
5
+
6
+ import { ArrayUtils } from '../common/ArrayUtils.mjs';
7
+ import { ObjectUtils } from '../common/ObjectUtils.mjs';
8
+ import { Dictionary } from './Dictionary.mjs';
9
+ import { Limits } from './Limits.mjs';
10
+ import { Pair } from './Pair.mjs';
11
+ import { StructureTypes } from './StructureTypes.mjs';
12
+ import { NexoModule } from './vocabulary/NexoModule.mjs';
13
+
14
+ export class ValueCapture {
15
+ #registry;
16
+
17
+ /** What capturing each named type takes, as `#namedCaptures` holds it for the registry. */
18
+ #captures;
19
+
20
+ /** @param {import('./vocabulary/NexoRegistry.mjs').NexoRegistry} registry The registry whose types the value is captured as, whose modules recognize a reference of another type. */
21
+ constructor(registry) {
22
+ this.#registry = registry;
23
+ this.#captures = ValueCapture.#namedCaptures.get(registry);
24
+
25
+ if (this.#captures === undefined) {
26
+ this.#captures = new Map();
27
+ ValueCapture.#namedCaptures.set(registry, this.#captures);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * What capturing a named type takes, settled once for each type a registry declares, by the registry and then the type's identity: a scalar's check, a reference type, a structure's data shape, as a list of each
33
+ * property's name beside its type, with its type without null, which records each structure captured, or none, which fails every value. A registry's declarations never change once registered, so an entry is valid
34
+ * for the registry's life and released with it; recognizing a reference asks the registry anew each time, since a registration can change it. Settling it for each value measured a tenth of binding.
35
+ * @type {WeakMap<object, Map<string, { kind: 'scalar', check: (value: unknown) => boolean } | { kind: 'reference' } | { kind: 'structured', shape: [string, import('./Type.mjs').Type][], type: import('./Type.mjs').Type } | { kind: 'none' }>>}
36
+ */
37
+ static #namedCaptures = new WeakMap();
38
+
39
+ /** What capturing the named type takes, settled at its first capture. */
40
+ #getNamedCapture(type) {
41
+ let named = this.#captures.get(type.identity);
42
+
43
+ if (named === undefined) {
44
+ const declaration = this.#registry.getTypeDeclaration(type);
45
+
46
+ if (type.moduleName === null && NexoModule.scalarTypeNames.includes(type.name)) {
47
+ named = { kind: 'scalar', check: ValueCapture.#scalarChecks.get(type.name) };
48
+ } else if (declaration?.kind === 'reference') {
49
+ named = { kind: 'reference' };
50
+ } else if (declaration?.kind === 'structured') {
51
+ named = { kind: 'structured', shape: [...this.#registry.getDataShape(type)], type: type.toNonNullable() };
52
+ } else {
53
+ named = { kind: 'none' };
54
+ }
55
+
56
+ this.#captures.set(type.identity, named);
57
+ }
58
+
59
+ return named;
60
+ }
61
+
62
+ /**
63
+ * The value captured as its type at every level of it: a value its type admits taken, a structure as its declared fields alone and a list item by item, from an array or a set, its items in their traversal
64
+ * order and no uniqueness kept; nothing, or null, null where its type is nullable; and a
65
+ * reference of another type null where a nullable reference type is declared, so the value keeps only the reference its type names. Anything else is given to `fail`, which throws, with the part of the value
66
+ * by its path within it, the value itself by none, the part and the type it is declared; and data that reaches itself, a list or an object holding one of the lists or objects it lies within, is given to `fail`
67
+ * as well, with the reason `'cycle'`, since data never does, data nesting deeper than `Limits.dataNesting` with the reason `'depth'`, and a dictionary holding a key twice, once captured, with the reason
68
+ * `'duplicate'` at the later key, since a dictionary holds each key once.
69
+ * @param {unknown} value
70
+ * @param {import('./Type.mjs').Type} type
71
+ * @param {(path: string, part: unknown, type: import('./Type.mjs').Type, reason?: 'cycle' | 'depth' | 'duplicate') => never} fail
72
+ * @param {string} [asked] What asks for the capture, as a recognition's fault names it.
73
+ */
74
+ capture(value, type, fail, asked = 'capturing a supplied value') {
75
+ this.#asked = asked;
76
+ this.#enclosing = new Set();
77
+ this.#steps = [];
78
+
79
+ return this.#capture(value, type, fail);
80
+ }
81
+
82
+ /** The lists and objects the part being captured lies within, which it never holds, since data doesn't reach itself. */
83
+ #enclosing = new Set();
84
+
85
+ /**
86
+ * The steps from the value to the part being captured: a field's name, a list item's index, or a dictionary entry's key or value. The path they make is written only where a part fails, so a capture that succeeds
87
+ * writes none.
88
+ * @type {(string | number | { entry: number, part: 'key' | 'value' })[]}
89
+ */
90
+ #steps = [];
91
+
92
+ /** What the capture under way was asked by, which a module's recognition, faulting, names. */
93
+ #asked = 'capturing a supplied value';
94
+
95
+ /** The path of the part being captured within the value, as `fail` is given it: `rooms[2].name`, and a dictionary entry's part as `.entries[0].key`. */
96
+ #path() {
97
+ let path = '';
98
+
99
+ for (const step of this.#steps) {
100
+ path = typeof step === 'number' ? `${path}[${step}]` : typeof step === 'string' ? ValueCapture.#pathOf(path, step) : `${path}.entries[${step.entry}].${step.part}`;
101
+ }
102
+
103
+ return path;
104
+ }
105
+
106
+ /** A part of the value captured as its type, one step further along the path. */
107
+ #captureStep(value, type, step, fail) {
108
+ this.#steps.push(step);
109
+
110
+ const captured = this.#capture(value, type, fail);
111
+
112
+ this.#steps.pop();
113
+
114
+ return captured;
115
+ }
116
+
117
+ /** Enters a list or an object of data, whose parts are captured next, refusing one of those it lies within; `#leave` releases it once they are. */
118
+ #enter(value, type, fail) {
119
+ if (this.#enclosing.has(value)) {
120
+ fail(this.#path(), value, type, 'cycle');
121
+ }
122
+
123
+ if (this.#enclosing.size >= Limits.dataNesting) {
124
+ fail(this.#path(), value, type, 'depth');
125
+ }
126
+
127
+ this.#enclosing.add(value);
128
+ }
129
+
130
+ #leave(value) {
131
+ this.#enclosing.delete(value);
132
+ }
133
+
134
+ /** The fields of an object or a structure, each captured as its type and defined as an ordinary field, in the order of their types. */
135
+ #captureFields(value, fieldTypes, fail) {
136
+ const captured = {};
137
+
138
+ for (const [name, fieldType] of fieldTypes) {
139
+ ObjectUtils.defineDataProperty(captured, name, this.#captureStep(value[name], fieldType, name, fail));
140
+ }
141
+
142
+ return captured;
143
+ }
144
+
145
+ #capture(value, type, fail) {
146
+ if (value === undefined || value === null) {
147
+ if (!type.isNullable) {
148
+ fail(this.#path(), value, type);
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ // A list is supplied as an array or a set, a set's items in their traversal order, the list it gives holding no uniqueness.
155
+ if (type.isList) {
156
+ const items = value instanceof Set ? [...value] : value;
157
+
158
+ if (!Array.isArray(items) || ArrayUtils.findMissingIndex(items) !== -1) {
159
+ fail(this.#path(), value, type);
160
+ }
161
+
162
+ this.#enter(value, type, fail);
163
+
164
+ try {
165
+ const captured = new Array(items.length);
166
+
167
+ for (let index = 0; index < items.length; index++) {
168
+ captured[index] = this.#captureStep(items[index], type.itemType, index, fail);
169
+ }
170
+
171
+ return Object.freeze(captured);
172
+ } finally {
173
+ this.#leave(value);
174
+ }
175
+ }
176
+
177
+ // A dictionary is supplied as a map or the language's, each key and value captured as its type declares, and a pair as the language's or an object of a key and a value alone.
178
+ if (type.isDictionary) {
179
+ const entries = value instanceof Map ? [...value] : value instanceof Dictionary ? value.entries.map(entry => [entry.key, entry.value]) : fail(this.#path(), value, type);
180
+ const [keyType, valueType] = type.typeArguments;
181
+
182
+ this.#enter(value, type, fail);
183
+
184
+ try {
185
+ const captured = entries.map(([key, entry], index) => [this.#captureStep(key, keyType, { entry: index, part: 'key' }, fail), this.#captureStep(entry, valueType, { entry: index, part: 'value' }, fail)]);
186
+ // Keys equal by key equality, which a set's keeps, -0 equal to 0 and a reference by its identity, are one key, and two of them are refused, as constructing a dictionary refuses them, never one kept.
187
+ const keys = new Set();
188
+
189
+ captured.forEach(([key], index) => {
190
+ if (keys.has(key)) {
191
+ this.#steps.push({ entry: index, part: 'key' });
192
+ fail(this.#path(), key, keyType, 'duplicate');
193
+ }
194
+
195
+ keys.add(key);
196
+ });
197
+
198
+ return Object.freeze(new Dictionary(captured));
199
+ } finally {
200
+ this.#leave(value);
201
+ }
202
+ }
203
+
204
+ if (type.isPair) {
205
+ const isPair = value instanceof Pair || (ObjectUtils.isPlainObject(value) && Object.keys(value).length === 2 && Object.hasOwn(value, 'key') && Object.hasOwn(value, 'value'));
206
+
207
+ if (!isPair) {
208
+ fail(this.#path(), value, type);
209
+ }
210
+
211
+ const [keyType, valueType] = type.typeArguments;
212
+
213
+ this.#enter(value, type, fail);
214
+
215
+ try {
216
+ return Object.freeze(new Pair(this.#captureStep(value.key, keyType, 'key', fail), this.#captureStep(value.value, valueType, 'value', fail)));
217
+ } finally {
218
+ this.#leave(value);
219
+ }
220
+ }
221
+
222
+ switch (type.kind) {
223
+ // A value of Any is of any type, which the evaluation that reads it checks where a type is declared for it, as a member's answer of Any is.
224
+ case 'any':
225
+ case 'typeParameter':
226
+ return value;
227
+ case 'object':
228
+ if (!ObjectUtils.isNonArrayObject(value)) {
229
+ fail(this.#path(), value, type);
230
+ }
231
+
232
+ this.#enter(value, type, fail);
233
+
234
+ try {
235
+ return Object.freeze(this.#captureFields(value, type.fieldTypes, fail));
236
+ } finally {
237
+ this.#leave(value);
238
+ }
239
+ default:
240
+ return this.#captureNamed(value, type, fail);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * A value captured as a named type: a built-in type's value and a reference of the type as they are, a reference of another type null where a nullable reference type is declared, and a structure as its declared
246
+ * fields, each captured in turn. A value from outside has no type until it is captured, so where it doesn't belong to a reference type, the registry's recognition tells a reference of another type from mistyped
247
+ * data, which fails whatever its nullability.
248
+ */
249
+ #captureNamed(value, type, fail) {
250
+ const named = this.#getNamedCapture(type);
251
+
252
+ if (named.kind === 'scalar') {
253
+ if (!named.check(value)) {
254
+ fail(this.#path(), value, type);
255
+ }
256
+
257
+ return value;
258
+ }
259
+
260
+ if (named.kind === 'reference') {
261
+ const asked = this.#asked;
262
+
263
+ if (this.#registry.belongsTo(value, type, asked)) {
264
+ return value;
265
+ }
266
+
267
+ if (type.isNullable && this.#registry.recognize(value, asked).length > 0) {
268
+ return null;
269
+ }
270
+
271
+ fail(this.#path(), value, type);
272
+ }
273
+
274
+ if (named.kind !== 'structured' || !ObjectUtils.isNonArrayObject(value)) {
275
+ fail(this.#path(), value, type);
276
+ }
277
+
278
+ this.#enter(value, type, fail);
279
+
280
+ let structure;
281
+
282
+ try {
283
+ structure = this.#captureFields(value, named.shape, fail);
284
+ } finally {
285
+ this.#leave(value);
286
+ }
287
+
288
+ // A captured structure keeps its type through a value of Any, as one a member answers does, recorded before it is frozen.
289
+ StructureTypes.recordType(structure, this.#registry, named.type);
290
+
291
+ return Object.freeze(structure);
292
+ }
293
+
294
+ /** The path of a field within the path of the value that holds it. */
295
+ static #pathOf(path, name) {
296
+ return path === '' ? name : `${path}.${name}`;
297
+ }
298
+
299
+ /** Each scalar type's check of a value, by its name: a finite number, a string and a Boolean. */
300
+ static #scalarChecks = new Map([
301
+ ['Number', value => Number.isFinite(value)],
302
+ ['String', value => typeof value === 'string'],
303
+ ['Boolean', value => typeof value === 'boolean'],
304
+ ]);
305
+
306
+ /** The check of the scalar type the name names, one of `NexoModule.scalarTypeNames`, which a check compiled once for the type calls with no name to tell; undefined for any other name. */
307
+ static getScalarCheck(typeName) {
308
+ return ValueCapture.#scalarChecks.get(typeName);
309
+ }
310
+
311
+ /** Whether the value belongs to the scalar type the name names, one of `NexoModule.scalarTypeNames`: a finite number, a string or a Boolean. */
312
+ static belongsToScalar(value, typeName) {
313
+ return ValueCapture.#scalarChecks.get(typeName)?.(value) ?? false;
314
+ }
315
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Versions — the versions the implementation reports: its release, the language specification it follows, and the definition format it supports
3
+ */
4
+
5
+ export class Versions {
6
+ /** The implementation's release version, which the packages and the bundle carry. */
7
+ static implementation = '1.0.0-preview.1';
8
+
9
+ /** The language specification version the implementation follows. */
10
+ static language = '1.0.0-preview.1';
11
+
12
+ /** The definition format version the implementation supports. */
13
+ static definition = 38;
14
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Declarations — the helpers that write a vocabulary's declarations: its parameters and members, each in the declaration shape with the defaults its contract gives it, constructing a record and registering nothing.
3
+ * Each writes every option it is given over its defaults, leaving none out, so a module refuses an option its declaration doesn't take, where a helper that dropped it would hide the mistake.
4
+ */
5
+
6
+ export class Declarations {
7
+ /** A value parameter, required unless it is given a default. */
8
+ static createValueParameterDeclaration(name, type, options = {}) {
9
+ return { name, kind: 'value', type, required: options.default === undefined, fixed: false, nulls: 'propagate', ...options };
10
+ }
11
+
12
+ /**
13
+ * An expression parameter, evaluated against each item of the collection it follows, a list's item or a dictionary's value, and producing the type. Its null policy says how a body reads a null item: `propagate`, the
14
+ * default, reads each item as a value that is not null, a null item answering null without the body being evaluated, and `accept` evaluates the body against every item, null included, its `it` of the item's type.
15
+ */
16
+ static createExpressionParameterDeclaration(type, options = {}) {
17
+ return { name: 'expression', kind: 'expression', type, context: 'item', required: true, fixed: false, nulls: 'propagate', ...options };
18
+ }
19
+
20
+ /**
21
+ * A required predicate parameter, an expression evaluated against each item whose truthiness its implementation receives. Its null policy says how a body reads a null item, as an expression parameter's does:
22
+ * `propagate`, the default, answers null for it, which is falsy, without the body being evaluated, and `accept` evaluates the body against it.
23
+ */
24
+ static createPredicateParameterDeclaration(options = {}) {
25
+ return { name: 'expression', kind: 'predicate', type: 'Boolean', context: 'item', required: true, fixed: false, nulls: 'propagate', ...options };
26
+ }
27
+
28
+ /** A function parameter, always required: its callback contract, the types of the arguments the member calls the function with, in order, and the type of the result it takes from it. */
29
+ static createFunctionParameterDeclaration(name, type, parameterTypes) {
30
+ return { name, kind: 'function', type, parameterTypes, required: true };
31
+ }
32
+
33
+ /**
34
+ * A property, a member read without arguments: one with updates, which the evaluator invokes, or, without them, a property of a structured type's data where it stands directly in that type, as the contract of
35
+ * where it stands decides.
36
+ */
37
+ static createPropertyDeclaration(name, result, options) {
38
+ return Declarations.#createMemberDeclaration('property', name, result, options);
39
+ }
40
+
41
+ /** A function, a member called with its arguments, positional unless it says they are options, or, with `invokedBy: 'host'`, one its host invokes, its result left out where its host supplies nothing. */
42
+ static createFunctionDeclaration(name, result, options) {
43
+ return Declarations.#createMemberDeclaration('function', name, result, options);
44
+ }
45
+
46
+ /** An indexer, given its one key in brackets: a named one, `x.name[key]`, or, where its name is null, the unnamed one of its receivers, `x[key]`. */
47
+ static createIndexerDeclaration(name, result, options) {
48
+ return Declarations.#createMemberDeclaration('indexer', name, result, options);
49
+ }
50
+
51
+ /**
52
+ * A member of the kind, named unless its name is null, with no receiver of its own: where it stands gives it one, the type or the extension it is declared within, and a module member, among a module's `members`,
53
+ * takes none. A function and an indexer take no parameters unless they are given some, and a function's arguments are positional where it has parameters and is given no argument form.
54
+ */
55
+ static #createMemberDeclaration(kind, name, result, options = {}) {
56
+ const declaration = { ...(name === null ? {} : { name }), kind, result, ...(kind === 'property' ? {} : { parameters: [] }), ...options };
57
+
58
+ if (kind === 'function' && declaration.arguments === undefined && Array.isArray(declaration.parameters) && declaration.parameters.length > 0) {
59
+ declaration.arguments = 'positional';
60
+ }
61
+
62
+ return declaration;
63
+ }
64
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ItemPositions — where in its receiver each item stood of a list one of Nexo's own members answered of its receiver's items, handed with that list to the invocation that answers it, so the item's lineage follows its
3
+ * occurrence rather than its value
4
+ */
5
+
6
+ export class ItemPositions {
7
+ /**
8
+ * The list `createAnswer` created last, and the receiver's position of each of its items, which `takePositions` hands over once. A member creates its answer after everything it evaluates, its functions' calls among
9
+ * them, and its invocation takes the positions as the member returns, so no other answer is created between the two. The slot is valid until another answer is created, and releases the list and its positions
10
+ * once taken; a global map of every answer's positions, written for each, measured a twentieth of an evaluation.
11
+ */
12
+ static #answer = null;
13
+
14
+ /** @type {readonly number[] | null} */
15
+ static #positions = null;
16
+
17
+ /**
18
+ * The receiver's items at the positions, in the positions' order, a position given twice giving its item twice, as a frozen list whose positions `takePositions` hands over. Each position is one the receiver has an
19
+ * item at, which its member has checked, and the member never changes the positions it gives.
20
+ */
21
+ static createAnswer(receiver, positions) {
22
+ const answer = Object.freeze(positions.map(position => receiver[position]));
23
+
24
+ ItemPositions.#answer = answer;
25
+ ItemPositions.#positions = positions;
26
+
27
+ return answer;
28
+ }
29
+
30
+ /** The position in its receiver of each item of the list, in the list's order, where `createAnswer` created it last and none has taken them, handed over once; undefined for any other list. */
31
+ static takePositions(list) {
32
+ if (list !== ItemPositions.#answer) {
33
+ return undefined;
34
+ }
35
+
36
+ const positions = ItemPositions.#positions;
37
+
38
+ ItemPositions.#answer = null;
39
+ ItemPositions.#positions = null;
40
+
41
+ return positions;
42
+ }
43
+
44
+ /** The positions from the start to before the end, in order, none where the end is not after the start. */
45
+ static createSpan(start, end) {
46
+ return Array.from({ length: Math.max(end - start, 0) }, (_, offset) => start + offset);
47
+ }
48
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * ListOrdering — the order `orderBy` and `thenBy` give a list: each item's keys, one for each criterion, kept with the list they ordered, so a later `thenBy` refines its ties without evaluating them again
3
+ */
4
+
5
+ import { ItemPositions } from './ItemPositions.mjs';
6
+ import { MemberFailure } from './MemberFailure.mjs';
7
+
8
+ /** The domains an ordering key is of, each compared within itself alone: numbers numerically, strings by code unit, without a locale, and Booleans, false before true. */
9
+ const KEY_DOMAINS = Object.freeze(['number', 'string', 'boolean']);
10
+
11
+ export class ListOrdering {
12
+ /**
13
+ * The ordering of each list `orderBy` or `thenBy` answered, or a copy of it carried that ordering, by the list. A list never changes, so an entry is valid for good, and it is released with its list, which the map holds
14
+ * weakly; no evaluation reads another's lists, so none shares an entry.
15
+ * @type {WeakMap<object, ListOrdering>}
16
+ */
17
+ static #orderings = new WeakMap();
18
+
19
+ /** Each item's keys, in the list's order, one for each criterion in the order they were applied. @type {readonly (readonly unknown[])[]} */
20
+ #keys;
21
+
22
+ /** @param {readonly (readonly unknown[])[]} keys Each item's keys, in the list's order. */
23
+ constructor(keys) {
24
+ this.#keys = keys;
25
+ }
26
+
27
+ /**
28
+ * The items ordered by the key each gives, its expression evaluated once for each item, ascending or descending, a null key last either way, items of equal keys keeping their order; the answer keeps its
29
+ * keys, so `thenBy` refines it. A key of no domain, or keys of two, are refused (NEXO5036).
30
+ */
31
+ static orderBy(list, keyOf, isDescending, memberName) {
32
+ const keys = ListOrdering.#evaluateKeys(list, keyOf, memberName);
33
+ const order = list.map((_, index) => index).sort((one, other) => ListOrdering.compareKeys(keys[one], keys[other], isDescending));
34
+
35
+ return ListOrdering.#createOrdered(list, order, order.map(index => Object.freeze([keys[index]])));
36
+ }
37
+
38
+ /**
39
+ * The items `orderBy` and any `thenBy` after it ordered, each run of items whose keys so far are equal ordered within itself by the key each gives, evaluated once for each item, ascending or descending, a null key
40
+ * last either way; items keep their place where their keys are equal, so the list is refined, never sorted again. A list no `orderBy` ordered is refused (NEXO5037), and a key of no domain, or keys of two
41
+ * (NEXO5036).
42
+ */
43
+ static thenBy(list, keyOf, isDescending, memberName) {
44
+ const ordering = ListOrdering.#orderings.get(list);
45
+
46
+ if (ordering === undefined) {
47
+ throw new MemberFailure('NEXO5037', `'${memberName}' refines the order 'orderBy' gave a list, and this list was ordered by no 'orderBy': order it with 'orderBy' first, which a local or 'thenBy' keeps, and no other member does`);
48
+ }
49
+
50
+ const keys = ListOrdering.#evaluateKeys(list, keyOf, memberName);
51
+ const order = [];
52
+
53
+ for (let start = 0; start < list.length;) {
54
+ let end = start + 1;
55
+
56
+ while (end < list.length && ListOrdering.#haveEqualKeys(ordering.#keys[start], ordering.#keys[end])) {
57
+ end++;
58
+ }
59
+
60
+ const run = Array.from({ length: end - start }, (_, offset) => start + offset);
61
+
62
+ order.push(...run.sort((one, other) => ListOrdering.compareKeys(keys[one], keys[other], isDescending)));
63
+ start = end;
64
+ }
65
+
66
+ return ListOrdering.#createOrdered(list, order, order.map(index => Object.freeze([...ordering.#keys[index], keys[index]])));
67
+ }
68
+
69
+ /** The first item whose key is the least, or the greatest, items whose key is null skipped, and null where none gives one; a key of no domain, or keys of two, are refused (NEXO5036). */
70
+ static findExtreme(list, keyOf, isGreatest, memberName) {
71
+ const keys = ListOrdering.#evaluateKeys(list, keyOf, memberName);
72
+ let best = -1;
73
+
74
+ keys.forEach((key, index) => {
75
+ if (key !== null && (best === -1 || ListOrdering.compareKeys(key, keys[best], isGreatest) < 0)) {
76
+ best = index;
77
+ }
78
+ });
79
+
80
+ return best === -1 ? null : list[best];
81
+ }
82
+
83
+ /** Carries the ordering of the list a member answered to the copy of it the evaluation keeps, its items in the same places, so a `thenBy` given the copy refines it. */
84
+ static carry(list, copy) {
85
+ const ordering = ListOrdering.#orderings.get(list);
86
+
87
+ if (ordering !== undefined) {
88
+ ListOrdering.#orderings.set(copy, ordering);
89
+ }
90
+ }
91
+
92
+ /** How two keys of one domain order, ascending or descending, a null key after any other whichever the direction. */
93
+ static compareKeys(one, other, isDescending) {
94
+ if (one === null || other === null) {
95
+ return one === other ? 0 : one === null ? 1 : -1;
96
+ }
97
+
98
+ const ascending = one < other ? -1 : one > other ? 1 : 0;
99
+
100
+ return isDescending ? -ascending : ascending;
101
+ }
102
+
103
+ /** Each item's key, its expression evaluated once for each item, checked to be null or of one domain, which every key of the criterion shares. */
104
+ static #evaluateKeys(list, keyOf, memberName) {
105
+ const keys = list.map(item => keyOf(item));
106
+ let domain = null;
107
+
108
+ for (const key of keys) {
109
+ if (key === null) {
110
+ continue;
111
+ }
112
+
113
+ if (!KEY_DOMAINS.includes(typeof key)) {
114
+ throw new MemberFailure('NEXO5036', `'${memberName}' orders by numbers, strings or Booleans, and a key is ${ListOrdering.#describe(key)}`);
115
+ }
116
+
117
+ domain ??= typeof key;
118
+
119
+ if (typeof key !== domain) {
120
+ throw new MemberFailure('NEXO5036', `'${memberName}' orders by keys of one domain, and its keys are ${domain}s and ${typeof key}s`);
121
+ }
122
+ }
123
+
124
+ return keys;
125
+ }
126
+
127
+ /** Whether two items' keys so far are equal, criterion by criterion, null equal to null and `-0` to `0`. */
128
+ static #haveEqualKeys(one, other) {
129
+ return one.every((key, index) => key === other[index]);
130
+ }
131
+
132
+ /** The list's items in the order of their positions, frozen, holding each item's keys as their ordering and, through `ItemPositions`, the position each stood at in the list. */
133
+ static #createOrdered(list, order, keys) {
134
+ const ordered = ItemPositions.createAnswer(list, order);
135
+
136
+ ListOrdering.#orderings.set(ordered, new ListOrdering(Object.freeze(keys)));
137
+
138
+ return ordered;
139
+ }
140
+
141
+ /** A key outside the ordering domains, as a message names it. */
142
+ static #describe(key) {
143
+ return Array.isArray(key) ? 'a list' : typeof key === 'object' ? 'an object or a reference' : `a ${typeof key}`;
144
+ }
145
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MemberFailure — a failure one of Nexo's own members reports of the values it is given, which the evaluator raises at the call with the code it carries
3
+ */
4
+
5
+ export class MemberFailure extends Error {
6
+ #code;
7
+
8
+ /**
9
+ * @param {string} code The code of the condition the values break.
10
+ * @param {string} message What the call is refused for.
11
+ */
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.#code = code;
15
+ }
16
+
17
+ /** The code of the condition the values break. */
18
+ get code() {
19
+ return this.#code;
20
+ }
21
+ }