@velarscript/compiler 0.12.0 → 0.13.0

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 (109) hide show
  1. package/dist/advisory-suppression.d.ts +62 -0
  2. package/dist/advisory-suppression.d.ts.map +1 -0
  3. package/dist/advisory-suppression.js +179 -0
  4. package/dist/advisory-suppression.js.map +1 -0
  5. package/dist/analyzer.d.ts +405 -17
  6. package/dist/analyzer.d.ts.map +1 -1
  7. package/dist/analyzer.js +1755 -211
  8. package/dist/analyzer.js.map +1 -1
  9. package/dist/ast.d.ts +6 -3
  10. package/dist/ast.d.ts.map +1 -1
  11. package/dist/ast.js.map +1 -1
  12. package/dist/binding-stability.d.ts +22 -0
  13. package/dist/binding-stability.d.ts.map +1 -0
  14. package/dist/binding-stability.js +63 -0
  15. package/dist/binding-stability.js.map +1 -0
  16. package/dist/collection-lowering-runtime.d.ts +1 -1
  17. package/dist/collection-lowering-runtime.d.ts.map +1 -1
  18. package/dist/collection-lowering-runtime.js +183 -56
  19. package/dist/collection-lowering-runtime.js.map +1 -1
  20. package/dist/diagnostic.d.ts +34 -0
  21. package/dist/diagnostic.d.ts.map +1 -1
  22. package/dist/diagnostic.js +11 -1
  23. package/dist/diagnostic.js.map +1 -1
  24. package/dist/embedded-javascript.d.ts +12 -1
  25. package/dist/embedded-javascript.d.ts.map +1 -1
  26. package/dist/embedded-javascript.js +111 -2
  27. package/dist/embedded-javascript.js.map +1 -1
  28. package/dist/embedded-module.d.ts.map +1 -1
  29. package/dist/embedded-module.js +22 -0
  30. package/dist/embedded-module.js.map +1 -1
  31. package/dist/emitter.d.ts +83 -1
  32. package/dist/emitter.d.ts.map +1 -1
  33. package/dist/emitter.js +518 -41
  34. package/dist/emitter.js.map +1 -1
  35. package/dist/error-runtime.d.ts +9 -0
  36. package/dist/error-runtime.d.ts.map +1 -1
  37. package/dist/error-runtime.js +21 -0
  38. package/dist/error-runtime.js.map +1 -1
  39. package/dist/extension.d.ts +5 -2
  40. package/dist/extension.d.ts.map +1 -1
  41. package/dist/extension.js +2 -1
  42. package/dist/extension.js.map +1 -1
  43. package/dist/formatter.d.ts.map +1 -1
  44. package/dist/formatter.js +222 -91
  45. package/dist/formatter.js.map +1 -1
  46. package/dist/index.d.ts +10 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +43 -5
  49. package/dist/index.js.map +1 -1
  50. package/dist/interpolated-string.d.ts.map +1 -1
  51. package/dist/interpolated-string.js +29 -2
  52. package/dist/interpolated-string.js.map +1 -1
  53. package/dist/json-runtime.d.ts.map +1 -1
  54. package/dist/json-runtime.js +27 -3
  55. package/dist/json-runtime.js.map +1 -1
  56. package/dist/lexer.d.ts +217 -1
  57. package/dist/lexer.d.ts.map +1 -1
  58. package/dist/lexer.js +799 -37
  59. package/dist/lexer.js.map +1 -1
  60. package/dist/limits.d.ts +9 -0
  61. package/dist/limits.d.ts.map +1 -1
  62. package/dist/limits.js +9 -0
  63. package/dist/limits.js.map +1 -1
  64. package/dist/mechanical-fix.d.ts.map +1 -1
  65. package/dist/mechanical-fix.js +38 -0
  66. package/dist/mechanical-fix.js.map +1 -1
  67. package/dist/parser.d.ts +30 -1
  68. package/dist/parser.d.ts.map +1 -1
  69. package/dist/parser.js +137 -21
  70. package/dist/parser.js.map +1 -1
  71. package/dist/promise-runtime.d.ts +20 -1
  72. package/dist/promise-runtime.d.ts.map +1 -1
  73. package/dist/promise-runtime.js +38 -3
  74. package/dist/promise-runtime.js.map +1 -1
  75. package/dist/reactive-bridge-runtime.d.ts.map +1 -1
  76. package/dist/reactive-bridge-runtime.js +7 -1
  77. package/dist/reactive-bridge-runtime.js.map +1 -1
  78. package/dist/runtime-abi.d.ts +21 -1
  79. package/dist/runtime-abi.d.ts.map +1 -1
  80. package/dist/runtime-abi.js +23 -3
  81. package/dist/runtime-abi.js.map +1 -1
  82. package/dist/source-names.d.ts +26 -0
  83. package/dist/source-names.d.ts.map +1 -1
  84. package/dist/source-names.js +49 -5
  85. package/dist/source-names.js.map +1 -1
  86. package/dist/source.d.ts.map +1 -1
  87. package/dist/source.js +13 -0
  88. package/dist/source.js.map +1 -1
  89. package/dist/stable-order.d.ts +16 -0
  90. package/dist/stable-order.d.ts.map +1 -0
  91. package/dist/stable-order.js +18 -0
  92. package/dist/stable-order.js.map +1 -0
  93. package/dist/text-runtime.d.ts.map +1 -1
  94. package/dist/text-runtime.js +182 -41
  95. package/dist/text-runtime.js.map +1 -1
  96. package/dist/token.d.ts +11 -0
  97. package/dist/token.d.ts.map +1 -1
  98. package/dist/token.js.map +1 -1
  99. package/dist/type-registry-runtime.d.ts.map +1 -1
  100. package/dist/type-registry-runtime.js +8 -1
  101. package/dist/type-registry-runtime.js.map +1 -1
  102. package/dist/type-validation-runtime.d.ts.map +1 -1
  103. package/dist/type-validation-runtime.js +129 -1
  104. package/dist/type-validation-runtime.js.map +1 -1
  105. package/dist/types.d.ts +17 -1
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +161 -17
  108. package/dist/types.js.map +1 -1
  109. package/package.json +1 -1
