@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,1265 @@
1
+ /**
2
+ * DefinitionValidator — a definition checked against Nexo's structure, as its canonical, frozen copy
3
+ */
4
+
5
+ import { ArrayUtils } from '../common/ArrayUtils.mjs';
6
+ import { ObjectUtils } from '../common/ObjectUtils.mjs';
7
+ import { DefinitionLocations } from './DefinitionLocations.mjs';
8
+ import { Imports } from './Imports.mjs';
9
+ import { Limits } from './Limits.mjs';
10
+ import { NexoErrors } from './NexoError.mjs';
11
+ import { Names } from './Names.mjs';
12
+ import { Versions } from './Versions.mjs';
13
+
14
+ /** @typedef {import('./TextLocations.mjs').TextLocations} TextLocations */
15
+
16
+ /** The properties each kind of node holds, and those it requires. */
17
+ const NODE_PROPERTIES = {
18
+ literal: { properties: ['kind', 'value'], required: ['value'] },
19
+ scriptParameter: { properties: ['kind', 'name'], required: ['name'] },
20
+ it: { properties: ['kind'], required: [] },
21
+ root: { properties: ['kind'], required: [] },
22
+ namespace: { properties: ['kind', 'name'], required: ['name'] },
23
+ local: { properties: ['kind', 'name'], required: ['name'] },
24
+ memberAccess: { properties: ['kind', 'receiver', 'name', 'optional', 'extension', 'continues'], required: ['receiver', 'name'] },
25
+ call: { properties: ['kind', 'callee', 'arguments'], required: ['callee', 'arguments'] },
26
+ indexerAccess: { properties: ['kind', 'receiver', 'optional', 'argument'], required: ['receiver', 'argument'] },
27
+ object: { properties: ['kind', 'context', 'locals', 'objectFields'], required: ['objectFields'] },
28
+ list: { properties: ['kind', 'items'], required: ['items'] },
29
+ dictionary: { properties: ['kind', 'entries'], required: ['entries'] },
30
+ optionalChain: { properties: ['kind', 'chain'], required: ['chain'] },
31
+ equality: { properties: ['kind', 'left', 'right'], required: ['left', 'right'] },
32
+ comparison: { properties: ['kind', 'operands', 'operators'], required: ['operands', 'operators'] },
33
+ interpolatedString: { properties: ['kind', 'parts'], required: ['parts'] },
34
+ conditional: { properties: ['kind', 'condition', 'binding', 'consequent', 'alternative'], required: ['consequent', 'alternative'] },
35
+ body: { properties: ['kind', 'locals', 'result'], required: ['locals', 'result'] },
36
+ do: { properties: ['kind', 'body'], required: ['body'] },
37
+ function: { properties: ['kind', 'parameters', 'body', 'arrow'], required: ['parameters', 'body'] },
38
+ arithmetic: { properties: ['kind', 'operator', 'left', 'right'], required: ['operator', 'left', 'right'] },
39
+ arithmeticNegation: { properties: ['kind', 'operand'], required: ['operand'] },
40
+ fromEndIndex: { properties: ['kind', 'offset'], required: ['offset'] },
41
+ range: { properties: ['kind', 'start', 'end'], required: [] },
42
+ coalescing: { properties: ['kind', 'value', 'fallback'], required: ['value', 'fallback'] },
43
+ not: { properties: ['kind', 'operand'], required: ['operand'] },
44
+ cast: { properties: ['kind', 'type', 'operand'], required: ['type', 'operand'] },
45
+ conversion: { properties: ['kind', 'type', 'operand'], required: ['type', 'operand'] },
46
+ try: { properties: ['kind', 'operand'], required: ['operand'] },
47
+ and: { properties: ['kind', 'operands'], required: ['operands'] },
48
+ or: { properties: ['kind', 'operands'], required: ['operands'] },
49
+ };
50
+
51
+ /** The kinds of node that continue a chain through their receiver, a call's member or an object expression's context, and so carry its optional chain to the access that is. */
52
+ const CHAIN_KINDS = new Set(['memberAccess', 'call', 'indexerAccess', 'object']);
53
+
54
+ /**
55
+ * The kinds of node a cast's operand starts from, its receiver, as text writes one: an input, a namespace's member, a call of the function a name holds and an object expression following no value among them, or a grouped
56
+ * cast.
57
+ */
58
+ const CAST_RECEIVER_KINDS = new Set(['it', 'scriptParameter', 'root', 'namespace', 'local', 'literal', 'interpolatedString', 'call', 'object', 'list', 'dictionary', 'cast']);
59
+
60
+ /** The keys a dictionary's entry holds, both written: its key, the string its key literal writes, and its value. */
61
+ const DICTIONARY_ENTRY_PROPERTIES = { properties: ['key', 'value'], required: ['key', 'value'] };
62
+
63
+ /** A surrogate standing alone, which a Unicode pattern reads as one, a pair being one character outside the Basic Multilingual Plane. */
64
+ const LONE_SURROGATE = /\p{Cs}/u;
65
+
66
+ /** The operators an arithmetic node holds. */
67
+ const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%'];
68
+
69
+ /** The direction each comparison operator points, which every operator of a chain shares. */
70
+ /** The properties each kind of part of an interpolated string has. */
71
+ const INTERPOLATION_PARTS = Object.freeze({
72
+ text: { properties: ['kind', 'value'], required: ['value'] },
73
+ expression: { properties: ['kind', 'expression'], required: ['expression'] },
74
+ });
75
+
76
+ const COMPARISON_DIRECTIONS = Object.freeze({ '<': 'ascending', '<=': 'ascending', '>': 'descending', '>=': 'descending' });
77
+
78
+ /** The kinds of node `try` attempts: the operations with a rejection of their own, which an attempt answers null for. */
79
+ const ATTEMPTED_KINDS = new Set(['conversion', 'cast']);
80
+
81
+ /** The keys a local, a conditional binding and an object field each hold: a name, always written, and a value. */
82
+ const ENTRY_PROPERTIES = { properties: ['name', 'value'], required: ['name', 'value'] };
83
+
84
+ /** Where a node's implicit parameter stands: absent at the top level, present in an object expression following a value, unknown in an argument. */
85
+ const ContextAvailability = Object.freeze({ absent: 'absent', present: 'present', unknown: 'unknown' });
86
+
87
+ export class DefinitionValidator {
88
+ /** Where in the definition its caller supplied each part of a canonical copy came from, for each copy whose validation moved a part. @type {WeakMap<object, DefinitionLocations>} */
89
+ static #suppliedLocations = new WeakMap();
90
+
91
+ /** Every canonical definition validation gave, which validating again answers as it is. @type {WeakSet<object>} */
92
+ static #canonicalDefinitions = new WeakSet();
93
+
94
+ /** The parameter nodes of canonical definitions that a text wrote alone, a bare name no local in scope has, rather than with `~`. @type {WeakSet<object>} */
95
+ static #scriptParametersWrittenAlone = new WeakSet();
96
+
97
+ /**
98
+ * The parameter nodes of canonical definitions written with `~`, or given in a definition, whose name a local, an object field, a conditional binding or a function parameter in scope where they stand hides,
99
+ * or a local or a conditional binding of their scope declares later, which canonical text therefore writes with `~`. @type {WeakSet<object>}
100
+ */
101
+ static #hiddenScriptParameters = new WeakSet();
102
+
103
+ #locations;
104
+
105
+ /** The index each part of an interpolated string had in the definition being validated, by the canonical node, for each node whose parts validation moved. @type {WeakMap<object, number[]>} */
106
+ #partIndexes = new WeakMap();
107
+
108
+ /** Whether validation moved a part of the definition, which its locations then say. */
109
+ #hasMovedParts = false;
110
+
111
+ /** The local names the definition's imports give modules, which a namespace and an extension selection's module name. @type {ReadonlySet<string>} */
112
+ #namespaceNames = Names.noNamespaceNames;
113
+
114
+ /** Whether an object expression's locals or fields hold the node being validated, lexically, so an outermost object is being built where it stands. */
115
+ #isInObjectExpression = false;
116
+
117
+ /** The names of the locals and the fields in scope where the node being validated stands. @type {Set<string>} */
118
+ #localNames = new Set();
119
+
120
+ /** The names of the locals declared later in a scope the node being validated stands in, the local whose value it is among them, which no parameter there may name, each with whether a local or a conditional binding declares it. @type {Map<string, 'local' | 'conditionalBinding'>} */
121
+ #laterNames = new Map();
122
+
123
+ /** The optional chain the node being validated continues, with whether an access of it is optional so far; null where it continues none. @type {{ hasOptional: boolean } | null} */
124
+ #optionalChain = null;
125
+
126
+ /** The nodes holding the node being validated, outermost first, so a definition nesting past `Limits.nesting` is refused, and a node holding itself is refused as no tree. @type {Set<object>} */
127
+ #enclosingNodes = new Set();
128
+
129
+ /**
130
+ * The simple expression the node being validated stands in, an argument, an indexer's key, a computed property access's name or an interpolation's expression, with the subject its messages open with and whether a conditional holds the node;
131
+ * null outside one. A simple expression nested in another keeps the conditional that holds it.
132
+ * @type {{ subject: string, isInConditional: boolean } | null}
133
+ */
134
+ #simpleExpression = null;
135
+
136
+ /** The parameters of a definition parsed from text that the text wrote alone, as the lowering gave them; null for a definition not parsed from text. @type {WeakSet<object> | null} */
137
+ #parametersWrittenAloneInText;
138
+
139
+ /**
140
+ * @param {TextLocations | null} locations Where in a text each part of the definition came from, for one parsed from text.
141
+ * @param {WeakSet<object> | null} scriptParametersWrittenAlone The parameters a text wrote alone, for one parsed from text.
142
+ */
143
+ constructor(locations = null, scriptParametersWrittenAlone = null) {
144
+ this.#locations = locations;
145
+ this.#parametersWrittenAloneInText = scriptParametersWrittenAlone;
146
+ }
147
+
148
+ /** Whether a canonical definition's parameter node is a name a text wrote alone, which reads a local or field of its name where one is in scope, rather than a script parameter written with `~` or a definition's. */
149
+ static isWrittenAlone(node) {
150
+ return DefinitionValidator.#scriptParametersWrittenAlone.has(node);
151
+ }
152
+
153
+ /**
154
+ * Whether a canonical definition's parameter node is a script parameter that a local, an object field, a conditional binding or a function parameter in scope where it stands hides, or a local or a conditional binding
155
+ * of its scope declares later, so that canonical text writes it with `~`; a parameter written alone is never one, since it reads no name a local in scope has.
156
+ */
157
+ static isHidden(node) {
158
+ return DefinitionValidator.#hiddenScriptParameters.has(node);
159
+ }
160
+
161
+ /**
162
+ * A deep copy of a canonical definition's node, its own keys alone, each parameter node a text wrote alone, and each one hidden where it stands, marked as one in the copy too. A node's keys are the definition
163
+ * format's own, so each is assigned as it is; only a parameter node is ever marked, so no other is looked up.
164
+ */
165
+ static copyOf(node) {
166
+ if (Array.isArray(node)) {
167
+ const copy = new Array(node.length);
168
+
169
+ for (let index = 0; index < node.length; index++) {
170
+ copy[index] = DefinitionValidator.copyOf(node[index]);
171
+ }
172
+
173
+ return copy;
174
+ }
175
+
176
+ if (node === null || typeof node !== 'object') {
177
+ return node;
178
+ }
179
+
180
+ const copy = {};
181
+
182
+ for (const key of Object.keys(node)) {
183
+ copy[key] = DefinitionValidator.copyOf(node[key]);
184
+ }
185
+
186
+ if (node.kind === 'scriptParameter') {
187
+ if (DefinitionValidator.#scriptParametersWrittenAlone.has(node)) {
188
+ DefinitionValidator.#scriptParametersWrittenAlone.add(copy);
189
+ }
190
+
191
+ if (DefinitionValidator.#hiddenScriptParameters.has(node)) {
192
+ DefinitionValidator.#hiddenScriptParameters.add(copy);
193
+ }
194
+ }
195
+
196
+ return copy;
197
+ }
198
+
199
+ /**
200
+ * Validates the definition's structure and answers its canonical, frozen copy, recording it as canonical, so validating it again answers it as it is, keeping the locations it has. For a definition its caller supplied,
201
+ * where validation moved a part of an interpolated string, it records the index each moved part had, so a diagnostic names each part as the caller did. It refuses a definition of another version (NEXO2007) before
202
+ * anything else, its imports where they break their rules (NEXO2008), and any other structure the definition format does not allow.
203
+ */
204
+ validate(definition) {
205
+ if (DefinitionValidator.#canonicalDefinitions.has(definition)) {
206
+ return definition;
207
+ }
208
+
209
+ // One version is read, and none is converted into another, so a definition of another version names both, before anything else its version's contract may hold.
210
+ if (definition !== null && typeof definition === 'object' && Object.hasOwn(definition, 'version') && definition.version !== Versions.definition) {
211
+ this.#fail('NEXO2007', `A definition is of version ${Versions.definition}, the one this implementation reads, and this one is of version ${JSON.stringify(definition.version)}`, '/version');
212
+ }
213
+
214
+ this.#requireProperties(definition, '', { properties: ['version', 'imports', 'expression'], required: ['version', 'imports', 'expression'] });
215
+
216
+ const imports = this.#validateImports(definition.imports);
217
+
218
+ this.#namespaceNames = Imports.namespaceNamesOf(imports);
219
+
220
+ const validated = Object.freeze({ version: Versions.definition, imports, expression: this.#validateBodyOrExpression(definition.expression, '/expression', ContextAvailability.absent) });
221
+
222
+ // A definition parsed from text has its text's locations; one a caller supplied, where a part moved, has locations that name its parts as the caller did.
223
+ if (this.#locations === null && this.#hasMovedParts) {
224
+ DefinitionValidator.#suppliedLocations.set(validated, new DefinitionLocations(validated, this.#partIndexes));
225
+ }
226
+
227
+ DefinitionValidator.#canonicalDefinitions.add(validated);
228
+
229
+ return validated;
230
+ }
231
+
232
+ /**
233
+ * Validates a definition's imports: a list, each import's shape as its rules say, and every import once, in order of the local name it gives, so two giving one local name to different declarations conflict; each
234
+ * refusal is NEXO2008, located at the import.
235
+ */
236
+ #validateImports(imports) {
237
+ this.#requireArray(imports, '/imports');
238
+
239
+ return Object.freeze(imports.map((entry, index) => {
240
+ const pointer = `/imports/${index}`;
241
+
242
+ Imports.validateImport(entry, message => this.#fail('NEXO2008', message, pointer));
243
+
244
+ const previous = imports[index - 1];
245
+
246
+ if (previous !== undefined && Imports.localNameOf(previous) === Imports.localNameOf(entry) && !Imports.isSameImport(previous, entry)) {
247
+ this.#fail('NEXO2008', Imports.describeConflict([previous, entry]), pointer);
248
+ }
249
+
250
+ if (previous !== undefined && !(Imports.localNameOf(previous) < Imports.localNameOf(entry))) {
251
+ this.#fail('NEXO2008', "A definition's imports are each once, in order of the local name each gives", pointer);
252
+ }
253
+
254
+ return Object.freeze({ module: entry.module, ...(entry.type === undefined ? {} : { type: entry.type }), ...(entry.alias === undefined ? {} : { alias: entry.alias }) });
255
+ }));
256
+ }
257
+
258
+ /** Where in the definition its caller supplied each part of the canonical copy came from, or null where validation moved none, so each part's pointer is the caller's. */
259
+ static locationsOf(definition) {
260
+ return DefinitionValidator.#suppliedLocations.get(definition) ?? null;
261
+ }
262
+
263
+ #validateNode(node, pointer, contextAvailability) {
264
+ // A definition is a tree, so no node holds itself, and it nests as deep as a text may.
265
+ if (this.#enclosingNodes.has(node)) {
266
+ this.#fail('NEXO2000', 'A definition is a tree, and this node holds itself', pointer);
267
+ }
268
+
269
+ if (this.#enclosingNodes.size >= Limits.nesting) {
270
+ this.#fail('NEXO2046', `A definition nests at most ${Limits.nesting} levels, and this node stands deeper`, pointer);
271
+ }
272
+
273
+ const isNode = ObjectUtils.isNonArrayObject(node);
274
+
275
+ if (isNode) {
276
+ this.#enclosingNodes.add(node);
277
+ }
278
+
279
+ try {
280
+ return this.#validateNodeOfKind(node, pointer, contextAvailability);
281
+ } finally {
282
+ if (isNode) {
283
+ this.#enclosingNodes.delete(node);
284
+ }
285
+ }
286
+ }
287
+
288
+ /** Validates a node by its kind, within the nesting `#validateNode` holds it to. */
289
+ #validateNodeOfKind(node, pointer, contextAvailability) {
290
+ // A member that takes an argument is written as a call or an indexer, so an infix call is no node, and the message says which nodes take its place.
291
+ if (ObjectUtils.isNonArrayObject(node) && node.kind === 'infixCall') {
292
+ this.#fail('NEXO2000', 'An infixCall is not a node: a function is a call node, or an indexer node for a named or unnamed indexer', pointer);
293
+ }
294
+
295
+ if (!ObjectUtils.isNonArrayObject(node) || !Object.hasOwn(NODE_PROPERTIES, node.kind)) {
296
+ this.#fail('NEXO2000', 'A node is an object whose kind is literal, scriptParameter, it, root, namespace, local, memberAccess, call, indexerAccess, object, list, dictionary, optionalChain, equality, comparison, interpolatedString, body, do, function, arithmetic, arithmeticNegation, fromEndIndex, range, conditional, coalescing, not, cast, conversion, try, and or or', pointer);
297
+ }
298
+
299
+ this.#requireProperties(node, pointer, NODE_PROPERTIES[node.kind]);
300
+
301
+ const enclosingOptionalChain = this.#optionalChain;
302
+
303
+ // A chain continues through a property access's and an indexer's receiver, a call's member and an object expression's context alone, so any other node ends it.
304
+ if (!CHAIN_KINDS.has(node.kind)) {
305
+ this.#optionalChain = null;
306
+ }
307
+
308
+ try {
309
+ return this.#validateKind(node, pointer, contextAvailability);
310
+ } finally {
311
+ this.#optionalChain = enclosingOptionalChain;
312
+ }
313
+ }
314
+
315
+ #validateKind(node, pointer, contextAvailability) {
316
+ switch (node.kind) {
317
+ case 'literal':
318
+ return this.#validateLiteral(node, pointer);
319
+ case 'scriptParameter': {
320
+ if (!Names.isIdentifier(node.name)) {
321
+ this.#fail('NEXO2000', 'A script parameter is named by a letter followed by letters and digits', `${pointer}/name`);
322
+ }
323
+
324
+ const parameter = Object.freeze({ kind: 'scriptParameter', name: node.name });
325
+
326
+ // A name a text wrote alone reads the local in scope of its name, so one before a later local of its name reads nothing; a script parameter written with `~` and a definition's parameter read the script parameter wherever they stand.
327
+ if (this.#parametersWrittenAloneInText?.has(node) === true) {
328
+ this.#requireScriptParameterName(node.name, pointer);
329
+ DefinitionValidator.#scriptParametersWrittenAlone.add(parameter);
330
+
331
+ return parameter;
332
+ }
333
+
334
+ // A local, an object field, a conditional binding or a function parameter of its name in scope hides it, and so does a local or a conditional binding declared later in its scope, which a bare name there would read.
335
+ if (this.#localNames.has(node.name) || this.#laterNames.has(node.name)) {
336
+ DefinitionValidator.#hiddenScriptParameters.add(parameter);
337
+ }
338
+
339
+ return parameter;
340
+ }
341
+ case 'root':
342
+ // The object root is a reference to the outermost object, which an object expression around the reference builds, lexically.
343
+ if (!this.#isInObjectExpression) {
344
+ this.#fail('NEXO2018', "'root' refers to the outermost object being built, and no object expression encloses it here", pointer);
345
+ }
346
+
347
+ return Object.freeze({ kind: 'root' });
348
+ case 'it':
349
+ if (contextAvailability === ContextAvailability.absent) {
350
+ this.#fail('NEXO2026', "'it' is the implicit parameter, and the top level has none; a chain there starts from a script parameter, root, a namespace's member, a local, a literal or another input", pointer);
351
+ }
352
+
353
+ return Object.freeze({ kind: 'it' });
354
+ case 'namespace':
355
+ this.#fail('NEXO2520', DefinitionValidator.#namespaceAloneMessageOf(node.name), pointer);
356
+ break;
357
+ case 'local':
358
+ this.#requireName(node.name, `${pointer}/name`);
359
+
360
+ // A local is read where one of its name, or a field, is in scope, which `@` and a reserved word name as a name written alone does any other.
361
+ if (!this.#localNames.has(node.name)) {
362
+ this.#fail('NEXO2012', `No local, object field, conditional binding or function parameter named '${node.name}' is in scope. Use '~${node.name}' to read the script parameter.`, pointer);
363
+ }
364
+
365
+ return Object.freeze({ kind: 'local', name: node.name });
366
+ case 'memberAccess':
367
+ case 'indexerAccess':
368
+ return this.#validateMemberAccess(node, pointer, contextAvailability);
369
+ case 'call':
370
+ return this.#validateCall(node, pointer, contextAvailability);
371
+ case 'equality':
372
+ return Object.freeze({
373
+ kind: 'equality',
374
+ left: this.#validateNode(node.left, `${pointer}/left`, contextAvailability),
375
+ right: this.#validateNode(node.right, `${pointer}/right`, contextAvailability),
376
+ });
377
+ case 'comparison':
378
+ return this.#validateComparison(node, pointer, contextAvailability);
379
+ case 'interpolatedString':
380
+ return this.#validateInterpolatedString(node, pointer, contextAvailability);
381
+ case 'list':
382
+ return this.#validateList(node, pointer, contextAvailability);
383
+ case 'dictionary':
384
+ return this.#validateDictionary(node, pointer, contextAvailability);
385
+ case 'arithmetic':
386
+ if (!ARITHMETIC_OPERATORS.includes(node.operator)) {
387
+ this.#fail('NEXO2000', `An arithmetic operator is ${ARITHMETIC_OPERATORS.join(', ')}`, `${pointer}/operator`);
388
+ }
389
+
390
+ return Object.freeze({
391
+ kind: 'arithmetic',
392
+ operator: node.operator,
393
+ left: this.#validateNode(node.left, `${pointer}/left`, contextAvailability),
394
+ right: this.#validateNode(node.right, `${pointer}/right`, contextAvailability),
395
+ });
396
+ case 'arithmeticNegation': {
397
+ const operand = this.#validateNode(node.operand, `${pointer}/operand`, contextAvailability);
398
+
399
+ // A numeric literal negated is the negative literal, as text reads one, so a negation has one definition whatever wrote it.
400
+ if (operand.kind === 'literal' && typeof operand.value === 'number') {
401
+ return Object.freeze({ kind: 'literal', value: -operand.value });
402
+ }
403
+
404
+ return Object.freeze({ kind: 'arithmeticNegation', operand });
405
+ }
406
+ case 'fromEndIndex':
407
+ return Object.freeze({ kind: 'fromEndIndex', offset: this.#validateNode(node.offset, `${pointer}/offset`, contextAvailability) });
408
+ case 'range': {
409
+ // A boundary the range leaves out is no property of its node.
410
+ const range = { kind: 'range' };
411
+
412
+ for (const boundary of ['start', 'end']) {
413
+ if (node[boundary] !== undefined) {
414
+ range[boundary] = this.#validateNode(node[boundary], `${pointer}/${boundary}`, contextAvailability);
415
+ }
416
+ }
417
+
418
+ return Object.freeze(range);
419
+ }
420
+ case 'conditional':
421
+ return this.#validateConditional(node, pointer, contextAvailability);
422
+ case 'body':
423
+ this.#fail('NEXO2017', "A body is the root expression's, a do block's, a function's or a conditional branch's, and stands nowhere else", pointer);
424
+ break;
425
+ case 'do':
426
+ if (this.#simpleExpression !== null) {
427
+ this.#fail('NEXO2011', `${this.#simpleExpression.subject} is a simple expression, which holds no do block: ${this.#simpleExpression.advice ?? "bind the block's value to a local first, and use the local"}`, pointer);
428
+ }
429
+
430
+ return Object.freeze({ kind: 'do', body: this.#validateBodyOrExpression(node.body, `${pointer}/body`, contextAvailability) });
431
+ case 'function':
432
+ this.#fail(node.arrow === true ? 'NEXO2027' : 'NEXO2004', node.arrow === true ? 'An arrow function is the value of a local or an argument, and stands nowhere else' : 'A function is the value of a local, as let name = function(…) … end, and stands nowhere else', pointer);
433
+ break;
434
+ case 'and':
435
+ case 'or':
436
+ return this.#validateLogical(node, pointer, contextAvailability);
437
+ case 'optionalChain':
438
+ return this.#validateOptionalChain(node, pointer, contextAvailability);
439
+ case 'not':
440
+ return Object.freeze({ kind: 'not', operand: this.#validateNode(node.operand, `${pointer}/operand`, contextAvailability) });
441
+ case 'cast':
442
+ return this.#validateCast(node, pointer, contextAvailability);
443
+ case 'conversion':
444
+ if (!DefinitionValidator.#isTypeName(node.type)) {
445
+ this.#fail('NEXO2000', "A conversion names the type it converts to by its name, a letter followed by letters and digits, or by its module's name, a dot and its name", `${pointer}/type`);
446
+ }
447
+
448
+ return Object.freeze({ kind: 'conversion', type: node.type, operand: this.#validateNode(node.operand, `${pointer}/operand`, contextAvailability) });
449
+ case 'try':
450
+ if (!ATTEMPTED_KINDS.has(node.operand?.kind)) {
451
+ this.#fail('NEXO2523', "What 'try' attempts is a conversion or a cast", `${pointer}/operand`);
452
+ }
453
+
454
+ return Object.freeze({ kind: 'try', operand: this.#validateNode(node.operand, `${pointer}/operand`, contextAvailability) });
455
+ case 'coalescing':
456
+ return Object.freeze({
457
+ kind: 'coalescing',
458
+ value: this.#validateNode(node.value, `${pointer}/value`, contextAvailability),
459
+ fallback: this.#validateNode(node.fallback, `${pointer}/fallback`, contextAvailability),
460
+ });
461
+ default:
462
+ return this.#validateObjectExpression(node, pointer, contextAvailability);
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Validates a conditional: a condition or a conditional binding, exactly one of them, its consequent and its alternative.
468
+ * A conditional binding's name is any name, which hides a local or a field of its name outside it, in scope in the consequent alone, never in its own value or the alternative.
469
+ */
470
+ #validateConditional(node, pointer, contextAvailability) {
471
+ if ((node.condition === undefined) === (node.binding === undefined)) {
472
+ this.#fail('NEXO2000', 'A conditional holds a condition or a conditional binding, one of them', pointer);
473
+ }
474
+
475
+ if (this.#simpleExpression !== null) {
476
+ return this.#validateSimpleConditional(node, pointer, contextAvailability);
477
+ }
478
+
479
+ // Every branch of one conditional is a body, whose `return` gives its result, or none is; an `elseif` continues its style, and the alternative of one whose branches are expressions may hold a conditional of either, as `else if … end` writes one.
480
+ const isExplicit = node.consequent?.kind === 'body';
481
+ const alternative = node.alternative;
482
+ const isConsistent = alternative?.kind === 'conditional' ? !isExplicit || alternative.consequent?.kind === 'body' : (alternative?.kind === 'body') === isExplicit;
483
+
484
+ if (!isConsistent) {
485
+ this.#fail('NEXO2524', 'All branches of this conditional must give their results with return, or none of them', pointer);
486
+ }
487
+
488
+ if (node.condition !== undefined) {
489
+ return Object.freeze({
490
+ kind: 'conditional',
491
+ condition: this.#validateNode(node.condition, `${pointer}/condition`, contextAvailability),
492
+ consequent: this.#validateBodyOrExpression(node.consequent, `${pointer}/consequent`, contextAvailability),
493
+ alternative: this.#validateBodyOrExpression(node.alternative, `${pointer}/alternative`, contextAvailability),
494
+ });
495
+ }
496
+
497
+ const bindingPointer = `${pointer}/binding`;
498
+
499
+ this.#requireProperties(node.binding, bindingPointer, ENTRY_PROPERTIES);
500
+ this.#requireName(node.binding.name, `${bindingPointer}/name`);
501
+
502
+ const { name } = node.binding;
503
+
504
+
505
+ const value = this.#validateWithLater([name], () => this.#validateNode(node.binding.value, `${bindingPointer}/value`, contextAvailability), 'conditionalBinding');
506
+
507
+ const enclosingLocalNames = this.#localNames;
508
+
509
+ this.#localNames = new Set(this.#localNames).add(name);
510
+
511
+ let consequent;
512
+
513
+ try {
514
+ consequent = this.#validateBodyOrExpression(node.consequent, `${pointer}/consequent`, contextAvailability);
515
+ } finally {
516
+ this.#localNames = enclosingLocalNames;
517
+ }
518
+
519
+ return Object.freeze({
520
+ kind: 'conditional',
521
+ binding: Object.freeze({ name, value }),
522
+ consequent,
523
+ alternative: this.#validateBodyOrExpression(node.alternative, `${pointer}/alternative`, contextAvailability),
524
+ });
525
+ }
526
+
527
+ /**
528
+ * Validates a conditional within a simple expression: a condition and two branches, each a simple value expression, so it binds no value, its branches give their values without `return` or locals,
529
+ * and it holds no other conditional, an `elseif` among them, through any path, a group, a call, an indexer or an interpolation included.
530
+ */
531
+ #validateSimpleConditional(node, pointer, contextAvailability) {
532
+ const simpleExpression = this.#simpleExpression;
533
+ const { subject } = simpleExpression;
534
+ const advice = simpleExpression.advice ?? "bind the conditional's value to a local first, and use the local";
535
+
536
+ if (simpleExpression.isInConditional) {
537
+ this.#fail('NEXO2011', `${subject} is a simple expression, whose conditional holds no other conditional, elseif included: ${advice}`, pointer);
538
+ }
539
+
540
+ if (node.binding !== undefined) {
541
+ this.#fail('NEXO2011', `${subject} is a simple expression, whose conditional tests a value and binds none: ${advice}`, `${pointer}/binding`);
542
+ }
543
+
544
+ for (const branch of ['consequent', 'alternative']) {
545
+ if (node[branch]?.kind === 'body') {
546
+ this.#fail('NEXO2011', `${subject} is a simple expression, whose conditional gives each branch's value without return or locals, as if … then … else … end does: ${advice}`, `${pointer}/${branch}`);
547
+ }
548
+ }
549
+
550
+ this.#simpleExpression = { ...simpleExpression, isInConditional: true };
551
+
552
+ try {
553
+ return Object.freeze({
554
+ kind: 'conditional',
555
+ condition: this.#validateNode(node.condition, `${pointer}/condition`, contextAvailability),
556
+ consequent: this.#validateNode(node.consequent, `${pointer}/consequent`, contextAvailability),
557
+ alternative: this.#validateNode(node.alternative, `${pointer}/alternative`, contextAvailability),
558
+ });
559
+ } finally {
560
+ this.#simpleExpression = simpleExpression;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Validates a simple expression the subject names: the node and everything within it, which hold no do block, no local, no conditional binding, no `return`, and no conditional beyond an if branch and an else branch of simple value expressions.
566
+ * A simple expression within another keeps the conditional that holds it, and the advice a refusal gives where a local cannot hold what it refuses, as within an arrow function's body.
567
+ */
568
+ #validateSimpleExpression(node, pointer, contextAvailability, subject, advice = undefined) {
569
+ const enclosingSimpleExpression = this.#simpleExpression;
570
+
571
+ this.#simpleExpression = { subject, isInConditional: enclosingSimpleExpression?.isInConditional ?? false, advice: advice ?? enclosingSimpleExpression?.advice };
572
+
573
+ try {
574
+ return this.#validateNode(node, pointer, contextAvailability);
575
+ } finally {
576
+ this.#simpleExpression = enclosingSimpleExpression;
577
+ }
578
+ }
579
+
580
+ /** Validates a list a collection expression writes: its items, a list, each a simple expression, which evaluates against the implicit parameter around it. */
581
+ #validateList(node, pointer, contextAvailability) {
582
+ this.#requireArray(node.items, `${pointer}/items`, "A list's items are a list of nodes, in order");
583
+
584
+ return Object.freeze({ kind: 'list', items: Object.freeze(node.items.map((item, index) => this.#validateSimpleExpression(item, `${pointer}/items/${index}`, contextAvailability, "A list's item"))) });
585
+ }
586
+
587
+ /**
588
+ * Validates a dictionary a collection expression writes: its entries, a list, each its key, a string, and its value, a simple expression, no two of one key, compared exactly, which its later entry is refused for
589
+ * (NEXO2043).
590
+ */
591
+ #validateDictionary(node, pointer, contextAvailability) {
592
+ this.#requireArray(node.entries, `${pointer}/entries`, "A dictionary's entries are a list, each its key and its value, in order");
593
+
594
+ const keys = new Set();
595
+ const entries = node.entries.map((entry, index) => {
596
+ const entryPointer = `${pointer}/entries/${index}`;
597
+
598
+ this.#requireProperties(entry, entryPointer, DICTIONARY_ENTRY_PROPERTIES);
599
+
600
+ if (typeof entry.key !== 'string') {
601
+ this.#fail('NEXO2000', "A dictionary entry's key is a string, which a collection expression writes as an inline string literal", `${entryPointer}/key`);
602
+ }
603
+
604
+ this.#requireWellFormed(entry.key, `${entryPointer}/key`);
605
+
606
+ if (keys.has(entry.key)) {
607
+ this.#fail('NEXO2043', `A dictionary holds each key once, and ${JSON.stringify(entry.key)} is the key of an entry before this one`, entryPointer);
608
+ }
609
+
610
+ keys.add(entry.key);
611
+
612
+ return Object.freeze({ key: entry.key, value: this.#validateSimpleExpression(entry.value, `${entryPointer}/value`, contextAvailability, "A dictionary's value") });
613
+ });
614
+
615
+ return Object.freeze({ kind: 'dictionary', entries: Object.freeze(entries) });
616
+ }
617
+
618
+ /** Whether the name is a type's as a cast or a conversion writes it: its local name, or its module's name, a dot and its local name, each an identifier. */
619
+ static #isTypeName(name) {
620
+ return typeof name === 'string' && name.split('.').length <= 2 && name.split('.').every(part => Names.isIdentifier(part));
621
+ }
622
+
623
+ /** Validates a cast: the name of the type it casts to, and its operand, which is a receiver and its accesses, a coalescing operator, another cast, or an optional chain, as text can write one. */
624
+ #validateCast(node, pointer, contextAvailability) {
625
+ if (!DefinitionValidator.#isTypeName(node.type)) {
626
+ this.#fail('NEXO2000', "A cast names the type it casts to by its name, a letter followed by letters and digits, or by its module's name, a dot and its name", `${pointer}/type`);
627
+ }
628
+
629
+ const operand = this.#validateNode(node.operand, `${pointer}/operand`, contextAvailability);
630
+
631
+ if (!DefinitionValidator.#isCastOperand(operand)) {
632
+ this.#fail('NEXO2002', "A cast's operand is a receiver and the members after it, a coalescing operator in parentheses, or an optional enclosed access", `${pointer}/operand`);
633
+ }
634
+
635
+ return Object.freeze({ kind: 'cast', type: node.type, operand });
636
+ }
637
+
638
+ /**
639
+ * Whether the node is a cast's operand as text writes one: a coalescing operator or a cast, each grouped as it is, or a receiver and the accesses after it, an optional chain of them included,
640
+ * the receiver an input or a grouped cast, so every operand a definition gives is written as text that parses.
641
+ */
642
+ static #isCastOperand(node) {
643
+ if (node.kind === 'coalescing' || node.kind === 'cast') {
644
+ return true;
645
+ }
646
+
647
+ let receiver = node.kind === 'optionalChain' ? node.chain : node;
648
+
649
+ for (;;) {
650
+ if (receiver.kind === 'memberAccess' || receiver.kind === 'indexerAccess') {
651
+ receiver = receiver.receiver;
652
+ } else if (receiver.kind === 'call' && receiver.callee.kind === 'memberAccess') {
653
+ receiver = receiver.callee.receiver;
654
+ } else {
655
+ break;
656
+ }
657
+ }
658
+
659
+ // A negative number is a negation in text, which only parentheses make a receiver, and a cast's receiver is never a group.
660
+ const isNegativeNumber = receiver.kind === 'literal' && typeof receiver.value === 'number' && (receiver.value < 0 || Object.is(receiver.value, -0));
661
+
662
+ return CAST_RECEIVER_KINDS.has(receiver.kind) && !(receiver.kind === 'object' && receiver.context !== undefined) && !isNegativeNumber;
663
+ }
664
+
665
+ /** Validates an optional chain: the chain whose optional accesses end at it, at least one of them, since a chain with none is the chain alone. */
666
+ #validateOptionalChain(node, pointer, contextAvailability) {
667
+ const optionalChain = { hasOptional: false };
668
+
669
+ this.#optionalChain = optionalChain;
670
+
671
+ const chain = this.#validateNode(node.chain, `${pointer}/chain`, contextAvailability);
672
+
673
+ if (!optionalChain.hasOptional) {
674
+ this.#fail('NEXO2030', 'An optional chain holds a chain with an optional access, written ?.', pointer);
675
+ }
676
+
677
+ return Object.freeze({ kind: 'optionalChain', chain });
678
+ }
679
+
680
+ /** Validates an and or an or: two or more expressions it combines, each with the implicit parameter the and or the or has. */
681
+ #validateLogical(node, pointer, contextAvailability) {
682
+ this.#requireArray(node.operands, `${pointer}/operands`);
683
+
684
+ if (node.operands.length < 2) {
685
+ this.#fail('NEXO2525', `An ${node.kind} combines two or more expressions`, `${pointer}/operands`);
686
+ }
687
+
688
+ return Object.freeze({ kind: node.kind, operands: Object.freeze(node.operands.map((operand, index) => this.#validateNode(operand, `${pointer}/operands/${index}`, contextAvailability))) });
689
+ }
690
+
691
+ /** Validates a comparison: two or more operands, each with the implicit parameter the comparison has, and between each two of them a comparison operator, every one pointing one way. */
692
+ #validateComparison(node, pointer, contextAvailability) {
693
+ this.#requireArray(node.operands, `${pointer}/operands`);
694
+
695
+ if (node.operands.length < 2) {
696
+ this.#fail('NEXO2029', 'A comparison compares two or more expressions', `${pointer}/operands`);
697
+ }
698
+
699
+ this.#requireArray(node.operators, `${pointer}/operators`);
700
+
701
+ if (node.operators.length !== node.operands.length - 1) {
702
+ this.#fail('NEXO2526', 'A comparison has an operator between each two of its operands, one fewer than they are', `${pointer}/operators`);
703
+ }
704
+
705
+ node.operators.forEach((operator, index) => {
706
+ if (!Object.hasOwn(COMPARISON_DIRECTIONS, operator)) {
707
+ this.#fail('NEXO2000', 'A comparison operator is <, <=, > or >=', `${pointer}/operators/${index}`);
708
+ }
709
+
710
+ if (COMPARISON_DIRECTIONS[operator] !== COMPARISON_DIRECTIONS[node.operators[0]]) {
711
+ this.#fail('NEXO2001', 'A comparison chain keeps one direction: its operators are all < and <=, or all > and >=', `${pointer}/operators/${index}`);
712
+ }
713
+ });
714
+
715
+ return Object.freeze({
716
+ kind: 'comparison',
717
+ operands: Object.freeze(node.operands.map((operand, index) => this.#validateNode(operand, `${pointer}/operands/${index}`, contextAvailability))),
718
+ operators: Object.freeze([...node.operators]),
719
+ });
720
+ }
721
+
722
+ /**
723
+ * Validates an interpolated string: its parts, each text, a string, or an expression, which has the implicit parameter the string has.
724
+ * Adjacent text is one part and empty text none, so a string written in text and its definition hold the same parts; where that moves a part, it records the index each part had, which `validate` keeps for the definition.
725
+ */
726
+ #validateInterpolatedString(node, pointer, contextAvailability) {
727
+ this.#requireArray(node.parts, `${pointer}/parts`);
728
+
729
+ const parts = [];
730
+ const indexes = [];
731
+
732
+ node.parts.forEach((part, index) => {
733
+ const partPointer = `${pointer}/parts/${index}`;
734
+
735
+ if (!ObjectUtils.isNonArrayObject(part) || !Object.hasOwn(INTERPOLATION_PARTS, part.kind)) {
736
+ this.#fail('NEXO2000', 'A part of an interpolated string is an object whose kind is text or expression', partPointer);
737
+ }
738
+
739
+ this.#requireProperties(part, partPointer, INTERPOLATION_PARTS[part.kind]);
740
+
741
+ if (part.kind === 'expression') {
742
+ parts.push(Object.freeze({ kind: 'expression', expression: this.#validateSimpleExpression(part.expression, `${partPointer}/expression`, contextAvailability, "An interpolation's expression") }));
743
+ indexes.push(index);
744
+
745
+ return;
746
+ }
747
+
748
+ if (typeof part.value !== 'string') {
749
+ this.#fail('NEXO2000', 'The text of an interpolated string is a string', `${partPointer}/value`);
750
+ }
751
+
752
+ this.#requireWellFormed(part.value, `${partPointer}/value`);
753
+
754
+ if (part.value === '') {
755
+ return;
756
+ }
757
+
758
+ const previous = parts.at(-1);
759
+
760
+ if (previous?.kind === 'text') {
761
+ parts[parts.length - 1] = Object.freeze({ kind: 'text', value: previous.value + part.value });
762
+ } else {
763
+ parts.push(Object.freeze({ kind: 'text', value: part.value }));
764
+ indexes.push(index);
765
+ }
766
+ });
767
+
768
+ const validated = Object.freeze({ kind: 'interpolatedString', parts: Object.freeze(parts) });
769
+
770
+ // A part joined into the text before it, or empty and dropped, moves each after it, which a diagnostic still names by the index the caller gave it.
771
+ if (indexes.some((original, index) => original !== index)) {
772
+ this.#partIndexes.set(validated, indexes);
773
+ this.#hasMovedParts = true;
774
+ }
775
+
776
+ return validated;
777
+ }
778
+
779
+ /** Validates what a body stands for, the root expression's, a do block's, a function's or a conditional branch's: a body, or the expression alone it is. */
780
+ #validateBodyOrExpression(node, pointer, contextAvailability) {
781
+ return ObjectUtils.isNonArrayObject(node) && node.kind === 'body' ? this.#validateBody(node, pointer, contextAvailability) : this.#validateNode(node, pointer, contextAvailability);
782
+ }
783
+
784
+ /**
785
+ * Validates a body: its locals, each with its name and value, in scope after its value, through the locals after it and the result, and seen by no value before it, and its result, each with the implicit parameter the body has.
786
+ * A body's local may reuse a name an enclosing scope has, which it hides, and two of one body never share one.
787
+ */
788
+ #validateBody(node, pointer, contextAvailability) {
789
+ this.#requireProperties(node, pointer, NODE_PROPERTIES.body);
790
+ this.#requireArray(node.locals, `${pointer}/locals`);
791
+
792
+ const names = node.locals.map(local => local?.name);
793
+ const enclosingLocalNames = this.#localNames;
794
+ const enclosingOptionalChain = this.#optionalChain;
795
+ const declared = new Set();
796
+
797
+ // A body's locals and its result continue no chain of what holds it.
798
+ this.#optionalChain = null;
799
+
800
+ try {
801
+ const locals = node.locals.map((local, index) => {
802
+ const localPointer = `${pointer}/locals/${index}`;
803
+
804
+ this.#requireProperties(local, localPointer, ENTRY_PROPERTIES);
805
+ this.#requireName(local.name, `${localPointer}/name`);
806
+
807
+ if (declared.has(local.name)) {
808
+ this.#fail('NEXO2015', `Two locals of one body are named '${local.name}'`, localPointer);
809
+ }
810
+
811
+ declared.add(local.name);
812
+
813
+ const value = this.#validateWithLater(names.slice(index), () => this.#validateLocalValue(local.value, `${localPointer}/value`, contextAvailability));
814
+
815
+ this.#localNames = new Set(this.#localNames).add(local.name);
816
+
817
+ return Object.freeze({ name: local.name, value });
818
+ });
819
+
820
+ return Object.freeze({ kind: 'body', locals: Object.freeze(locals), result: this.#validateNode(node.result, `${pointer}/result`, contextAvailability) });
821
+ } finally {
822
+ this.#localNames = enclosingLocalNames;
823
+ this.#optionalChain = enclosingOptionalChain;
824
+ }
825
+ }
826
+
827
+ /** Validates a local's value, which a function may be, as no other node's is. */
828
+ #validateLocalValue(value, pointer, contextAvailability) {
829
+ return value?.kind === 'function' ? this.#validateFunction(value, pointer, contextAvailability) : this.#validateNode(value, pointer, contextAvailability);
830
+ }
831
+
832
+ /**
833
+ * Validates a function: its parameters, each a name of its own, and its body, which sees them and the locals in scope where the function is declared, with the implicit parameter it has there.
834
+ * Its own local and those declared after it stay later names in its body, so it calls neither directly. An arrow function's body is one simple expression, which declares no locals.
835
+ */
836
+ #validateFunction(node, pointer, contextAvailability) {
837
+ this.#requireProperties(node, pointer, NODE_PROPERTIES.function);
838
+ this.#requireArray(node.parameters, `${pointer}/parameters`);
839
+
840
+ if (node.arrow !== undefined && node.arrow !== true) {
841
+ this.#fail('NEXO2000', 'An arrow function is marked by arrow: true, and a regular function leaves it out', `${pointer}/arrow`);
842
+ }
843
+
844
+ const seen = new Set();
845
+
846
+ node.parameters.forEach((parameter, index) => {
847
+ const parameterPointer = `${pointer}/parameters/${index}`;
848
+
849
+ this.#requireName(parameter, parameterPointer);
850
+
851
+ if (seen.has(parameter)) {
852
+ this.#fail('NEXO2032', `Two parameters of one function are named '${parameter}'`, parameterPointer);
853
+ }
854
+
855
+ seen.add(parameter);
856
+ });
857
+
858
+ const enclosingLocalNames = this.#localNames;
859
+ const enclosingLaterNames = this.#laterNames;
860
+ const enclosingOptionalChain = this.#optionalChain;
861
+
862
+ // Its parameters are declared in its own scope, so in its body each hides a local of its name declared later around it.
863
+ this.#localNames = new Set([...this.#localNames, ...node.parameters]);
864
+ this.#laterNames = new Map([...this.#laterNames].filter(([name]) => !node.parameters.includes(name)));
865
+ this.#optionalChain = null;
866
+
867
+ try {
868
+ if (node.arrow !== true) {
869
+ return Object.freeze({ kind: 'function', parameters: Object.freeze([...node.parameters]), body: this.#validateBodyOrExpression(node.body, `${pointer}/body`, contextAvailability) });
870
+ }
871
+
872
+ if (node.body?.kind === 'body' || node.body?.kind === 'function') {
873
+ this.#fail('NEXO2006', "An arrow function's body is a simple expression, which gives its result: a function that holds more is a regular function bound to a local, passed by its name", `${pointer}/body`);
874
+ }
875
+
876
+ return Object.freeze({ kind: 'function', parameters: Object.freeze([...node.parameters]), body: this.#validateSimpleExpression(node.body, `${pointer}/body`, contextAvailability, "An arrow function's body", 'a function that holds more is a regular function bound to a local, passed by its name'), arrow: true });
877
+ } finally {
878
+ this.#localNames = enclosingLocalNames;
879
+ this.#laterNames = enclosingLaterNames;
880
+ this.#optionalChain = enclosingOptionalChain;
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Validates a call: its callee, the member it calls, a property access named after its dot, whose receiver continues the chain, or the local, the function parameter or the conditional binding holding the function it
886
+ * calls, which no function calls before the local of that name is declared, itself included; and its arguments, which continue no chain.
887
+ */
888
+ #validateCall(node, pointer, contextAvailability) {
889
+ const { callee } = node;
890
+ const calleePointer = `${pointer}/callee`;
891
+
892
+ if (!ObjectUtils.isNonArrayObject(callee) || (callee.kind !== 'memberAccess' && callee.kind !== 'local')) {
893
+ this.#fail('NEXO2000', "A call's callee is a memberAccess node, the member it calls, or a local node, the name holding the function it calls", calleePointer);
894
+ }
895
+
896
+ let validatedCallee;
897
+
898
+ if (callee.kind === 'local') {
899
+ this.#requireProperties(callee, calleePointer, NODE_PROPERTIES.local);
900
+ this.#requireName(callee.name, `${calleePointer}/name`);
901
+
902
+ if (this.#laterNames.has(callee.name)) {
903
+ this.#fail('NEXO2016', `'${callee.name}' is called before the local of that name is declared in its scope, so no function calls itself or one declared after it`, pointer);
904
+ }
905
+
906
+ validatedCallee = Object.freeze({ kind: 'local', name: callee.name });
907
+ } else {
908
+ this.#requireProperties(callee, calleePointer, NODE_PROPERTIES.memberAccess);
909
+
910
+ if (typeof callee.name !== 'string') {
911
+ this.#fail('NEXO2000', 'A call names the member it calls after its dot, a letter followed by letters and digits', `${calleePointer}/name`);
912
+ }
913
+
914
+ validatedCallee = this.#validateMemberAccess(callee, calleePointer, contextAvailability);
915
+ }
916
+
917
+ this.#optionalChain = null;
918
+
919
+ // Whether an argument has an implicit parameter is the function's to say, a reference's as its qualified form's member does, which only resolution knows, so an `it` there is resolution's to refuse.
920
+ return Object.freeze({ kind: 'call', callee: validatedCallee, arguments: this.#validateArguments(node.arguments, `${pointer}/arguments`, ContextAvailability.unknown) });
921
+ }
922
+
923
+ /** Validates a call's arguments, each a simple expression or an arrow function; a regular function is bound to a local and passed by its name. */
924
+ #validateArguments(argumentNodes, pointer, contextAvailability) {
925
+ this.#requireArray(argumentNodes, pointer);
926
+
927
+ return Object.freeze(argumentNodes.map((argument, index) => {
928
+ const argumentPointer = `${pointer}/${index}`;
929
+
930
+ if (argument?.kind !== 'function') {
931
+ return this.#validateSimpleExpression(argument, argumentPointer, contextAvailability, 'An argument');
932
+ }
933
+
934
+ if (argument.arrow !== true) {
935
+ this.#fail('NEXO2005', "A function declared with 'function' cannot appear directly as an argument. Bind it to a local and pass its name, or use an arrow function.", argumentPointer);
936
+ }
937
+
938
+ return this.#validateFunction(argument, argumentPointer, contextAvailability);
939
+ }));
940
+ }
941
+
942
+ /** Refuses a conditional or a body as the receiver of a chain, or the context an object expression follows, whose value a local holds first. */
943
+ #refuseBlockReceiver(receiver, pointer) {
944
+ if (receiver?.kind === 'conditional' || receiver?.kind === 'do') {
945
+ this.#fail('NEXO2003', `${receiver.kind === 'do' ? 'A do block' : 'A conditional'} is never the receiver of a chain: bind its value to a local first, and continue from the local`, pointer);
946
+ }
947
+ }
948
+
949
+ #validateLiteral(node, pointer) {
950
+ const { value } = node;
951
+
952
+ if (value !== null && typeof value !== 'string' && typeof value !== 'boolean' && !Number.isFinite(value)) {
953
+ this.#fail(typeof value === 'number' ? 'NEXO2019' : 'NEXO2000', 'A literal is a string, a finite number, a Boolean or null', `${pointer}/value`);
954
+ }
955
+
956
+ if (typeof value === 'string') {
957
+ this.#requireWellFormed(value, `${pointer}/value`);
958
+ }
959
+
960
+ return Object.freeze({ kind: 'literal', value });
961
+ }
962
+
963
+ /** Refuses a string a definition holds that holds a surrogate standing alone, which no text writes, so that every string a script holds is text a host's text and canonical text hold alike. */
964
+ #requireWellFormed(value, pointer) {
965
+ if (LONE_SURROGATE.test(value)) {
966
+ this.#fail('NEXO2000', 'A string a definition holds is well-formed text, and this one holds a surrogate standing alone', pointer);
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Validates a property access, a call's member or an indexer: its name, its receiver, whether it is optional, which it is within an optional chain alone, the extension it selects explicitly, and an indexer's key, a simple
972
+ * expression whose implicit parameter its declaration decides.
973
+ */
974
+ #validateMemberAccess(node, pointer, contextAvailability) {
975
+ this.#refuseBlockReceiver(node.receiver, `${pointer}/receiver`);
976
+
977
+ const name = node.kind === 'memberAccess' && ObjectUtils.isNonArrayObject(node.name) ? this.#validateNameExpression(node.name, `${pointer}/name`, contextAvailability) : node.name;
978
+
979
+ // A name a computed property access's expression gave is checked as that expression, and an indexer has none; any other is a member's name, which any word can be, a reserved word among them.
980
+ if (name === node.name && node.kind !== 'indexerAccess' && !Names.isIdentifier(name)) {
981
+ this.#fail('NEXO2000', 'A member is named by a letter followed by letters and digits', `${pointer}/name`);
982
+ }
983
+
984
+ if (node.optional !== undefined) {
985
+ if (node.optional !== true) {
986
+ this.#fail('NEXO2000', 'An optional access holds optional: true, and a required one leaves it out', `${pointer}/optional`);
987
+ }
988
+
989
+ if (this.#optionalChain === null) {
990
+ this.#fail('NEXO2010', 'An optional access stands within an optional chain, which ends where a null receiver ends it', pointer);
991
+ }
992
+
993
+ this.#optionalChain.hasOptional = true;
994
+ }
995
+
996
+ this.#validateExtensionAccess(node, pointer);
997
+
998
+ const receiver = node.receiver?.kind === 'namespace' ? this.#validateNamespace(node, pointer) : this.#validateNode(node.receiver, `${pointer}/receiver`, contextAvailability);
999
+ const marks = { ...(node.extension === true ? { extension: true } : {}), ...(node.continues === true ? { continues: true } : {}) };
1000
+ const access = Object.freeze({ kind: node.kind, receiver, ...(name === undefined ? {} : { name }), ...(node.optional === true ? { optional: true } : {}), ...marks });
1001
+
1002
+ if (node.kind === 'memberAccess') {
1003
+ return access;
1004
+ }
1005
+
1006
+ this.#optionalChain = null;
1007
+
1008
+ return Object.freeze({ ...access, argument: this.#validateSimpleExpression(node.argument, `${pointer}/argument`, ContextAvailability.unknown, "An indexer's key") });
1009
+ }
1010
+
1011
+ /**
1012
+ * Validates the marks of an extension access's names: `extension`, true on its first name, which the colon is written before, and `continues`, true on each later one, which a dot joins to the one before it, its
1013
+ * receiver, a name of the access, so neither is optional, both being written by name, and no name is both. A computed property access is no name of one, since it reads a property by a name it computes.
1014
+ */
1015
+ #validateExtensionAccess(node, pointer) {
1016
+ if (node.extension !== undefined && node.extension !== true) {
1017
+ this.#fail('NEXO2000', "An extension access's first name holds extension: true", `${pointer}/extension`);
1018
+ }
1019
+
1020
+ if (node.continues !== undefined && node.continues !== true) {
1021
+ this.#fail('NEXO2000', "A name continuing an extension access holds continues: true", `${pointer}/continues`);
1022
+ }
1023
+
1024
+ if (node.extension === undefined && node.continues === undefined) {
1025
+ return;
1026
+ }
1027
+
1028
+ if (node.kind !== 'memberAccess' || typeof node.name !== 'string') {
1029
+ this.#fail('NEXO2000', "An extension access's names are written names, never a computed property access's", pointer);
1030
+ }
1031
+
1032
+ if (node.continues === true) {
1033
+ const { receiver } = node;
1034
+
1035
+ if (node.extension === true || node.optional === true || receiver?.kind !== 'memberAccess' || (receiver.extension !== true && receiver.continues !== true)) {
1036
+ this.#fail('NEXO2000', "A name continuing an extension access follows one of its names after a dot, and is neither optional nor its first", `${pointer}/continues`);
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ /** Refuses a namespace's or an extension's module named by a local name the definition's imports give no module (NEXO2028). */
1042
+ #requireImportedNamespace(name, pointer) {
1043
+ if (!this.#namespaceNames.has(name)) {
1044
+ this.#fail('NEXO2028', `'${name}' names a module no import of the definition gives that local name`, pointer);
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * The message refusing a namespace that stands alone, where a value stands: a module's namespace is read through, never a value, and the forms that read the script parameter and the local of its name are given, since
1050
+ * `@Home` reads the import binding only where no local of its name is in scope.
1051
+ */
1052
+ static #namespaceAloneMessageOf(name) {
1053
+ const shown = typeof name === 'string' && Names.isIdentifier(name) ? name : 'Home';
1054
+
1055
+ return `'${shown}' is the namespace of the module the script imports under that name, the receiver of a member read or called through it, as ${shown}.member, and stands nowhere else; write '~${shown}' for the script parameter of that name, or declare a local of that name for '@${shown}' to read`;
1056
+ }
1057
+
1058
+ /**
1059
+ * Validates the namespace a member read or called stands on: a local name the definition's imports give a module (NEXO2028), through which a member is named by a dot, never optional, since a namespace is never null,
1060
+ * and never through brackets or an indexer, which no namespace has. Which modules a registry holds is the registry's to check.
1061
+ */
1062
+ #validateNamespace(node, pointer) {
1063
+ const namespace = node.receiver;
1064
+ const namespacePointer = `${pointer}/receiver`;
1065
+
1066
+ this.#requireProperties(namespace, namespacePointer, NODE_PROPERTIES.namespace);
1067
+ this.#requireName(namespace.name, `${namespacePointer}/name`);
1068
+ this.#requireImportedNamespace(namespace.name, namespacePointer);
1069
+
1070
+ if (node.kind === 'indexerAccess') {
1071
+ this.#fail('NEXO2520', 'No indexer is written through a namespace, which has none', pointer);
1072
+ }
1073
+
1074
+ if (typeof node.name !== 'string') {
1075
+ this.#fail('NEXO2521', `A member is read through the namespace '${namespace.name}' by the name it is declared by, and by no computed name`, `${pointer}/name`);
1076
+ }
1077
+
1078
+ if (node.optional === true) {
1079
+ this.#fail('NEXO2522', "A namespace is never null, so a member is read through it after '.', and never optionally", `${pointer}/optional`);
1080
+ }
1081
+
1082
+ if (node.extension === true || node.continues === true) {
1083
+ this.#fail('NEXO2520', "No extension extends a namespace, whose members are read after '.', as Home.member", pointer);
1084
+ }
1085
+
1086
+ return Object.freeze({ kind: 'namespace', name: namespace.name });
1087
+ }
1088
+
1089
+ /** Validates the expression a computed property access holds, which continues no chain; a string literal that is a name is the name itself, as `.` writes it, so an access has one definition. */
1090
+ #validateNameExpression(node, pointer, contextAvailability) {
1091
+ const enclosingOptionalChain = this.#optionalChain;
1092
+
1093
+ this.#optionalChain = null;
1094
+
1095
+ try {
1096
+ const expression = this.#validateSimpleExpression(node, pointer, contextAvailability, 'The name a computed property access reads');
1097
+
1098
+ return expression.kind === 'literal' && Names.isIdentifier(expression.value) ? expression.value : expression;
1099
+ } finally {
1100
+ this.#optionalChain = enclosingOptionalChain;
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * Validates an object expression and answers it in canonical form: the context it follows, never a block, validated outside it; its locals, against the implicit parameter, which the context supplies and the enclosing
1106
+ * one otherwise, none within a simple expression; and its object fields, at least one, each a name and a value, no two of one name, each in scope for the fields after it and hiding a local or a field of its name outside
1107
+ * it. Its entries continue no chain of the context, and are validated within the outermost object; the names in scope and whether an object expression encloses them are restored after it.
1108
+ */
1109
+ #validateObjectExpression(node, pointer, contextAvailability) {
1110
+ this.#refuseBlockReceiver(node.context, `${pointer}/context`);
1111
+
1112
+ const context = node.context === undefined ? undefined : this.#validateNode(node.context, `${pointer}/context`, contextAvailability);
1113
+
1114
+ this.#requireArray(node.objectFields, `${pointer}/objectFields`);
1115
+
1116
+ if (node.objectFields.length === 0) {
1117
+ this.#fail('NEXO2009', 'An object expression names at least one object field', pointer);
1118
+ }
1119
+
1120
+ const fieldContextAvailability = context === undefined ? contextAvailability : ContextAvailability.present;
1121
+ const enclosingLocalNames = this.#localNames;
1122
+ const wasInObjectExpression = this.#isInObjectExpression;
1123
+
1124
+ // An object expression's entries continue no chain of the one it follows, and are built within the outermost object.
1125
+ this.#optionalChain = null;
1126
+ this.#isInObjectExpression = true;
1127
+
1128
+ try {
1129
+ if (node.locals !== undefined && this.#simpleExpression !== null) {
1130
+ this.#fail('NEXO2527', `${this.#simpleExpression.subject} is a simple expression, whose object expressions hold no locals: ${this.#simpleExpression.advice ?? 'bind each value to a local before the expression, and use the local'}`, `${pointer}/locals/0`);
1131
+ }
1132
+
1133
+ const locals = node.locals === undefined ? [] : this.#validateLocals(node.locals, `${pointer}/locals`, fieldContextAvailability);
1134
+
1135
+ /** @type {Set<string>} */
1136
+ const names = new Set();
1137
+
1138
+ const objectFields = node.objectFields.map((field, index) => {
1139
+ const fieldPointer = `${pointer}/objectFields/${index}`;
1140
+ this.#requireProperties(field, fieldPointer, ENTRY_PROPERTIES);
1141
+ this.#requireName(field.name, `${fieldPointer}/name`);
1142
+
1143
+ const value = this.#validateNode(field.value, `${fieldPointer}/value`, fieldContextAvailability);
1144
+ const { name } = field;
1145
+
1146
+ if (names.has(name)) {
1147
+ this.#fail('NEXO2031', `Two fields of one object expression are named '${name}'`, fieldPointer);
1148
+ }
1149
+
1150
+ // A field is in scope after its value, through the fields after it, hiding a local or a field of its name outside it.
1151
+ names.add(name);
1152
+ this.#localNames = new Set(this.#localNames).add(name);
1153
+
1154
+ return Object.freeze({ name, value });
1155
+ });
1156
+
1157
+ return Object.freeze({
1158
+ kind: 'object',
1159
+ ...(context === undefined ? {} : { context }),
1160
+ ...(locals.length === 0 ? {} : { locals: Object.freeze(locals) }),
1161
+ objectFields: Object.freeze(objectFields),
1162
+ });
1163
+ } finally {
1164
+ this.#localNames = enclosingLocalNames;
1165
+ this.#isInObjectExpression = wasInObjectExpression;
1166
+ }
1167
+ }
1168
+
1169
+ /** Validates an object expression's locals in order, each seeing those before it, and leaves every one in scope for its fields; a local may hide one of its name outside its object expression, and two of one never share a name. */
1170
+ #validateLocals(locals, pointer, contextAvailability) {
1171
+ this.#requireArray(locals, pointer);
1172
+
1173
+ const names = locals.map(local => local?.name);
1174
+ const declared = new Set();
1175
+
1176
+ return locals.map((local, index) => {
1177
+ const localPointer = `${pointer}/${index}`;
1178
+ this.#requireProperties(local, localPointer, ENTRY_PROPERTIES);
1179
+ this.#requireName(local.name, `${localPointer}/name`);
1180
+
1181
+ // A local's value and the locals before it see neither it nor the locals after it, whose names no parameter there reads.
1182
+ const value = this.#validateWithLater(names.slice(index), () => this.#validateLocalValue(local.value, `${localPointer}/value`, contextAvailability));
1183
+ const { name } = local;
1184
+
1185
+ if (declared.has(name)) {
1186
+ this.#fail('NEXO2015', `Two locals of one object expression are named '${name}'`, localPointer);
1187
+ }
1188
+
1189
+ declared.add(name);
1190
+ this.#localNames = new Set(this.#localNames).add(name);
1191
+
1192
+ return Object.freeze({ name, value });
1193
+ });
1194
+ }
1195
+
1196
+ /** What validating answers, with the names given declared later in its scope besides those already, by a local or, for a conditional binding's name, by the conditional binding. */
1197
+ #validateWithLater(names, validate, declaredBy = 'local') {
1198
+ const enclosingLaterNames = this.#laterNames;
1199
+
1200
+ this.#laterNames = new Map([...this.#laterNames, ...names.map(name => [name, declaredBy])]);
1201
+
1202
+ try {
1203
+ return validate();
1204
+ } finally {
1205
+ this.#laterNames = enclosingLaterNames;
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Refuses a script parameter a text wrote as a bare name where that name cannot denote one: one named as a local or a field in scope where it stands, which the name reads there instead, and one named as a local declared
1211
+ * later in its scope, the local whose value it is among them, which a name reads only after its declaration and never as a binding of that name before it. It is not the general check of an identifier, and a parameter
1212
+ * written with `~` follows its own rules.
1213
+ */
1214
+ #requireScriptParameterName(name, pointer) {
1215
+ if (this.#localNames.has(name)) {
1216
+ this.#fail('NEXO2528', `'${name}' names the local or field in scope here, so no binding of that name is read where it is`, pointer);
1217
+ }
1218
+
1219
+ if (this.#laterNames.has(name)) {
1220
+ this.#fail(this.#laterNames.get(name) === 'conditionalBinding' ? 'NEXO2014' : 'NEXO2013', `'${name}' is used before the local of that name is declared in its scope: a local is read after its declaration, and the name reads no binding before it`, pointer);
1221
+ }
1222
+ }
1223
+
1224
+ #requireProperties(part, pointer, { properties, required }) {
1225
+ if (!ObjectUtils.isNonArrayObject(part)) {
1226
+ this.#fail('NEXO2000', 'This part of a definition is an object', pointer);
1227
+ }
1228
+
1229
+ for (const key of Object.keys(part)) {
1230
+ if (!properties.includes(key)) {
1231
+ this.#fail('NEXO2000', `'${key}' is no property of this part of a definition`, pointer);
1232
+ }
1233
+ }
1234
+
1235
+ for (const key of required) {
1236
+ if (!Object.hasOwn(part, key)) {
1237
+ this.#fail('NEXO2000', `This part of a definition requires '${key}'`, pointer);
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ /** Refuses what is no array, with the message given, and an array with no entry at an index, since a definition's arrays hold one at each, at the first index missing. */
1243
+ #requireArray(value, pointer, message = 'This part of a definition is an array') {
1244
+ if (!Array.isArray(value)) {
1245
+ this.#fail('NEXO2000', message, pointer);
1246
+ }
1247
+
1248
+ const hole = ArrayUtils.findMissingIndex(value);
1249
+
1250
+ if (hole !== -1) {
1251
+ this.#fail('NEXO2000', 'This part of a definition is an array with an entry at every index', `${pointer}/${hole}`);
1252
+ }
1253
+ }
1254
+
1255
+ #requireName(value, pointer) {
1256
+ if (!Names.isIdentifier(value)) {
1257
+ this.#fail('NEXO2000', 'A name is a letter followed by letters and digits', pointer);
1258
+ }
1259
+ }
1260
+
1261
+ /** Refuses the definition with a structure error of the code and the message, located at the source of the pointer. */
1262
+ #fail(code, message, pointer) {
1263
+ throw NexoErrors.structure(code, message, this.#locations?.locate(pointer) ?? { pointer });
1264
+ }
1265
+ }