@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,475 @@
1
+ /**
2
+ * DefinitionWriter — a definition's node as the Nexo text that writes it, parenthesized where its grouping needs it: compact, as a diagnostic quotes it, or laid out on lines, as formatting gives its canonical text
3
+ */
4
+
5
+ import { DefinitionValidator } from './DefinitionValidator.mjs';
6
+ import { Imports } from './Imports.mjs';
7
+ import { Names } from './Names.mjs';
8
+
9
+ /** The escape the canonical text writes for each character it escapes with a letter or the character itself; any other character it escapes it writes as a Unicode escape. */
10
+ const ESCAPED = Object.freeze({ '"': '\\"', '\\': '\\\\', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\u001B': '\\e' });
11
+
12
+ /** The characters the canonical text escapes: a double quote, a backslash, and every character that shows nothing of itself, a control, a format, a line or paragraph separator, or a space separator but the space. */
13
+ const ESCAPED_CHARACTERS = /["\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]|(?! )\p{Zs}/gu;
14
+
15
+ /** How tightly each kind of expression binds, the loosest lowest: one written where a tighter one stands is parenthesized. An optional chain is a cast's target as it is, and is grouped where a chain continues from it, since its short-circuit ends where it does. */
16
+ const PRECEDENCE = { conditional: 0, body: 0, do: 0, function: 0, range: 1, coalescing: 1, or: 2, and: 3, not: 4, equality: 5, comparison: 5, additive: 6, multiplicative: 7, arithmeticNegation: 8, fromEndIndex: 8, try: 9, conversion: 10, cast: 11, optionalChain: 12 };
17
+
18
+ /** How tightly a chain binds that no optional chain holds: a property access, a call, an indexer, an object expression, `it`, `root`, a namespace, a local, a literal or a script parameter. */
19
+ const CHAIN_PRECEDENCE = 13;
20
+
21
+ /** The indentation of one level of an object expression laid out on lines. */
22
+ const INDENTATION = ' ';
23
+
24
+ export class DefinitionWriter {
25
+ /**
26
+ * Quotes the node: its compact text, as a diagnostic quotes it, which parses: on one line but where a boundary or a keyword block requires a line break, a body that uses `return` its locals and its result each on a
27
+ * line of its own and every keyword block around it in block form, and what follows a block's `end` that is no closing punctuation on the line after it.
28
+ * @param {object} node A node of a definition.
29
+ * @param {ReadonlySet<string>} [namespaceNames] The local names the script's imports give modules, each beginning its namespace, as the language's words keep their meaning; none where its imports give none.
30
+ */
31
+ static quote(node, namespaceNames = Names.noNamespaceNames) {
32
+ return DefinitionWriter.#writing(namespaceNames, true, () => (node.kind === 'body' ? DefinitionWriter.#bodyText(node, 0) : DefinitionWriter.#write(node, 0)));
33
+ }
34
+
35
+ /**
36
+ * The canonical text of the node, laid out on lines: an object expression of one field and no locals, whose value is on one line, stays on one line, and any other has a line for each entry, indented a level; a conditional with an `elseif` has a line for each clause.
37
+ * @param {object} node A node of a definition.
38
+ * @param {ReadonlySet<string>} [namespaceNames] The local names the script's imports give modules, each beginning its namespace, as the language's words keep their meaning; none where its imports give none.
39
+ */
40
+ static format(node, namespaceNames = Names.noNamespaceNames) {
41
+ // An expression that is a body writes its locals and its return alone, which a do block elsewhere encloses.
42
+ return DefinitionWriter.#writing(namespaceNames, false, () => (node.kind === 'body' ? DefinitionWriter.#bodyText(node, 0) : DefinitionWriter.#write(node, 0)));
43
+ }
44
+
45
+ /**
46
+ * The canonical text of a script's definition, as `nexo.format` gives it: each import it records but those its destination supplies, on a line of its own in order of the local name it gives, an empty line, and its
47
+ * body laid out on lines, its namespaces written by the local names every import gives.
48
+ * @param {object} definition A validated definition.
49
+ * @param {readonly object[]} [suppliedImports] The imports the text's destination supplies, which it leaves out.
50
+ */
51
+ static formatScript(definition, suppliedImports = []) {
52
+ const imports = DefinitionWriter.#writtenImports(definition, suppliedImports);
53
+ const body = DefinitionWriter.format(definition.expression, Imports.namespaceNamesOf(definition.imports));
54
+
55
+ return imports.length === 0 ? body : `${imports.join('\n')}\n\n${body}`;
56
+ }
57
+
58
+ /**
59
+ * The compact text of a script's definition, as `nexo.quote` gives it: each import it records but those its destination supplies, on a line of its own, since each begins one, and its compact body on the next line,
60
+ * as a diagnostic quotes it.
61
+ * @param {object} definition A validated definition.
62
+ * @param {readonly object[]} [suppliedImports] The imports the text's destination supplies, which it leaves out.
63
+ */
64
+ static quoteScript(definition, suppliedImports = []) {
65
+ const body = DefinitionWriter.quote(definition.expression, Imports.namespaceNamesOf(definition.imports));
66
+
67
+ return [...DefinitionWriter.#writtenImports(definition, suppliedImports), body].join('\n');
68
+ }
69
+
70
+ /** The text of each import the definition records that its destination does not supply, in the definition's order. */
71
+ static #writtenImports(definition, suppliedImports) {
72
+ return definition.imports.filter(entry => !suppliedImports.some(supplied => Imports.isSameImport(supplied, entry))).map(entry => Imports.quote(entry));
73
+ }
74
+
75
+ /** The namespace names the text being written is written with. @type {ReadonlySet<string>} */
76
+ static #namespaceNames = Names.noNamespaceNames;
77
+
78
+ /** Whether the text being written is compact, breaking lines only where line structure requires, rather than laid out on lines. */
79
+ static #isCompact = false;
80
+
81
+ /**
82
+ * What the writing gives with the namespace names and the layout in effect, in the writer's static state, which holds one writing at a time; those of any writing around it are restored in `finally`, whether the
83
+ * writing completes or throws.
84
+ */
85
+ static #writing(namespaceNames, isCompact, write) {
86
+ const enclosingNamespaceNames = DefinitionWriter.#namespaceNames;
87
+ const enclosingIsCompact = DefinitionWriter.#isCompact;
88
+
89
+ try {
90
+ DefinitionWriter.#namespaceNames = namespaceNames;
91
+ DefinitionWriter.#isCompact = isCompact;
92
+
93
+ return write();
94
+ } finally {
95
+ DefinitionWriter.#namespaceNames = enclosingNamespaceNames;
96
+ DefinitionWriter.#isCompact = enclosingIsCompact;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * The text that writes the node, compact or laid out on lines as the writing is, each line it breaks beginning at the level's indentation.
102
+ * @param {object} node
103
+ * @param {number} level
104
+ */
105
+ static #write(node, level) {
106
+ switch (node.kind) {
107
+ case 'literal':
108
+ return DefinitionWriter.#literal(node.value);
109
+ case 'scriptParameter':
110
+ // A script parameter a reserved word or a namespace name names, or one hidden where it stands, which validation marks on its canonical node, is written with `~`.
111
+ return Names.needsPrefix(node.name, DefinitionWriter.#namespaceNames) || DefinitionValidator.isHidden(node) ? `~${node.name}` : node.name;
112
+ case 'it':
113
+ return 'it';
114
+ case 'root':
115
+ return 'root';
116
+ case 'namespace':
117
+ // A namespace a reserved word names is its import binding, which `@` reads where the word alone keeps its meaning.
118
+ return Names.isReservedWord(node.name) ? `@${node.name}` : node.name;
119
+ case 'local':
120
+ return DefinitionWriter.#localNameText(node.name);
121
+ case 'memberAccess':
122
+ return typeof node.name === 'string'
123
+ ? `${DefinitionWriter.#receiverText(node, level)}${DefinitionWriter.#joiner(node)}${node.name}`
124
+ : `${DefinitionWriter.#within(node.receiver, CHAIN_PRECEDENCE, level)}${DefinitionWriter.#dot(node)}[${DefinitionWriter.#write(node.name, level)}]`;
125
+ case 'call':
126
+ return `${node.callee.kind === 'local' ? DefinitionWriter.#localNameText(node.callee.name) : DefinitionWriter.#write(node.callee, level)}(${DefinitionWriter.#arguments(node, level)})`;
127
+ case 'indexerAccess':
128
+ return `${DefinitionWriter.#within(node.receiver, CHAIN_PRECEDENCE, level)}${node.optional === true ? '?[' : '['}${DefinitionWriter.#write(node.argument, level)}]`;
129
+ case 'optionalChain':
130
+ return DefinitionWriter.#write(node.chain, level);
131
+ case 'list':
132
+ return `[${DefinitionWriter.#items(node.items, level)}]`;
133
+ case 'dictionary':
134
+ return DefinitionWriter.#dictionary(node, level);
135
+ case 'equality':
136
+ return DefinitionWriter.#equality(node, '==', level);
137
+ case 'interpolatedString':
138
+ return DefinitionWriter.#interpolatedString(node, level);
139
+ case 'comparison':
140
+ // A chain is written as one, an operator between each two of its operands.
141
+ return node.operands.map((operand, index) => `${index === 0 ? '' : ` ${node.operators[index - 1]} `}${DefinitionWriter.#within(operand, PRECEDENCE.additive, level)}`).join('');
142
+ case 'conditional':
143
+ return DefinitionWriter.#conditional(node, level);
144
+ case 'function':
145
+ return DefinitionWriter.#function(node, level);
146
+ case 'do':
147
+ return DefinitionWriter.#block('do', node.body, level);
148
+ case 'body':
149
+ // The root expression's body, which a do block, a function and a conditional's branch each write their own way.
150
+ return DefinitionWriter.#bodyText(node, level);
151
+ case 'coalescing':
152
+ // A coalescing operator associates to the right, so a fallback that is one is written as it is.
153
+ return `${DefinitionWriter.#within(node.value, PRECEDENCE.arithmeticNegation, level)} ?? ${node.fallback.kind === 'coalescing' ? DefinitionWriter.#write(node.fallback, level) : DefinitionWriter.#within(node.fallback, PRECEDENCE.arithmeticNegation, level)}`;
154
+ case 'arithmetic': {
155
+ // Operators of one level associate to the left, so a right operand of that level is grouped.
156
+ const precedence = DefinitionWriter.#precedenceOf(node);
157
+
158
+ return `${DefinitionWriter.#within(node.left, precedence, level)} ${node.operator} ${DefinitionWriter.#within(node.right, precedence + 1, level)}`;
159
+ }
160
+ case 'arithmeticNegation': {
161
+ // A negated negative number, or an arithmetic negation, is grouped, so the signs never run together.
162
+ const { operand } = node;
163
+ const runsTogether = operand.kind === 'arithmeticNegation' || (operand.kind === 'literal' && typeof operand.value === 'number' && (operand.value < 0 || Object.is(operand.value, -0)));
164
+
165
+ return `-${runsTogether ? `(${DefinitionWriter.#write(operand, level)})` : DefinitionWriter.#within(operand, PRECEDENCE.arithmeticNegation, level)}`;
166
+ }
167
+ case 'fromEndIndex':
168
+ // An index's offset is an equality operand, so an operator expression or a negative number is grouped after its `^`.
169
+ return `^${DefinitionWriter.#within(node.offset, PRECEDENCE.try, level)}`;
170
+ case 'range':
171
+ // A boundary is an arithmetic negation, so an operator expression or another range is grouped, and `..` stands between them with no space.
172
+ return `${node.start === undefined ? '' : DefinitionWriter.#within(node.start, PRECEDENCE.arithmeticNegation, level)}..${node.end === undefined ? '' : DefinitionWriter.#within(node.end, PRECEDENCE.arithmeticNegation, level)}`;
173
+ case 'not':
174
+ // The negation of an equality is written as `!=`, which is its canonical text.
175
+ return node.operand.kind === 'equality' ? DefinitionWriter.#equality(node.operand, '!=', level) : `not ${DefinitionWriter.#within(node.operand, PRECEDENCE.not, level)}`;
176
+ case 'cast':
177
+ return `${DefinitionWriter.#within(node.operand, PRECEDENCE.optionalChain, level)} as ${node.type}`;
178
+ case 'conversion':
179
+ // A conversion converts what is before it, so one that converts another is written as it is.
180
+ return `${DefinitionWriter.#within(node.operand, PRECEDENCE.conversion, level)} to ${node.type}`;
181
+ case 'try':
182
+ return `try ${DefinitionWriter.#attempted(node.operand, level)}`;
183
+ case 'and':
184
+ case 'or':
185
+ return node.operands.map(operand => DefinitionWriter.#within(operand, PRECEDENCE[node.kind] + 1, level)).join(` ${node.kind} `);
186
+ default: {
187
+ const objectText = DefinitionWriter.#object(node, level);
188
+
189
+ return node.context === undefined ? objectText : `${DefinitionWriter.#within(node.context, CHAIN_PRECEDENCE, level)} ${objectText}`;
190
+ }
191
+ }
192
+ }
193
+
194
+ /** The dot before an access or a call: `?.` for an optional one, and `.` for one that requires its receiver. */
195
+ static #dot(node) {
196
+ return node.optional === true ? '?.' : '.';
197
+ }
198
+
199
+ /**
200
+ * What joins a named access to its receiver: its dot, a dot for a name that continues an extension access, or `:` or `?:` for an extension access's first name; a selection a resolver gives an access it reads,
201
+ * an extension's name, qualified by its module or not, follows the colon with a dot after each name.
202
+ */
203
+ static #joiner(node) {
204
+ if (node.extension === undefined) {
205
+ return DefinitionWriter.#dot(node);
206
+ }
207
+
208
+ const colon = node.optional === true ? '?:' : ':';
209
+
210
+ if (node.extension === true) {
211
+ return colon;
212
+ }
213
+
214
+ const { module, name } = node.extension;
215
+
216
+ return `${colon}${module === undefined ? '' : `${module}.`}${name === undefined ? '' : `${name}.`}`;
217
+ }
218
+
219
+ /**
220
+ * The receiver of a named access as it is written before its joiner: grouped where it is a name of an extension access and an ordinary dot follows it, which would continue the access's names, as
221
+ * `(device:readings).first` writes it, and as its precedence writes it otherwise.
222
+ */
223
+ static #receiverText(node, level) {
224
+ const { receiver } = node;
225
+ const continuesAccess = node.extension !== undefined || node.continues === true || node.optional === true;
226
+
227
+ if (!continuesAccess && receiver.kind === 'memberAccess' && (receiver.extension !== undefined || receiver.continues === true)) {
228
+ return `(${DefinitionWriter.#write(receiver, level)})`;
229
+ }
230
+
231
+ return DefinitionWriter.#within(node.receiver, CHAIN_PRECEDENCE, level);
232
+ }
233
+
234
+ /**
235
+ * A literal as Nexo writes it: a string in double quotes, a double quote, a backslash, a line feed, a carriage return, a tab and an escape character escaped by their letters,
236
+ * any other control, format or separator character but the space as a Unicode escape in upper-case hexadecimal, and every other character as itself; negative zero as `-0`, which JSON writes as zero; anything else as JSON writes it.
237
+ */
238
+ static #literal(value) {
239
+ if (typeof value === 'string') {
240
+ return `"${DefinitionWriter.#stringText(value)}"`;
241
+ }
242
+
243
+ return Object.is(value, -0) ? '-0' : JSON.stringify(value);
244
+ }
245
+
246
+ /** A string's characters as a string writes them between its quotes, each escape as the canonical text writes it. */
247
+ static #stringText(value) {
248
+ return value.replaceAll(ESCAPED_CHARACTERS, character => ESCAPED[character] ?? `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`);
249
+ }
250
+
251
+ /**
252
+ * An interpolated string: its text as a string's, each brace doubled, and each interpolation's expression compact, as a diagnostic quotes it, which an interpolation's simple expression writes on its one line, so no layout
253
+ * breaks the string, the expression grouped where it begins with an object expression, whose `{` an interpolation never begins with.
254
+ */
255
+ static #interpolatedString(node, level) {
256
+ const parts = node.parts.map(part => {
257
+ if (part.kind === 'text') {
258
+ return DefinitionWriter.#stringText(part.value.replaceAll(/[{}]/g, '$&$&'));
259
+ }
260
+
261
+ const expression = DefinitionWriter.#writing(DefinitionWriter.#namespaceNames, true, () => DefinitionWriter.#write(part.expression, level));
262
+
263
+ return `{${expression.startsWith('{') ? `(${expression})` : expression}}`;
264
+ });
265
+
266
+ return `$"${parts.join('')}"`;
267
+ }
268
+
269
+ /**
270
+ * An object expression's braces and entries, its locals first: on one line, or with a line for each entry, indented a level, where it is laid out on lines and is more than one field on one line, or where an entry
271
+ * spans lines, compact or not, since what follows a block's `end` on its line is closing punctuation alone.
272
+ */
273
+ static #object(node, level) {
274
+ const entries = [
275
+ ...(node.locals ?? []).map(local => `let ${local.name} = ${DefinitionWriter.#write(local.value, level + 1)}`),
276
+ ...node.objectFields.map(field => `${field.name} = ${DefinitionWriter.#write(field.value, level + 1)}`),
277
+ ];
278
+ const isOneLine = DefinitionWriter.#isCompact || (node.locals === undefined && entries.length === 1);
279
+
280
+ if (isOneLine && !entries.some(entry => entry.includes('\n'))) {
281
+ return `{ ${entries.join(', ')} }`;
282
+ }
283
+
284
+ const indentation = INDENTATION.repeat(level + 1);
285
+
286
+ return `{\n${entries.map(entry => `${indentation}${entry}`).join(',\n')}\n${INDENTATION.repeat(level)}}`;
287
+ }
288
+
289
+ /**
290
+ * A dictionary's brackets and entries, each its key as a string is written, `=` and its value: `[=]` where it holds none, on one line where it is compact or holds one entry, and otherwise with a line for each entry,
291
+ * indented a level, as an object expression is laid out, and so wherever an entry spans lines.
292
+ */
293
+ static #dictionary(node, level) {
294
+ if (node.entries.length === 0) {
295
+ return '[=]';
296
+ }
297
+
298
+ const entries = node.entries.map(entry => `${DefinitionWriter.#literal(entry.key)} = ${DefinitionWriter.#write(entry.value, level + 1)}`);
299
+ const isOneLine = DefinitionWriter.#isCompact || entries.length === 1;
300
+
301
+ if (isOneLine && !entries.some(entry => entry.includes('\n'))) {
302
+ return `[${entries.join(', ')}]`;
303
+ }
304
+
305
+ const indentation = INDENTATION.repeat(level + 1);
306
+
307
+ return `[\n${entries.map(entry => `${indentation}${entry}`).join(',\n')}\n${INDENTATION.repeat(level)}]`;
308
+ }
309
+
310
+ /** The operation `try` attempts: a cast as it is, and a conversion whose value is grouped where it is a conversion or a cast, so `try` attempts the conversion alone. */
311
+ static #attempted(node, level) {
312
+ if (node.kind !== 'conversion') {
313
+ return DefinitionWriter.#write(node, level);
314
+ }
315
+
316
+ const value = node.operand.kind === 'conversion' || node.operand.kind === 'cast' ? `(${DefinitionWriter.#write(node.operand, level)})` : DefinitionWriter.#within(node.operand, PRECEDENCE.conversion, level);
317
+
318
+ return `${value} to ${node.type}`;
319
+ }
320
+
321
+ /** An equality, or its negation, as its operator writes it: two equality operands, each grouped where it binds more loosely than arithmetic. */
322
+ static #equality(node, operator, level) {
323
+ return `${DefinitionWriter.#within(node.left, PRECEDENCE.additive, level)} ${operator} ${DefinitionWriter.#within(node.right, PRECEDENCE.additive, level)}`;
324
+ }
325
+
326
+ /**
327
+ * How tightly the node binds: an arithmetic node by its operator's level, a negative number as an arithmetic negation, since its `-` is one in text, so what follows it, a conversion or an access, groups it,
328
+ * and any other by its kind's.
329
+ */
330
+ static #precedenceOf(node) {
331
+ if (node.kind === 'arithmetic') {
332
+ return node.operator === '+' || node.operator === '-' ? PRECEDENCE.additive : PRECEDENCE.multiplicative;
333
+ }
334
+
335
+ if (node.kind === 'literal' && typeof node.value === 'number' && (node.value < 0 || Object.is(node.value, -0))) {
336
+ return PRECEDENCE.arithmeticNegation;
337
+ }
338
+
339
+ return PRECEDENCE[node.kind] ?? CHAIN_PRECEDENCE;
340
+ }
341
+
342
+ /** The text of a name a lexical reference reads, a local's, an object field's, a conditional binding's or a function parameter's: its name, after `@` where a reserved word or a namespace's local name names it, which written alone keeps its own meaning. */
343
+ static #localNameText(name) {
344
+ return Names.needsPrefix(name, DefinitionWriter.#namespaceNames) ? `@${name}` : name;
345
+ }
346
+
347
+ /** A call's arguments, as a list of nodes is written. */
348
+ static #arguments(node, level) {
349
+ return DefinitionWriter.#items(node.arguments, level);
350
+ }
351
+
352
+ /**
353
+ * Nodes in order, a call's arguments or a list's items, separated by commas: on one line, the last one's lines after it where it spans lines, as `f(x => {` begins one. Only closing punctuation follows a block's `end` on
354
+ * its line, so a node after one ending in it stands on the next line: laid out, each node on a line of its own, a level within its brackets, which close on the line after the last, and compact, on the line after
355
+ * the `end`. A node written a level within holds an indentation more after each of its line breaks, since no string the writer writes holds one.
356
+ */
357
+ static #items(nodes, level) {
358
+ const written = nodes.map(item => DefinitionWriter.#write(item, level));
359
+ const isFollowingBlock = written.some((text, index) => index < written.length - 1 && DefinitionWriter.#endsWithBlock(text));
360
+
361
+ if (isFollowingBlock && !DefinitionWriter.#isCompact) {
362
+ const indentation = INDENTATION.repeat(level + 1);
363
+
364
+ return `\n${written.map(text => `${indentation}${text.replaceAll('\n', `\n${INDENTATION}`)}`).join(',\n')}\n${INDENTATION.repeat(level)}`;
365
+ }
366
+
367
+ return written.map((argument, index) => (index === 0 ? argument : `${DefinitionWriter.#endsWithBlock(written[index - 1]) ? `\n${INDENTATION.repeat(level)}` : ' '}${argument}`)).join(',');
368
+ }
369
+
370
+ /** Whether the text ends with a block's `end` on a line of its own, which only closing punctuation may follow on that line. */
371
+ static #endsWithBlock(text) {
372
+ return /\n\s*end$/.test(text);
373
+ }
374
+
375
+ /** A function: its parameters, and its body as a block writes it; an arrow function its parameters, one alone or several in parentheses, `=>` and the expression that is its body. */
376
+ static #function(node, level) {
377
+ if (node.arrow === true) {
378
+ const parameters = node.parameters.length === 1 ? node.parameters[0] : `(${node.parameters.join(', ')})`;
379
+
380
+ return `${parameters} => ${DefinitionWriter.#write(node.body, level)}`;
381
+ }
382
+
383
+ return DefinitionWriter.#block(`function(${node.parameters.join(', ')})`, node.body, level);
384
+ }
385
+
386
+ /**
387
+ * A block, a function or a do block, after its header: a body of one expression on its line, or a body that returns its result, or one expression that spans lines, in block form, on the lines a level within it, its
388
+ * `end` back at the block's own, compact or not, since a keyword block that spans lines at all is in block form.
389
+ */
390
+ static #block(header, body, level) {
391
+ if (body.kind !== 'body') {
392
+ const written = DefinitionWriter.#write(body, level + 1);
393
+
394
+ return written.includes('\n') ? `${header}\n${INDENTATION.repeat(level + 1)}${written}\n${INDENTATION.repeat(level)}end` : `${header} ${written} end`;
395
+ }
396
+
397
+ return `${header}\n${INDENTATION.repeat(level + 1)}${DefinitionWriter.#bodyText(body, level + 1)}\n${INDENTATION.repeat(level)}end`;
398
+ }
399
+
400
+ /**
401
+ * A body's locals and the `return` that gives its result, each on a line of the level's indentation, as each starts a line of its own, and, laid out on lines, an empty line between the locals and the `return`, so
402
+ * the result stands apart from what it is computed from.
403
+ */
404
+ static #bodyText(node, level) {
405
+ const lineBreak = `\n${INDENTATION.repeat(level)}`;
406
+ const locals = node.locals.map(local => `let ${local.name} = ${DefinitionWriter.#write(local.value, level)}`);
407
+ const result = `return ${DefinitionWriter.#write(node.result, level)}`;
408
+
409
+ if (locals.length === 0) {
410
+ return result;
411
+ }
412
+
413
+ // The empty line holds no indentation, so a line of canonical text never ends in a space.
414
+ return `${locals.join(lineBreak)}${DefinitionWriter.#isCompact ? '' : '\n'}${lineBreak}${result}`;
415
+ }
416
+
417
+ /**
418
+ * A conditional's clauses, each a condition or a conditional binding, and the `end` that closes them: a conditional of its own style nested in its alternative is written as an `elseif`, and one in its condition or consequent in parentheses; every branch gives its result with `return` where the branches are bodies.
419
+ * On one line where none of its branches is a body and none of its text spans lines, compact, or laid out where it has no `elseif` too; otherwise in block form, each clause on a line of its own ending in `then` or
420
+ * `else`, its branch on the lines a level within it, and `end` on a line of its own.
421
+ */
422
+ static #conditional(node, level) {
423
+ const clauses = [];
424
+ let current = node;
425
+
426
+ for (;;) {
427
+ const test = current.binding === undefined
428
+ ? DefinitionWriter.#within(current.condition, PRECEDENCE.coalescing, level)
429
+ : `let ${current.binding.name} = ${DefinitionWriter.#within(current.binding.value, PRECEDENCE.coalescing, level)}`;
430
+
431
+ clauses.push({ header: `${clauses.length === 0 ? 'if' : 'elseif'} ${test} then`, branch: current.consequent, isAlternative: false });
432
+
433
+ // An alternative that is a conditional of the same style is an `elseif`; one of the other is the expression an implicit conditional's `else` gives.
434
+ if (current.alternative.kind !== 'conditional' || (current.alternative.consequent.kind === 'body') !== (current.consequent.kind === 'body')) {
435
+ clauses.push({ header: 'else', branch: current.alternative, isAlternative: true });
436
+ break;
437
+ }
438
+
439
+ current = current.alternative;
440
+ }
441
+
442
+ const branchText = ({ branch, isAlternative }, branchLevel) => {
443
+ if (branch.kind === 'body') {
444
+ return DefinitionWriter.#bodyText(branch, branchLevel);
445
+ }
446
+
447
+ // A do block closes itself with its `end`, so it is a branch as it is; a conditional in a consequent is grouped.
448
+ return isAlternative || branch.kind === 'do' ? DefinitionWriter.#write(branch, branchLevel) : DefinitionWriter.#within(branch, PRECEDENCE.coalescing, branchLevel);
449
+ };
450
+ const hasBody = clauses.some(clause => clause.branch.kind === 'body');
451
+
452
+ // A conditional that spans lines at all, one of its branches' or tests' texts among them, is in block form, as line structure requires, and a body's locals and its `return` each start a line.
453
+ const oneLine = `${clauses.map(clause => `${clause.header} ${branchText(clause, level)}`).join(' ')} end`;
454
+
455
+ if ((DefinitionWriter.#isCompact || clauses.length === 2) && !hasBody && !oneLine.includes('\n')) {
456
+ return oneLine;
457
+ }
458
+
459
+ const lineBreak = `\n${INDENTATION.repeat(level)}`;
460
+
461
+ return [...clauses.map(clause => `${clause.header}\n${INDENTATION.repeat(level + 1)}${branchText(clause, level + 1)}`), 'end'].join(lineBreak);
462
+ }
463
+
464
+ /** The text of the node where an expression of at least the given precedence stands: in parentheses where its own precedence is lower. */
465
+ static #within(node, precedence, level) {
466
+ const text = DefinitionWriter.#write(node, level);
467
+
468
+ if (DefinitionWriter.#precedenceOf(node) >= precedence) {
469
+ return text;
470
+ }
471
+
472
+ // A block's `end` ends its line, so the group around a block that spans lines closes on the line after it, where what follows continues.
473
+ return DefinitionWriter.#endsWithBlock(text) ? `(${text}\n${INDENTATION.repeat(level)})` : `(${text})`;
474
+ }
475
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Dictionary — a Nexo dictionary, immutable: its entries in the order they were inserted, each key at most once by Nexo's key equality, a number's zero and negative zero one key, a string by its code units, a
3
+ * Boolean by its value, a reference by its identity and null as one key of its own
4
+ */
5
+
6
+ import { Pair } from './Pair.mjs';
7
+
8
+ export class Dictionary {
9
+ /** Each key's value, in the order the keys were inserted, a JavaScript map's key equality being Nexo's for the keys a dictionary holds. @type {Map<unknown, unknown>} */
10
+ #entries;
11
+
12
+ /** @param {Iterable<[unknown, unknown]>} entries Its entries, in order, each key once, which its caller has checked. */
13
+ constructor(entries) {
14
+ this.#entries = new Map(entries);
15
+ Object.freeze(this);
16
+ }
17
+
18
+ /** How many entries the dictionary holds. */
19
+ get count() {
20
+ return this.#entries.size;
21
+ }
22
+
23
+ /** Whether the dictionary holds the key, a key whose value is null among them. */
24
+ has(key) {
25
+ return this.#entries.has(key);
26
+ }
27
+
28
+ /** The key's value, or null where the dictionary holds no such key, so a key whose value is null is told apart by `has` alone. */
29
+ get(key) {
30
+ return this.#entries.has(key) ? this.#entries.get(key) : null;
31
+ }
32
+
33
+ /** The key's entry as a pair, its key the one the dictionary holds, 0 where -0 looks it up, or null where the dictionary holds no such key. */
34
+ getEntry(key) {
35
+ return this.#entries.has(key) ? new Pair(key === 0 ? 0 : key, this.#entries.get(key)) : null;
36
+ }
37
+
38
+ /** The keys, in their order, as a frozen list. */
39
+ get keys() {
40
+ return Object.freeze([...this.#entries.keys()]);
41
+ }
42
+
43
+ /** The values, in their keys' order, as a frozen list. */
44
+ get values() {
45
+ return Object.freeze([...this.#entries.values()]);
46
+ }
47
+
48
+ /** The entries, in their order, each a pair, as a frozen list. */
49
+ get entries() {
50
+ return Object.freeze([...this.#entries].map(([key, value]) => new Pair(key, value)));
51
+ }
52
+ }