package/dist/formatter.js CHANGED
@@ -2,7 +2,7 @@ import { CORE_STATEMENT_HEAD_KEYWORDS } from "./core-vocabulary.js";
2
2
  import { scanEmbeddedJavaScriptLiteral } from "./embedded-javascript.js";
3
3
  import { MAX_VELAR_SOURCE_CODE_UNITS } from "./limits.js";
4
4
  import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } from "./interpolated-string.js";
5
- import { isSourceIdentifierPart, isSourceIdentifierStart } from "./source-names.js";
5
+ import { isSourceIdentifierPart, isSourceIdentifierStart, isTypeEvidenceName } from "./source-names.js";
6
6
  import { keywordKinds } from "./token.js";
7
7
  const multiCharacterOperators = [">>>=", "<<=", ">>=", ">>>", "<<", ">>", "...", "?.", "??", "->", "=>", "==", "!=", "<=", ">=", "**", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^="];
8
8
  const binaryWords = new Set(["and", "or", "in", "is"]);
@@ -49,7 +49,7 @@ export function formatSource(text, options = {}) {
49
49
  const lines = protectedStrings.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
50
50
  const indentation = [0];
51
51
  const formatted = [];
52
- let embeddedDepth = 0;
52
+ let embedded = closedEmbeddedScan;
53
53
  let statementLevel = 0;
54
54
  /** The last token of the previous line — the context a continuation reads. */
55
55
  let preceding;
@@ -66,7 +66,7 @@ export function formatSource(text, options = {}) {
66
66
  // A leading-dot chain continuation keeps its own canonical indentation —
67
67
  // one level past the statement it continues — without opening a block for
68
68
  // the lines that follow it.
69
- if (embeddedDepth === 0 && isChainContinuationLine(content) && formatted.length > 0) {
69
+ if (!isEmbeddedLine(embedded) && isChainContinuationLine(content) && formatted.length > 0) {
70
70
  const column = (statementLevel + 1) * indentWidth;
71
71
  const line = formatInlineLine(content, angleEmbedding, markupLayout(indentWidth, column, angleEmbedding), preceding);
72
72
  formatted.push(`${" ".repeat(column)}${line.text}`);
@@ -86,7 +86,7 @@ export function formatSource(text, options = {}) {
86
86
  statementLevel = indentation.length - 1;
87
87
  const indent = " ".repeat(statementLevel * indentWidth);
88
88
  const layout = markupLayout(indentWidth, statementLevel * indentWidth, angleEmbedding);
89
- if (embeddedDepth > 0) {
89
+ if (isEmbeddedLine(embedded)) {
90
90
  formatted.push(`${indent}${formatEmbeddedContent(content, angleEmbedding, layout, layout.column)}`);
91
91
  preceding = undefined;
92
92
  }
@@ -95,14 +95,55 @@ export function formatSource(text, options = {}) {
95
95
  formatted.push(`${indent}${line.text}`);
96
96
  preceding = line.trailing ?? preceding;
97
97
  }
98
- embeddedDepth = nextEmbeddedDepth(content, embeddedDepth, angleEmbedding);
98
+ embedded = nextEmbeddedScan(content, embedded, angleEmbedding);
99
99
  }
100
100
  while (formatted.at(-1) === "")
101
101
  formatted.pop();
102
- return protectedStrings.restore(`${formatted.join("\n")}\n`);
102
+ // The terminating newline is settled on the restored text. An unterminated
103
+ // block comment or layout string runs to the end of the module, so its
104
+ // placeholder value carries the file's final newline where neither the trim
105
+ // above nor an append here can see it — appending one to the joined lines
106
+ // adds a newline on every run, and charter line 422 makes formatting
107
+ // idempotent.
108
+ const restored = protectedStrings.restore(formatted.join("\n"));
109
+ return restored.endsWith("\n") || restored.endsWith("\r") ? restored : `${restored}\n`;
103
110
  }
104
111
  function protectMultilineStrings(source, opaqueSourceScanners) {
105
112
  const replacements = [];
113
+ // One scan settles a prefix the module does not already contain, so every
114
+ // placeholder built from it is unique by construction. Asking the same
115
+ // question again per replacement re-reads the whole module each time, which
116
+ // costs O(placeholders x module) on a file full of layout strings.
117
+ //
118
+ // The prefix is the marker plus the smallest serial the module does not
119
+ // already write after it. Growing the marker by a character per collision
120
+ // instead would let a module spelling the marker followed by a long run of
121
+ // underscores grow the prefix without bound, and `placeholderPattern` below
122
+ // then builds a pattern the regular-expression engine refuses — a crashed
123
+ // formatter rather than a diagnostic. A serial cannot collide: an occurrence
124
+ // of `marker + serial + "_"` is an occurrence of the marker whose digit run
125
+ // is exactly that serial, which this scan already recorded.
126
+ const marker = "__velar_formatter_";
127
+ const writtenSerials = new Set();
128
+ for (let at = source.indexOf(marker); at !== -1; at = source.indexOf(marker, at + 1)) {
129
+ let end = at + marker.length;
130
+ while (end < source.length && source[end] >= "0" && source[end] <= "9")
131
+ end += 1;
132
+ writtenSerials.add(source.slice(at + marker.length, end));
133
+ }
134
+ let serial = 0;
135
+ while (writtenSerials.has(String(serial)))
136
+ serial += 1;
137
+ const prefix = `${marker}${serial}_`;
138
+ // Walking back to the line break costs a column, while asking the module for
139
+ // its last `\r` before an index costs the whole prefix of the module when it
140
+ // has none — which is every module with Unix line endings.
141
+ const lineStartBefore = (position) => {
142
+ let start = position;
143
+ while (start > 0 && source[start - 1] !== "\n" && source[start - 1] !== "\r")
144
+ start -= 1;
145
+ return start;
146
+ };
106
147
  let output = "";
107
148
  let index = 0;
108
149
  while (index < source.length) {
@@ -121,12 +162,8 @@ function protectMultilineStrings(source, opaqueSourceScanners) {
121
162
  index = end;
122
163
  continue;
123
164
  }
124
- let marker = `__velar_formatter_multiline_comment_${replacements.length}__`;
125
- while (source.includes(marker))
126
- marker += "_";
127
- const placeholder = JSON.stringify(marker);
128
- const lineStart = Math.max(source.lastIndexOf("\n", index - 1), source.lastIndexOf("\r", index - 1)) + 1;
129
- const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, index))?.[0] ?? "";
165
+ const placeholder = JSON.stringify(`${prefix}multiline_comment_${replacements.length}__`);
166
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStartBefore(index), index))?.[0] ?? "";
130
167
  replacements.push({ placeholder, value, kind: "blockComment", originalIndent });
131
168
  output += placeholder;
132
169
  index = end;
@@ -137,14 +174,10 @@ function protectMultilineStrings(source, opaqueSourceScanners) {
137
174
  const start = index;
138
175
  index = opaqueSource.end;
139
176
  const value = source.slice(start, index);
140
- let marker = opaqueSource.attachedToPrevious
141
- ? `__velar_formatter_attached_opaque_source_${replacements.length}__`
142
- : `__velar_formatter_opaque_source_${replacements.length}__`;
143
- while (source.includes(marker))
144
- marker += "_";
145
- const placeholder = JSON.stringify(marker);
146
- const lineStart = Math.max(source.lastIndexOf("\n", start - 1), source.lastIndexOf("\r", start - 1)) + 1;
147
- const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, start))?.[0] ?? "";
177
+ const placeholder = JSON.stringify(opaqueSource.attachedToPrevious
178
+ ? `${prefix}attached_opaque_source_${replacements.length}__`
179
+ : `${prefix}opaque_source_${replacements.length}__`);
180
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStartBefore(start), start))?.[0] ?? "";
148
181
  replacements.push({ placeholder, value, kind: "opaqueString", originalIndent });
