@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/parser.js CHANGED
@@ -5,6 +5,7 @@ import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } fr
5
5
  import { declarationKeywordGuidance, sourceTypeNameGuidance, REST_PARAMETER_ELEMENT_TYPE_MESSAGE } from "./language-guidance.js";
6
6
  import { Lexer } from "./lexer.js";
7
7
  import { span } from "./source.js";
8
+ import { isTypeEvidenceName } from "./source-names.js";
8
9
  import { keywordKinds } from "./token.js";
9
10
  import { formatTypeSyntax } from "./types.js";
10
11
  const memberNameKinds = new Set(["identifier", ...Object.values(keywordKinds)]);
@@ -22,7 +23,35 @@ const statementStarterWords = new Set(CORE_STATEMENT_HEAD_KEYWORDS);
22
23
  // Token kinds that legally appear at the top level of a record literal's
23
24
  // field list: field names, shorthand entries, and their separators.
24
25
  const recordFieldLevelKinds = new Set(["identifier", "string", "comma", ...Object.values(keywordKinds)]);
26
+ // A generic close that runs into the next operator lexes as one token: `>>`,
27
+ // `>>>`, and — where a default value or an assignment follows the annotation —
28
+ // `>=`, `>>=`, `>>>=`. Each maps to what is left after one `>` is taken for
29
+ // the close, so `List<number>=[1]` reads as the annotation and the `=` the
30
+ // author wrote. The lexer keeps emitting the compound operators, so the
31
+ // expression grammar is untouched.
32
+ const typeGreaterRemainderKinds = new Map([
33
+ ["rightShift", "greater"],
34
+ ["unsignedRightShift", "rightShift"],
35
+ ["greaterEqual", "assign"],
36
+ ["rightShiftAssign", "greaterEqual"],
37
+ ["unsignedRightShiftAssign", "rightShiftAssign"],
38
+ ]);
39
+ // The token kinds a type argument list can contain: names and the punctuation
40
+ // of optional, union, function, qualified and nested types. Anything else
41
+ // between `<` and `>` leaves the comparison reading as the only one.
42
+ const typeArgumentTokenKinds = new Set([
43
+ "identifier", "null", "comma", "dot", "question", "pipe", "arrow", "fatArrow",
44
+ "leftParen", "rightParen", "leftBracket", "rightBracket", "colon", "ellipsis",
45
+ ]);
25
46
  const MAX_PARSE_DEPTH = 512;
47
+ // An interpolation re-enters the compiler — a fresh lex and a fresh parse of
48
+ // the fragment — so one nest holds far more JavaScript stack than one ordinary
49
+ // expression level. Charging it a block of the budget keeps
50
+ // PARSER_COMPLEXITY_FAILURE ahead of the stack, which is what turns a
51
+ // pathological f-string into one diagnostic instead of seconds of work ended
52
+ // by a stack overflow. 64 nested interpolations remain available, which is
53
+ // orders of magnitude past any spelling a reader can follow.
54
+ const NESTED_EXPRESSION_PARSE_COST = 8;
26
55
  const PARSER_COMPLEXITY_FAILURE = Object.freeze({ kind: "VelarParserComplexityFailure" });
