@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.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +148 -6
- package/package.json +23 -3
- package/src/common/ArrayUtils.mjs +16 -0
- package/src/common/ObjectUtils.mjs +33 -0
- package/src/language/DefinitionLocations.mjs +41 -0
- package/src/language/DefinitionValidator.mjs +1265 -0
- package/src/language/DefinitionWriter.mjs +475 -0
- package/src/language/Dictionary.mjs +52 -0
- package/src/language/Evaluator.mjs +3480 -0
- package/src/language/ImportedVocabulary.mjs +137 -0
- package/src/language/Imports.mjs +141 -0
- package/src/language/Index.mjs +29 -0
- package/src/language/LanguageResolver.mjs +4068 -0
- package/src/language/Limits.mjs +78 -0
- package/src/language/LineStructure.mjs +385 -0
- package/src/language/Lowering.mjs +453 -0
- package/src/language/MemberSelector.mjs +89 -0
- package/src/language/Names.mjs +65 -0
- package/src/language/NexoError.mjs +145 -0
- package/src/language/Pair.mjs +21 -0
- package/src/language/ParameterBindings.mjs +361 -0
- package/src/language/ParsedTexts.mjs +62 -0
- package/src/language/Parser.mjs +1817 -0
- package/src/language/Range.mjs +51 -0
- package/src/language/RecentlyUsed.mjs +50 -0
- package/src/language/ResolvedDefinition.mjs +219 -0
- package/src/language/ResultPath.mjs +101 -0
- package/src/language/ScopeLocals.mjs +102 -0
- package/src/language/StructureTypes.mjs +48 -0
- package/src/language/SyntaxTree.mjs +203 -0
- package/src/language/TextLocations.mjs +185 -0
- package/src/language/TextPositions.mjs +23 -0
- package/src/language/Tokenizer.mjs +917 -0
- package/src/language/Type.mjs +401 -0
- package/src/language/TypeChecker.mjs +449 -0
- package/src/language/ValueCapture.mjs +315 -0
- package/src/language/Versions.mjs +14 -0
- package/src/language/vocabulary/Declarations.mjs +64 -0
- package/src/language/vocabulary/ItemPositions.mjs +48 -0
- package/src/language/vocabulary/ListOrdering.mjs +145 -0
- package/src/language/vocabulary/MemberFailure.mjs +21 -0
- package/src/language/vocabulary/NexoModule.mjs +1119 -0
- package/src/language/vocabulary/NexoRegistry.mjs +1540 -0
- package/src/language/vocabulary/TypeNameScope.mjs +63 -0
- package/src/language/vocabulary/Vocabulary.mjs +640 -0
- package/src/nexo.d.ts +743 -0
- package/src/nexo.js +338 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary — Nexo's built-in vocabulary, what every expression has whatever its host: the language's value types, the members of lists, numbers and strings, and the conversion operators of strings,
|
|
3
|
+
* numbers and Booleans, each carrying its implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Dictionary } from '../Dictionary.mjs';
|
|
7
|
+
import { Limits } from '../Limits.mjs';
|
|
8
|
+
import { Names } from '../Names.mjs';
|
|
9
|
+
import { Pair } from '../Pair.mjs';
|
|
10
|
+
import { Declarations } from './Declarations.mjs';
|
|
11
|
+
import { ItemPositions } from './ItemPositions.mjs';
|
|
12
|
+
import { NexoModule } from './NexoModule.mjs';
|
|
13
|
+
import { ListOrdering } from './ListOrdering.mjs';
|
|
14
|
+
import { MemberFailure } from './MemberFailure.mjs';
|
|
15
|
+
|
|
16
|
+
/** The text a conversion to a number takes: HTML's valid floating-point number, an optional minus, digits with an optional fraction or a fraction alone, and an optional exponent whose sign is optional. */
|
|
17
|
+
const VALID_FLOATING_POINT_NUMBER = /^-?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][-+]?[0-9]+)?$/;
|
|
18
|
+
|
|
19
|
+
/** The ASCII whitespace a text converted to a number is trimmed of at either end: tab, line feed, form feed, carriage return and space. */
|
|
20
|
+
const SURROUNDING_ASCII_WHITESPACE = /^[\t\n\f\r ]+|[\t\n\f\r ]+$/g;
|
|
21
|
+
|
|
22
|
+
/** The updates of a member that reads and changes as what it selects and evaluates does. */
|
|
23
|
+
const DERIVED_UPDATES = Object.freeze({ changes: 'derived', reads: 'derived' });
|
|
24
|
+
|
|
25
|
+
/** The most items a list holds, 2^32 - 1, as ECMAScript's arrays do. */
|
|
26
|
+
const MAX_LIST_LENGTH = Limits.listLength;
|
|
27
|
+
|
|
28
|
+
export class Vocabulary {
|
|
29
|
+
/** The name of Nexo's own module, spelled as the specification spells it. */
|
|
30
|
+
static moduleName = Names.nexoModuleName;
|
|
31
|
+
|
|
32
|
+
/** @type {readonly object[] | undefined} */
|
|
33
|
+
static #valueTypes;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The value types Nexo's own module owns, each declared through a form internal to Nexo, which no other module declares through: its scalar types, of kind `scalar`, each holding its conversion operators to
|
|
37
|
+
* the others, and its generic collections, of kind `generic`, their type parameters under `typeParameters` and the variance of each, in order, under `variances`, with their members within them, whose receiver
|
|
38
|
+
* gives them its parameters: `List<T>`, covariant in its items, since a list is immutable; `Dictionary<K, V>`, invariant in its keys, which a lookup takes, and covariant in its values; and `Pair<K, V>`, covariant
|
|
39
|
+
* in both; and its position types, of kind `position`, `Index`, an offset from a list's end, and `Range`, two boundaries, which declare no members. Created once, frozen, and read by identity: the registry
|
|
40
|
+
* registers their members with Nexo's own module.
|
|
41
|
+
*/
|
|
42
|
+
static get valueTypes() {
|
|
43
|
+
return Vocabulary.#valueTypes ??= Object.freeze([
|
|
44
|
+
...NexoModule.scalarTypeNames.map(name => Object.freeze({ name, kind: 'scalar', members: Vocabulary.#createMembers(name, name, [...Vocabulary.#createConversions(name), ...Vocabulary.#createScalarMembers(name)]) })),
|
|
45
|
+
Vocabulary.#createListType(),
|
|
46
|
+
Object.freeze({ name: 'Dictionary', kind: 'generic', typeParameters: Object.freeze(['K', 'V']), variances: Object.freeze(['invariant', 'covariant']), members: Vocabulary.#createMembers('Dictionary<K, V>', 'Dictionary<K, V>', Vocabulary.#createDictionaryMembers()) }),
|
|
47
|
+
...['Index', 'Range'].map(name => Object.freeze({ name, kind: 'position', members: Object.freeze([]) })),
|
|
48
|
+
Object.freeze({ name: 'Pair', kind: 'generic', typeParameters: Object.freeze(['K', 'V']), variances: Object.freeze(['covariant', 'covariant']), members: Vocabulary.#createMembers('Pair<K, V>', 'Pair<K, V>', Vocabulary.#createPairMembers()) }),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @type {readonly string[] | undefined} */
|
|
53
|
+
static #builtInTypeNames;
|
|
54
|
+
|
|
55
|
+
/** The names of the value types Nexo's own module owns, which a declaration or a script names unqualified with no module declaring them, derived from those types: `Number`, `String`, `Boolean`, `List`, `Dictionary`, `Index`, `Range` and `Pair`. */
|
|
56
|
+
static get builtInTypeNames() {
|
|
57
|
+
return Vocabulary.#builtInTypeNames ??= Object.freeze(Vocabulary.valueTypes.map(type => type.name));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @type {readonly string[] | undefined} */
|
|
61
|
+
static #ownTypeNames;
|
|
62
|
+
|
|
63
|
+
/** Every type name Nexo's own vocabulary gives, unqualified, wherever a type is named: its value types' and the reserved types', `Any` and `Never`. */
|
|
64
|
+
static get ownTypeNames() {
|
|
65
|
+
return Vocabulary.#ownTypeNames ??= Object.freeze([...Vocabulary.builtInTypeNames, 'Any', 'Never']);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @type {ReadonlySet<object> | undefined} */
|
|
69
|
+
static #itemKeepingMembers;
|
|
70
|
+
|
|
71
|
+
/** @type {ReadonlySet<object> | undefined} */
|
|
72
|
+
static #projectingMembers;
|
|
73
|
+
|
|
74
|
+
/** @type {ReadonlySet<object> | undefined} */
|
|
75
|
+
static #limitedMembers;
|
|
76
|
+
|
|
77
|
+
/** Whether the member is one of Nexo's own that makes a list whose size is not its receiver's, `sequence` and `concat`, which is given the limits its evaluator's host set, as no other member is. */
|
|
78
|
+
static takesLimits(declaration) {
|
|
79
|
+
Vocabulary.#limitedMembers ??= new Set(['Number', 'List'].flatMap(name => Vocabulary.getTypeDeclaration(name).members).filter(member => member.name === 'sequence' || member.name === 'concat'));
|
|
80
|
+
|
|
81
|
+
return Vocabulary.#limitedMembers.has(declaration);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @type {ReadonlySet<object> | undefined} */
|
|
85
|
+
static #itemGivingMembers;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether the member is one of Nexo's own lists' that answers items of its receiver, as they stand there, kept, left out, reordered, taken once each or repeated, so its answer carries each answered item's
|
|
89
|
+
* lineage, which the position it stood at, handed by `ItemPositions` with the answer, says: `where`, `take`, `drop`, `takeWhile`, `dropWhile`, `distinct`, `distinctBy`, `reverse`, the members that order,
|
|
90
|
+
* `orderBy`, `orderByDescending`, `thenBy` and `thenByDescending`, the unnamed indexer's signature that slices by a range, and `pick`.
|
|
91
|
+
*/
|
|
92
|
+
static answersReceiverItems(declaration) {
|
|
93
|
+
Vocabulary.#itemKeepingMembers ??= new Set(Vocabulary.getTypeDeclaration('List').members.filter(member =>
|
|
94
|
+
['where', 'take', 'drop', 'takeWhile', 'dropWhile', 'distinct', 'distinctBy', 'reverse', 'orderBy', 'orderByDescending', 'thenBy', 'thenByDescending', 'pick'].includes(member.name)
|
|
95
|
+
|| (member.kind === 'indexer' && member.name === undefined && member.parameters[0].type === 'Range')));
|
|
96
|
+
|
|
97
|
+
return Vocabulary.#itemKeepingMembers.has(declaration);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether the member is one of Nexo's own lists' that answers one value for each of its receiver's items, in their order, as an object projection does, so its answer records each item as its source: `select` alone. */
|
|
101
|
+
static projectsItems(declaration) {
|
|
102
|
+
Vocabulary.#projectingMembers ??= Vocabulary.#listMembersNamed(['select']);
|
|
103
|
+
|
|
104
|
+
return Vocabulary.#projectingMembers.has(declaration);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the member is one of Nexo's own that calls each function it is given with one argument alone, an item of its receiver as it stands there: every member of a list that takes an expression or a predicate,
|
|
109
|
+
* and a dictionary's `where` and `select`, which give each of its values.
|
|
110
|
+
*/
|
|
111
|
+
static givesReceiverItems(declaration) {
|
|
112
|
+
Vocabulary.#itemGivingMembers ??= new Set(['List', 'Dictionary'].flatMap(name => Vocabulary.getTypeDeclaration(name).members).filter(member => member.parameters?.some(parameter => parameter.kind === 'expression' || parameter.kind === 'predicate')));
|
|
113
|
+
|
|
114
|
+
return Vocabulary.#itemGivingMembers.has(declaration);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @type {ReadonlyMap<object, string> | undefined} */
|
|
118
|
+
static #keyComparingMembers;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The type parameter whose values the member compares by key equality, where it is one of Nexo's own lists' that does: `T`, the items, for `contains`, `indexOf` and `distinct`, and `K`, the key its expression
|
|
122
|
+
* gives, for `distinctBy`; null for any other member. A dictionary `toDictionary` and `groupBy` answer is keyed by its own key type, which a call answering a dictionary is checked for.
|
|
123
|
+
*/
|
|
124
|
+
static getKeyTypeParameter(declaration) {
|
|
125
|
+
Vocabulary.#keyComparingMembers ??= new Map(Vocabulary.getTypeDeclaration('List').members.filter(member => ['contains', 'indexOf', 'distinct', 'distinctBy'].includes(member.name)).map(member => [member, member.name === 'distinctBy' ? 'K' : 'T']));
|
|
126
|
+
|
|
127
|
+
return Vocabulary.#keyComparingMembers.get(declaration) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The members of Nexo's own lists of the names, by identity. */
|
|
131
|
+
static #listMembersNamed(names) {
|
|
132
|
+
return new Set(Vocabulary.getTypeDeclaration('List').members.filter(member => names.includes(member.name)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The declaration of the value type Nexo's own module owns under the name, or undefined for a name it owns none of. */
|
|
136
|
+
static getTypeDeclaration(name) {
|
|
137
|
+
return Vocabulary.valueTypes.find(type => type.name === name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @type {NexoModule | undefined} */
|
|
141
|
+
static #module;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Nexo's own module, each member and operator carrying its implementation: one module, created here alone, which every registry `nexo.createRegistry` makes holds and whose conversions between the language's
|
|
145
|
+
* types no other module may declare. It owns the value types, whose members and operators the registry registers with it, and declares no extension. A module of its name made elsewhere is an ordinary module,
|
|
146
|
+
* so the exception belongs to this one alone.
|
|
147
|
+
*/
|
|
148
|
+
static get module() {
|
|
149
|
+
return Vocabulary.#module ??= new NexoModule(Vocabulary.moduleName, {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `List<T>`, its members and those it declares as its own for a list of lists, a receiver narrower than its own, which `receivers` gives each of them by its declaration: `flatten`, the items of each inner list
|
|
154
|
+
* in order. An inner list that can be null is no list of lists, so none is ever treated as empty.
|
|
155
|
+
*/
|
|
156
|
+
static #createListType() {
|
|
157
|
+
const nested = Vocabulary.#createMembers('List<T>', 'List<List<T>>', [Declarations.createFunctionDeclaration('flatten', 'List<T>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.flat() })]);
|
|
158
|
+
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
name: 'List',
|
|
161
|
+
kind: 'generic',
|
|
162
|
+
typeParameters: Object.freeze(['T']),
|
|
163
|
+
variances: Object.freeze(['covariant']),
|
|
164
|
+
members: Object.freeze([...Vocabulary.#createMembers('List<T>', 'List<T>', Vocabulary.#createListMembers()), ...nested]),
|
|
165
|
+
receivers: new Map(nested.map(member => [member, 'List<List<T>>'])),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The members of one of Nexo's own value types, validated, copied and frozen as a module's are, by a module created for that alone and never registered: an extension of the type gives them the receiver and the
|
|
171
|
+
* scope its members read, a list's `T` among them, and no module declares Nexo's value types, whose form is Nexo's alone.
|
|
172
|
+
*/
|
|
173
|
+
static #createMembers(name, target, members) {
|
|
174
|
+
return new NexoModule(Vocabulary.moduleName, { types: [{ name, kind: 'extension', target, members }] }).types[0].members;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The conversion operators of the scalar type, each taking the value, never null, and `reject`, which stops it for a value it does not convert: a string to a number, as HTML's valid floating-point number, and to a
|
|
179
|
+
* Boolean, written "true" or "false"; and a number and a Boolean to a string.
|
|
180
|
+
*/
|
|
181
|
+
static #createConversions(name) {
|
|
182
|
+
const convert = (result, impl) => ({ kind: 'operator', name: 'convert', result, impl });
|
|
183
|
+
|
|
184
|
+
switch (name) {
|
|
185
|
+
case 'String':
|
|
186
|
+
return [
|
|
187
|
+
convert('Number', ({ value, reject }) => {
|
|
188
|
+
const trimmed = value.replace(SURROUNDING_ASCII_WHITESPACE, '');
|
|
189
|
+
|
|
190
|
+
if (!VALID_FLOATING_POINT_NUMBER.test(trimmed)) {
|
|
191
|
+
reject(`${JSON.stringify(value)} is no number, which is written as HTML's valid floating-point number, as -12, 3.5, .5 or 1e3 are`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const number = Number(trimmed);
|
|
195
|
+
|
|
196
|
+
if (!Number.isFinite(number)) {
|
|
197
|
+
reject(`${JSON.stringify(value)} is too large a number to be finite`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return number;
|
|
201
|
+
}),
|
|
202
|
+
convert('Boolean', ({ value, reject }) => {
|
|
203
|
+
if (value !== 'true' && value !== 'false') {
|
|
204
|
+
reject(`${JSON.stringify(value)} is no Boolean, which is written "true" or "false"`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return value === 'true';
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
// The shortest text that reads back as the same number, as ECMAScript writes it, and negative zero as "0".
|
|
211
|
+
case 'Number':
|
|
212
|
+
return [convert('String', ({ value }) => String(value))];
|
|
213
|
+
// A Boolean's text, "true" or "false".
|
|
214
|
+
default:
|
|
215
|
+
return [convert('String', ({ value }) => String(value))];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The members of the scalar type beside its conversions: a number's and a string's, and none of a Boolean. */
|
|
220
|
+
static #createScalarMembers(name) {
|
|
221
|
+
switch (name) {
|
|
222
|
+
case 'Number':
|
|
223
|
+
return Vocabulary.#createNumberMembers();
|
|
224
|
+
case 'String':
|
|
225
|
+
return Vocabulary.#createStringMembers();
|
|
226
|
+
default:
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The members of a string, each counting UTF-16 code units, as ordering compares strings: its length; the text trimmed of whitespace and in either case, without a locale; whether it begins with, ends with or
|
|
233
|
+
* holds a text, and where it first does; the text with a text replaced, and split at a separator, each refusing the empty text, which stands at every position; and its slice by a range, strict as a list's, which
|
|
234
|
+
* never parts a surrogate pair.
|
|
235
|
+
*/
|
|
236
|
+
static #createStringMembers() {
|
|
237
|
+
// What a text has is read as a property, and what is made of it is computed by a function.
|
|
238
|
+
const of = (name, result, compute) => Declarations.createPropertyDeclaration(name, result, { updates: DERIVED_UPDATES, impl: ({ receiver }) => compute(receiver) });
|
|
239
|
+
const made = (name, compute) => Declarations.createFunctionDeclaration(name, 'String', { updates: DERIVED_UPDATES, impl: ({ receiver }) => compute(receiver) });
|
|
240
|
+
const textParameter = name => Declarations.createValueParameterDeclaration(name, 'String');
|
|
241
|
+
const testing = (name, test) => Declarations.createFunctionDeclaration(name, 'Boolean', { updates: DERIVED_UPDATES, parameters: [textParameter('text')], impl: ({ receiver, args }) => test(receiver, args.text) });
|
|
242
|
+
// The empty text stands at every position, so it has no occurrence to replace or to split at.
|
|
243
|
+
const requireOccurrence = (member, text) => {
|
|
244
|
+
if (text === '') {
|
|
245
|
+
throw new MemberFailure('NEXO5040', `'${member}' is given the empty text, which stands at every position and has no occurrence to ${member === 'replace' ? 'replace' : 'split at'}`);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return [
|
|
250
|
+
of('length', 'Number', text => text.length),
|
|
251
|
+
// Tab, line feed, vertical tab, form feed, carriage return, U+FEFF, U+2028, U+2029 and every space separator, at either end.
|
|
252
|
+
made('trim', text => text.trim()),
|
|
253
|
+
made('lowercase', text => text.toLowerCase()),
|
|
254
|
+
made('uppercase', text => text.toUpperCase()),
|
|
255
|
+
testing('startsWith', (text, prefix) => text.startsWith(prefix)),
|
|
256
|
+
testing('endsWith', (text, suffix) => text.endsWith(suffix)),
|
|
257
|
+
testing('contains', (text, part) => text.includes(part)),
|
|
258
|
+
Declarations.createFunctionDeclaration('indexOf', 'Number?', { updates: DERIVED_UPDATES, parameters: [textParameter('text')], impl: ({ receiver, args }) => Vocabulary.#positionOrNull(receiver.indexOf(args.text)) }),
|
|
259
|
+
Declarations.createFunctionDeclaration('replace', 'String', {
|
|
260
|
+
updates: DERIVED_UPDATES,
|
|
261
|
+
parameters: [textParameter('old'), textParameter('new')],
|
|
262
|
+
impl: ({ receiver, args }) => (requireOccurrence('replace', args.old), receiver.split(args.old).join(args.new)),
|
|
263
|
+
}),
|
|
264
|
+
Declarations.createFunctionDeclaration('split', 'List<String>', { updates: DERIVED_UPDATES, parameters: [textParameter('separator')], impl: ({ receiver, args }) => (requireOccurrence('split', args.separator), receiver.split(args.separator)) }),
|
|
265
|
+
Declarations.createIndexerDeclaration(null, 'String', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('range', 'Range')], impl: ({ receiver, args }) => Vocabulary.#sliceText(receiver, args.range) }),
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The code units within the range, applied to the text's length once, its start at or before its end and both within the text, neither parting a surrogate pair, or refused. */
|
|
270
|
+
static #sliceText(text, range) {
|
|
271
|
+
const { start, end } = range.resolveIn(text.length);
|
|
272
|
+
|
|
273
|
+
if (start < 0 || end > text.length || start > end) {
|
|
274
|
+
throw new MemberFailure('NEXO5033', `The range ${range} is applied to a text of ${text.length} code units, and gives ${start}..${end}, which ${start > end ? 'starts after it ends' : 'stands beyond the text'}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// A boundary between a high and a low surrogate would leave half a character on either side.
|
|
278
|
+
const parts = boundary => boundary > 0 && boundary < text.length && /[\uD800-\uDBFF]/.test(text[boundary - 1]) && /[\uDC00-\uDFFF]/.test(text[boundary]);
|
|
279
|
+
|
|
280
|
+
if (parts(start) || parts(end)) {
|
|
281
|
+
throw new MemberFailure('NEXO5041', `The range ${range} gives ${start}..${end}, which parts the surrogate pair at ${parts(start) ? start : end}, so the slice would hold half a character`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return text.slice(start, end);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The members of a number beside its conversions, each a function computing a number from it: its rounding, `round` a half away from zero, `floor`, `ceiling` and `truncate`; its absolute value; the lesser and the greater of it and another, and it clamped
|
|
289
|
+
* within two, a low above the high refused; each answering 0 where it would answer -0; and `sequence`, the numbers from it, generated apart from any range.
|
|
290
|
+
*/
|
|
291
|
+
static #createNumberMembers() {
|
|
292
|
+
// A number a member answers is never -0, which no script tells from 0 but a host's division could.
|
|
293
|
+
const unsigned = number => (number === 0 ? 0 : number);
|
|
294
|
+
const of = (name, compute) => Declarations.createFunctionDeclaration(name, 'Number', { updates: DERIVED_UPDATES, impl: ({ receiver }) => unsigned(compute(receiver)) });
|
|
295
|
+
const against = (name, compute) => Declarations.createFunctionDeclaration(name, 'Number', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('other', 'Number')], impl: ({ receiver, args }) => unsigned(compute(receiver, args.other)) });
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
// Half away from zero, so a number and its negation round to a number and its negation.
|
|
299
|
+
of('round', number => Math.sign(number) * Math.round(Math.abs(number))),
|
|
300
|
+
of('floor', Math.floor),
|
|
301
|
+
of('ceiling', Math.ceil),
|
|
302
|
+
of('truncate', Math.trunc),
|
|
303
|
+
of('abs', Math.abs),
|
|
304
|
+
against('min', Math.min),
|
|
305
|
+
against('max', Math.max),
|
|
306
|
+
Declarations.createFunctionDeclaration('clamp', 'Number', {
|
|
307
|
+
updates: DERIVED_UPDATES,
|
|
308
|
+
parameters: [Declarations.createValueParameterDeclaration('low', 'Number'), Declarations.createValueParameterDeclaration('high', 'Number')],
|
|
309
|
+
impl: ({ receiver, args: { low, high } }) => {
|
|
310
|
+
if (low > high) {
|
|
311
|
+
throw new MemberFailure('NEXO5039', `'clamp' takes a low no greater than its high, and is given ${low} and ${high}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return unsigned(Math.min(Math.max(receiver, low), high));
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
// The count's numbers from the receiver, each the step after the one before, the step 1 where it is left out; a count that is no nonnegative safe integer is refused.
|
|
318
|
+
Declarations.createFunctionDeclaration('sequence', 'List<Number>', {
|
|
319
|
+
updates: DERIVED_UPDATES,
|
|
320
|
+
parameters: [Declarations.createValueParameterDeclaration('count', 'Number'), Declarations.createValueParameterDeclaration('step', 'Number', { default: 1 })],
|
|
321
|
+
impl: ({ receiver, args, limits }) => {
|
|
322
|
+
// A list holds at most 2^32 - 1 items, so a longer count is refused as one no list can hold.
|
|
323
|
+
if (!Number.isSafeInteger(args.count) || args.count < 0 || args.count > MAX_LIST_LENGTH) {
|
|
324
|
+
throw new MemberFailure('NEXO5035', `'sequence' takes a count that is a whole number from 0 to ${MAX_LIST_LENGTH}, and is given ${args.count}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Vocabulary.#requireWithinLimit('sequence', args.count, limits);
|
|
328
|
+
|
|
329
|
+
const numbers = Array.from({ length: args.count }, (_, index) => unsigned(receiver + index * args.step));
|
|
330
|
+
|
|
331
|
+
if (numbers.some(number => !Number.isFinite(number))) {
|
|
332
|
+
throw new MemberFailure('NEXO5020', `'sequence' reaches a number no finite number holds, from ${receiver} by steps of ${args.step}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return numbers;
|
|
336
|
+
},
|
|
337
|
+
}),
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Refuses a list of the size given where its host limits the lists Nexo's own members make to fewer items, before the list is made (NEXO5054). */
|
|
342
|
+
static #requireWithinLimit(member, size, { items }) {
|
|
343
|
+
if (items !== null && size > items) {
|
|
344
|
+
throw new MemberFailure('NEXO5054', `'${member}' makes a list of ${size} items, and its host limits a list Nexo makes to ${items}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A position a search found, null for the -1 of one it didn't. */
|
|
349
|
+
static #positionOrNull(position) {
|
|
350
|
+
return position === -1 ? null : position;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The item at the offset from the start, null for one the list has no item at. */
|
|
354
|
+
static #itemAt(list, offset) {
|
|
355
|
+
return offset >= 0 && offset < list.length ? list[offset] : null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The items within the range, applied to the list's length once, its start at or before its end and both within the list, or refused. */
|
|
359
|
+
static #slice(list, range) {
|
|
360
|
+
const { start, end } = range.resolveIn(list.length);
|
|
361
|
+
|
|
362
|
+
if (start < 0 || end > list.length || start > end) {
|
|
363
|
+
throw new MemberFailure('NEXO5033', `The range ${range} is applied to a list of ${list.length} items, and gives ${start}..${end}, which ${start > end ? 'starts after it ends' : 'stands beyond the list'}; a slice is strict, as take and drop are not`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return ItemPositions.createAnswer(list, ItemPositions.createSpan(start, end));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The item at each position `pick` is given, in the positions' order, each a whole offset from the start of the list, the first that is none refused. */
|
|
370
|
+
static #pick(list, positions) {
|
|
371
|
+
for (const position of positions) {
|
|
372
|
+
if (!Number.isInteger(position) || position < 0 || position >= list.length) {
|
|
373
|
+
throw new MemberFailure('NEXO5034', `'pick' is given the position ${position}, and a list of ${list.length} items has no item there`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return ItemPositions.createAnswer(list, positions);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The members of lists, each reading and changing as the items it selects and the expression it evaluates do. */
|
|
381
|
+
static #createListMembers() {
|
|
382
|
+
return [
|
|
383
|
+
Declarations.createPropertyDeclaration('count', 'Number', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.length }),
|
|
384
|
+
Declarations.createPropertyDeclaration('first', 'T?', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.at(0) ?? null }),
|
|
385
|
+
Declarations.createPropertyDeclaration('last', 'T?', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.at(-1) ?? null }),
|
|
386
|
+
// The item at a whole index from zero, null for any other index, a negative one included, which never counts from the end.
|
|
387
|
+
Declarations.createIndexerDeclaration(null, 'T?', {
|
|
388
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('index', 'Number')],
|
|
389
|
+
impl: ({ receiver, args }) => (Number.isInteger(args.index) && args.index >= 0 && args.index < receiver.length ? receiver[args.index] : null),
|
|
390
|
+
}),
|
|
391
|
+
// The item at an index from the end, `^1` the last, null for `^0` and for one before the start.
|
|
392
|
+
Declarations.createIndexerDeclaration(null, 'T?', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('index', 'Index')], impl: ({ receiver, args }) => Vocabulary.#itemAt(receiver, args.index.resolveIn(receiver.length)) }),
|
|
393
|
+
// The items within a range, applied to the length once, strictly: a reversed range, or one beyond the list, is refused, never trimmed.
|
|
394
|
+
Declarations.createIndexerDeclaration(null, 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('range', 'Range')], impl: ({ receiver, args }) => Vocabulary.#slice(receiver, args.range) }),
|
|
395
|
+
Declarations.createFunctionDeclaration('max', 'Number?', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('Number?')], impl: ({ receiver, args }) => Vocabulary.#extreme(receiver, args.expression, Math.max) }),
|
|
396
|
+
Declarations.createFunctionDeclaration('min', 'Number?', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('Number?')], impl: ({ receiver, args }) => Vocabulary.#extreme(receiver, args.expression, Math.min) }),
|
|
397
|
+
Declarations.createFunctionDeclaration('sum', 'Number', {
|
|
398
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('Number?')],
|
|
399
|
+
impl: ({ receiver, args }) => {
|
|
400
|
+
const total = Vocabulary.#numbers(receiver, args.expression).reduce((sum, number) => sum + number, 0);
|
|
401
|
+
|
|
402
|
+
// A sum no finite number holds is refused, as an addition's is, so no infinity reaches a result.
|
|
403
|
+
if (!Number.isFinite(total)) {
|
|
404
|
+
throw new MemberFailure('NEXO5020', "The result of 'sum' is no finite number");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return total;
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
Declarations.createFunctionDeclaration('where', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, Vocabulary.#positionsWhere(receiver, item => args.expression(item) === true)) }),
|
|
411
|
+
// Every item's value, null items included, its expression's `it` of the item's type, so `select(it)` keeps the list and `select(it ?? 0)` replaces its nulls.
|
|
412
|
+
Declarations.createFunctionDeclaration('select<U>', 'List<U>', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('U', { nulls: 'accept' })], impl: ({ receiver, args }) => receiver.map(item => args.expression(item)) }),
|
|
413
|
+
// Each item's list, in order, joined; a null item's, whose body answers null, and a null list are refused, never treated as empty.
|
|
414
|
+
Declarations.createFunctionDeclaration('selectMany<U>', 'List<U>', {
|
|
415
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('List<U>')],
|
|
416
|
+
impl: ({ receiver, args }) => receiver.flatMap(item => {
|
|
417
|
+
const items = args.expression(item);
|
|
418
|
+
|
|
419
|
+
if (items === null) {
|
|
420
|
+
throw new MemberFailure('NEXO5031', "'selectMany' joins the list each item gives, and an item gave null, which is no list: give an empty list for an item that has none");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return [items];
|
|
424
|
+
}).flat(),
|
|
425
|
+
}),
|
|
426
|
+
// The receiver's items, then the other's, refused where its host limits a list to fewer.
|
|
427
|
+
Declarations.createFunctionDeclaration('concat', 'List<T>', {
|
|
428
|
+
updates: DERIVED_UPDATES,
|
|
429
|
+
parameters: [Declarations.createValueParameterDeclaration('other', 'List<T>')],
|
|
430
|
+
impl: ({ receiver, args, limits }) => {
|
|
431
|
+
Vocabulary.#requireWithinLimit('concat', receiver.length + args.other.length, limits);
|
|
432
|
+
|
|
433
|
+
return [...receiver, ...args.other];
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
// The items at an index less than the count, or at one the count or past it, whatever the count: a count past the end takes or leaves every item, and one of zero or less none.
|
|
437
|
+
Declarations.createFunctionDeclaration('take', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('count', 'Number')], impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, Vocabulary.#positionsWhere(receiver, (_, position) => position < args.count)) }),
|
|
438
|
+
Declarations.createFunctionDeclaration('drop', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('count', 'Number')], impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, Vocabulary.#positionsWhere(receiver, (_, position) => position >= args.count)) }),
|
|
439
|
+
// The items before the first its predicate is falsy for, which it is evaluated against last, or the items from it on, the predicate evaluated against none after it.
|
|
440
|
+
Declarations.createFunctionDeclaration('takeWhile', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, ItemPositions.createSpan(0, Vocabulary.#firstIndex(receiver, item => args.expression(item) !== true))) }),
|
|
441
|
+
Declarations.createFunctionDeclaration('dropWhile', 'List<T>', {
|
|
442
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()],
|
|
443
|
+
impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, ItemPositions.createSpan(Vocabulary.#firstIndex(receiver, item => args.expression(item) !== true), receiver.length)),
|
|
444
|
+
}),
|
|
445
|
+
// Whether some item's predicate is truthy, stopping at the first that is, false for no items; whether every item's is, stopping at the first that is not, true for no items.
|
|
446
|
+
Declarations.createFunctionDeclaration('any', 'Boolean', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => receiver.some(item => args.expression(item) === true) }),
|
|
447
|
+
Declarations.createFunctionDeclaration('all', 'Boolean', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => receiver.every(item => args.expression(item) === true) }),
|
|
448
|
+
// Whether an item equals the value by the language's key equality, stopping at the first that does; null equals a null item.
|
|
449
|
+
Declarations.createFunctionDeclaration('contains', 'Boolean', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('value', 'T?', { nulls: 'accept' })], impl: ({ receiver, args }) => receiver.indexOf(args.value) !== -1 }),
|
|
450
|
+
// The position of the value's first occurrence, compared as `contains` compares, and of the first item the predicate is truthy for, evaluating it against none after; null where none stands.
|
|
451
|
+
Declarations.createFunctionDeclaration('indexOf', 'Number?', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('value', 'T?', { nulls: 'accept' })], impl: ({ receiver, args }) => Vocabulary.#positionOrNull(receiver.indexOf(args.value)) }),
|
|
452
|
+
Declarations.createFunctionDeclaration('indexWhere', 'Number?', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => Vocabulary.#positionOrNull(receiver.findIndex(item => args.expression(item) === true)) }),
|
|
453
|
+
// The one item, null for none, and refused where the list holds several, which no one item stands for.
|
|
454
|
+
Declarations.createPropertyDeclaration('single', 'T?', {
|
|
455
|
+
updates: DERIVED_UPDATES,
|
|
456
|
+
impl: ({ receiver }) => {
|
|
457
|
+
if (receiver.length > 1) {
|
|
458
|
+
throw new MemberFailure('NEXO5030', `'single' answers the one item of a list, and the list holds ${receiver.length}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return receiver.at(0) ?? null;
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
Declarations.createFunctionDeclaration('average', 'Number?', {
|
|
465
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('Number?')],
|
|
466
|
+
impl: ({ receiver, args }) => {
|
|
467
|
+
const numbers = Vocabulary.#numbers(receiver, args.expression);
|
|
468
|
+
|
|
469
|
+
if (numbers.length === 0) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The mean of finite numbers lies between them, so where their sum overflows, the sum of their shares gives it.
|
|
474
|
+
const total = numbers.reduce((sum, number) => sum + number, 0);
|
|
475
|
+
|
|
476
|
+
return Number.isFinite(total) ? total / numbers.length : numbers.reduce((mean, number) => mean + number / numbers.length, 0);
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
// The first item whose key is the least or the greatest, ordered as `orderBy` orders, items whose key is null skipped, and null where none has one.
|
|
480
|
+
Declarations.createFunctionDeclaration('minBy<K>', 'T?', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')], impl: ({ receiver, args }) => ListOrdering.findExtreme(receiver, args.expression, false, 'minBy') }),
|
|
481
|
+
Declarations.createFunctionDeclaration('maxBy<K>', 'T?', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')], impl: ({ receiver, args }) => ListOrdering.findExtreme(receiver, args.expression, true, 'maxBy') }),
|
|
482
|
+
// The items ordered by the key each gives, stable, a null key last in either direction; and each run of items equal so far refined by another key, the list ordered once and never again.
|
|
483
|
+
...[['orderBy', false], ['orderByDescending', true]].map(([name, isDescending]) => Declarations.createFunctionDeclaration(`${name}<K>`, 'List<T>', {
|
|
484
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')], impl: ({ receiver, args }) => ListOrdering.orderBy(receiver, args.expression, isDescending, name),
|
|
485
|
+
})),
|
|
486
|
+
...[['thenBy', false], ['thenByDescending', true]].map(([name, isDescending]) => Declarations.createFunctionDeclaration(`${name}<K>`, 'List<T>', {
|
|
487
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')], impl: ({ receiver, args }) => ListOrdering.thenBy(receiver, args.expression, isDescending, name),
|
|
488
|
+
})),
|
|
489
|
+
// Each value's first occurrence, in order, values compared by the language's key equality; and each key's first item, its key the expression's value, keys compared alike.
|
|
490
|
+
Declarations.createFunctionDeclaration('distinct', 'List<T>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => ItemPositions.createAnswer(receiver, Vocabulary.#firstPositions(receiver, item => item)) }),
|
|
491
|
+
Declarations.createFunctionDeclaration('distinctBy<K>', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')], impl: ({ receiver, args }) => ItemPositions.createAnswer(receiver, Vocabulary.#firstPositions(receiver, item => args.expression(item))) }),
|
|
492
|
+
Declarations.createFunctionDeclaration('reverse', 'List<T>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => ItemPositions.createAnswer(receiver, ItemPositions.createSpan(0, receiver.length).reverse()) }),
|
|
493
|
+
// A dictionary of the items by the key each gives, in the items' order, two items of one key refused, which grouping collects instead.
|
|
494
|
+
Declarations.createFunctionDeclaration('toDictionary<K>', 'Dictionary<K, T>', {
|
|
495
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')],
|
|
496
|
+
impl: ({ receiver, args }) => {
|
|
497
|
+
const entries = new Map();
|
|
498
|
+
|
|
499
|
+
for (const item of receiver) {
|
|
500
|
+
const key = args.expression(item);
|
|
501
|
+
|
|
502
|
+
if (entries.has(key)) {
|
|
503
|
+
throw new MemberFailure('NEXO5029', `'toDictionary' keys each item once, and two items give the key ${Vocabulary.#describeKey(key)}: group them with groupBy`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
entries.set(key, item);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return new Dictionary(entries);
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
// A dictionary of the items by the key each gives, each key's items in their order, the keys in the order their first items stand.
|
|
513
|
+
Declarations.createFunctionDeclaration('groupBy<K>', 'Dictionary<K, List<T>>', {
|
|
514
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('K')],
|
|
515
|
+
impl: ({ receiver, args }) => new Dictionary([...Vocabulary.#groupsOf(receiver, args.expression)].map(([key, group]) => [key, Object.freeze(group)])),
|
|
516
|
+
}),
|
|
517
|
+
// The item at a whole index from zero as a pair of its index and itself, null for any other index, as the list's unnamed indexer reads it.
|
|
518
|
+
Declarations.createIndexerDeclaration('entry', 'Pair<Number, T>?', {
|
|
519
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('index', 'Number')],
|
|
520
|
+
impl: ({ receiver, args }) => (Number.isInteger(args.index) && args.index >= 0 && args.index < receiver.length ? new Pair(args.index === 0 ? 0 : args.index, receiver[args.index]) : null),
|
|
521
|
+
}),
|
|
522
|
+
// The item at an index from the end as a pair of its index from the start and itself, null where the unnamed indexer answers null.
|
|
523
|
+
Declarations.createIndexerDeclaration('entry', 'Pair<Number, T>?', {
|
|
524
|
+
updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('index', 'Index')],
|
|
525
|
+
impl: ({ receiver, args }) => {
|
|
526
|
+
const offset = args.index.resolveIn(receiver.length);
|
|
527
|
+
|
|
528
|
+
return offset >= 0 && offset < receiver.length ? new Pair(offset, receiver[offset]) : null;
|
|
529
|
+
},
|
|
530
|
+
}),
|
|
531
|
+
// The item at each position, in the positions' order, one given twice giving its item twice; a position no item stands at is refused.
|
|
532
|
+
Declarations.createFunctionDeclaration('pick', 'List<T>', { updates: DERIVED_UPDATES, parameters: [Declarations.createValueParameterDeclaration('positions', 'List<Number>')], impl: ({ receiver, args }) => Vocabulary.#pick(receiver, args.positions) }),
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The members of dictionaries, each reading and changing as the entries it reads do. A key a lookup is given may be null, which reads the null key where the dictionary's keys admit one, and an absent key otherwise,
|
|
538
|
+
* so a key's parameter consumes null rather than propagating it; a lookup answers null for an absent key, `containsKey` and `entry` telling it from a key whose value is null.
|
|
539
|
+
*/
|
|
540
|
+
static #createDictionaryMembers() {
|
|
541
|
+
const key = () => Declarations.createValueParameterDeclaration('key', 'K?', { nulls: 'accept' });
|
|
542
|
+
|
|
543
|
+
return [
|
|
544
|
+
Declarations.createPropertyDeclaration('count', 'Number', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.count }),
|
|
545
|
+
Declarations.createIndexerDeclaration(null, 'V?', { updates: DERIVED_UPDATES, parameters: [key()], impl: ({ receiver, args }) => receiver.get(args.key) }),
|
|
546
|
+
Declarations.createIndexerDeclaration('entry', 'Pair<K, V>?', { updates: DERIVED_UPDATES, parameters: [key()], impl: ({ receiver, args }) => receiver.getEntry(args.key) }),
|
|
547
|
+
Declarations.createFunctionDeclaration('containsKey', 'Boolean', { updates: DERIVED_UPDATES, parameters: [key()], impl: ({ receiver, args }) => receiver.has(args.key) }),
|
|
548
|
+
Declarations.createPropertyDeclaration('keys', 'List<K>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.keys }),
|
|
549
|
+
Declarations.createPropertyDeclaration('values', 'List<V>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.values }),
|
|
550
|
+
Declarations.createPropertyDeclaration('entries', 'List<Pair<K, V>>', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.entries }),
|
|
551
|
+
// The entries whose value's predicate is truthy, and each entry's value replaced by its expression's, every value null included, each keeping its key and its place; an operation needing the key reads the entries.
|
|
552
|
+
Declarations.createFunctionDeclaration('where', 'Dictionary<K, V>', { updates: DERIVED_UPDATES, parameters: [Declarations.createPredicateParameterDeclaration()], impl: ({ receiver, args }) => new Dictionary(receiver.entries.filter(entry => args.expression(entry.value) === true).map(entry => [entry.key, entry.value])) }),
|
|
553
|
+
Declarations.createFunctionDeclaration('select<U>', 'Dictionary<K, U>', { updates: DERIVED_UPDATES, parameters: [Declarations.createExpressionParameterDeclaration('U', { nulls: 'accept' })], impl: ({ receiver, args }) => new Dictionary(receiver.entries.map(entry => [entry.key, args.expression(entry.value)])) }),
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The members of pairs: its key and its value, each reading as the pair does. */
|
|
558
|
+
static #createPairMembers() {
|
|
559
|
+
return [
|
|
560
|
+
Declarations.createPropertyDeclaration('key', 'K', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.key }),
|
|
561
|
+
Declarations.createPropertyDeclaration('value', 'V', { updates: DERIVED_UPDATES, impl: ({ receiver }) => receiver.value }),
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The index of the first item the test holds for, the list's length where it holds for none, testing no item after it. */
|
|
566
|
+
static #firstIndex(items, test) {
|
|
567
|
+
const index = items.findIndex(test);
|
|
568
|
+
|
|
569
|
+
return index === -1 ? items.length : index;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** The position of each item the test holds for, in order, the test given each item and its position once. */
|
|
573
|
+
static #positionsWhere(items, test) {
|
|
574
|
+
const positions = [];
|
|
575
|
+
|
|
576
|
+
for (let position = 0; position < items.length; position++) {
|
|
577
|
+
if (test(items[position], position)) {
|
|
578
|
+
positions.push(position);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return positions;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** The position of each key's first item, in the items' order, the key the expression gives each item, evaluated once for each, keys compared by the language's key equality. */
|
|
586
|
+
static #firstPositions(items, keyOf) {
|
|
587
|
+
const firsts = new Map();
|
|
588
|
+
|
|
589
|
+
items.forEach((item, position) => {
|
|
590
|
+
const key = keyOf(item);
|
|
591
|
+
|
|
592
|
+
if (!firsts.has(key)) {
|
|
593
|
+
firsts.set(key, position);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return [...firsts.values()];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** The items grouped by the key the expression gives each, keys compared by the language's key equality, each group in the items' order, the groups in the order their first items stand. */
|
|
601
|
+
static #groupsOf(items, expression) {
|
|
602
|
+
const groups = new Map();
|
|
603
|
+
|
|
604
|
+
for (const item of items) {
|
|
605
|
+
const key = expression(item);
|
|
606
|
+
|
|
607
|
+
groups.has(key) ? groups.get(key).push(item) : groups.set(key, [item]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return groups;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** A key as a message writes it: a string quoted, and any other value as its text. */
|
|
614
|
+
static #describeKey(key) {
|
|
615
|
+
return typeof key === 'string' ? JSON.stringify(key) : String(key);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** The greatest or the least of the numbers an expression produces for the items, or null where it produces none. */
|
|
619
|
+
static #extreme(items, expression, choose) {
|
|
620
|
+
const numbers = Vocabulary.#numbers(items, expression);
|
|
621
|
+
|
|
622
|
+
// A member answers 0 where it would answer -0.
|
|
623
|
+
return numbers.length === 0 ? null : numbers.reduce((current, number) => choose(current, number)) || 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** The numbers an expression produces for the items, leaving out the items it produces null for. */
|
|
627
|
+
static #numbers(items, expression) {
|
|
628
|
+
const numbers = [];
|
|
629
|
+
|
|
630
|
+
for (let index = 0; index < items.length; index++) {
|
|
631
|
+
const number = expression(items[index]);
|
|
632
|
+
|
|
633
|
+
if (number !== null) {
|
|
634
|
+
numbers.push(number);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return numbers;
|
|
639
|
+
}
|
|
640
|
+
}
|