149
182
  output += placeholder;
150
183
  continue;
@@ -163,30 +196,45 @@ function protectMultilineStrings(source, opaqueSourceScanners) {
163
196
  output += value;
164
197
  continue;
165
198
  }
166
- let marker = `__velar_formatter_multiline_string_${replacements.length}__`;
167
- while (source.includes(marker))
168
- marker += "_";
169
- const placeholder = JSON.stringify(marker);
170
- const lineStart = Math.max(source.lastIndexOf("\n", start - 1), source.lastIndexOf("\r", start - 1)) + 1;
171
- const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, start))?.[0] ?? "";
199
+ const placeholder = JSON.stringify(`${prefix}multiline_string_${replacements.length}__`);
200
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStartBefore(start), start))?.[0] ?? "";
172
201
  replacements.push({ placeholder, value, kind: scanned.layout ? "layout" : "opaqueString", originalIndent });
173
202
  output += placeholder;
174
203
  }
204
+ // Every placeholder shares the settled prefix, so one pass over the formatted
205
+ // text finds them all wherever the printer moved them. Searching for each
206
+ // placeholder from the start and splicing it into a rebuilt string is the
207
+ // other half of the same quadratic cost.
208
+ const byPlaceholder = new Map(replacements.map((replacement) => [replacement.placeholder, replacement]));
209
+ const placeholderPattern = new RegExp(`"${prefix}[A-Za-z0-9_]*"`, "gu");
175
210
  return {
176
211
  text: output,
177
- restore: (formatted) => replacements.reduce((current, replacement) => {
178
- const marker = current.indexOf(replacement.placeholder);
179
- if (marker < 0)
180
- return current;
181
- const lineStart = Math.max(current.lastIndexOf("\n", marker - 1), current.lastIndexOf("\r", marker - 1)) + 1;
182
- const formattedIndent = /^[ \t]*/u.exec(current.slice(lineStart, marker))?.[0] ?? "";
183
- const value = replacement.kind === "layout"
184
- ? reindentLayoutLiteral(replacement.value, replacement.originalIndent, formattedIndent)
185
- : replacement.kind === "blockComment"
186
- ? reindentBlockComment(replacement.value, replacement.originalIndent, formattedIndent)
187
- : replacement.value;
188
- return `${current.slice(0, marker)}${value}${current.slice(marker + replacement.placeholder.length)}`;
189
- }, formatted),
212
+ restore: (formatted) => {
213
+ let restored = "";
214
+ /** The restored text after its last line break — the indent a value reads. */
215
+ let lineTail = "";
216
+ let cursor = 0;
217
+ const append = (segment) => {
218
+ restored += segment;
219
+ const lineStart = Math.max(segment.lastIndexOf("\n"), segment.lastIndexOf("\r"));
220
+ lineTail = lineStart === -1 ? lineTail + segment : segment.slice(lineStart + 1);
221
+ };
222
+ for (const match of formatted.matchAll(placeholderPattern)) {
223
+ const replacement = byPlaceholder.get(match[0]);
224
+ if (!replacement)
225
+ continue;
226
+ append(formatted.slice(cursor, match.index));
227
+ const formattedIndent = /^[ \t]*/u.exec(lineTail)?.[0] ?? "";
228
+ append(replacement.kind === "layout"
229
+ ? reindentLayoutLiteral(replacement.value, replacement.originalIndent, formattedIndent)
230
+ : replacement.kind === "blockComment"
231
+ ? reindentBlockComment(replacement.value, replacement.originalIndent, formattedIndent)
232
+ : replacement.value);
233
+ cursor = match.index + match[0].length;
234
+ }
235
+ append(formatted.slice(cursor));
236
+ return restored;
237
+ },
190
238
  };