27
56
  export function isParserComplexityFailure(value) {
28
57
  return value === PARSER_COMPLEXITY_FAILURE;
@@ -73,6 +102,13 @@ function describeStatementToken(token) {
73
102
  return token.kind;
74
103
  return text.length > 24 ? `${text.slice(0, 24)}…` : text;
75
104
  }
105
+ // D90 R6: the spelling a numeric token was written with. The lexer keeps it
106
+ // only where it differs from the value the token carries — digit separators and
107
+ // an uppercase radix prefix — so a representability report quotes the author's
108
+ // own line rather than the normalized digits it was read from.
109
+ function writtenNumber(token) {
110
+ return token.payload?.written ?? token.value;
111
+ }
76
112
  const assignmentOperators = {
77
113
  assign: "=",
78
114
  plusAssign: "+=",
@@ -95,6 +131,7 @@ export class Parser {
95
131
  tokens;
96
132
  lexicalExtensions;
97
133
  diagnostics = [];
134
+ advisories = [];
98
135
  genericCallableNames = new Set();
99
136
  /** Extension-owned contextual keywords: names until a shape claims them. */
100
137
  contextualKeywords;
@@ -141,6 +178,7 @@ export class Parser {
141
178
  return {
142
179
  program: { kind: "Program", body, span: span(0, end) },
143
180
  diagnostics: this.diagnostics,
181
+ advisories: this.advisories,
144
182
  };
145
183
  }
146
184
  parseExpressionFragment() {
@@ -150,7 +188,7 @@ export class Parser {
150
188
  if (!this.check("eof")) {
151
189
  this.diagnostics.push(diagnostic("VEL2006", "Unexpected tokens in interpolated expression", this.current().span));
152
190
  }
153
- return { expression, diagnostics: this.diagnostics };
191
+ return { expression, diagnostics: this.diagnostics, advisories: this.advisories };
154
192
  }
155
193
  // An assignment written where only an expression is valid (an interpolated
156
194
  // fragment or an arrow body) receives directive guidance and recovers as an
@@ -317,6 +355,22 @@ export class Parser {
317
355
  this.diagnostics.push(diagnostic("VEL2033", `Use '${target} = not ${target}'; the 'invert' statement was removed`, removed));
318
356
  return { kind: "PassStatement", span: removed };
319
357
  }
358
+ // D89 (message correction): 'raise E(...)' is Python's spelling of the
359
+ // statement VelarScript writes with 'throw'. Left alone it falls into the
360
+ // unknown-declaration-keyword message below, which lists 'def', 'type',
361
+ // 'enum', 'class', 'const', and 'let' and never names the one word that
362
+ // was wrong. 'raise' stayed an ordinary identifier, so only the Python
363
+ // statement's own shape — the bare word followed by the error value — is
364
+ // claimed here; 'raise(...)', 'raise.field', and 'raise = ...' keep their
365
+ // ordinary meanings. The recovery parses the rest as the throw it meant,
366
+ // so the thrown value is checked in the same compile.
367
+ if (this.check("identifier") && this.current().value === "raise" && this.peekKind(1) === "identifier") {
368
+ const keyword = this.current();
369
+ this.diagnostics.push(recoveredDiagnostic("VEL2026", "Use 'throw'; VelarScript raises an error with 'throw value'", keyword.span, mechanicalFix(keyword.span, "throw", "Use 'throw'")));
370
+ this.advance();
371
+ const value = this.parseExpression();
372
+ return { kind: "ThrowStatement", value, span: span(keyword.span.start, value.span.end) };
373
+ }
320
374
  if (this.check("identifier") && this.peekKind(1) === "identifier") {
321
375
  const first = this.current();
322
376
  const guidance = declarationKeywordGuidance(first.value);
@@ -2776,25 +2830,32 @@ export class Parser {
2776
2830
  const callableName = expression.kind === "IdentifierExpression" ? expression.name
2777
2831
  : expression.kind === "MemberExpression" ? expression.property
2778
2832
  : null;
2779
- if (callableName === null || !this.check("less") || this.current().span.start !== expression.span.end)
2833
+ if (callableName === null || !this.check("less"))
2780
2834
  return null;
2781
2835
  // Beyond the same-file generic-name list, the recovery extends to any
2782
2836
  // callee — imported generics and methods — when the angle content carries
2783
2837
  // type evidence: a builtin type name, a capitalized name, optional or
2784
2838
  // union syntax, or nested generics. Without that evidence the comparison
2785
2839
  // reading wins, so `a<b>(c)` over three numbers stays a chain (rule #41).
2840
+ //
2841
+ // D90: the reading does not depend on the spacing around `<` and `>`, so a
2842
+ // formatter pass that adds or removes a space can never move a line
2843
+ // between the two grammars — `Map < string, number > ()` earns the same
2844
+ // teaching diagnostic as `Map<string, number>()`. What stands in for the
2845
+ // adjacency is grammar evidence, on two axes. Every token inside the
2846
+ // angles must be one a type argument list can contain, so
2847
+ // `a < Limit and g > (c)` stays the pair of comparisons it is. And every
2848
+ // argument of a `,`-separated list must carry evidence of its own, so
2849
+ // `two(a < Limit, g > (c))` stays two ordinary arguments — a program that
2850
+ // works — while `mapValues<string, bool>([1])` is still claimed.
2786
2851
  const knownGeneric = this.genericCallableNames.has(callableName);
2787
2852
  const typeEvidence = (token) => {
2788
2853
  if (token.kind === "question" || token.kind === "pipe" || token.kind === "arrow")
2789
2854
  return true;
2790
- if (token.kind !== "identifier")
2791
- return false;
2792
- if (["string", "number", "bool", "null", "unknown", "any", "List", "Set", "Map", "Record", "Promise", "Function", "Type", "readonly"].includes(token.value))
2793
- return true;
2794
- const first = token.value[0] ?? "";
2795
- return first >= "A" && first <= "Z";
2855
+ return token.kind === "identifier" && isTypeEvidenceName(token.value);
2796
2856
  };
2797
- let sawTypeEvidence = false;
2857
+ let everyArgumentIsTyped = true;
2858
+ let argumentIsTyped = false;
2798
2859
  let depth = 0;
2799
2860
  for (let index = this.index; index < this.tokens.length; index += 1) {
2800
2861
  const token = this.tokens[index];
@@ -2803,19 +2864,25 @@ export class Parser {
2803
2864
  if (token.kind === "less") {
2804
2865
  depth += 1;
2805
2866
  if (depth >= 2)
2806
- sawTypeEvidence = true;
2867
+ argumentIsTyped = true;
2807
2868
  }
2808
2869
  else if (token.kind === "greater") {
2809
2870
  depth -= 1;
2810
2871
  if (depth === 0) {
2811
- const call = this.tokens[index + 1];
2812
- if (call?.kind !== "leftParen" || call.span.start !== token.span.end)
2872
+ if (this.tokens[index + 1]?.kind !== "leftParen")
2813
2873
  return null;
2814
- return knownGeneric || sawTypeEvidence ? index : null;
2874
+ return knownGeneric || (everyArgumentIsTyped && argumentIsTyped) ? index : null;
2815
2875
  }
2816
2876
  }
2877
+ else if (!typeArgumentTokenKinds.has(token.kind)) {
2878
+ return null;
2879
+ }
2880
+ else if (token.kind === "comma" && depth === 1) {
2881
+ everyArgumentIsTyped &&= argumentIsTyped;
2882
+ argumentIsTyped = false;
2883
+ }
2817
2884
  else if (typeEvidence(token)) {
2818
- sawTypeEvidence = true;
2885
+ argumentIsTyped = true;
2819
2886
  }
2820
2887
  }
2821
2888
  return null;
@@ -2873,9 +2940,10 @@ export class Parser {
2873
2940
  return { kind: "LiteralExpression", value: 0, raw: "0", span: token.span };
2874
2941
  }
2875
2942
  const value = Number(match[1]);
2943
+ const exact = Number.isFinite(value) && this.checkExactIntegerLiteral(match[1], writtenNumber(token).slice(0, -match[2].length), token.span);
2876
2944
  if (!Number.isFinite(value))
2877
2945
  this.diagnostics.push(diagnostic("VEL2017", "Numeric literals must be finite", token.span));
2878
- const extensionExpression = this.parseExtensionNumericLiteral(token, Number.isFinite(value) ? value : 0, match[2]);
2946
+ const extensionExpression = this.parseExtensionNumericLiteral(token, exact ? value : 0, match[2]);
2879
2947
  if (extensionExpression)
2880
2948
  return extensionExpression;
2881
2949
  this.diagnostics.push(diagnostic("VEL2002", `No compiler extension accepts numeric suffix '${match[2]}'`, token.span));
@@ -3164,15 +3232,40 @@ export class Parser {
3164
3232
  }
3165
3233
  numberLiteral(token, negative = false, literalSpan = token.span) {
3166
3234
  const value = Number(token.value) * (negative ? -1 : 1);
3235
+ const exact = Number.isFinite(value) && this.checkExactIntegerLiteral(token.value, writtenNumber(token), literalSpan, negative);
3167
3236
  if (!Number.isFinite(value))
3168
3237
  this.diagnostics.push(diagnostic("VEL2017", "Numeric literals must be finite", literalSpan));
3169
3238
  return {
3170
3239
  kind: "LiteralExpression",
3171
- value: Number.isFinite(value) ? value : 0,
3240
+ value: exact ? value : 0,
3172
3241
  raw: `${negative ? "-" : ""}${token.value}`,
3173
3242
  span: literalSpan,
3174
3243
  };
3175
3244
  }
3245
+ /**
3246
+ * D90 R6: an integer literal whose value cannot be held exactly is rejected
3247
+ * rather than rounded, so `9007199254740993` is an error instead of silently
3248
+ * becoming `9007199254740992`. Only a literal *written* as an integer is one
3249
+ * here: a fraction part or an exponent spells a decimal value, which keeps
3250
+ * the ordinary nearest-value reading. An explicit radix takes the same test,
3251
+ * because a hex literal is written precisely when the exact bit pattern is
3252
+ * the point.
3253
+ *
3254
+ * The report quotes the author's own spelling — the token carries it when the
3255
+ * two differ — so a source line reading `1_000_000_000_000_000_000_1` is
3256
+ * quoted with its separators rather than as the normalized digits the value
3257
+ * was read from.
3258
+ */
3259
+ checkExactIntegerLiteral(text, written, literalSpan, negative = false) {
3260
+ if (!/^(?:[0-9]+|0[xXbBoO][0-9a-fA-F]+)$/u.test(text))
3261
+ return true;
3262
+ const rounded = Number(text);
3263
+ if (BigInt(text) === BigInt(rounded))
3264
+ return true;
3265
+ const sign = negative ? "-" : "";
3266
+ this.diagnostics.push(diagnostic("VEL2017", `Numeric literals must be exactly representable; '${sign}${written}' becomes ${sign}${rounded}`, literalSpan));
3267
+ return false;
3268
+ }
3176
3269
  // TXT-I2: the offset of a top-level ':' in an interpolation fragment, or
3177
3270
  // null. Ternary colons are consumed by their pending '?', bracket and
3178
3271
  // string contents never count, and '?.'/'??' are not ternary heads — so
@@ -3262,15 +3355,38 @@ export class Parser {
3262
3355
  const mappedSpan = (local) => sourceOffsets
3263
3356
  ? span(sourceOffsets[local.start] ?? offset, sourceOffsets[local.end] ?? sourceOffsets.at(-1) ?? offset)
3264
3357
  : span(local.start + offset, local.end + offset);
3358
+ // A report carries two coordinate systems: its own span and the spans of
3359
+ // the rewrite it names. Both are fragment-local here, so both are mapped;
3360
+ // mapping only the span leaves `velar fix` splicing an interpolation
3361
+ // offset into module text, which rewrites whatever happens to sit there.
3362
+ const mappedFix = (item) => item.fix
3363
+ ? { ...item, fix: { ...item.fix, edits: item.fix.edits.map((edit) => ({ ...edit, span: mappedSpan(edit.span) })) } }
3364
+ : item;
3265
3365
  const shiftedTokens = lexed.tokens.map((item) => ({ ...item, span: mappedSpan(item.span) }));
3266
- const shiftedDiagnostics = lexed.diagnostics.map((item) => ({ ...item, span: mappedSpan(item.span) }));
3267
- const parsed = this.createNestedParser(shiftedTokens).parseExpressionFragment();
3366
+ const shiftedDiagnostics = lexed.diagnostics.map((item) => mappedFix({ ...item, span: mappedSpan(item.span) }));
3367
+ const shiftedAdvisories = lexed.advisories.map((item) => mappedFix({ ...item, span: mappedSpan(item.span) }));
3368
+ const nested = this.createNestedParser(shiftedTokens);
3369
+ nested.inheritParseBudget(this);
3370
+ const parsed = nested.parseExpressionFragment();
3268
3371
  this.diagnostics.push(...shiftedDiagnostics, ...parsed.diagnostics);
3372
+ this.advisories.push(...shiftedAdvisories, ...parsed.advisories);
3269
3373
  return parsed.expression;
3270
3374
  }
3271
3375
  createNestedParser(tokens) {
3272
3376
  return new Parser(tokens, this.lexicalExtensions);
3273
3377
  }
3378
+ /**
3379
+ * An interpolation is parsed by a nested parser, so the nest continues the
3380
+ * budget rather than restarting it: without this, `MAX_PARSE_DEPTH` resets
3381
+ * at every level and only a JavaScript stack overflow ends a deeply nested
3382
+ * f-string, after the superlinear work is already paid. It is a method
3383
+ * rather than a constructor argument because `packages/web` and
3384
+ * `packages/node` override `createNestedParser`, so an added argument would
3385
+ * be silently dropped for exactly the modules that build one.
3386
+ */
3387
+ inheritParseBudget(parent) {
3388
+ this.parseDepth = parent.parseDepth + NESTED_EXPRESSION_PARSE_COST;
3389
+ }
3274
3390
  withParseDepth(parse) {
3275
3391
  this.parseDepth += 1;
3276
3392
  if (this.parseDepth > MAX_PARSE_DEPTH) {
@@ -3510,16 +3626,16 @@ export class Parser {
3510
3626
  return { kind, value: "", span: token.span };
3511
3627
  }
3512
3628
  checkTypeGreater() {
3513
- return this.check("greater") || this.check("rightShift") || this.check("unsignedRightShift");
3629
+ return typeGreaterRemainderKinds.has(this.current().kind) || this.check("greater");
3514
3630
  }
3515
3631
  /** Consume one `>` from a generic close without changing shift lexing. */
3516
3632
  expectTypeGreater(message) {
3517
3633
  if (this.check("greater"))
3518
3634
  return this.advance();
3519
3635
  const token = this.current();
3520
- if (token.kind === "rightShift" || token.kind === "unsignedRightShift") {
3636
+ const remainderKind = typeGreaterRemainderKinds.get(token.kind);
3637
+ if (remainderKind) {
3521
3638
  const close = { kind: "greater", value: ">", span: span(token.span.start, token.span.start + 1) };
3522
- const remainderKind = token.kind === "rightShift" ? "greater" : "rightShift";
3523
3639
  this.tokens[this.index] = {
3524
3640
  kind: remainderKind,
3525
3641
  value: token.value.slice(1),