@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,1817 @@
1
+ /**
2
+ * Parser — a Nexo text read into its lossless syntax tree, which the lowering turns into its definition
3
+ */
4
+
5
+ import { Imports } from './Imports.mjs';
6
+ import { Limits } from './Limits.mjs';
7
+ import { NexoErrors } from './NexoError.mjs';
8
+ import { LineStructure } from './LineStructure.mjs';
9
+ import { Names } from './Names.mjs';
10
+ import { SyntaxTree } from './SyntaxTree.mjs';
11
+ import { Tokenizer } from './Tokenizer.mjs';
12
+
13
+ /** @typedef {import('./Tokenizer.mjs').Token} Token */
14
+ /** @typedef {import('./SyntaxTree.mjs').SyntaxNode} SyntaxNode */
15
+ /** @typedef {{ start: number, end: number }} Span */
16
+
17
+ const TOKEN_DESCRIPTIONS = {
18
+ identifier: 'an identifier',
19
+ literal: 'a literal',
20
+ end: 'the end of the source',
21
+ };
22
+
23
+ /** The tokens that continue a chain after what is before them, and the colons a chain refuses, which no member follows. */
24
+ const CHAIN_CONTINUATIONS = new Set(['.', '?.', '[', '?[', ':', '?:', '{']);
25
+
26
+ /** The tokens that, after a space, make the name before them a member written without its receiver, which no local is: an argument. */
27
+ const ARGUMENT_STARTS = new Set(['it', 'literal', 'interpolated-string-start', 'script-parameter', 'lexical-reference']);
28
+
29
+ /** The tokens a range's end begins with, a position: a value, a name, a group, an index from the end, a negation or a collection expression; any other token after `..` leaves the range's end out. */
30
+ const BOUNDARY_STARTS = new Set([...ARGUMENT_STARTS, 'identifier', '(', '(?', '^', '-', '[', '{']);
31
+
32
+ /** The syntax kinds of the operator expressions no range's boundary is, which a group holding one makes a position. */
33
+ const OPERATOR_KINDS = new Set(['disjunction', 'conjunction', 'negation', 'equality', 'comparison', 'additive', 'multiplicative', 'coalescing']);
34
+
35
+ /** The tokens an operator expression continues with, which follow no range's end ungrouped. */
36
+ const OPERATOR_TOKENS = new Set(['+', '-', '*', '/', '%', '<', '<=', '>', '>=', '==', '!=', '??']);
37
+
38
+ /** The tokens that close what encloses a body, the end of a group, a list, an object or a field, before which a body's closer is missing rather than anything following its result. */
39
+ const BODY_ENCLOSURE_CLOSINGS = new Set([')', ']', '}', ',']);
40
+
41
+ /** The keywords that close or divide a construct, whose own messages say what it lacks where a value would stand. */
42
+ const STRUCTURAL_KEYWORDS = new Set(['then', 'elseif', 'else', 'end']);
43
+
44
+ /** The direction each comparison operator points, which every operator of a chain shares. */
45
+ const COMPARISON_DIRECTIONS = Object.freeze({ '<': 'ascending', '<=': 'ascending', '>': 'descending', '>=': 'descending' });
46
+
47
+ /** The kinds of node a member expression is, each beginning with the token that joins it to its receiver. */
48
+ const MEMBER_EXPRESSION_KINDS = new Set(['dot-property-access', 'computed-property-access', 'member-call', 'indexer', 'extension-access']);
49
+
50
+ export class Parser {
51
+ #text;
52
+ /** @type {Token[]} */
53
+ #tokens;
54
+ #index = 0;
55
+
56
+ /** The local names the script's imports give modules, its own and those a host supplies, each beginning its module's namespace where a value stands. @type {ReadonlySet<string>} */
57
+ #namespaceNames = Names.noNamespaceNames;
58
+
59
+ /** The imports a host supplies, in a definition's shape, which the script's own imports share their local names with. @type {readonly object[]} */
60
+ #suppliedImports;
61
+
62
+ /** How many bodies' locals are being read, where a name on a new line after a value is a statement no body holds rather than a member written without its dot. */
63
+ #bodyLocalsBeingRead = 0;
64
+
65
+ /** How many value expressions are being read, each within the one before it, which the brackets, braces and parentheses holding them nest, so a text nesting past `Limits.nesting` is refused where it does. */
66
+ #valueDepth = 0;
67
+
68
+ /** The coalescing operators read where no group holds them, which no `and`, `or` or equality may stand beside. @type {WeakSet<SyntaxNode>} */
69
+ #ungroupedCoalescings = new WeakSet();
70
+
71
+ /** The conversions read directly after another conversion or a cast of the same chain, without a group between them, which `try` refuses to attempt. @type {WeakSet<SyntaxNode>} */
72
+ #chainedConversions = new WeakSet();
73
+
74
+ /**
75
+ * @param {string} text The source text.
76
+ * @param {readonly object[]} [suppliedImports] The imports a host supplies, in a definition's shape, checked already.
77
+ */
78
+ constructor(text, suppliedImports = []) {
79
+ this.#text = text;
80
+ this.#suppliedImports = suppliedImports;
81
+ this.#tokens = new Tokenizer(text).tokenize();
82
+ }
83
+
84
+ /**
85
+ * Reads the text into its syntax tree, whose script node holds the script's imports, each its own import declaration, then its body, an expression alone or locals and the `return` that gives its result, and the end of
86
+ * the text, with the trivia before it. The imports decide which names begin namespaces in the body, the host's beside the script's own.
87
+ * @returns {import('./SyntaxTree.mjs').ScriptNode}
88
+ */
89
+ parse() {
90
+ const declarations = [];
91
+ const imports = [];
92
+
93
+ while (this.#isKeyword('import')) {
94
+ declarations.push(this.#parseImportDeclaration(imports));
95
+ }
96
+
97
+ this.#namespaceNames = Imports.namespaceNamesOf([...imports, ...this.#suppliedImports]);
98
+
99
+ const body = this.#parseBody();
100
+
101
+ if (this.#token.type !== 'end') {
102
+ throw this.#createAfterBodyError('NEXO1000', TOKEN_DESCRIPTIONS.end);
103
+ }
104
+
105
+ const script = { kind: 'script', text: this.#text, children: [...declarations, body, this.#take()] };
106
+
107
+ // Its layout is checked once it is read, which it never changes: each failure passes through the parser's one error path.
108
+ new LineStructure(this.#text, this.#tokens).validate(script, (code, message, offset) => {
109
+ throw this.#createSyntaxError(code, message, offset);
110
+ });
111
+
112
+ return script;
113
+ }
114
+
115
+ /**
116
+ * Reads an import declaration at the script's start: `import`, an alias and `=` where it gives one, and its source, a module's name and, where it imports one type, a dot and the type's local name, each any word. It imports
117
+ * nothing of Nexo's own (NEXO1046), and gives no local name another import, written or supplied, gives a different declaration (NEXO1048); line structure places it on a line of its own. It adds the import, in
118
+ * a definition's shape, to those read before it.
119
+ */
120
+ #parseImportDeclaration(imports) {
121
+ const keyword = this.#take();
122
+
123
+ const first = this.#takeImportWord("a module's name, or an alias and '='");
124
+ const children = [keyword, first];
125
+ let moduleToken = first;
126
+
127
+ if (this.#token.type === '=') {
128
+ children.push(this.#take());
129
+ moduleToken = this.#takeImportWord("the module's name after the alias's '='");
130
+ children.push(moduleToken);
131
+ }
132
+
133
+ if (this.#token.type === '.') {
134
+ children.push(this.#take());
135
+ children.push(this.#takeImportWord("the type's name after its module's dot"));
136
+ }
137
+
138
+ const declaration = { kind: 'import-declaration', children };
139
+ const entry = SyntaxTree.importOf(declaration, this.#text);
140
+
141
+ if (entry.module === Names.nexoModuleName) {
142
+ throw this.#createSyntaxError('NEXO1046', "Nexo's own vocabulary is always available, so no script imports it or one of its types", moduleToken.start);
143
+ }
144
+
145
+ const conflict = [...imports, ...this.#suppliedImports].find(other => Imports.localNameOf(other) === Imports.localNameOf(entry) && !Imports.isSameImport(other, entry));
146
+
147
+ if (conflict !== undefined) {
148
+ throw this.#createSyntaxError('NEXO1048', Imports.describeConflict([conflict, entry]), keyword.start);
149
+ }
150
+
151
+ imports.push(entry);
152
+
153
+ return declaration;
154
+ }
155
+
156
+ /** Takes a word an import names a module, a type or an alias by, any word, a reserved one among them, refusing anything else as the description says what was expected. */
157
+ #takeImportWord(expected) {
158
+ if (!this.#isWord(this.#token)) {
159
+ throw this.#createUnexpectedTokenError('NEXO1000', expected);
160
+ }
161
+
162
+ return this.#take();
163
+ }
164
+
165
+ /**
166
+ * Reads an expression: a conditional, a do block, or a simple value expression; a function stands as a local's initializer alone. A conditional or a do block an operator follows is that operator's left operand,
167
+ * which parentheses enclose, as they enclose a right one.
168
+ */
169
+ #parseExpression() {
170
+ if (this.#isKeyword('if') || this.#isKeyword('do')) {
171
+ const start = this.#token.start;
172
+ const isConditional = this.#isKeyword('if');
173
+ const block = isConditional ? this.#parseConditionalExpression([this.#take()]) : this.#parseDoExpression();
174
+
175
+ if (OPERATOR_TOKENS.has(this.#token.type) || this.#token.type === '..' || ['and', 'or', 'as', 'to'].some(keyword => this.#isKeyword(keyword))) {
176
+ throw this.#createSyntaxError('NEXO1027', isConditional ? 'A conditional that is an operand is enclosed in parentheses: write (if … then … else … end)' : 'A do block that is an operand is enclosed in parentheses: write (do … end)', start);
177
+ }
178
+
179
+ return block;
180
+ }
181
+
182
+ return this.#parseSimpleValueExpression();
183
+ }
184
+
185
+ /**
186
+ * Reads a body: an expression alone, or the locals evaluated in order, each `let`, a name, `=` and a value, and the `return` that gives its result, which completes the body and nothing follows; `return value` alone is a body of no locals.
187
+ * What closes it, the end of the text, `end`, `elseif` or `else`, is its holder's to read.
188
+ */
189
+ #parseBody() {
190
+ if (!this.#isKeyword('let') && !this.#isKeyword('return')) {
191
+ const start = this.#token.start;
192
+ const expression = this.#parseExpression();
193
+
194
+ // An expression alone is its body's result, so a `return` after it follows what no local holds.
195
+ if (this.#isKeyword('return')) {
196
+ throw this.#createSyntaxError('NEXO1031', "Only locals may precede a body's result.", start);
197
+ }
198
+
199
+ return expression;
200
+ }
201
+
202
+ const children = [];
203
+
204
+ this.#bodyLocalsBeingRead++;
205
+
206
+ try {
207
+ while (this.#isKeyword('let')) {
208
+ children.push(this.#parseLocal());
209
+ }
210
+ } finally {
211
+ this.#bodyLocalsBeingRead--;
212
+ }
213
+
214
+ // After its locals a body's result follows its `return`; an expression there is one a `return` follows, which only a local may precede, or the result its `return` was left off.
215
+ if (!this.#isKeyword('return')) {
216
+ const start = this.#token.start;
217
+
218
+ if (this.#token.type !== 'end' && !this.#isKeyword('end') && !this.#isKeyword('elseif') && !this.#isKeyword('else')) {
219
+ this.#parseExpression();
220
+
221
+ if (this.#isKeyword('return')) {
222
+ throw this.#createSyntaxError('NEXO1031', "Only locals may precede a body's result.", start);
223
+ }
224
+ }
225
+
226
+ throw this.#createSyntaxError('NEXO1030', "Introduce the body's result with return.", start);
227
+ }
228
+
229
+ const returnKeyword = this.#take();
230
+
231
+ if (this.#token.type === 'end' || this.#isKeyword('end') || this.#isKeyword('elseif') || this.#isKeyword('else')) {
232
+ throw this.#createSyntaxError('NEXO1056', "'return' gives its body's result: write the value after it, as in 'return width'", returnKeyword.start);
233
+ }
234
+
235
+ children.push(returnKeyword, this.#parseExpression());
236
+
237
+ if (this.#isKeyword('let') || this.#isKeyword('return')) {
238
+ throw this.#createSyntaxError('NEXO1055', "Nothing follows a body's result, which ends it", this.#token.start);
239
+ }
240
+
241
+ return Parser.#node('body', children);
242
+ }
243
+
244
+ /** Reads a function: `function`, its parameters' names in parentheses, separated by commas, the body a call evaluates and the `end` that closes it. */
245
+ #parseFunctionExpression() {
246
+ const children = [this.#take(), this.#expect('(')];
247
+
248
+ // A comma stands between two parameters alone, so one before ')' is refused.
249
+ if (this.#token.type !== ')') {
250
+ for (;;) {
251
+ if (this.#token.type === 'lexical-reference') {
252
+ throw this.#createSyntaxError('NEXO1008', `A parameter is declared as it is written, with no '@': write '${this.#token.value}'`, this.#token.start);
253
+ }
254
+
255
+ if (!this.#isWord(this.#token)) {
256
+ throw this.#createUnexpectedTokenError('NEXO1000', "a parameter's name");
257
+ }
258
+
259
+ children.push(this.#take());
260
+
261
+ if (this.#token.type !== ',') {
262
+ break;
263
+ }
264
+
265
+ children.push(this.#take());
266
+ }
267
+
268
+ if (this.#token.type !== ')') {
269
+ throw this.#createUnexpectedTokenError('NEXO1000', "',' before the next parameter, or ')' closing them");
270
+ }
271
+ }
272
+
273
+ children.push(this.#take());
274
+
275
+ if (this.#isKeyword('end')) {
276
+ throw this.#createSyntaxError('NEXO1511', "A function holds a body: write 'function(…) return … end' or the expression it gives", this.#token.start);
277
+ }
278
+
279
+ children.push(this.#parseBody());
280
+
281
+ if (!this.#isKeyword('end')) {
282
+ throw this.#createAfterBodyError('NEXO1029', "'end' closing the function");
283
+ }
284
+
285
+ children.push(this.#take());
286
+
287
+ return Parser.#node('function-expression', children);
288
+ }
289
+
290
+ /** Whether an arrow function starts here: a parameter's name and `=>`, or the parameters' names in parentheses, separated by commas, and `=>`. */
291
+ #startsArrowFunction() {
292
+ const tokenAt = offset => this.#tokens[this.#index + offset];
293
+ const isWordAt = offset => tokenAt(offset) !== undefined && this.#isWord(tokenAt(offset));
294
+
295
+ if (isWordAt(0)) {
296
+ return tokenAt(1)?.type === '=>';
297
+ }
298
+
299
+ if (this.#token.type !== '(') {
300
+ return false;
301
+ }
302
+
303
+ let offset = 1;
304
+
305
+ if (tokenAt(offset)?.type !== ')') {
306
+ for (;;) {
307
+ if (!isWordAt(offset)) {
308
+ return false;
309
+ }
310
+
311
+ offset++;
312
+
313
+ if (tokenAt(offset)?.type !== ',') {
314
+ break;
315
+ }
316
+
317
+ offset++;
318
+ }
319
+
320
+ if (tokenAt(offset)?.type !== ')') {
321
+ return false;
322
+ }
323
+ }
324
+
325
+ return tokenAt(offset + 1)?.type === '=>';
326
+ }
327
+
328
+ /**
329
+ * Reads an arrow function: its parameters, one name alone or the names in parentheses, separated by commas, `=>`, and the one simple expression that is its body and gives its result.
330
+ * It declares no locals and holds no body of its own, so what more a function holds is a regular function's, bound to a local.
331
+ */
332
+ #parseArrowFunction() {
333
+ const children = [];
334
+
335
+ if (this.#token.type === '(') {
336
+ children.push(this.#take());
337
+
338
+ while (this.#token.type !== ')') {
339
+ children.push(this.#take());
340
+ }
341
+ }
342
+
343
+ children.push(this.#take(), this.#take());
344
+
345
+ if (this.#isKeyword('let') || this.#isKeyword('return') || this.#isKeyword('function') || this.#startsArrowFunction()) {
346
+ throw this.#createSyntaxError('NEXO1036', "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", this.#token.start);
347
+ }
348
+
349
+ children.push(this.#parseExpression());
350
+
351
+ return Parser.#node('arrow-function', children);
352
+ }
353
+
354
+ /** Reads a call of the function a local, a function parameter or a conditional binding holds, the name it is written by given: its name, or a reserved word after `@`, and its arguments in parentheses. */
355
+ #parseFunctionCall(written) {
356
+ const name = this.#take();
357
+
358
+ return Parser.#node('function-call', [name, ...this.#parseArguments(written, false)]);
359
+ }
360
+
361
+ /** Refuses an optional enclosed access opening a call's parentheses: `(?` is one token, so the call's parenthesis cannot also open the access that is its argument. */
362
+ #refuseEnclosedAccessArgument(token, written) {
363
+ if (token.type === '(?' && !token.spaced) {
364
+ throw this.#createSyntaxError('NEXO1015', `A call whose argument is an optional enclosed access writes it in parentheses of its own: '${written}((?…))'`, token.start);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Reads a call's arguments: its opening parenthesis, the arguments separated by commas, a comma standing between two of them alone, and its closing parenthesis.
370
+ * An argument is written as its value alone, so one written as a named one would be is refused, a member's with the options object a member taking options is given instead.
371
+ */
372
+ #parseArguments(written, isMember) {
373
+ const children = [this.#take()];
374
+
375
+ if (this.#token.type !== ')') {
376
+ for (;;) {
377
+ if (this.#startsEntry()) {
378
+ const options = isMember ? `; a member that takes options is given them as the object fields of an object, as { ${this.#token.value} = … }` : '';
379
+
380
+ throw this.#createSyntaxError('NEXO1012', `An argument is written as its value alone, as '${written}(…)' takes it${options}`, this.#token.start);
381
+ }
382
+
383
+ if (this.#isKeyword('function')) {
384
+ throw this.#createSyntaxError('NEXO1035', "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.", this.#token.start);
385
+ }
386
+
387
+ children.push(this.#startsArrowFunction() ? this.#parseArrowFunction() : this.#parseExpression());
388
+
389
+ if (this.#token.type !== ',') {
390
+ break;
391
+ }
392
+
393
+ children.push(this.#take());
394
+ }
395
+
396
+ if (this.#token.type !== ')') {
397
+ throw this.#createUnexpectedTokenError('NEXO1000', "',' before the next argument, or ')' closing them");
398
+ }
399
+ }
400
+
401
+ children.push(this.#take());
402
+
403
+ return children;
404
+ }
405
+
406
+ /** Reads a do block: `do`, the body it evaluates where execution reaches it, and the `end` that closes it. */
407
+ #parseDoExpression() {
408
+ const doKeyword = this.#take();
409
+
410
+ if (this.#isKeyword('end')) {
411
+ throw this.#createSyntaxError('NEXO1511', "A do block holds a body: write 'do return … end'", this.#token.start);
412
+ }
413
+
414
+ const body = this.#parseBody();
415
+
416
+ if (!this.#isKeyword('end')) {
417
+ throw this.#createAfterBodyError('NEXO1029', "'end' closing the do block");
418
+ }
419
+
420
+ const node = Parser.#node('do-expression', [doKeyword, body, this.#take()]);
421
+
422
+ this.#refuseChainFrom('do-expression');
423
+
424
+ return node;
425
+ }
426
+
427
+ /** Reads a simple value expression: a range, a disjunction, or an or-operand alone, a coalescing operator among them, which stands beside neither `and` nor `or`. */
428
+ #parseSimpleValueExpression() {
429
+ if (this.#valueDepth >= Limits.nesting) {
430
+ throw this.#createSyntaxError('NEXO1072', `A script nests at most ${Limits.nesting} levels, and this value stands deeper`, this.#token.start);
431
+ }
432
+
433
+ this.#valueDepth++;
434
+
435
+ try {
436
+ return this.#parseNestedSimpleValueExpression();
437
+ } finally {
438
+ this.#valueDepth--;
439
+ }
440
+ }
441
+
442
+ /** Reads a simple value expression where it stands within the nesting `#parseSimpleValueExpression` counts. */
443
+ #parseNestedSimpleValueExpression() {
444
+ // A range's `..` stands after its start, or begins it where its start is left out.
445
+ if (this.#token.type === '..') {
446
+ return this.#parseRange(null);
447
+ }
448
+
449
+ const value = this.#parseLogical('or', () => this.#parseOrOperand());
450
+
451
+ return this.#token.type === '..' ? this.#parseRange(value) : value;
452
+ }
453
+
454
+ /**
455
+ * Reads a range: its start, the position read before its `..` or none, its `..`, and its end where a position follows. A boundary is an arithmetic negation, a group making any operator expression one, so an
456
+ * ungrouped operator expression is no boundary (NEXO1069), and a range is no range's boundary (NEXO1070).
457
+ */
458
+ #parseRange(start) {
459
+ if (start !== null && OPERATOR_KINDS.has(start.kind)) {
460
+ const written = this.#text.slice(SyntaxTree.startOf(start), SyntaxTree.endOf(start));
461
+
462
+ throw this.#createSyntaxError('NEXO1069', `A range's boundary is a position, a number or an index, never an operator expression: write '(${written})..' to bound it by its value`, SyntaxTree.startOf(start));
463
+ }
464
+
465
+ const dots = this.#take();
466
+ const end = BOUNDARY_STARTS.has(this.#token.type) || this.#isKeyword('try') ? this.#parseArithmeticNegation() : null;
467
+
468
+ if (end !== null && OPERATOR_TOKENS.has(this.#token.type)) {
469
+ throw this.#createSyntaxError('NEXO1069', `A range's boundary is a position, a number or an index, never an operator expression: group its end, as ..(…)`, SyntaxTree.startOf(end));
470
+ }
471
+
472
+ if (this.#token.type === '..') {
473
+ throw this.#createSyntaxError('NEXO1070', "A range has two boundaries, and is no range's boundary: write one range, or group one as a value", this.#token.start);
474
+ }
475
+
476
+ return Parser.#node('range-expression', [...(start === null ? [] : [start]), dots, ...(end === null ? [] : [end])]);
477
+ }
478
+
479
+ /** Reads an or-operand: a conjunction, or an and-operand alone. */
480
+ #parseOrOperand() {
481
+ return this.#parseLogical('and', () => this.#parseAndOperand());
482
+ }
483
+
484
+ /** Reads an and-operand: a negation, an equality, a comparison, or an equality operand alone. */
485
+ #parseAndOperand() {
486
+ return this.#isKeyword('not') ? this.#parseNegation() : this.#parseEquality();
487
+ }
488
+
489
+ /** Reads a negation: `not` and what it negates, another negation, an equality, a comparison or an equality operand, so it associates to the right; a coalescing operator stands beside it only where a group holds it. */
490
+ #parseNegation() {
491
+ const notKeyword = this.#take();
492
+ const operand = this.#parseAndOperand();
493
+
494
+ if (this.#ungroupedCoalescings.has(operand)) {
495
+ throw this.#createSyntaxError('NEXO1021', "'??' and 'not' stand apart, one of them grouped: write 'not (… ?? …)' or '(not …) ?? …'", SyntaxTree.startOf(operand));
496
+ }
497
+
498
+ return Parser.#node('negation', [notKeyword, operand]);
499
+ }
500
+
501
+ /**
502
+ * Reads a conditional after its `if`, which the children begin with: each clause's test, a condition or a conditional binding, `let`, a name and a value, and the expression `then` gives it, an `elseif` beginning the next clause,
503
+ * and the alternative `else` gives, which every conditional ends with.
504
+ */
505
+ #parseConditionalExpression(children) {
506
+ const branches = [];
507
+
508
+ for (;;) {
509
+ if (this.#isKeyword('if')) {
510
+ throw this.#createSyntaxError('NEXO1027', 'A conditional that is a condition is enclosed in parentheses: write if (if … then … else … end) then …', this.#token.start);
511
+ }
512
+
513
+ children.push(this.#isKeyword('let') ? this.#parseConditionalBinding() : this.#parseSimpleValueExpression());
514
+
515
+ if (!this.#isKeyword('then')) {
516
+ throw this.#createUnexpectedTokenError('NEXO1000', "'then' after the condition");
517
+ }
518
+
519
+ const thenKeyword = this.#take();
520
+ const consequent = this.#parseBody();
521
+
522
+ children.push(thenKeyword, consequent);
523
+ branches.push(consequent);
524
+
525
+ if (this.#isKeyword('elseif')) {
526
+ children.push(this.#take());
527
+ } else if (this.#isKeyword('else')) {
528
+ const elseKeyword = this.#take();
529
+ const alternative = this.#parseBody();
530
+
531
+ children.push(elseKeyword, alternative);
532
+ branches.push(alternative);
533
+
534
+ if (!this.#isKeyword('end')) {
535
+ throw this.#createAfterBodyError('NEXO1029', "'end' closing the conditional");
536
+ }
537
+
538
+ // Every branch of one conditional gives its result with `return`, or none does; an `else` whose expression is a conditional, `else if … end`, follows branches of expressions in either style, and branches that return in theirs, as an `elseif` would.
539
+ const isExplicit = branch => !SyntaxTree.isToken(branch) && branch.kind === 'body';
540
+ const fits = branch => (branch === alternative && !SyntaxTree.isToken(branch) && branch.kind === 'conditional-expression'
541
+ ? !isExplicit(branches[0]) || isExplicit(branch.children[3])
542
+ : isExplicit(branch) === isExplicit(branches[0]));
543
+ const differing = branches.find(branch => !fits(branch));
544
+
545
+ if (differing !== undefined) {
546
+ throw this.#createSyntaxError('NEXO1054', 'All branches of this conditional must give their results with return, or none of them', SyntaxTree.startOf(isExplicit(differing) ? branches[0] : differing));
547
+ }
548
+
549
+ children.push(this.#take());
550
+ this.#refuseChainFrom('conditional-expression');
551
+
552
+ return Parser.#node('conditional-expression', children);
553
+ } else {
554
+ throw this.#createAfterBodyError('NEXO1505', "'elseif' or 'else', which every conditional ends with");
555
+ }
556
+ }
557
+ }
558
+
559
+ /** Reads a conditional's binding after its `if` or `elseif`: `let`, its name, `=` and the value it binds, a simple value expression. */
560
+ #parseConditionalBinding() {
561
+ const letKeyword = this.#take();
562
+
563
+ if (!this.#startsEntry()) {
564
+ this.#refuseName('NEXO1051', 'A conditional binding takes its value after =', 'A conditional binding is written with let, its name and = before its value, as if let mode = it.setting["mode"] then …', 'if let ');
565
+ }
566
+
567
+ return Parser.#node('conditional-binding', [letKeyword, this.#take(), this.#take(), this.#parseSimpleValueExpression()]);
568
+ }
569
+
570
+ /**
571
+ * Reads an additive expression and the one operator that may follow it here: `==` or `!=` as an equality, comparison operators as a comparison chain, `??` as a coalescing operator, recorded in `#ungroupedCoalescings`,
572
+ * or the additive expression alone; it refuses an equality that chains or stands beside `??` or a comparison. The grammar places `coalescing` above the logical operators, and `??` is read here, after its first operand,
573
+ * so that what stands beside it without a group is refused.
574
+ */
575
+ #parseEquality() {
576
+ this.#refuseOperandStart(this.#token.start);
577
+
578
+ const left = this.#parseAdditive();
579
+
580
+ if (this.#token.type === '??') {
581
+ return this.#parseCoalescingOperator(left);
582
+ }
583
+
584
+ if (this.#isComparisonOperator()) {
585
+ return this.#parseComparison(left);
586
+ }
587
+
588
+ if (!this.#isEqualityOperator()) {
589
+ return left;
590
+ }
591
+
592
+ const operator = this.#take();
593
+
594
+ this.#refuseOperandStart(this.#token.start);
595
+
596
+ const equality = Parser.#node('equality', [left, operator, this.#parseAdditive()]);
597
+
598
+ if (this.#token.type === '??') {
599
+ throw this.#createSyntaxError('NEXO1021', `'??' and '${operator.type}' stand apart, one of them grouped: write '(… ${operator.type} …) ?? …' or '… ${operator.type} (… ?? …)'`, this.#token.start);
600
+ }
601
+
602
+ if (this.#isEqualityOperator()) {
603
+ throw this.#createSyntaxError('NEXO1020', "An equality compares two values and does not chain: write '(… == …) == …' to compare its result", this.#token.start);
604
+ }
605
+
606
+ if (this.#isComparisonOperator()) {
607
+ throw this.#createSyntaxError('NEXO1052', Parser.#apartMessage(operator.type, this.#token.type), this.#token.start);
608
+ }
609
+
610
+ return equality;
611
+ }
612
+
613
+ /**
614
+ * Reads a comparison after its first operand: each comparison operator and the equality operand it compares with, so `0 < width <= 600` compares `width` with both, every operator of the chain pointing one way.
615
+ * A comparison stands beside an equality or a coalescing operator only where a group holds one of them.
616
+ */
617
+ #parseComparison(first) {
618
+ const children = [first];
619
+ const direction = COMPARISON_DIRECTIONS[this.#token.type];
620
+
621
+ while (this.#isComparisonOperator()) {
622
+ if (COMPARISON_DIRECTIONS[this.#token.type] !== direction) {
623
+ const previous = children.at(-2).type;
624
+
625
+ throw this.#createSyntaxError('NEXO1019', `A comparison chain keeps one direction, and '${this.#token.type}' turns it from '${previous}': join two comparisons with 'and', as in 'a ${previous} b and b ${this.#token.type} c'`, this.#token.start);
626
+ }
627
+
628
+ children.push(this.#take());
629
+ this.#refuseOperandStart(this.#token.start);
630
+ children.push(this.#parseAdditive());
631
+ }
632
+
633
+ const operator = children[1].type;
634
+
635
+ if (this.#token.type === '??' || this.#isEqualityOperator()) {
636
+ throw this.#createSyntaxError(this.#token.type === '??' ? 'NEXO1021' : 'NEXO1052', Parser.#apartMessage(operator, this.#token.type), this.#token.start);
637
+ }
638
+
639
+ return Parser.#node('comparison', children);
640
+ }
641
+
642
+ /** The message for two operators that stand apart, the first written before the second with no group holding either, giving the two ways to group them. */
643
+ static #apartMessage(first, second) {
644
+ return `'${first}' and '${second}' stand apart, one of them grouped: write '(… ${first} …) ${second} …' or '… ${first} (… ${second} …)'`;
645
+ }
646
+
647
+ /** Whether the current token compares the value before it: `==` or `!=`. */
648
+ #isEqualityOperator() {
649
+ return this.#token.type === '==' || this.#token.type === '!=';
650
+ }
651
+
652
+ /** Whether the current token orders the value before it with the one after: `<`, `<=`, `>` or `>=`. */
653
+ #isComparisonOperator() {
654
+ return Object.hasOwn(COMPARISON_DIRECTIONS, this.#token.type);
655
+ }
656
+
657
+ /**
658
+ * Reads an attempt after its `try`: the one conversion or cast the equality operand that follows ends with, which answers null where it rejects its value.
659
+ * It attempts that operation alone, so a conversion of a conversion or of a cast is grouped to say which it attempts, and anything else is refused.
660
+ */
661
+ #parseAttempt(start) {
662
+ const tryKeyword = this.#take();
663
+
664
+ if (this.#isKeyword('try')) {
665
+ throw this.#createSyntaxError('NEXO1022', "'try' attempts a conversion or a cast, and no other attempt: write 'try … to type' or 'try … as type'", this.#token.start);
666
+ }
667
+
668
+ const operation = this.#parseEqualityOperand();
669
+ const kind = Parser.#syntaxKindOf(operation);
670
+
671
+ if (kind !== 'conversion' && kind !== 'cast') {
672
+ throw this.#createSyntaxError('NEXO1022', "'try' attempts a conversion or a cast: write 'try … to type' or 'try … as type'", start);
673
+ }
674
+
675
+ if (this.#chainedConversions.has(operation)) {
676
+ const [target] = operation.children;
677
+ const inner = `${target.kind === 'cast' ? 'as' : 'to'} ${SyntaxTree.typeNameOf(target.children[2], this.#text)}`;
678
+
679
+ throw this.#createSyntaxError('NEXO1022', `'try' attempts one operation: group the value it converts, as in 'try (… ${inner}) to ${SyntaxTree.typeNameOf(operation.children[2], this.#text)}'`, start);
680
+ }
681
+
682
+ return Parser.#node('attempt', [tryKeyword, operation]);
683
+ }
684
+
685
+ /** Refuses what an equality operand cannot start with: an equality or a comparison operator, which compares the value written before it, and a conditional, which is grouped where it is an operand. */
686
+ #refuseOperandStart(start) {
687
+ if (this.#isEqualityOperator() || this.#isComparisonOperator()) {
688
+ throw this.#createSyntaxError('NEXO1013', `'${this.#token.type}' compares the value written before it: write 'it ${this.#token.type} …' to compare the implicit parameter`, start);
689
+ }
690
+
691
+ if (this.#isKeyword('if')) {
692
+ throw this.#createSyntaxError('NEXO1027', 'A conditional that is an operand is enclosed in parentheses: write (if … then … else … end)', start);
693
+ }
694
+
695
+ if (this.#isKeyword('do')) {
696
+ throw this.#createSyntaxError('NEXO1027', 'A do block that is an operand is enclosed in parentheses: write (do … end)', start);
697
+ }
698
+
699
+ if (this.#isKeyword('function')) {
700
+ throw this.#createSyntaxError('NEXO1034', 'A function is the value of a local: write let name = function(…) … end, and call it by its name', start);
701
+ }
702
+
703
+ if (this.#startsArrowFunction()) {
704
+ throw this.#createSyntaxError('NEXO1057', 'An arrow function is the value of a local or an argument: write let name = … => …, or pass it to a call', start);
705
+ }
706
+ }
707
+
708
+ /** Reads a coalescing operator after its value: `??` and its fallback, an equality operand or another coalescing operator, so it associates to the right; binary arithmetic stands beside it only where a group holds one of them. */
709
+ #parseCoalescingOperator(value) {
710
+ this.#refuseArithmeticBeside(value, true);
711
+
712
+ const operator = this.#take();
713
+ const fallbackStart = this.#token.start;
714
+
715
+ if (this.#isKeyword('not')) {
716
+ throw this.#createSyntaxError('NEXO1021', "'??' and 'not' stand apart, one of them grouped: write '… ?? (not …)'", fallbackStart);
717
+ }
718
+
719
+ this.#refuseOperandStart(fallbackStart);
720
+
721
+ const fallbackValue = this.#parseAdditive();
722
+
723
+ this.#refuseArithmeticBeside(fallbackValue, false);
724
+
725
+ const fallback = this.#token.type === '??' ? this.#parseCoalescingOperator(fallbackValue) : fallbackValue;
726
+
727
+ if (this.#isEqualityOperator() || this.#isComparisonOperator()) {
728
+ throw this.#createSyntaxError('NEXO1021', Parser.#apartMessage('??', this.#token.type), this.#token.start);
729
+ }
730
+
731
+ const node = Parser.#node('coalescing', [value, operator, fallback]);
732
+
733
+ this.#ungroupedCoalescings.add(node);
734
+
735
+ return node;
736
+ }
737
+
738
+ /** Refuses binary arithmetic no group holds as a side of `??`, which stands apart from it, as it does from `and`, `or` and an equality; an arithmetic negation is no binary arithmetic, so `value ?? -1` stands. */
739
+ #refuseArithmeticBeside(side, isValue) {
740
+ if (side.kind !== 'additive' && side.kind !== 'multiplicative') {
741
+ return;
742
+ }
743
+
744
+ const operator = side.children[1].type;
745
+ const grouped = isValue ? `'(… ${operator} …) ?? …' or '… ${operator} (… ?? …)'` : `'(… ?? …) ${operator} …' or '… ?? (… ${operator} …)'`;
746
+
747
+ throw this.#createSyntaxError('NEXO1021', `'??' and '${operator}' stand apart, one of them grouped: write ${grouped}`, side.children[1].start);
748
+ }
749
+
750
+ /** Reads an additive expression: multiplicative expressions joined by `+` and `-`, left to right; the one alone where none follows. */
751
+ #parseAdditive() {
752
+ return this.#parseBinary('additive', ['+', '-'], () => this.#parseMultiplicative());
753
+ }
754
+
755
+ /** Reads a multiplicative expression: arithmetic negations joined by `*`, `/` and `%`, left to right; the one alone where none follows. */
756
+ #parseMultiplicative() {
757
+ return this.#parseBinary('multiplicative', ['*', '/', '%'], () => this.#parseArithmeticNegation());
758
+ }
759
+
760
+ /** Reads operands the operators join, left to right, as the node of the kind, or the operand alone where no operator follows it. */
761
+ #parseBinary(kind, operators, parseOperand) {
762
+ const first = parseOperand();
763
+
764
+ if (!operators.includes(this.#token.type)) {
765
+ return first;
766
+ }
767
+
768
+ const children = [first];
769
+
770
+ while (operators.includes(this.#token.type)) {
771
+ children.push(this.#take());
772
+ this.#refuseOperandStart(this.#token.start);
773
+ children.push(parseOperand());
774
+ }
775
+
776
+ return Parser.#node(kind, children);
777
+ }
778
+
779
+ /** Reads an arithmetic negation: `-` and what it negates, another arithmetic negation or an equality operand, so `-x to Number` negates the converted value; or an equality operand alone. */
780
+ #parseArithmeticNegation() {
781
+ // An index from the end: `^` and its offset, an equality operand, never another index.
782
+ if (this.#token.type === '^') {
783
+ const caret = this.#take();
784
+
785
+ this.#refuseOperandStart(this.#token.start);
786
+
787
+ return Parser.#node('from-end-index', [caret, this.#parseEqualityOperand()]);
788
+ }
789
+
790
+ if (this.#token.type !== '-') {
791
+ return this.#parseEqualityOperand();
792
+ }
793
+
794
+ const minus = this.#take();
795
+
796
+ this.#refuseOperandStart(this.#token.start);
797
+
798
+ return Parser.#node('arithmetic-negation', [minus, this.#parseArithmeticNegation()]);
799
+ }
800
+
801
+ /** Reads an equality operand: a chain, the cast that may end it, and the conversions that follow, each converting what is before it. */
802
+ #parseEqualityOperand() {
803
+ const start = this.#token.start;
804
+
805
+ if (this.#isKeyword('try')) {
806
+ return this.#parseAttempt(start);
807
+ }
808
+
809
+ const chain = this.#parseChain();
810
+ let value = chain.node;
811
+
812
+ if (this.#isKeyword('as')) {
813
+ value = this.#parseChainCast(value, chain, start);
814
+ }
815
+
816
+ while (this.#isKeyword('to')) {
817
+ const converted = this.#parseConversion(value);
818
+
819
+ // A conversion of a conversion or a cast before it in this chain is written ungrouped, which an attempt of one operation refuses.
820
+ if (value.kind === 'conversion' || value.kind === 'cast') {
821
+ this.#chainedConversions.add(converted);
822
+ }
823
+
824
+ value = converted;
825
+ }
826
+
827
+ this.#refuseNameAfterOperand(value, start);
828
+
829
+ return value;
830
+ }
831
+
832
+ /**
833
+ * Refuses a name after the value, where nothing joins it to the value: one `=` follows names the next object field, which a comma comes before, and any other is a member written after its receiver without the dot
834
+ * that joins it, so the message gives the call to write, of the receiver in a group where it is a cast or a conversion, which a chain continues from only so. A name on a line of its own after a body local's value ends
835
+ * that value, and its body says what the name begins.
836
+ */
837
+ #refuseNameAfterOperand(value, start) {
838
+ if (this.#token.type !== 'identifier') {
839
+ return;
840
+ }
841
+
842
+ if (this.#bodyLocalsBeingRead > 0 && Tokenizer.holdsLineBreak(this.#text, this.#tokens[this.#index - 1].end, this.#token.start)) {
843
+ return;
844
+ }
845
+
846
+ if (this.#nextToken.type === '=') {
847
+ throw this.#createUnexpectedTokenError('NEXO1000', "',' before the next object field");
848
+ }
849
+
850
+ const written = this.#text.slice(start, this.#tokens[this.#index - 1].end);
851
+ const kind = Parser.#syntaxKindOf(value);
852
+ const receiver = kind === 'cast' || kind === 'conversion' ? `(${written})` : written;
853
+ // A name an argument or an options object follows is a call written without its dot, and any other a property access.
854
+ const next = this.#nextToken;
855
+ const arguments_ = next.type === '(' || next.type === '{' || ARGUMENT_STARTS.has(next.type) ? '(…)' : '';
856
+
857
+ throw this.#createSyntaxError('NEXO1011', `A member follows its receiver after a dot: write '${receiver}.${this.#token.value}${arguments_}'`, this.#token.start);
858
+ }
859
+
860
+ /** Whether the keyword `to` and a type's name stand here, which only a conversion writes, after the value it converts, or a string written in the name's place. */
861
+ #startsConversion() {
862
+ const next = this.#nextToken;
863
+
864
+ return this.#isKeyword('to') && (this.#isWord(next) || (next.type === 'literal' && typeof next.value === 'string'));
865
+ }
866
+
867
+ /** Reads a conversion after the value it converts: `to` and the name of the type it converts to, written bare. It ends its chain, and another conversion may follow it. */
868
+ #parseConversion(target) {
869
+ const toKeyword = this.#take();
870
+ const typeName = this.#parseTypeName('to', 'converts to', 'Number');
871
+ const node = Parser.#node('conversion', [target, toKeyword, typeName]);
872
+ const name = SyntaxTree.typeNameOf(typeName, this.#text);
873
+
874
+ if (this.#continuesChain()) {
875
+ throw this.#createSyntaxError('NEXO1501', `A conversion ends its chain; group it to continue from its result, as in '(… to ${name}).value'`, this.#token.start);
876
+ }
877
+
878
+ if (this.#isKeyword('as')) {
879
+ throw this.#createSyntaxError('NEXO1025', `A conversion's result is cast only in a group: write '(… to ${name}) as …'`, this.#token.start);
880
+ }
881
+
882
+ return node;
883
+ }
884
+
885
+ /**
886
+ * Reads the name of the type a cast or a conversion names after its keyword, written bare: any word, a reserved one included, as a member's name is after its dot, qualified by its module's name and a dot where one follows it,
887
+ * which makes a type-name node of the three. A string in its place is refused with the bare name to write, and anything else with the form the keyword takes, as type arguments written after the name are.
888
+ */
889
+ #parseTypeName(keyword, what, example) {
890
+ const typeName = this.#parseBareTypeName(keyword, what, example);
891
+
892
+ this.#refuseTypeArguments(keyword, what, SyntaxTree.typeNameOf(typeName, this.#text));
893
+
894
+ return typeName;
895
+ }
896
+
897
+ #parseBareTypeName(keyword, what, example) {
898
+ const token = this.#token;
899
+
900
+ if (token.type === 'literal' && typeof token.value === 'string') {
901
+ const bare = Names.isIdentifier(token.value) ? token.value : example;
902
+
903
+ throw this.#createSyntaxError('NEXO1026', `'${keyword}' names the type it ${what} bare, as a member's name is written: write '${keyword} ${bare}'`, token.start);
904
+ }
905
+
906
+ if (!this.#isWord(token)) {
907
+ throw this.#createUnexpectedTokenError('NEXO1000', `the name of the type it ${what}, as in ${keyword} ${example}`);
908
+ }
909
+
910
+ const name = this.#take();
911
+
912
+ if (this.#token.type !== '.') {
913
+ return name;
914
+ }
915
+
916
+ const dot = this.#take();
917
+
918
+ if (!this.#isWord(this.#token)) {
919
+ throw this.#createUnexpectedTokenError('NEXO1000', `the name of the type '${SyntaxTree.wordOf(name, this.#text)}' declares, as in ${keyword} ${SyntaxTree.wordOf(name, this.#text)}.${example}`);
920
+ }
921
+
922
+ return Parser.#node('type-name', [name, dot, this.#take()]);
923
+ }
924
+
925
+ /**
926
+ * Refuses type arguments written after the name a cast or a conversion names, `List<Number>`, which no type either names takes. Angle brackets are read as type arguments only where reading them as comparisons gives
927
+ * no expression: one argument, which turns the comparisons' direction, or several where the `>` closing them is followed by no operand, as `f(a to D<b, c>d)` passes two comparisons.
928
+ */
929
+ #refuseTypeArguments(keyword, what, name) {
930
+ const token = this.#token;
931
+
932
+ if (token.type !== '<') {
933
+ return;
934
+ }
935
+
936
+ let depth = 0;
937
+ let separated = false;
938
+ let index = this.#index;
939
+
940
+ for (; index < this.#tokens.length; index++) {
941
+ const { type } = this.#tokens[index];
942
+
943
+ if (type === '<' || type === '>') {
944
+ depth += type === '<' ? 1 : -1;
945
+ } else if (type === ',') {
946
+ separated ||= depth === 1;
947
+ } else if (type !== '.' && !this.#isWord(this.#tokens[index])) {
948
+ return;
949
+ }
950
+
951
+ if (depth === 0) {
952
+ break;
953
+ }
954
+ }
955
+
956
+ const following = this.#tokens[index + 1];
957
+
958
+ if (depth !== 0 || (separated && following !== undefined && (BOUNDARY_STARTS.has(following.type) || (following.type === 'keyword' && following.value === 'try')))) {
959
+ return;
960
+ }
961
+
962
+ const written = this.#text.slice(token.start, this.#tokens[index].end);
963
+
964
+ throw this.#createSyntaxError('NEXO1026', `'${keyword}' names the type it ${what} bare, and '${name}${written}' writes type arguments, which no type it names takes`, token.start);
965
+ }
966
+
967
+ /**
968
+ * Reads a chain: a chain input and the members and object expressions that continue it, and what a cast after it needs to know of it: the syntax kind of its input, the syntax kind of what a group it starts with holds,
969
+ * and whether anything continues it or an object expression is among what does.
970
+ */
971
+ #parseChain() {
972
+ const { start } = this.#token;
973
+ const inputKind = this.#token.type === '(' ? 'parenthesized-expression' : this.#token.type === '(?' ? 'optional-enclosed-access' : 'input';
974
+ const first = this.#parseChainInput();
975
+ const groupedKind = Parser.#syntaxKindOf(first);
976
+
977
+ this.#refuseChainFrom(groupedKind);
978
+ const children = [first];
979
+ let hasObject = false;
980
+
981
+ for (;;) {
982
+ if (this.#token.type === '[' || this.#token.type === '?[') {
983
+ children.push(this.#parseIndexer());
984
+ } else if (this.#token.type === ':' || this.#token.type === '?:') {
985
+ children.push(this.#parseExtensionAccess(start));
986
+ } else if (this.#token.type === '.' || this.#token.type === '?.') {
987
+ children.push(this.#parseAfterDot(this.#take()));
988
+ } else if (this.#token.type === '{') {
989
+ children.push(this.#parseObject());
990
+ hasObject = true;
991
+ } else {
992
+ const node = children.length === 1 ? first : Parser.#node('chain', children);
993
+
994
+ return { node, inputKind, groupedKind, isContinued: children.length > 1, hasObject };
995
+ }
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Reads a cast after its target: `as` and the name of the type it casts to, written bare. Its target is a receiver and the members after it, a coalescing operator in parentheses, or an optional enclosed access, and a cast continues a chain only from a group.
1001
+ */
1002
+ #parseChainCast(target, chain, start) {
1003
+ const isTarget = !chain.hasObject && (chain.inputKind === 'input'
1004
+ || (chain.inputKind === 'parenthesized-expression' && (chain.groupedKind === 'cast' || (chain.groupedKind === 'coalescing' && !chain.isContinued)))
1005
+ || (chain.inputKind === 'optional-enclosed-access' && !chain.isContinued));
1006
+
1007
+ if (chain.inputKind === 'parenthesized-expression' && chain.groupedKind === 'attempt' && !chain.isContinued) {
1008
+ throw this.#createSyntaxError('NEXO1023', "An attempt is no cast's target: cast what it attempts, or attempt the cast, as in 'try a as Light'", start);
1009
+ }
1010
+
1011
+ if (!isTarget) {
1012
+ throw this.#createSyntaxError('NEXO1025', `A cast's target is a receiver and the members after it, a coalescing operator in parentheses, or an optional enclosed access, and this is none of them`, start);
1013
+ }
1014
+
1015
+ return this.#parseCast(target);
1016
+ }
1017
+
1018
+ /** Reads `as` and the type's name after a cast's target, which nothing continues but a group. */
1019
+ #parseCast(target) {
1020
+ const asKeyword = this.#take();
1021
+ const typeName = this.#parseTypeName('as', 'casts to', 'Light');
1022
+ const node = Parser.#node('cast', [target, asKeyword, typeName]);
1023
+ const name = SyntaxTree.typeNameOf(typeName, this.#text);
1024
+
1025
+ if (this.#continuesChain()) {
1026
+ throw this.#createSyntaxError('NEXO1053', `A cast continues a chain only from a group: write '(… as ${name})…'`, this.#token.start);
1027
+ }
1028
+
1029
+ if (this.#isKeyword('as')) {
1030
+ throw this.#createSyntaxError('NEXO1024', `A cast's target holds a cast only in a group: write '(… as ${name}) as …'`, this.#token.start);
1031
+ }
1032
+
1033
+ return node;
1034
+ }
1035
+
1036
+ /** Reads a chain input: an input, a parenthesized expression, or an optional enclosed access. */
1037
+ #parseChainInput() {
1038
+ switch (this.#token.type) {
1039
+ case '(': {
1040
+ // A parenthesized expression holds the expression in its parentheses, which a chain may then continue from, and groups a coalescing operator it holds.
1041
+ const open = this.#take();
1042
+ const expression = this.#parseExpression();
1043
+ const close = this.#expect(')');
1044
+
1045
+ this.#ungroupedCoalescings.delete(expression);
1046
+
1047
+ return Parser.#node('parenthesized-expression', [open, expression, close]);
1048
+ }
1049
+ case '(?':
1050
+ return this.#parseOptionalEnclosedAccess();
1051
+ default:
1052
+ return this.#parseInput();
1053
+ }
1054
+ }
1055
+
1056
+ /** Reads an optional enclosed access: its member chain, a receiver followed by at least one member, every access of which is optional, which its closing parenthesis ends. */
1057
+ #parseOptionalEnclosedAccess() {
1058
+ const open = this.#take();
1059
+ const { children, accessCount } = this.#parseMemberExpressions(this.#parseReceiver());
1060
+
1061
+ if (accessCount === 0) {
1062
+ throw this.#createSyntaxError('NEXO1016', 'An optional enclosed access holds a receiver and at least one access after it, as (?room.device(name).brightness)', this.#token.start);
1063
+ }
1064
+
1065
+ if (this.#token.type !== ')') {
1066
+ throw this.#createUnexpectedTokenError('NEXO1016', "')' closing the optional enclosed access, which holds accesses and calls alone");
1067
+ }
1068
+
1069
+ return Parser.#node('optional-enclosed-access', [open, Parser.#node('member-chain', children), this.#take()]);
1070
+ }
1071
+
1072
+ /** Reads a receiver within an optional enclosed access: an input, or a grouped cast, its own target's accesses optional as the enclosed access's are, where its target is no group that keeps its own rules. */
1073
+ #parseReceiver() {
1074
+ if (this.#token.type === '(?') {
1075
+ throw this.#createSyntaxError('NEXO1502', 'The receiver of an optional enclosed access is an input or a grouped cast, and no other optional enclosed access, as in (?room.device(name).brightness)', this.#token.start);
1076
+ }
1077
+
1078
+ if (this.#token.type !== '(') {
1079
+ return this.#parseInput();
1080
+ }
1081
+
1082
+ const open = this.#take();
1083
+ const target = this.#parseCastTarget(this.#token.start);
1084
+
1085
+ if (!this.#isKeyword('as')) {
1086
+ throw this.#createSyntaxError('NEXO1502', 'A group as the receiver of an optional enclosed access holds a cast, as in (?(room.device(name) as Light).brightness)', this.#token.start);
1087
+ }
1088
+
1089
+ const cast = this.#parseCast(target);
1090
+
1091
+ return Parser.#node('receiver', [open, cast, this.#expect(')')]);
1092
+ }
1093
+
1094
+ /**
1095
+ * Reads the target of a cast that is the receiver of an optional enclosed access: a coalescing operator in parentheses or an optional enclosed access, each keeping its own rules as it does outside one,
1096
+ * or a receiver and the members after it, every one of them optional.
1097
+ */
1098
+ #parseCastTarget(start) {
1099
+ if (this.#token.type === '(?' || (this.#token.type === '(' && !this.#groupHoldsCast())) {
1100
+ const target = this.#parseChainInput();
1101
+ if (Parser.#syntaxKindOf(target) !== 'coalescing' && !Parser.#lowersToOptionalChain(target)) {
1102
+ throw this.#createSyntaxError('NEXO1025', `A cast's target is a receiver and the members after it, a coalescing operator in parentheses, or an optional enclosed access, and this is none of them`, start);
1103
+ }
1104
+
1105
+ if (this.#continuesChain() && this.#token.type !== '{') {
1106
+ throw this.#createSyntaxError('NEXO1025', 'A grouped target is cast as it is, with nothing after it but its cast, as in ((preferred ?? fallback) as Light)', this.#token.start);
1107
+ }
1108
+
1109
+ return target;
1110
+ }
1111
+
1112
+ const receiver = this.#parseReceiver();
1113
+ const { children, accessCount } = this.#parseMemberExpressions(receiver);
1114
+
1115
+ return accessCount > 0 ? Parser.#node('member-chain', children) : receiver;
1116
+ }
1117
+
1118
+ /**
1119
+ * Whether the group the current parenthesis opens holds a cast at its own level, outside any group, brackets or braces within it: one that holds `as` there and no `??` or other keyword,
1120
+ * since a cast's own level holds its target's receiver and accesses alone, while a coalescing operator's side or a conditional's branch may be a cast.
1121
+ */
1122
+ #groupHoldsCast() {
1123
+ let depth = 0;
1124
+ let holdsAs = false;
1125
+
1126
+ for (let index = this.#index; index < this.#tokens.length; index++) {
1127
+ const token = this.#tokens[index];
1128
+
1129
+ if (token.type === '(' || token.type === '(?' || token.type === '[' || token.type === '?[' || token.type === '{' || token.type === 'interpolation-start') {
1130
+ depth++;
1131
+ } else if (token.type === ')' || token.type === ']' || token.type === '}' || token.type === 'interpolation-end') {
1132
+ depth--;
1133
+
1134
+ if (depth === 0) {
1135
+ return holdsAs;
1136
+ }
1137
+ } else if (depth === 1 && token.type === '??') {
1138
+ return false;
1139
+ } else if (depth === 1 && token.type === 'keyword' && token.value !== 'to') {
1140
+ if (token.value !== 'as') {
1141
+ return false;
1142
+ }
1143
+
1144
+ // The type's name after `as`, and after `to` below, may be any word, a keyword among them, qualified or not, which tells nothing of the group.
1145
+ holdsAs = true;
1146
+ index = this.#lastOfTypeName(index + 1);
1147
+ } else if (depth === 1 && token.type === 'keyword') {
1148
+ index = this.#lastOfTypeName(index + 1);
1149
+ } else if (token.type === 'end') {
1150
+ return false;
1151
+ }
1152
+ }
1153
+
1154
+ return false;
1155
+ }
1156
+
1157
+ /** The index of the last token of the type's name the token at the index starts: the word itself, or, for a qualified name, the last word after its dots. */
1158
+ #lastOfTypeName(index) {
1159
+ let last = index;
1160
+
1161
+ while (this.#tokens[last + 1]?.type === '.' && this.#tokens[last + 2] !== undefined && this.#isWord(this.#tokens[last + 2])) {
1162
+ last += 2;
1163
+ }
1164
+
1165
+ return last;
1166
+ }
1167
+
1168
+ /** Reads the member expressions after a receiver within an optional enclosed access, every one of them optional, and answers the receiver followed by them, as `children`, and how many follow it, as `accessCount`; the member chain node is its caller's to build. */
1169
+ #parseMemberExpressions(receiver) {
1170
+ const children = [receiver];
1171
+ const start = SyntaxTree.startOf(receiver);
1172
+
1173
+ while (this.#continuesChain() && this.#token.type !== '{') {
1174
+ if (this.#token.type === '[' || this.#token.type === '?[') {
1175
+ children.push(this.#parseIndexer());
1176
+ } else if (this.#token.type === ':' || this.#token.type === '?:') {
1177
+ children.push(this.#parseExtensionAccess(start));
1178
+ } else {
1179
+ children.push(this.#parseAfterDot(this.#take()));
1180
+ }
1181
+ }
1182
+
1183
+ return { children, accessCount: children.length - 1 };
1184
+ }
1185
+
1186
+ /**
1187
+ * Reads an input: `it`, a literal, a name, which the lowering reads as a local, a field, the object root or a script parameter, a reserved word after `@`, which it reads as a local or a field, a script parameter after `~`,
1188
+ * a namespace access, which a module's name begins in text read with it, an object expression, or a collection expression, which a `[` begins where an input stands. A keyword alone keeps its meaning, so one where a
1189
+ * value stands gives the form with `@` for the local or the field of its name.
1190
+ */
1191
+ #parseInput() {
1192
+ const token = this.#token;
1193
+ const next = this.#nextToken;
1194
+
1195
+ // A `to` a keyword of a construct's structure follows, as `then` or `and`, names a local rather than converting, which the keyword's own refusal below says.
1196
+ if (this.#startsConversion() && !(next.type === 'keyword' && (STRUCTURAL_KEYWORDS.has(next.value) || next.value === 'and' || next.value === 'or'))) {
1197
+ throw this.#createSyntaxError('NEXO1010', `'to' converts the value written before it: write 'it to ${this.#text.slice(next.start, next.end)}' to convert the implicit parameter`, token.start);
1198
+ }
1199
+
1200
+ if (token.type === 'identifier' && this.#namespaceNames.has(token.value)) {
1201
+ return this.#parseNamespaceAccess();
1202
+ }
1203
+
1204
+ // A name that starts a chain is a local, a field or a conditional binding, which takes no argument, so one an argument follows after a space is a member written without its receiver; parentheses after it call the function it holds.
1205
+ if (token.type === 'identifier' || token.type === 'lexical-reference') {
1206
+ const written = token.type === 'identifier' ? token.value : `@${token.value}`;
1207
+
1208
+ this.#refuseEnclosedAccessArgument(next, written);
1209
+
1210
+ if (next.type === '(') {
1211
+ return this.#parseFunctionCall(written);
1212
+ }
1213
+
1214
+ // A body's local ends at a line break, so what the next line starts is the body's to read, as a name after an operand is.
1215
+ const endsLocal = this.#bodyLocalsBeingRead > 0 && Tokenizer.holdsLineBreak(this.#text, token.end, next.start);
1216
+
1217
+ if (token.type === 'identifier' && next.spaced && ARGUMENT_STARTS.has(next.type) && !endsLocal) {
1218
+ throw this.#createSyntaxError('NEXO1503', `'${token.value}' follows no receiver: write 'it.${token.value}(…)' for a member of the implicit parameter`, token.start);
1219
+ }
1220
+ }
1221
+
1222
+ switch (token.type) {
1223
+ case 'it':
1224
+ case 'literal':
1225
+ case 'identifier':
1226
+ case 'script-parameter':
1227
+ case 'lexical-reference':
1228
+ return this.#take();
1229
+ case '{':
1230
+ return this.#parseObject();
1231
+ case '[':
1232
+ return this.#parseCollectionExpression();
1233
+ case 'interpolated-string-start':
1234
+ return this.#parseInterpolatedString();
1235
+ case 'keyword':
1236
+ if (token.value === 'import') {
1237
+ throw this.#createSyntaxError('NEXO1045', "Imports stand at the start of the script, before the first local or expression of its body", token.start);
1238
+ }
1239
+
1240
+ if (!STRUCTURAL_KEYWORDS.has(token.value)) {
1241
+ // A `return`, or a `let` and the name of the local it declares, where a value stands is a declaration or a result written where only a value may stand.
1242
+ const declares = token.value === 'return' || (token.value === 'let' && this.#isWord(next));
1243
+ // A keyword an import gives a module as its local name reads that namespace after `@` where no local of its name is in scope.
1244
+ const binding = this.#namespaceNames.has(token.value) ? `, or, where none is in scope, the namespace the script's import gives that name` : '';
1245
+
1246
+ if (declares) {
1247
+ const message = token.value === 'let'
1248
+ ? "'let' declares a local, which stands in a body before its result, never where a value stands: declare the local before the expression that reads it"
1249
+ : "'return' gives a body's result, which ends a body, never where a value stands: write the value itself here";
1250
+
1251
+ throw this.#createSyntaxError('NEXO1032', message, token.start);
1252
+ }
1253
+
1254
+ throw this.#createSyntaxError('NEXO1010', `Found '${token.value}', a keyword: write '@${token.value}' for the local, object field, conditional binding or function parameter of that name${binding}`, token.start);
1255
+ }
1256
+
1257
+ throw this.#createUnexpectedTokenError('NEXO1010', 'a value');
1258
+ default:
1259
+ throw this.#createUnexpectedTokenError('NEXO1000', 'a value');
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ * Reads a namespace access: a module's name, its dot and the name of one of the module's members, and the arguments of a call through it where parentheses follow. A namespace is never null and has no indexer,
1265
+ * so only `.` follows it, and a module's local name alone, which reads no value, gives the forms that read the local and the script parameter of its name. A computed property access after it is read by the chain,
1266
+ * as one after `@` and its name is, so the namespace access is its name alone.
1267
+ */
1268
+ #parseNamespaceAccess() {
1269
+ const namespaceName = this.#take();
1270
+ const name = namespaceName.value;
1271
+
1272
+ if (this.#token.type === '.' && this.#nextToken.type === '[') {
1273
+ return Parser.#node('namespace-access', [namespaceName]);
1274
+ }
1275
+
1276
+ if (this.#token.type !== '.') {
1277
+ const isOptional = this.#token.type === '?.';
1278
+ const access = isOptional ? `A namespace is never null, so its members are read after '.': write '${name}.…'` : `'${name}' is the namespace of the module the script imports under that name, whose members are read after a dot, as ${name}.member`;
1279
+
1280
+ throw this.#createSyntaxError('NEXO1506', `${access}; write '@${name}' for the local, object field, conditional binding or function parameter of that name, or '~${name}' for the script parameter`, isOptional ? this.#token.start : namespaceName.start);
1281
+ }
1282
+
1283
+ const dot = this.#take();
1284
+
1285
+ if (this.#token.type === 'lexical-reference') {
1286
+ throw this.#createSyntaxError('NEXO1008', `A member is named as it is written after its dot, with no '@': write '${name}.${this.#token.value}'`, this.#token.start);
1287
+ }
1288
+
1289
+ const nameToken = this.#isWord(this.#token) ? this.#take() : this.#expect('identifier');
1290
+ const written = `${name}.${SyntaxTree.wordOf(nameToken, this.#text)}`;
1291
+
1292
+ this.#refuseEnclosedAccessArgument(this.#token, written);
1293
+
1294
+ if (this.#token.type !== '(') {
1295
+ return Parser.#node('namespace-access', [namespaceName, dot, nameToken]);
1296
+ }
1297
+
1298
+ return Parser.#node('namespace-access', [namespaceName, dot, nameToken, ...this.#parseArguments(written, true)]);
1299
+ }
1300
+
1301
+ /** Reads an interpolated string: its `$"`, its runs of text and its interpolations, in the order written, and its closing quote. */
1302
+ #parseInterpolatedString() {
1303
+ const children = [this.#take()];
1304
+
1305
+ for (;;) {
1306
+ switch (this.#token.type) {
1307
+ case 'interpolated-text':
1308
+ children.push(this.#take());
1309
+ break;
1310
+ case 'interpolation-start':
1311
+ children.push(this.#parseInterpolation());
1312
+ break;
1313
+ case 'interpolated-string-end':
1314
+ children.push(this.#take());
1315
+
1316
+ return Parser.#node('interpolated-string', children);
1317
+ default:
1318
+ throw this.#createUnexpectedTokenError('NEXO1000', `'"' closing the interpolated string`);
1319
+ }
1320
+ }
1321
+ }
1322
+
1323
+ /** Reads an interpolation: the `{` that opens it, the one expression it holds and the `}` that closes it. */
1324
+ #parseInterpolation() {
1325
+ const open = this.#take();
1326
+
1327
+ if (this.#token.type === 'interpolation-end') {
1328
+ throw this.#createSyntaxError('NEXO1039', "An interpolation holds an expression between '{' and '}'", open.start);
1329
+ }
1330
+
1331
+ const expression = this.#parseExpression();
1332
+
1333
+ if (this.#token.type !== 'interpolation-end') {
1334
+ throw this.#createUnexpectedTokenError('NEXO1000', "'}' closing the interpolation");
1335
+ }
1336
+
1337
+ return Parser.#node('interpolation', [open, expression, this.#take()]);
1338
+ }
1339
+
1340
+ /** Refuses a chain continuing from the conditional or the do block of the kind just read, grouped or not, whose value is no receiver: a local holds it first. */
1341
+ #refuseChainFrom(kind) {
1342
+ if ((kind === 'conditional-expression' || kind === 'do-expression') && this.#continuesChain()) {
1343
+ throw this.#createSyntaxError('NEXO1028', `${kind === 'do-expression' ? 'A do block' : 'A conditional'} is never the receiver of a chain: bind its value to a local first, and continue from the local`, this.#token.start);
1344
+ }
1345
+ }
1346
+
1347
+ /** Whether the current token continues a chain: an access, a computed property access, an indexer, each optional or not, or an object expression, or a colon, which the chain refuses. */
1348
+ #continuesChain() {
1349
+ return CHAIN_CONTINUATIONS.has(this.#token.type);
1350
+ }
1351
+
1352
+ /**
1353
+ * Reads an indexer: `[`, or `?[` where it is optional, and its one key, an expression, before `]`. The parser knows no indexer: the vocabulary says whether the access before the brackets names a named indexer
1354
+ * or a property whose answer the brackets index.
1355
+ */
1356
+ #parseIndexer() {
1357
+ const open = this.#take();
1358
+
1359
+ if (this.#token.type === ']') {
1360
+ throw this.#createSyntaxError('NEXO1017', 'An indexer takes one key in its brackets', this.#token.start);
1361
+ }
1362
+
1363
+ this.#refuseFunctionAsData("An indexer's key");
1364
+
1365
+ const key = this.#parseExpression();
1366
+
1367
+ if (this.#token.type === ',') {
1368
+ throw this.#createSyntaxError('NEXO1017', 'An indexer takes one key', this.#token.start);
1369
+ }
1370
+
1371
+ return Parser.#node('indexer', [open, key, this.#expect(']')]);
1372
+ }
1373
+
1374
+ /**
1375
+ * Reads an extension access: `:`, or `?:` where it is optional, its names, each after a dot but the first, and a call's arguments where parentheses follow its last. Which of its names is the member, which name its
1376
+ * extension and that extension's module, and which are ordinary accesses of the member's answer, is resolution's to read, so the text records them as written. A colon no name follows names no member, brackets after
1377
+ * it giving the unnamed indexer and the computed property access they may mean (NEXO1014).
1378
+ */
1379
+ #parseExtensionAccess(start) {
1380
+ const colon = this.#token;
1381
+ const written = this.#text.slice(start, colon.start);
1382
+
1383
+ if (!this.#isWord(this.#nextToken)) {
1384
+ const next = this.#nextToken;
1385
+ const isOptional = colon.type === '?:';
1386
+ const message = next.type === '[' || next.type === '?['
1387
+ ? `write '${written}${isOptional ? '?[' : '['}…]' for the unnamed indexer, or '${written}${isOptional ? '?.' : '.'}[…]' for a property read by a computed name`
1388
+ : `the name of the member it reads follows it${isOptional ? "; no '?:' chooses between two values, which '??' does" : ''}`;
1389
+
1390
+ throw this.#createSyntaxError('NEXO1014', `A colon in a chain begins an extension access: ${message}`, colon.start);
1391
+ }
1392
+
1393
+ const children = [this.#take(), this.#take()];
1394
+ const names = [children[1]];
1395
+
1396
+ // The text alone decides where the names end: every dot a word follows continues them.
1397
+ while (this.#token.type === '.' && this.#isWord(this.#nextToken)) {
1398
+ const name = this.#nextToken;
1399
+
1400
+ children.push(this.#take(), this.#take());
1401
+ names.push(name);
1402
+ }
1403
+
1404
+ const member = names.at(-1);
1405
+ const memberName = SyntaxTree.wordOf(member, this.#text);
1406
+
1407
+ this.#refuseEnclosedAccessArgument(this.#token, `${colon.type}${memberName}`);
1408
+
1409
+ if (this.#token.type === '(') {
1410
+ children.push(...this.#parseArguments(memberName, true));
1411
+ } else if (this.#token.type === 'literal') {
1412
+ throw this.#createSyntaxError('NEXO1508', `An argument after a member's name is enclosed in parentheses, as a call's is, '${colon.type}${memberName}(…)'`, this.#token.start);
1413
+ }
1414
+
1415
+ return Parser.#node('extension-access', children);
1416
+ }
1417
+
1418
+ /** Reads what follows a dot, or the `?.` of an optional access: a property read by a computed name in brackets, a dot property access, or a member call. */
1419
+ #parseAfterDot(dot) {
1420
+ if (this.#token.type === '?[') {
1421
+ throw this.#createSyntaxError('NEXO1507', "An optional computed property access writes its '?' before the dot, as '?.[…]', its brackets directly after the dot", this.#token.start);
1422
+ }
1423
+
1424
+ if (this.#token.type !== '[') {
1425
+ return this.#parseDotPropertyAccessOrMemberCall(dot);
1426
+ }
1427
+
1428
+ const open = this.#take();
1429
+ const expression = this.#parseExpression();
1430
+
1431
+ return Parser.#node('computed-property-access', [dot, open, expression, this.#expect(']')]);
1432
+ }
1433
+
1434
+ /** Reads a member after its dot, or the `?.` of an optional access: a dot property access, or a member call where parentheses follow the name. */
1435
+ #parseDotPropertyAccessOrMemberCall(dot) {
1436
+ // After a dot only a member's name stands, so any word names one there, a reserved word among them, written as it is.
1437
+ if (this.#token.type === 'lexical-reference') {
1438
+ throw this.#createSyntaxError('NEXO1008', `A member is named as it is written after its dot, with no '@': write '.${this.#token.value}'`, this.#token.start);
1439
+ }
1440
+
1441
+ const nameToken = this.#isWord(this.#token) ? this.#take() : this.#expect('identifier');
1442
+ const name = SyntaxTree.wordOf(nameToken, this.#text);
1443
+
1444
+ this.#refuseEnclosedAccessArgument(this.#token, `.${name}`);
1445
+
1446
+ if (this.#token.type !== '(') {
1447
+ if (this.#token.type === 'literal') {
1448
+ throw this.#createSyntaxError('NEXO1508', `An argument after a dot is enclosed in parentheses, as a call's is, '.${name}(…)'`, this.#token.start);
1449
+ }
1450
+
1451
+ return Parser.#node('dot-property-access', [dot, nameToken]);
1452
+ }
1453
+
1454
+ return Parser.#node('member-call', [dot, nameToken, ...this.#parseArguments(name, true)]);
1455
+ }
1456
+
1457
+ /** Whether a word and `=` begin what stands here, as a field, a local's name or a conditional binding's does, whatever the word, a reserved word among them. */
1458
+ #startsEntry() {
1459
+ return this.#isWord(this.#token) && this.#nextToken.type === '=';
1460
+ }
1461
+
1462
+ /** Whether a word and a colon begin what stands here, where a field, a local or a conditional binding is named, which writes its value after `=`, whatever follows the colon and however it is spaced. */
1463
+ #startsColonEntry() {
1464
+ return this.#isWord(this.#token) && this.#nextToken.type === ':';
1465
+ }
1466
+
1467
+ /** Whether the token writes a word: a name, a keyword, `it`, or the word of a literal, which a declaration, a member's name and a type's take as they are, whatever the word means where an expression stands. */
1468
+ #isWord(token) {
1469
+ return token.type === 'identifier' || token.type === 'keyword' || token.type === 'it' || (token.type === 'literal' && (typeof token.value === 'boolean' || token.value === null));
1470
+ }
1471
+
1472
+ /** Written text as a message quotes it: in single quotes, or in double quotes where it holds a single quote and no double one, as a raw string does. */
1473
+ static #quote(text) {
1474
+ return text.includes("'") && !text.includes('"') ? `"${text}"` : `'${text}'`;
1475
+ }
1476
+
1477
+ /** Reads operands joined by the operator, answering the operand alone where there is one. */
1478
+ #parseLogical(operator, parseOperand) {
1479
+ const first = parseOperand();
1480
+
1481
+ if (!this.#isKeyword(operator)) {
1482
+ return first;
1483
+ }
1484
+
1485
+ const children = [first];
1486
+
1487
+ while (this.#isKeyword(operator)) {
1488
+ this.#refuseUngroupedCoalescing(children.at(-1), operator);
1489
+ children.push(this.#take(), parseOperand());
1490
+ }
1491
+
1492
+ this.#refuseUngroupedCoalescing(children.at(-1), operator);
1493
+
1494
+ return Parser.#node(operator === 'or' ? 'disjunction' : 'conjunction', children);
1495
+ }
1496
+
1497
+ /** Refuses a coalescing operator no group holds as an operand of `and` or `or`, which it never stands beside. */
1498
+ #refuseUngroupedCoalescing(operand, operator) {
1499
+ if (this.#ungroupedCoalescings.has(operand)) {
1500
+ throw this.#createSyntaxError('NEXO1021', `'??' and '${operator}' stand apart, one of them grouped: write '(… ?? …) ${operator} …' or '… ?? (… ${operator} …)'`, SyntaxTree.startOf(operand));
1501
+ }
1502
+ }
1503
+
1504
+ /** Reads an object expression: its locals, each followed by a comma, and then its fields, at least one. */
1505
+ #parseObject() {
1506
+ const children = [this.#expect('{')];
1507
+ let locals = 0;
1508
+
1509
+ while (this.#startsLocal()) {
1510
+ children.push(this.#parseLocal());
1511
+ locals++;
1512
+
1513
+ if (this.#token.type !== ',') {
1514
+ // An object expression closed after its local holds no field.
1515
+ throw this.#createUnexpectedTokenError(this.#token.type === '}' ? 'NEXO1018' : 'NEXO1000', "',' after the local, which the object fields follow");
1516
+ }
1517
+
1518
+ children.push(this.#take());
1519
+ }
1520
+
1521
+ if (this.#token.type === '}') {
1522
+ const example = locals > 0 ? '{ let climate = it.climate, warmth = climate?.temperature }' : '{ size = it.size }';
1523
+
1524
+ throw this.#createSyntaxError('NEXO1018', `An object expression holds at least one object field${locals > 0 ? ' after its locals' : ''}, as ${example}`, this.#token.start);
1525
+ }
1526
+
1527
+ while (this.#token.type !== '}') {
1528
+ children.push(this.#parseField());
1529
+
1530
+ if (this.#token.type === ',') {
1531
+ children.push(this.#take());
1532
+ } else if (this.#token.type !== '}') {
1533
+ throw this.#createUnexpectedTokenError('NEXO1000', "',' or '}'");
1534
+ }
1535
+ }
1536
+
1537
+ children.push(this.#take());
1538
+
1539
+ return Parser.#node('object', children);
1540
+ }
1541
+
1542
+ /**
1543
+ * Reads a collection expression: `[`, a list's items or a dictionary's entries, each a key, `=` and its value, separated by commas, a trailing one among them, and `]`; `[]` is an empty list and `[=]` an empty
1544
+ * dictionary. Its written form decides its kind, so a first entry a key and `=` begin is a dictionary's, and any other a list's. A key is an inline string literal (NEXO1066), keyed and unkeyed entries never
1545
+ * mix (NEXO1067), and no item is left out between commas (NEXO1068). An item may be written as an arrow function, which validation refuses as a function standing in no argument.
1546
+ */
1547
+ #parseCollectionExpression() {
1548
+ const children = [this.#take()];
1549
+
1550
+ if (this.#token.type === ']') {
1551
+ children.push(this.#take());
1552
+
1553
+ return Parser.#node('list-expression', children);
1554
+ }
1555
+
1556
+ if (this.#token.type === '=' && this.#nextToken.type === ']') {
1557
+ children.push(this.#take(), this.#take());
1558
+
1559
+ return Parser.#node('dictionary-expression', children);
1560
+ }
1561
+
1562
+ const isDictionary = this.#startsDictionaryEntry();
1563
+
1564
+ while (this.#token.type !== ']') {
1565
+ if (this.#token.type === ',') {
1566
+ throw this.#createSyntaxError('NEXO1068', 'A collection expression holds an item between each two commas: write one there, or remove the comma', this.#token.start);
1567
+ }
1568
+
1569
+ // A collection the source ends in awaits its next item or entry, whichever kind it is.
1570
+ if (this.#token.type === 'end') {
1571
+ throw this.#createUnexpectedTokenError('NEXO1000', isDictionary ? 'a key' : 'a value');
1572
+ }
1573
+
1574
+ if (isDictionary !== this.#startsDictionaryEntry()) {
1575
+ const kind = isDictionary ? 'an item without a key' : 'an entry of a key and a value';
1576
+
1577
+ throw this.#createSyntaxError('NEXO1067', `A collection expression holds a list's items or a dictionary's entries, never both, and this one, ${isDictionary ? 'a dictionary' : 'a list'}, holds ${kind}`, this.#token.start);
1578
+ }
1579
+
1580
+ children.push(isDictionary ? this.#parseDictionaryEntry() : this.#parseListItem());
1581
+
1582
+ if (this.#token.type === ',') {
1583
+ children.push(this.#take());
1584
+ } else if (this.#token.type !== ']') {
1585
+ throw this.#createUnexpectedTokenError('NEXO1000', "',' or ']'");
1586
+ }
1587
+ }
1588
+
1589
+ children.push(this.#take());
1590
+
1591
+ return Parser.#node(isDictionary ? 'dictionary-expression' : 'list-expression', children);
1592
+ }
1593
+
1594
+ /** Whether a dictionary's entry starts here: a key, one token, and the `=` after it, a key no string writes among them, which its entry refuses. */
1595
+ #startsDictionaryEntry() {
1596
+ return this.#nextToken.type === '=' && this.#token.type !== ',' && this.#token.type !== ']';
1597
+ }
1598
+
1599
+ /** Reads a list's item: an expression, never an arrow function, since a collection holds data (NEXO1057); one an `=` follows is a dictionary's key no single token writes, which is refused (NEXO1066). */
1600
+ #parseListItem() {
1601
+ const start = this.#token.start;
1602
+
1603
+ this.#refuseFunctionAsData("A list's item");
1604
+
1605
+ const item = this.#parseExpression();
1606
+
1607
+ if (this.#token.type === '=') {
1608
+ throw this.#createSyntaxError('NEXO1066', "A dictionary key must be an inline string literal, as [\"coffee\" = 3]. For a computed key, construct the dictionary with toDictionary.", start);
1609
+ }
1610
+
1611
+ return item;
1612
+ }
1613
+
1614
+ /** Refuses an arrow function where the part named stands, a list's item, a dictionary's value or an indexer's key, which is data, never a function (NEXO1057). */
1615
+ #refuseFunctionAsData(part) {
1616
+ if (this.#startsArrowFunction()) {
1617
+ throw this.#createSyntaxError('NEXO1057', `${part} is data, never a function: an arrow function is the value of a local or an argument, so call one and hold what it answers`, this.#token.start);
1618
+ }
1619
+ }
1620
+
1621
+ /**
1622
+ * Reads a dictionary's entry: its key, an inline string literal, ordinary or raw, `=` and its value. A name as a key is refused giving the literal key of its spelling, and any other key giving the form of one, each with
1623
+ * `toDictionary` for a computed key (NEXO1066).
1624
+ */
1625
+ #parseDictionaryEntry() {
1626
+ const key = this.#take();
1627
+ const equals = this.#take();
1628
+
1629
+ this.#refuseFunctionAsData("A dictionary's value");
1630
+
1631
+ const value = this.#parseExpression();
1632
+ const isInlineString = key.type === 'literal' && typeof key.value === 'string' && key.delimiter === 1;
1633
+
1634
+ if (!isInlineString) {
1635
+ const written = this.#text.slice(SyntaxTree.startOf(value), SyntaxTree.endOf(value));
1636
+ const literal = this.#isWord(key) ? `Write ["${this.#text.slice(key.start, key.end)}" = ${written}] for the literal key "${this.#text.slice(key.start, key.end)}".` : 'Write it between quotes, as ["coffee" = 3].';
1637
+
1638
+ throw this.#createSyntaxError('NEXO1066', `A dictionary key must be an inline string literal. ${literal} For a computed key, construct the dictionary with toDictionary.`, key.start);
1639
+ }
1640
+
1641
+ return Parser.#node('dictionary-entry', [key, equals, value]);
1642
+ }
1643
+
1644
+ /** Reads a local, of a body or of an object expression, whose holder reads what separates it from the next: `let`, its name, `=` and its initializer, an expression, a function or an arrow function. */
1645
+ #parseLocal() {
1646
+ const letKeyword = this.#take();
1647
+
1648
+ if (!this.#startsEntry()) {
1649
+ this.#refuseName('NEXO1051', 'A local takes its value after =', 'A local is written with let, its name and = before its value, as let width = …', 'let ');
1650
+ }
1651
+
1652
+ const name = this.#take();
1653
+ const equals = this.#take();
1654
+ let initializer;
1655
+
1656
+ if (this.#isKeyword('function')) {
1657
+ initializer = this.#parseFunctionExpression();
1658
+ } else {
1659
+ initializer = this.#startsArrowFunction() ? this.#parseArrowFunction() : this.#parseExpression();
1660
+ }
1661
+
1662
+ return Parser.#node('local', [letKeyword, name, equals, initializer]);
1663
+ }
1664
+
1665
+ /** Reads a field: its name, `=` and its value. */
1666
+ #parseField() {
1667
+ if (this.#startsLocal()) {
1668
+ throw this.#createSyntaxError('NEXO1504', 'A local is written before the object fields of its object expression', this.#token.start);
1669
+ }
1670
+
1671
+ if (!this.#startsEntry()) {
1672
+ // A field written without its name, and a `return` among the fields, each have a code of their own.
1673
+ this.#refuseName(this.#isKeyword('return') ? 'NEXO1033' : 'NEXO1051', 'An object field takes its value after =', 'An object field is written with its name and = before its value, as size = it.size', '');
1674
+ }
1675
+
1676
+ return Parser.#node('field', [this.#take(), this.#take(), this.#parseExpression()]);
1677
+ }
1678
+
1679
+ /**
1680
+ * Refuses what stands where a field, a local or a conditional binding is named: a reserved word after `@`, since a declaration writes its name as it is; a word a colon follows, which gives its value as `=` does,
1681
+ * as the colon message says with the form to write, its prefix written before the word; a number or a string `=` or a colon follows, which names nothing; and anything else is written as the message gives,
1682
+ * the last two with the code given, the one of the declaration written without its name.
1683
+ */
1684
+ #refuseName(code, colonMessage, message, prefix) {
1685
+ const token = this.#token;
1686
+
1687
+ if (token.type === 'lexical-reference') {
1688
+ throw this.#createSyntaxError('NEXO1008', `A name is declared as it is written, with no '@': write '${prefix}${token.value} = …'`, token.start);
1689
+ }
1690
+
1691
+ if (this.#startsColonEntry()) {
1692
+ throw this.#createSyntaxError('NEXO1003', `${colonMessage}: write '${prefix}${SyntaxTree.wordOf(token, this.#text)} = …'`, this.#nextToken.start);
1693
+ }
1694
+
1695
+ if (token.type === 'literal' && (this.#nextToken.type === '=' || this.#nextToken.type === ':')) {
1696
+ throw this.#createSyntaxError(code, `${Parser.#quote(SyntaxTree.wordOf(token, this.#text))} is a literal, and names nothing`, token.start);
1697
+ }
1698
+
1699
+ throw this.#createSyntaxError(code, message, token.start);
1700
+ }
1701
+
1702
+ get #token() {
1703
+ return this.#tokens[this.#index];
1704
+ }
1705
+
1706
+ /** The token after the current one, the end of the source where the current one is that end, so a lookahead at the source's end reads the end again. */
1707
+ get #nextToken() {
1708
+ return this.#tokens[Math.min(this.#index + 1, this.#tokens.length - 1)];
1709
+ }
1710
+
1711
+ /** Whether a local starts here: `let`, which `=` or a colon after it would make a field's name instead. */
1712
+ #startsLocal() {
1713
+ const next = this.#nextToken.type;
1714
+
1715
+ return this.#isKeyword('let') && next !== '=' && next !== ':';
1716
+ }
1717
+
1718
+ #isKeyword(word) {
1719
+ return this.#token.type === 'keyword' && this.#token.value === word;
1720
+ }
1721
+
1722
+ #advance() {
1723
+ this.#index++;
1724
+ }
1725
+
1726
+ /** The current token, moving past it. */
1727
+ #take() {
1728
+ return this.#tokens[this.#index++];
1729
+ }
1730
+
1731
+ #expect(type) {
1732
+ const token = this.#token;
1733
+
1734
+ if (token.type !== type) {
1735
+ throw this.#createUnexpectedTokenError('NEXO1000', TOKEN_DESCRIPTIONS[type] ?? `'${type}'`);
1736
+ }
1737
+
1738
+ this.#advance();
1739
+
1740
+ return token;
1741
+ }
1742
+
1743
+ /**
1744
+ * Creates the syntax error of the code and the message at the offset, which its caller throws, every one the parser reports passing through here, as a strict parse throws it. It is where recovery would report from,
1745
+ * a foundation for it alone: recovering also needs points to resume from, missing and unexpected tokens handled, and progress through the text guaranteed.
1746
+ */
1747
+ #createSyntaxError(code, message, offset) {
1748
+ return NexoErrors.syntax(code, message, this.#text, offset);
1749
+ }
1750
+
1751
+ /** Creates the syntax error of a token other than the one expected, which its caller throws: of the code given, or of the one of an `=` written where no name takes its value. */
1752
+ #createUnexpectedTokenError(code, expected) {
1753
+ const token = this.#token;
1754
+
1755
+ // `=` gives a field, a local or a conditional binding its value, so one anywhere else is an equality written with one sign, or the second half of a comparison operator written apart.
1756
+ if (token.type === '=') {
1757
+ const previous = this.#tokens[this.#index - 1];
1758
+ const next = this.#nextToken;
1759
+
1760
+ if (previous?.type === '<' || previous?.type === '>') {
1761
+ return this.#createSyntaxError('NEXO1001', `'${previous.type}=' is one operator, written with nothing between its signs`, previous.start);
1762
+ }
1763
+
1764
+ if (next?.type === '=' || next?.type === '>') {
1765
+ return this.#createSyntaxError('NEXO1001', `'=${next.type}' is one operator, written with nothing between its signs`, token.start);
1766
+ }
1767
+
1768
+ return this.#createSyntaxError('NEXO1004', "Unexpected '=' in an expression. Use '==' to compare values.", token.start);
1769
+ }
1770
+ const found = token.type === 'end' ? TOKEN_DESCRIPTIONS.end : Parser.#quote(this.#text.slice(token.start, token.end));
1771
+
1772
+ return this.#createSyntaxError(code, `Expected ${expected}, found ${found}`, token.start);
1773
+ }
1774
+
1775
+ /**
1776
+ * Creates the syntax error of a body's closer not found after its result, which its caller throws: of the code given where the closer is missing, the text ending or what encloses the body closing,
1777
+ * and of anything else written after the result, which ends its body.
1778
+ */
1779
+ #createAfterBodyError(code, expected) {
1780
+ // An import after a body's result stands where no import does, as one anywhere but the script's start.
1781
+ if (this.#isKeyword('import')) {
1782
+ return this.#createSyntaxError('NEXO1045', 'Imports stand at the start of the script, before the first local or expression of its body', this.#token.start);
1783
+ }
1784
+
1785
+ if (this.#token.type === 'end' || this.#token.type === '=' || BODY_ENCLOSURE_CLOSINGS.has(this.#token.type) || (this.#token.type === 'keyword' && STRUCTURAL_KEYWORDS.has(this.#token.value))) {
1786
+ return this.#createUnexpectedTokenError(code, expected);
1787
+ }
1788
+
1789
+ // Parentheses after a call's own call a function its result is, which only a name holding it calls.
1790
+ if (this.#token.type === '(' && this.#tokens[this.#index - 1]?.type === ')') {
1791
+ return this.#createSyntaxError('NEXO1055', "A call's result is called through a name that holds it: write let g = f(…), and call g(…)", this.#token.start);
1792
+ }
1793
+
1794
+ return this.#createSyntaxError('NEXO1055', "Nothing follows a body's result, which ends it", this.#token.start);
1795
+ }
1796
+
1797
+ static #node(kind, children) {
1798
+ return { kind, children };
1799
+ }
1800
+
1801
+ /** The syntax kind of what the node holds: a group's is what it holds, and a token's its type. */
1802
+ static #syntaxKindOf(child) {
1803
+ return child.kind === 'parenthesized-expression' ? Parser.#syntaxKindOf(child.children[1]) : child.kind ?? child.type;
1804
+ }
1805
+
1806
+ /** Whether the node lowers to an optional chain: an optional enclosed access, or a chain with an optional access, a group answering for what it holds. */
1807
+ static #lowersToOptionalChain(child) {
1808
+ switch (child.kind) {
1809
+ case 'parenthesized-expression':
1810
+ return Parser.#lowersToOptionalChain(child.children[1]);
1811
+ case 'chain':
1812
+ return child.children.some(part => MEMBER_EXPRESSION_KINDS.has(part.kind) && SyntaxTree.isOptionalAccess(part.children[0].type));
1813
+ default:
1814
+ return child.kind === 'optional-enclosed-access';
1815
+ }
1816
+ }
1817
+ }