191
239
  }
192
240
  function scanFormattingOpaqueSource(source, start, extensionScanners) {
@@ -270,8 +318,11 @@ function reindentLayoutLiteral(value, originalIndent, formattedIndent) {
270
318
  // Blank layout-string lines participate in the literal without needing
271
319
  // an indentation payload. Reintroducing the content margin here writes
272
320
  // trailing spaces into otherwise canonical source and makes a formatter
273
- // result fail `git diff --check`.
274
- if (text.trim().length === 0)
321
+ // result fail `git diff --check`. That holds only for a line whose whole
322
+ // indentation is the margin or less: whitespace past the margin is the
323
+ // string's own value (charter lines 381-384 preserve it exactly), so a
324
+ // line carrying it is re-margined like any other and keeps its payload.
325
+ if (originalMargin.startsWith(text))
275
326
  text = "";
276
327
  else if (text.startsWith(originalMargin))
277
328
  text = `${formattedMargin}${text.slice(originalMargin.length)}`;
@@ -283,72 +334,102 @@ function isChainContinuationLine(content) {
283
334
  const member = content.startsWith("?.") ? content[2] : content[1];
284
335
  return (content.startsWith(".") || content.startsWith("?.")) && Boolean(member && isSourceIdentifierStart(member));
285
336
  }
286
- function nextEmbeddedDepth(source, currentDepth, embedding) {
337
+ const closedEmbeddedScan = { depth: 0, pending: null };
338
+ /** A line inside an embedding, or inside an open tag, is copied, not formatted. */
339
+ function isEmbeddedLine(scan) {
340
+ return scan.depth > 0 || scan.pending !== null;
341
+ }
342
+ function nextEmbeddedScan(source, current, embedding) {
287
343
  if (!embedding)
288
- return 0;
289
- let depth = currentDepth;
344
+ return closedEmbeddedScan;
345
+ let depth = current.depth;
346
+ let pending = current.pending;
290
347
  let index = 0;
291
- if (depth === 0) {
348
+ if (depth === 0 && pending === null) {
292
349
  const start = embeddedStart(source);
293
350
  if (start === -1)
294
- return 0;
351
+ return closedEmbeddedScan;
295
352
  index = start;
296
353
  }
297
354
  while (index < source.length) {
298
- if (source.startsWith("<!--", index)) {
299
- const end = source.indexOf("-->", index + 4);
300
- if (end === -1)
301
- return depth;
302
- index = end + 3;
303
- continue;
304
- }
305
- if (source.startsWith("/*", index)) {
306
- index = blockCommentEnd(source, index);
307
- continue;
308
- }
309
- if (source[index] !== "<" || !/[A-Za-z/>]/u.test(source[index + 1] ?? "")) {
310
- index += 1;
311
- continue;
312
- }
313
- const closing = source[index + 1] === "/";
314
- let cursor = index + (closing ? 2 : 1);
315
- const nameStart = cursor;
316
- while (/[A-Za-z0-9_.:-]/u.test(source[cursor] ?? ""))
317
- cursor += 1;
318
- const name = source.slice(nameStart, cursor);
319
- const fragment = name === "" && source[cursor] === ">";
320
- if (!name && !fragment) {
321
- index += 1;
322
- continue;
355
+ if (pending === null) {
356
+ if (source.startsWith("<!--", index)) {
357
+ const end = source.indexOf("-->", index + 4);
358
+ if (end === -1)
359
+ break;
360
+ index = end + 3;
361
+ continue;
362
+ }
363
+ if (source.startsWith("/*", index)) {
364
+ index = blockCommentEnd(source, index);
365
+ continue;
366
+ }
367
+ if (source[index] !== "<" || !/[A-Za-z/>]/u.test(source[index + 1] ?? "")) {
368
+ index += 1;
369
+ continue;
370
+ }
371
+ const closing = source[index + 1] === "/";
372
+ let cursor = index + (closing ? 2 : 1);
373
+ const nameStart = cursor;
374
+ while (/[A-Za-z0-9_.:-]/u.test(source[cursor] ?? ""))
375
+ cursor += 1;
376
+ const name = source.slice(nameStart, cursor);
377
+ const fragment = name === "" && source[cursor] === ">";
378
+ if (!name && !fragment) {
379
+ index += 1;
380
+ continue;
381
+ }
382
+ pending = { name, closing, slash: false };
383
+ index = cursor;
323
384
  }
324
385
  let quote = "";
325
386
  let braces = 0;
326
- while (cursor < source.length) {
327
- const character = source[cursor++];
387
+ let slash = pending.slash;
388
+ let terminated = false;
389
+ while (index < source.length) {
390
+ const character = source[index++];
328
391
  if (quote) {
329
392
  if (character === "\\")
330
- cursor += 1;
393
+ index += 1;
331
394
  else if (character === quote)
332
395
  quote = "";
396
+ continue;
333
397
  }
334
- else if (character === '"' || character === "'")
398
+ if (character === '"' || character === "'") {
335
399
  quote = character;
336
- else if (character === "{")
400
+ slash = false;
401
+ }
402
+ else if (character === "{") {
337
403
  braces += 1;
338
- else if (character === "}")
404
+ slash = false;
405
+ }
406
+ else if (character === "}") {
339
407
  braces = Math.max(0, braces - 1);
340
- else if (character === ">" && braces === 0)
408
+ slash = false;
409
+ }
410
+ else if (braces > 0)
411
+ continue;
412
+ else if (character === ">") {
413
+ terminated = true;
341
414
  break;
415
+ }
416
+ else if (character === "/")
417
+ slash = true;
418
+ else if (!/\s/u.test(character))
419
+ slash = false;
342
420
  }
343
- const tag = source.slice(index, cursor);
344
- const selfClosing = /\/\s*>$/u.test(tag) || embedding.voidElements?.has(name) === true;
345
- if (closing)
421
+ if (!terminated) {
422
+ pending = { ...pending, slash };
423
+ break;
424
+ }
425
+ const selfClosing = slash || embedding.voidElements?.has(pending.name) === true;
426
+ if (pending.closing)
346
427
  depth = Math.max(0, depth - 1);
347
428
  else if (!selfClosing)
348
429
  depth += 1;
349
- index = cursor;
430
+ pending = null;
350
431
  }
351
- return depth;
432
+ return { depth, pending };
352
433
  }
353
434
  function embeddedStart(source) {
354
435
  for (let index = 0; index < source.length - 1; index += 1) {
@@ -526,7 +607,8 @@ function tokenizeInline(source, embedding, layout = heldLayoutFor(embedding)) {
526
607
  const generic = previous?.kind === "word"
527
608
  && (genericStack.at(-1) === true
528
609
  || beginsTypeBracket(tokens)
529
- || (opensAnnotatedType(tokens) && closesAsTypeArguments(source, index)));
610
+ || (opensAnnotatedType(tokens) && closesAsTypeArguments(source, index))
611
+ || opensCallTypeArguments(source, index));
530
612
  if (generic)
531
613
  genericStack.push(true);
532
614
  tokens.push({ kind: generic ? "open" : "operator", text: character, generic });
@@ -613,14 +695,43 @@ function isTypeAliasLine(tokens) {
613
695
  * comparisons, so the paren depth is what separates them.
614
696
  */
615
697
  function closesAsTypeArguments(source, start) {
698
+ return scanTypeArguments(source, start) !== null;
699
+ }
700
+ /**
701
+ * D90 (compiler-front-15): the type argument list a TypeScript habit puts on a
702
+ * call — `Map<string, number>()`, `id<string>("a")`. VelarScript infers type
703
+ * arguments, so the spelling is always an error and the parser reads it without
704
+ * regard to spacing. The formatter has to read it too: respacing it into
705
+ * `Map < string, number > ()` rewrites the author's line into a spelling that
706
+ * appears nowhere in their source, in exactly the file `velar format` is
707
+ * pointed at while the teaching diagnostic is on screen.
708
+ *
709
+ * The evidence is the parser's (`explicitTypeArgumentsEnd`) minus the same-file
710
+ * generic roster a line-based formatter cannot see: the brackets close on this
711
+ * line with `(` directly behind them, and every `,`-separated argument carries
712
+ * type evidence of its own — so `two(a < Limit, g > (c))`, two comparisons and
713
+ * a working program, keeps its operator spacing. Being narrower than the parser
714
+ * can only cost a respace on a line that is an error either way; it can never
715
+ * change which reading the compiler gives a line, because the parser does not
716
+ * consult the spacing.
717
+ */
718
+ function opensCallTypeArguments(source, start) {
719
+ const scanned = scanTypeArguments(source, start);
720
+ return scanned !== null && scanned.typed && source[scanned.end] === "(";
721
+ }
722
+ function scanTypeArguments(source, start) {
616
723
  let depth = 0;
617
724
  let parenthesized = 0;
618
725
  let index = start;
726
+ let everyArgumentIsTyped = true;
727
+ let argumentIsTyped = false;
619
728
  while (index < source.length) {
620
729
  const character = source[index];
621
730
  if (character === "<") {
622
731
  depth += 1;
623
732
  index += 1;
733
+ if (depth >= 2)
734
+ argumentIsTyped = true;
624
735
  continue;
625
736
  }
626
737
  if (character === ">") {
@@ -629,11 +740,12 @@ function closesAsTypeArguments(source, start) {
629
740
  depth -= 1;
630
741
  index += 1;
631
742
  if (depth === 0)
632
- return true;
743
+ return { end: index, typed: everyArgumentIsTyped && argumentIsTyped };
633
744
  continue;
634
745
  }
635
746
  if (source.startsWith("->", index)) {
636
747
  index += 2;
748
+ argumentIsTyped = true;
637
749
  continue;
638
750
  }
639
751
  if (isSourceIdentifierStart(character)) {
@@ -643,7 +755,9 @@ function closesAsTypeArguments(source, start) {
643
755
  index += 1;
644
756
  const word = source.slice(wordStart, index);
645
757
  if (binaryWords.has(word) || prefixWords.has(word))
646
- return false;
758
+ return null;
759
+ if (isTypeEvidenceName(word))
760
+ argumentIsTyped = true;
647
761
  continue;
648
762
  }
649
763
  if (character === "(") {
@@ -654,7 +768,7 @@ function closesAsTypeArguments(source, start) {
654
768
  if (character === ")") {
655
769
  parenthesized -= 1;
656
770
  if (parenthesized < 0)
657
- return false;
771
+ return null;
658
772
  index += 1;
659
773
  continue;
660
774
  }
@@ -662,13 +776,24 @@ function closesAsTypeArguments(source, start) {
662
776
  index += 1;
663
777
  continue;
664
778
  }
665
- if (" \t,.?|".includes(character)) {
779
+ if (character === "," && depth === 1) {
780
+ everyArgumentIsTyped &&= argumentIsTyped;
781
+ argumentIsTyped = false;
666
782
  index += 1;
667
783
  continue;
668
784
  }
669
- return false;
785
+ if (character === "?" || character === "|") {
786
+ argumentIsTyped = true;
787
+ index += 1;
788
+ continue;
789
+ }
790
+ if (" \t,.".includes(character)) {
791
+ index += 1;
792
+ continue;
793
+ }
794
+ return null;
670
795
  }
671
- return false;
796
+ return null;
672
797
  }
673
798
  function formatInterpolatedString(source, start, scanned, embedding) {
674
799
  if (!scanned.closed)
@@ -1131,7 +1256,13 @@ function needsSpace(previous, current, next, tokens, index, preceding) {
1131
1256
  return true;
1132
1257
  }
1133
1258
  function isAttachedOpaqueSourcePlaceholder(token) {
1134
- return token.kind === "string" && token.text.includes("__velar_formatter_attached_opaque_source_");
1259
+ // The marker carries a settled serial, so the placeholder is recognised by
1260
+ // the fixed middle `protectMultilineStrings` gives it rather than by the
1261
+ // marker spelled out — which would stop matching the moment a module made
1262
+ // the marker settle on anything but its first spelling.
1263
+ return token.kind === "string"
1264
+ && token.text.includes("__velar_formatter_")
1265
+ && token.text.includes("attached_opaque_source_");
1135
1266
  }
1136
1267
  function isUnaryOperator(token, previous, statementHead = false) {
1137
1268
  if (prefixWords.has(token.text))