@velarscript/compiler 0.10.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 (159) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +70 -0
  3. package/dist/analyzer.d.ts +1175 -0
  4. package/dist/analyzer.d.ts.map +1 -0
  5. package/dist/analyzer.js +12130 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/application-package-host.d.ts +28 -0
  8. package/dist/application-package-host.d.ts.map +1 -0
  9. package/dist/application-package-host.js +2 -0
  10. package/dist/application-package-host.js.map +1 -0
  11. package/dist/ast.d.ts +853 -0
  12. package/dist/ast.d.ts.map +1 -0
  13. package/dist/ast.js +245 -0
  14. package/dist/ast.js.map +1 -0
  15. package/dist/class-runtime.d.ts +6 -0
  16. package/dist/class-runtime.d.ts.map +1 -0
  17. package/dist/class-runtime.js +45 -0
  18. package/dist/class-runtime.js.map +1 -0
  19. package/dist/collection-lowering-runtime.d.ts +7 -0
  20. package/dist/collection-lowering-runtime.d.ts.map +1 -0
  21. package/dist/collection-lowering-runtime.js +952 -0
  22. package/dist/collection-lowering-runtime.js.map +1 -0
  23. package/dist/collection-runtime.d.ts +16 -0
  24. package/dist/collection-runtime.d.ts.map +1 -0
  25. package/dist/collection-runtime.js +231 -0
  26. package/dist/collection-runtime.js.map +1 -0
  27. package/dist/core-vocabulary.d.ts +141 -0
  28. package/dist/core-vocabulary.d.ts.map +1 -0
  29. package/dist/core-vocabulary.js +88 -0
  30. package/dist/core-vocabulary.js.map +1 -0
  31. package/dist/diagnostic.d.ts +42 -0
  32. package/dist/diagnostic.d.ts.map +1 -0
  33. package/dist/diagnostic.js +66 -0
  34. package/dist/diagnostic.js.map +1 -0
  35. package/dist/embedded-javascript.d.ts +74 -0
  36. package/dist/embedded-javascript.d.ts.map +1 -0
  37. package/dist/embedded-javascript.js +341 -0
  38. package/dist/embedded-javascript.js.map +1 -0
  39. package/dist/embedded-module.d.ts +11 -0
  40. package/dist/embedded-module.d.ts.map +1 -0
  41. package/dist/embedded-module.js +67 -0
  42. package/dist/embedded-module.js.map +1 -0
  43. package/dist/embedded-source.d.ts +19 -0
  44. package/dist/embedded-source.d.ts.map +1 -0
  45. package/dist/embedded-source.js +61 -0
  46. package/dist/embedded-source.js.map +1 -0
  47. package/dist/emitter.d.ts +188 -0
  48. package/dist/emitter.d.ts.map +1 -0
  49. package/dist/emitter.js +3254 -0
  50. package/dist/emitter.js.map +1 -0
  51. package/dist/error-runtime.d.ts +17 -0
  52. package/dist/error-runtime.d.ts.map +1 -0
  53. package/dist/error-runtime.js +109 -0
  54. package/dist/error-runtime.js.map +1 -0
  55. package/dist/extension.d.ts +373 -0
  56. package/dist/extension.d.ts.map +1 -0
  57. package/dist/extension.js +25 -0
  58. package/dist/extension.js.map +1 -0
  59. package/dist/formatter.d.ts +13 -0
  60. package/dist/formatter.d.ts.map +1 -0
  61. package/dist/formatter.js +1440 -0
  62. package/dist/formatter.js.map +1 -0
  63. package/dist/framework-host.d.ts +87 -0
  64. package/dist/framework-host.d.ts.map +1 -0
  65. package/dist/framework-host.js +2 -0
  66. package/dist/framework-host.js.map +1 -0
  67. package/dist/index.d.ts +75 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +932 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/interpolated-string.d.ts +47 -0
  72. package/dist/interpolated-string.d.ts.map +1 -0
  73. package/dist/interpolated-string.js +344 -0
  74. package/dist/interpolated-string.js.map +1 -0
  75. package/dist/json-runtime.d.ts +2 -0
  76. package/dist/json-runtime.d.ts.map +1 -0
  77. package/dist/json-runtime.js +193 -0
  78. package/dist/json-runtime.js.map +1 -0
  79. package/dist/language-guidance.d.ts +31 -0
  80. package/dist/language-guidance.d.ts.map +1 -0
  81. package/dist/language-guidance.js +187 -0
  82. package/dist/language-guidance.js.map +1 -0
  83. package/dist/lexer.d.ts +85 -0
  84. package/dist/lexer.d.ts.map +1 -0
  85. package/dist/lexer.js +1004 -0
  86. package/dist/lexer.js.map +1 -0
  87. package/dist/limits.d.ts +2 -0
  88. package/dist/limits.d.ts.map +1 -0
  89. package/dist/limits.js +2 -0
  90. package/dist/limits.js.map +1 -0
  91. package/dist/mechanical-fix.d.ts +26 -0
  92. package/dist/mechanical-fix.d.ts.map +1 -0
  93. package/dist/mechanical-fix.js +61 -0
  94. package/dist/mechanical-fix.js.map +1 -0
  95. package/dist/narrowing-runtime.d.ts +5 -0
  96. package/dist/narrowing-runtime.d.ts.map +1 -0
  97. package/dist/narrowing-runtime.js +27 -0
  98. package/dist/narrowing-runtime.js.map +1 -0
  99. package/dist/number-runtime.d.ts +3 -0
  100. package/dist/number-runtime.d.ts.map +1 -0
  101. package/dist/number-runtime.js +37 -0
  102. package/dist/number-runtime.js.map +1 -0
  103. package/dist/parser.d.ts +245 -0
  104. package/dist/parser.d.ts.map +1 -0
  105. package/dist/parser.js +3582 -0
  106. package/dist/parser.js.map +1 -0
  107. package/dist/primitive-runtime.d.ts +4 -0
  108. package/dist/primitive-runtime.d.ts.map +1 -0
  109. package/dist/primitive-runtime.js +39 -0
  110. package/dist/primitive-runtime.js.map +1 -0
  111. package/dist/promise-runtime.d.ts +6 -0
  112. package/dist/promise-runtime.d.ts.map +1 -0
  113. package/dist/promise-runtime.js +86 -0
  114. package/dist/promise-runtime.js.map +1 -0
  115. package/dist/reactive-bridge-runtime.d.ts +7 -0
  116. package/dist/reactive-bridge-runtime.d.ts.map +1 -0
  117. package/dist/reactive-bridge-runtime.js +112 -0
  118. package/dist/reactive-bridge-runtime.js.map +1 -0
  119. package/dist/runtime-abi.d.ts +24 -0
  120. package/dist/runtime-abi.d.ts.map +1 -0
  121. package/dist/runtime-abi.js +24 -0
  122. package/dist/runtime-abi.js.map +1 -0
  123. package/dist/semantic.d.ts +142 -0
  124. package/dist/semantic.d.ts.map +1 -0
  125. package/dist/semantic.js +845 -0
  126. package/dist/semantic.js.map +1 -0
  127. package/dist/source-names.d.ts +32 -0
  128. package/dist/source-names.d.ts.map +1 -0
  129. package/dist/source-names.js +101 -0
  130. package/dist/source-names.js.map +1 -0
  131. package/dist/source.d.ts +19 -0
  132. package/dist/source.d.ts.map +1 -0
  133. package/dist/source.js +64 -0
  134. package/dist/source.js.map +1 -0
  135. package/dist/text-runtime.d.ts +2 -0
  136. package/dist/text-runtime.d.ts.map +1 -0
  137. package/dist/text-runtime.js +285 -0
  138. package/dist/text-runtime.js.map +1 -0
  139. package/dist/token.d.ts +19 -0
  140. package/dist/token.d.ts.map +1 -0
  141. package/dist/token.js +51 -0
  142. package/dist/token.js.map +1 -0
  143. package/dist/type-registry-runtime.d.ts +7 -0
  144. package/dist/type-registry-runtime.d.ts.map +1 -0
  145. package/dist/type-registry-runtime.js +51 -0
  146. package/dist/type-registry-runtime.js.map +1 -0
  147. package/dist/type-validation-runtime.d.ts +17 -0
  148. package/dist/type-validation-runtime.d.ts.map +1 -0
  149. package/dist/type-validation-runtime.js +150 -0
  150. package/dist/type-validation-runtime.js.map +1 -0
  151. package/dist/types.d.ts +356 -0
  152. package/dist/types.d.ts.map +1 -0
  153. package/dist/types.js +1170 -0
  154. package/dist/types.js.map +1 -0
  155. package/dist/utf8-runtime.d.ts +7 -0
  156. package/dist/utf8-runtime.d.ts.map +1 -0
  157. package/dist/utf8-runtime.js +43 -0
  158. package/dist/utf8-runtime.js.map +1 -0
  159. package/package.json +48 -0
package/dist/lexer.js ADDED
@@ -0,0 +1,1004 @@
1
+ import { CORE_NUMERIC_SUFFIXES } from "./core-vocabulary.js";
2
+ import { diagnostic, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
3
+ import { scanEmbeddedJavaScriptLiteral } from "./embedded-javascript.js";
4
+ import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } from "./interpolated-string.js";
5
+ import { webNumericUnitOwner } from "./language-guidance.js";
6
+ import { forbiddenSourceIdentifiers, isForbiddenPrototypeMember, isSourceIdentifierPart, isSourceIdentifierStart } from "./source-names.js";
7
+ import { span } from "./source.js";
8
+ import { keywordKinds } from "./token.js";
9
+ const MAX_TOKENS = 250000;
10
+ const MAX_NESTING = 512;
11
+ /**
12
+ * D51 rule 104: all twelve `Bidi_Control` code points. LRM/RLM/ALM were the
13
+ * three missing, and CVE-2021-42574 names them in the same breath as the nine
14
+ * that were already banned — three open doors is the same as no door. ZWJ and
15
+ * the variation selectors stay legal: they compose emoji, they do not reorder
16
+ * a reviewer's line.
17
+ */
18
+ const bidirectionalControls = new Set([
19
+ 0x061c, 0x200e, 0x200f,
20
+ 0x202a, 0x202b, 0x202c, 0x202d, 0x202e,
21
+ 0x2066, 0x2067, 0x2068, 0x2069,
22
+ ]);
23
+ // A logical line may continue onto the next physical line when that line's
24
+ // first token is '.' or '?.' member access (a leading-dot method chain). The
25
+ // previous line must end with a token that can end an expression, so block
26
+ // headers, operators, and empty lines never join accidentally.
27
+ const chainContinuationEndKinds = new Set([
28
+ "identifier", "number", "unitNumber", "string", "fstring",
29
+ "true", "false", "null", "super", "rightParen", "rightBracket", "rightBrace",
30
+ "extensionToken",
31
+ ]);
32
+ export class Lexer {
33
+ text;
34
+ extensionForbiddenIdentifiers = new Map();
35
+ extensionScanners = [];
36
+ // D39-52: milliseconds and seconds are Core duration literals. Extensions
37
+ // may add visual units, but Core owns these two spellings. D62 rule 158:
38
+ // the pair is read from Core's roster rather than spelled here, so a gate
39
+ // that reverse-queries the language surface can see them without an
40
+ // extension republishing them.
41
+ numericSuffixes = new Set(CORE_NUMERIC_SUFFIXES);
42
+ tokens = [];
43
+ diagnostics = [];
44
+ diagnosedBidirectionalOffsets = new Set();
45
+ indentStack = [0];
46
+ index = 0;
47
+ atLineStart = true;
48
+ nesting = 0;
49
+ // A bracket fragment is an expression lexed inside an enclosing bracket
50
+ // context, such as an extension-owned bracket interpolation: newlines are insignificant
51
+ // and physical-line indentation never opens or closes blocks, exactly as
52
+ // between ordinary parentheses.
53
+ bracketFragment;
54
+ scanSourceHygiene;
55
+ constructor(text, extensions = [], options = {}) {
56
+ this.text = text;
57
+ this.bracketFragment = options.bracketFragment ?? false;
58
+ this.scanSourceHygiene = options.scanSourceHygiene ?? true;
59
+ for (const extension of extensions) {
60
+ for (const [name, guidance] of Object.entries(extension.forbiddenIdentifiers ?? {})) {
61
+ this.extensionForbiddenIdentifiers.set(name, guidance);
62
+ }
63
+ for (const suffix of extension.numericSuffixes ?? [])
64
+ this.numericSuffixes.add(suffix);
65
+ if (extension.scan)
66
+ this.extensionScanners.push(extension.scan);
67
+ }
68
+ }
69
+ lex() {
70
+ if (this.scanSourceHygiene)
71
+ this.diagnoseForbiddenSourceCharacters();
72
+ while (!this.isAtEnd()) {
73
+ if (this.tokens.length >= MAX_TOKENS) {
74
+ this.diagnostics.push(diagnostic("VEL1005", `A VelarScript module cannot exceed ${MAX_TOKENS} tokens`, span(this.index, this.index)));
75
+ this.index = this.text.length;
76
+ break;
77
+ }
78
+ if (this.nesting > MAX_NESTING) {
79
+ this.diagnostics.push(diagnostic("VEL1006", `Delimiter nesting cannot exceed ${MAX_NESTING} levels`, span(this.index, this.index)));
80
+ this.index = this.text.length;
81
+ break;
82
+ }
83
+ if (this.atLineStart && this.nesting === 0 && !this.bracketFragment) {
84
+ this.readIndentation();
85
+ }
86
+ if (this.isAtEnd()) {
87
+ break;
88
+ }
89
+ const start = this.index;
90
+ const character = this.peek();
91
+ if (character === " " || character === "\t") {
92
+ this.advance();
93
+ continue;
94
+ }
95
+ if (character === "\n" || character === "\r") {
96
+ this.readNewline();
97
+ continue;
98
+ }
99
+ if (character === "/" && this.peek(1) === "/") {
100
+ this.readComment();
101
+ continue;
102
+ }
103
+ if (character === "/" && this.peek(1) === "*") {
104
+ this.readBlockComment();
105
+ continue;
106
+ }
107
+ if (this.readExtensionToken())
108
+ continue;
109
+ // D53 rule 117: only the two complete statement-head shapes claim a
110
+ // multiline backtick. Ordinary backtick strings keep their existing
111
+ // inline-only scanner and diagnostics everywhere else.
112
+ const embeddedJavaScript = scanEmbeddedJavaScriptLiteral(this.text, start);
113
+ if (embeddedJavaScript) {
114
+ this.readEmbeddedJavaScript(embeddedJavaScript);
115
+ continue;
116
+ }
117
+ // A raw inline string may legally start with a doubled delimiter:
118
+ // r"""quoted"" text". Prefer that unambiguous current spelling over
119
+ // the removed triple-quote migration scanner.
120
+ const rawString = scanStringLiteral(this.text, start);
121
+ if (rawString?.raw && rawString.closed && !rawString.layout) {
122
+ this.readString(rawString);
123
+ continue;
124
+ }
125
+ const legacyTriple = this.legacyTripleQuotePrefix();
126
+ if (legacyTriple) {
127
+ this.readLegacyTripleQuote(legacyTriple);
128
+ continue;
129
+ }
130
+ const string = scanStringLiteral(this.text, start);
131
+ if (string) {
132
+ this.readString(string);
133
+ continue;
134
+ }
135
+ if (this.isIdentifierStart(character)) {
136
+ this.readIdentifier();
137
+ continue;
138
+ }
139
+ if (this.isDigit(character)) {
140
+ this.readNumber();
141
+ continue;
142
+ }
143
+ switch (character) {
144
+ case "(":
145
+ this.simple("leftParen", start, 1);
146
+ this.nesting += 1;
147
+ break;
148
+ case ")":
149
+ this.simple("rightParen", start, 1);
150
+ this.nesting = Math.max(0, this.nesting - 1);
151
+ break;
152
+ case "[":
153
+ this.simple("leftBracket", start, 1);
154
+ this.nesting += 1;
155
+ break;
156
+ case "]":
157
+ this.simple("rightBracket", start, 1);
158
+ this.nesting = Math.max(0, this.nesting - 1);
159
+ break;
160
+ case "{":
161
+ this.simple("leftBrace", start, 1);
162
+ this.nesting += 1;
163
+ break;
164
+ case "}":
165
+ this.simple("rightBrace", start, 1);
166
+ this.nesting = Math.max(0, this.nesting - 1);
167
+ break;
168
+ case ":":
169
+ if (this.peek(1) === "=") {
170
+ // ':=' reads as the walrus operator to authors from the father
171
+ // language; recovery as '=' keeps 'x := 5' one diagnostic.
172
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "VelarScript has no ':=' binding operator; declare with 'const x = ...' or assign with 'x = ...'", span(start, start + 2)));
173
+ this.simple("assign", start, 2);
174
+ }
175
+ else {
176
+ this.simple("colon", start, 1);
177
+ }
178
+ break;
179
+ case ";":
180
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "A statement ends at its newline; VelarScript does not use ';'", span(start, start + 1),
181
+ // Only a semicolon the line ends with is mechanical: deleting it
182
+ // leaves the same one statement. A semicolon between two
183
+ // statements asks for a line break instead, which is a change of
184
+ // layout rather than of spelling, so it stays advice.
185
+ this.trailingSemicolonFix(start)));
186
+ this.advance();
187
+ break;
188
+ case ",":
189
+ this.simple("comma", start, 1);
190
+ break;
191
+ // D43 item 67: '@name' marks a name the language owns, in the very
192
+ // positions where a user's own names also appear — class and component
193
+ // members. '@' is not an identifier character, so the two namespaces
194
+ // cannot collide however the surrounding words are softened.
195
+ case "@":
196
+ this.simple("at", start, 1);
197
+ break;
198
+ case ".":
199
+ if (this.isDigit(this.peek(1))) {
200
+ this.readLeadingDotNumber();
201
+ }
202
+ else if (this.peek(1) === "." && this.peek(2) === ".") {
203
+ this.simple("ellipsis", start, 3);
204
+ }
205
+ else {
206
+ this.simple("dot", start, 1);
207
+ }
208
+ break;
209
+ case "?":
210
+ if (this.peek(1) === ".") {
211
+ this.simple("optionalDot", start, 2);
212
+ }
213
+ else if (this.peek(1) === "?") {
214
+ this.simple("nullish", start, 2);
215
+ }
216
+ else {
217
+ this.simple("question", start, 1);
218
+ }
219
+ break;
220
+ case "+":
221
+ this.operator("plus", "plusAssign", start);
222
+ break;
223
+ case "-":
224
+ if (this.peek(1) === ">") {
225
+ this.simple("arrow", start, 2);
226
+ }
227
+ else {
228
+ this.operator("minus", "minusAssign", start);
229
+ }
230
+ break;
231
+ case "*":
232
+ if (this.peek(1) === "*")
233
+ this.simple("starStar", start, 2);
234
+ else
235
+ this.operator("star", "starAssign", start);
236
+ break;
237
+ case "/":
238
+ this.operator("slash", "slashAssign", start);
239
+ break;
240
+ case "%":
241
+ this.operator("percent", "percentAssign", start);
242
+ break;
243
+ case "=":
244
+ if (this.peek(1) === ">") {
245
+ this.simple("fatArrow", start, 2);
246
+ }
247
+ else if (this.peek(1) === "=" && this.peek(2) === "=") {
248
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use '=='; equality is already strict in VelarScript", span(start, start + 3), mechanicalFix(span(start, start + 3), "==", "Use VelarScript strict equality '=='")));
249
+ this.simple("equal", start, 3);
250
+ }
251
+ else {
252
+ this.simple(this.peek(1) === "=" ? "equal" : "assign", start, this.peek(1) === "=" ? 2 : 1);
253
+ }
254
+ break;
255
+ case "!":
256
+ if (this.peek(1) === "=" && this.peek(2) === "=") {
257
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use '!='; inequality is already strict in VelarScript", span(start, start + 3), mechanicalFix(span(start, start + 3), "!=", "Use VelarScript strict inequality '!='")));
258
+ this.simple("notEqual", start, 3);
259
+ }
260
+ else if (this.peek(1) === "=") {
261
+ this.simple("notEqual", start, 2);
262
+ }
263
+ else {
264
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use 'not'; VelarScript uses readable logical operators", span(start, start + 1), this.wordOperatorFix(start, start + 1, "not", "Use readable 'not'")));
265
+ this.simple("not", start, 1);
266
+ }
267
+ break;
268
+ case "&":
269
+ if (this.peek(1) === "&") {
270
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use 'and'; VelarScript uses readable logical operators", span(start, start + 2), this.wordOperatorFix(start, start + 2, "and", "Use readable 'and'")));
271
+ this.simple("and", start, 2);
272
+ }
273
+ else
274
+ this.operator("amp", "bitAndAssign", start);
275
+ break;
276
+ case "^":
277
+ this.operator("caret", "bitXorAssign", start);
278
+ break;
279
+ case "~":
280
+ this.simple("tilde", start, 1);
281
+ break;
282
+ case "<":
283
+ if (this.peek(1) === "<")
284
+ this.simple(this.peek(2) === "=" ? "leftShiftAssign" : "leftShift", start, this.peek(2) === "=" ? 3 : 2);
285
+ else
286
+ this.simple(this.peek(1) === "=" ? "lessEqual" : "less", start, this.peek(1) === "=" ? 2 : 1);
287
+ break;
288
+ case ">":
289
+ if (this.peek(1) === ">" && this.peek(2) === ">")
290
+ this.simple(this.peek(3) === "=" ? "unsignedRightShiftAssign" : "unsignedRightShift", start, this.peek(3) === "=" ? 4 : 3);
291
+ else if (this.peek(1) === ">")
292
+ this.simple(this.peek(2) === "=" ? "rightShiftAssign" : "rightShift", start, this.peek(2) === "=" ? 3 : 2);
293
+ else
294
+ this.simple(this.peek(1) === "=" ? "greaterEqual" : "greater", start, this.peek(1) === "=" ? 2 : 1);
295
+ break;
296
+ case "|":
297
+ if (this.peek(1) === "|") {
298
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use 'or'; VelarScript uses readable logical operators", span(start, start + 2), this.wordOperatorFix(start, start + 2, "or", "Use readable 'or'")));
299
+ this.simple("or", start, 2);
300
+ }
301
+ else
302
+ this.operator("pipe", "bitOrAssign", start);
303
+ break;
304
+ // D43 item 67: `@name` is the one spelling for a name the language
305
+ // owns in a position where an author's own name may also appear. '@'
306
+ // is not an identifier character, so the two can never collide.
307
+ case "@":
308
+ this.simple("at", start, 1);
309
+ break;
310
+ case "#":
311
+ if (this.readJavaScriptPrivateIdentifier(start))
312
+ break;
313
+ if (this.readHexColor(start))
314
+ break;
315
+ if (this.readHashComment(start))
316
+ break;
317
+ this.invalidCharacter(character, start);
318
+ break;
319
+ default:
320
+ this.invalidCharacter(character, start);
321
+ break;
322
+ }
323
+ }
324
+ if (this.tokens.at(-1)?.kind !== "newline") {
325
+ this.tokens.push({ kind: "newline", value: "", span: span(this.index, this.index) });
326
+ }
327
+ while (this.indentStack.length > 1) {
328
+ this.indentStack.pop();
329
+ this.tokens.push({ kind: "dedent", value: "", span: span(this.index, this.index) });
330
+ }
331
+ this.tokens.push({ kind: "eof", value: "", span: span(this.index, this.index) });
332
+ return { tokens: this.tokens, diagnostics: this.diagnostics };
333
+ }
334
+ readIndentation() {
335
+ const start = this.index;
336
+ let width = 0;
337
+ while (!this.isAtEnd()) {
338
+ if (this.peek() === " ") {
339
+ width += 1;
340
+ this.advance();
341
+ }
342
+ else if (this.peek() === "\t") {
343
+ this.diagnostics.push(diagnostic("VEL1002", "Tabs are not allowed for indentation", span(this.index, this.index + 1), mechanicalFix(span(this.index, this.index + 1), " ", "Replace the indentation tab with four spaces")));
344
+ width += 4;
345
+ this.advance();
346
+ }
347
+ else {
348
+ break;
349
+ }
350
+ }
351
+ const blank = this.peek() === "\n" || this.peek() === "\r" || this.isAtEnd();
352
+ const comment = (this.peek() === "/" && this.peek(1) === "/") || this.blockCommentOwnsLine();
353
+ this.atLineStart = false;
354
+ if (blank || comment) {
355
+ return;
356
+ }
357
+ // A leading-dot line continues the previous logical line: the newline
358
+ // tokens that ended it are withdrawn and this line's indentation does not
359
+ // open or close a block, so '.filter(...)' chains span physical lines.
360
+ if (this.isChainContinuation()) {
361
+ while (this.tokens.at(-1)?.kind === "newline")
362
+ this.tokens.pop();
363
+ return;
364
+ }
365
+ const current = this.indentStack.at(-1) ?? 0;
366
+ if (width > current) {
367
+ if (this.indentStack.length > MAX_NESTING) {
368
+ this.diagnostics.push(diagnostic("VEL1006", `Indentation nesting cannot exceed ${MAX_NESTING} levels`, span(start, this.index)));
369
+ this.index = this.text.length;
370
+ return;
371
+ }
372
+ this.indentStack.push(width);
373
+ this.tokens.push({ kind: "indent", value: "", span: span(start, this.index) });
374
+ return;
375
+ }
376
+ if (width < current) {
377
+ while (this.indentStack.length > 1 && width < (this.indentStack.at(-1) ?? 0)) {
378
+ this.indentStack.pop();
379
+ this.tokens.push({ kind: "dedent", value: "", span: span(start, this.index) });
380
+ }
381
+ if (width !== (this.indentStack.at(-1) ?? 0)) {
382
+ this.diagnostics.push(diagnostic("VEL1004", "Indentation does not match an outer block", span(start, this.index)));
383
+ }
384
+ }
385
+ }
386
+ isChainContinuation() {
387
+ const dotWidth = this.peek() === "." ? 1 : this.peek() === "?" && this.peek(1) === "." ? 2 : 0;
388
+ if (dotWidth === 0 || !this.isIdentifierStart(this.peek(dotWidth)))
389
+ return false;
390
+ let index = this.tokens.length - 1;
391
+ if (this.tokens[index]?.kind !== "newline")
392
+ return false;
393
+ while (this.tokens[index]?.kind === "newline")
394
+ index -= 1;
395
+ const previous = this.tokens[index];
396
+ return previous !== undefined && chainContinuationEndKinds.has(previous.kind);
397
+ }
398
+ readNewline() {
399
+ const start = this.index;
400
+ if (this.peek() === "\r" && this.peek(1) === "\n") {
401
+ this.index += 2;
402
+ }
403
+ else {
404
+ this.index += 1;
405
+ }
406
+ if (this.nesting === 0 && !this.bracketFragment) {
407
+ this.tokens.push({ kind: "newline", value: "", span: span(start, this.index) });
408
+ this.atLineStart = true;
409
+ }
410
+ }
411
+ readComment() {
412
+ while (!this.isAtEnd() && this.peek() !== "\n" && this.peek() !== "\r") {
413
+ this.advance();
414
+ }
415
+ }
416
+ readBlockComment() {
417
+ const start = this.index;
418
+ const openingLineStart = this.lineStart(start);
419
+ const openingStandalone = this.text.slice(openingLineStart, start).trim().length === 0;
420
+ this.index += 2;
421
+ let depth = 1;
422
+ let firstNewline = -1;
423
+ while (!this.isAtEnd() && depth > 0) {
424
+ if (this.text.startsWith("/*", this.index)) {
425
+ depth += 1;
426
+ this.index += 2;
427
+ }
428
+ else if (this.text.startsWith("*/", this.index)) {
429
+ depth -= 1;
430
+ this.index += 2;
431
+ }
432
+ else {
433
+ if (firstNewline < 0 && (this.peek() === "\n" || this.peek() === "\r"))
434
+ firstNewline = this.index;
435
+ this.advance();
436
+ }
437
+ }
438
+ if (depth > 0) {
439
+ this.diagnostics.push(diagnostic("VEL1003", "Unterminated block comment; close it with '*/'", span(start, this.index)));
440
+ return;
441
+ }
442
+ if (firstNewline < 0)
443
+ return;
444
+ const closeStart = this.index - 2;
445
+ const closingLineStart = this.lineStart(closeStart);
446
+ const closingPrefixEmpty = this.text.slice(closingLineStart, closeStart).trim().length === 0;
447
+ let closingLineEnd = this.index;
448
+ while (closingLineEnd < this.text.length && this.text[closingLineEnd] !== "\n" && this.text[closingLineEnd] !== "\r")
449
+ closingLineEnd += 1;
450
+ const closingSuffixEmpty = this.text.slice(this.index, closingLineEnd).trim().length === 0;
451
+ const openingSuffixEmpty = this.text.slice(start + 2, firstNewline).trim().length === 0;
452
+ if (!openingStandalone || !openingSuffixEmpty || !closingPrefixEmpty || !closingSuffixEmpty) {
453
+ this.diagnostics.push(diagnostic("VEL1010", "A multiline block comment must occupy whole lines: write only '/*' on its opening line and only '*/' on its closing line", span(start, this.index)));
454
+ }
455
+ }
456
+ blockCommentOwnsLine() {
457
+ if (this.peek() !== "/" || this.peek(1) !== "*")
458
+ return false;
459
+ let cursor = this.index + 2;
460
+ let depth = 1;
461
+ while (cursor < this.text.length && this.text[cursor] !== "\n" && this.text[cursor] !== "\r") {
462
+ if (this.text.startsWith("/*", cursor)) {
463
+ depth += 1;
464
+ cursor += 2;
465
+ }
466
+ else if (this.text.startsWith("*/", cursor)) {
467
+ depth -= 1;
468
+ cursor += 2;
469
+ if (depth === 0)
470
+ return this.text.slice(cursor, this.lineEnd(cursor)).trim().length === 0;
471
+ }
472
+ else {
473
+ cursor += 1;
474
+ }
475
+ }
476
+ return true;
477
+ }
478
+ readIdentifier() {
479
+ const start = this.index;
480
+ while (this.isIdentifierPart(this.peek())) {
481
+ this.advance();
482
+ }
483
+ const value = this.text.slice(start, this.index);
484
+ const previous = this.tokens.at(-1)?.kind;
485
+ // `int` remains forbidden as a type or binding, but velar/random owns the
486
+ // method spelling Random.int(...). Member names are not type vocabulary.
487
+ const rule = value === "int" && (previous === "dot" || previous === "optionalDot")
488
+ ? undefined
489
+ : forbiddenSourceIdentifiers.get(value);
490
+ const extensionGuidance = rule ? undefined : this.extensionForbiddenIdentifiers.get(value);
491
+ if ((value === "Infinity" || value === "NaN") && previous !== "dot" && previous !== "optionalDot") {
492
+ this.diagnostics.push(diagnostic("VEL1007", value === "Infinity"
493
+ ? "Infinity is not a literal in VelarScript; produce it with arithmetic such as 1 / 0"
494
+ : "NaN is not a literal in VelarScript; produce it with arithmetic such as 0 / 0 and detect it with value.isNaN()", span(start, this.index)));
495
+ this.tokens.push({ kind: "number", value: "0", span: span(start, this.index) });
496
+ return;
497
+ }
498
+ if (rule) {
499
+ if (rule.recovery) {
500
+ // The rule carries its successor only when the guidance names exactly
501
+ // one ('var' names 'let' or 'const', so it names none).
502
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", rule.guidance, span(start, this.index), rule.fix === null ? undefined : mechanicalFix(span(start, rule.fix === "" ? this.skipHorizontalWhitespace(this.index) : this.index), rule.fix, rule.fix === "" ? `Remove '${value}'` : `Use '${rule.fix}'`)));
503
+ for (const item of rule.recovery) {
504
+ this.tokens.push({ kind: item.kind, value: item.value, span: span(start, this.index) });
505
+ }
506
+ return;
507
+ }
508
+ this.diagnostics.push(diagnostic("VEL1005", rule.guidance, span(start, this.index)));
509
+ }
510
+ else if (extensionGuidance) {
511
+ this.diagnostics.push(diagnostic("VEL1005", extensionGuidance, span(start, this.index)));
512
+ }
513
+ else if (isForbiddenPrototypeMember(value) && (previous === "dot" || previous === "optionalDot")) {
514
+ this.diagnostics.push(diagnostic("VEL1005", "VelarScript does not expose prototype manipulation", span(start, this.index)));
515
+ }
516
+ const keyword = Object.hasOwn(keywordKinds, value) ? keywordKinds[value] : undefined;
517
+ this.tokens.push({ kind: keyword ?? "identifier", value, span: span(start, this.index) });
518
+ }
519
+ readNumber() {
520
+ const start = this.index;
521
+ if (this.peek() === "0" && ["x", "X", "b", "B", "o", "O"].includes(this.peek(1))) {
522
+ this.readRadixNumber();
523
+ return;
524
+ }
525
+ const integer = this.readDigitsWithSeparators();
526
+ if (integer.length > 1 && integer.startsWith("0")) {
527
+ this.diagnostics.push(diagnostic("VEL1007", "Remove the leading zeros; octal literals are not part of VelarScript", span(start, this.index)));
528
+ }
529
+ let value = integer;
530
+ if (this.peek() === "." && (this.isDigit(this.peek(1)) || this.peek(1) === "_")) {
531
+ this.advance();
532
+ value += `.${this.readDigitsWithSeparators()}`;
533
+ }
534
+ else if (this.peek() === "." && !this.isIdentifierStart(this.peek(1)) && this.peek(1) !== ".") {
535
+ const point = this.index;
536
+ this.advance();
537
+ value += ".0";
538
+ this.diagnostics.push(recoveredDiagnostic("VEL1007", `Write '${integer}.0'; decimal literals require a digit after the point`, span(point, this.index), mechanicalFix(span(point, this.index), ".0", `Write '${integer}.0'`)));
539
+ }
540
+ if ((this.peek() === "e" || this.peek() === "E")
541
+ && (this.isDigit(this.peek(1)) || this.peek(1) === "_"
542
+ || ((this.peek(1) === "+" || this.peek(1) === "-") && (this.isDigit(this.peek(2)) || this.peek(2) === "_")))) {
543
+ const exponent = this.advance();
544
+ value += exponent;
545
+ if (this.peek() === "+" || this.peek() === "-")
546
+ value += this.advance();
547
+ value += this.readDigitsWithSeparators();
548
+ }
549
+ const numberEnd = this.index;
550
+ // LOK-I5: where the percentage unit does not exist, Core still reads `50%`
551
+ // as the percentage shape in the positions where no remainder operand can
552
+ // follow — end of line, `)`, `]`, `,` — so the author gets the unit's own
553
+ // guidance instead of a statement-continuation error about a spelling they
554
+ // never meant as arithmetic. `10 % 3` and `10%3` both keep a right operand
555
+ // and stay remainder in Core.
556
+ if (this.peek() === "%" && (this.numericSuffixes.has("%") || this.isPercentUnitPosition()))
557
+ this.advance();
558
+ else
559
+ while (this.isIdentifierPart(this.peek()))
560
+ this.advance();
561
+ const suffix = this.text.slice(numberEnd, this.index);
562
+ if (suffix && this.numericSuffixes.has(suffix)) {
563
+ this.tokens.push({ kind: "unitNumber", value: `${value}${suffix}`, span: span(start, this.index) });
564
+ return;
565
+ }
566
+ if (suffix) {
567
+ const radix = integer === "0" && suffix.length > 1 ? suffix[0]?.toLowerCase() : null;
568
+ const radixName = radix === "x" ? "Hexadecimal" : radix === "b" ? "Binary" : radix === "o" ? "Octal" : null;
569
+ // The unit vocabulary is the Web extension's. A Core file that spells a
570
+ // Look unit names the extension that owns it and how to enable it —
571
+ // D37 rule 45's cross-extension voice — instead of calling a perfectly
572
+ // good spelling unknown.
573
+ const owner = webNumericUnitOwner(suffix);
574
+ this.diagnostics.push(diagnostic("VEL1007", radixName
575
+ ? `${radixName} literals are not part of VelarScript; write the decimal value`
576
+ : owner
577
+ ? `The numeric unit '${suffix}' belongs to ${owner}; add "${owner}" to velar.json extensions, or move this module into a Web project`
578
+ : this.numericSuffixes.size > 0
579
+ ? `Unknown numeric unit '${suffix}'`
580
+ : `Unexpected characters '${suffix}' after a number`, span(numberEnd, this.index)));
581
+ }
582
+ this.tokens.push({ kind: "number", value, span: span(start, numberEnd) });
583
+ }
584
+ readRadixNumber() {
585
+ const start = this.index;
586
+ const prefix = this.peek(1).toLowerCase();
587
+ const radix = prefix === "x" ? 16 : prefix === "b" ? 2 : 8;
588
+ const radixName = radix === 16 ? "hexadecimal" : radix === 2 ? "binary" : "octal";
589
+ this.advance();
590
+ this.advance();
591
+ let digits = "";
592
+ let sawDigit = false;
593
+ while (this.isIdentifierPart(this.peek())) {
594
+ const character = this.peek();
595
+ if (character === "_") {
596
+ const separator = this.index;
597
+ const previous = this.text[this.index - 1] ?? "";
598
+ const next = this.peek(1);
599
+ this.advance();
600
+ if (!this.isRadixDigit(previous, radix) || !this.isRadixDigit(next, radix)) {
601
+ this.diagnostics.push(diagnostic("VEL1007", "Numeric separators must appear only between digits", span(separator, this.index)));
602
+ }
603
+ continue;
604
+ }
605
+ if (!this.isRadixDigit(character, radix)) {
606
+ const invalidStart = this.index;
607
+ while (this.isIdentifierPart(this.peek()))
608
+ this.advance();
609
+ this.diagnostics.push(diagnostic("VEL1007", `Invalid digit in ${radixName} integer literal`, span(invalidStart, this.index)));
610
+ break;
611
+ }
612
+ sawDigit = true;
613
+ digits += this.advance();
614
+ }
615
+ if (!sawDigit) {
616
+ this.diagnostics.push(diagnostic("VEL1007", `${radixName[0].toUpperCase()}${radixName.slice(1)} integer literals require at least one digit`, span(start, this.index)));
617
+ digits = "0";
618
+ }
619
+ this.tokens.push({ kind: "number", value: `0${prefix}${digits}`, span: span(start, this.index) });
620
+ }
621
+ isRadixDigit(character, radix) {
622
+ if (character >= "0" && character <= "9")
623
+ return Number(character) < radix;
624
+ if (radix !== 16)
625
+ return false;
626
+ const lower = character.toLowerCase();
627
+ return lower >= "a" && lower <= "f";
628
+ }
629
+ /**
630
+ * LOK-I5: `%` right after a number is the percentage unit only where a
631
+ * remainder operator could not stand — the operator always takes a right
632
+ * operand, so a `%` followed by a line end, a closing bracket, or a
633
+ * separator is a unit spelling and nothing else.
634
+ */
635
+ isPercentUnitPosition() {
636
+ const next = this.peek(1);
637
+ return next === "\0" || next === "\n" || next === "\r" || next === ")" || next === "]" || next === "}" || next === "," || next === ";" || next === ":";
638
+ }
639
+ readLeadingDotNumber() {
640
+ const start = this.index;
641
+ this.advance();
642
+ let value = `0.${this.readDigitsWithSeparators()}`;
643
+ if ((this.peek() === "e" || this.peek() === "E")
644
+ && (this.isDigit(this.peek(1)) || this.peek(1) === "_"
645
+ || ((this.peek(1) === "+" || this.peek(1) === "-") && (this.isDigit(this.peek(2)) || this.peek(2) === "_")))) {
646
+ value += this.advance();
647
+ if (this.peek() === "+" || this.peek() === "-")
648
+ value += this.advance();
649
+ value += this.readDigitsWithSeparators();
650
+ }
651
+ this.diagnostics.push(recoveredDiagnostic("VEL1007", `Write '${value}'; decimal literals require a digit before the point`, span(start, this.index), mechanicalFix(span(start, this.index), value, `Write '${value}'`)));
652
+ this.tokens.push({ kind: "number", value, span: span(start, this.index) });
653
+ }
654
+ readDigitsWithSeparators() {
655
+ let value = "";
656
+ while (this.isDigit(this.peek()) || this.peek() === "_") {
657
+ if (this.isDigit(this.peek())) {
658
+ value += this.advance();
659
+ continue;
660
+ }
661
+ const separator = this.index;
662
+ const valid = this.isDigit(this.text[this.index - 1] ?? "") && this.isDigit(this.peek(1));
663
+ this.advance();
664
+ if (!valid) {
665
+ this.diagnostics.push(diagnostic("VEL1007", "Numeric separators must appear only between digits", span(separator, this.index)));
666
+ }
667
+ }
668
+ return value;
669
+ }
670
+ readString(scanned) {
671
+ const start = this.index;
672
+ this.index = scanned.end;
673
+ this.diagnoseStringContents(scanned);
674
+ if (!scanned.closed) {
675
+ const message = scanned.layout
676
+ ? "Unterminated layout string; close it with a quote at the opening line's indentation"
677
+ : scanned.quote === "`"
678
+ ? "Inline strings cannot contain a line break; use a double-quoted layout string with the opening quote at the end of its line"
679
+ : `Unterminated ${scanned.interpolated ? "interpolated " : ""}string literal before the end of the line`;
680
+ this.diagnostics.push(diagnostic("VEL1003", message, span(start, this.index)));
681
+ }
682
+ if (scanned.indentationError) {
683
+ this.diagnostics.push(diagnostic("VEL1004", "Layout string lines must keep the indentation established by the first content line", span(scanned.indentationError.start, scanned.indentationError.end)));
684
+ }
685
+ if (!scanned.canonical) {
686
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use 'rf' rather than 'fr' for raw interpolated strings", span(start, start + scanned.prefixLength), mechanicalFix(span(start, start + scanned.prefixLength), "rf", "Use the 'rf' raw interpolated string prefix")));
687
+ }
688
+ if (scanned.quote === "'") {
689
+ this.diagnostics.push(diagnostic("VEL1005", "Use double quotes or backticks for strings; single-quoted strings are not part of VelarScript", span(start + scanned.prefixLength, Math.min(this.index, start + scanned.prefixLength + 1))));
690
+ }
691
+ const payload = {
692
+ prefixLength: scanned.prefixLength,
693
+ quote: scanned.quote,
694
+ raw: scanned.raw,
695
+ layout: scanned.layout,
696
+ ...(scanned.contentOffsets ? { contentOffsets: scanned.contentOffsets } : {}),
697
+ };
698
+ this.tokens.push({
699
+ kind: scanned.interpolated ? "fstring" : "string",
700
+ value: scanned.interpolated ? scanned.content : this.decodeStringText(scanned.content, scanned.raw, scanned.quote, scanned.layout),
701
+ span: span(start, this.index),
702
+ payload,
703
+ });
704
+ // An unterminated layout string swallows its line breaks, so recovery also
705
+ // closes the logical line. Without this the next physical line would be
706
+ // read as a leftover token on the broken line, and the statement-boundary
707
+ // rule would report it instead of letting it declare its own names.
708
+ if (scanned.recoverAtLineStart) {
709
+ this.atLineStart = true;
710
+ this.tokens.push({ kind: "newline", value: "", span: span(this.index, this.index) });
711
+ }
712
+ }
713
+ readEmbeddedJavaScript(scanned) {
714
+ this.index = scanned.end;
715
+ if (!scanned.openingLineBreak) {
716
+ this.diagnostics.push(diagnostic("VEL1003", "An inline JavaScript source block begins on the line after its opening backtick", span(scanned.start, Math.min(this.text.length, scanned.start + 1))));
717
+ }
718
+ if (!scanned.closed) {
719
+ this.diagnostics.push(diagnostic("VEL1003", scanned.kind === "checked"
720
+ ? "Unterminated checked JavaScript source block; close it with '`:' alone at the declaration's indentation"
721
+ : "Unterminated unsafe JavaScript source block; close it with '`' alone at the declaration's indentation", span(scanned.start, scanned.end)));
722
+ }
723
+ const sourceSpan = span(scanned.sourceStart, scanned.sourceEnd);
724
+ this.tokens.push({
725
+ kind: "string",
726
+ value: this.text.slice(sourceSpan.start, sourceSpan.end),
727
+ span: span(scanned.start, scanned.end),
728
+ payload: {
729
+ embeddedJavaScript: true,
730
+ kind: scanned.kind,
731
+ sourceSpan,
732
+ },
733
+ });
734
+ }
735
+ diagnoseStringContents(scanned) {
736
+ const sourceOffset = (index) => scanned.contentOffsets?.[index] ?? scanned.contentStart + index;
737
+ for (let index = 0; index < scanned.content.length; index += 1) {
738
+ const character = scanned.content[index];
739
+ const next = scanned.content[index + 1];
740
+ if (!scanned.raw && character === "\\") {
741
+ const escaped = scanStringEscape(scanned.content, index);
742
+ if (escaped.error !== null) {
743
+ const start = sourceOffset(index);
744
+ const messages = {
745
+ legacyUnicode: "Use a braced Unicode escape such as '\\u{E9}'; '\\uXXXX' escapes are not part of VelarScript",
746
+ hex: "Use a braced Unicode escape such as '\\u{E9}'; '\\xNN' escapes are not part of VelarScript",
747
+ unicodeForm: "A Unicode escape must be '\\u{' followed by 1 to 6 hexadecimal digits and '}'",
748
+ unicodeRange: "A Unicode escape cannot exceed U+10FFFF",
749
+ unicodeSurrogate: "A Unicode escape cannot encode a surrogate from U+D800 through U+DFFF",
750
+ unknown: `Unknown string escape '${next === "\n" || next === "\r" ? "line break" : `\\${next ?? ""}`}'; use '\\\\' for a literal backslash or an r\"...\" raw string`,
751
+ };
752
+ this.diagnostics.push(diagnostic("VEL1008", messages[escaped.error], span(start, sourceOffset(escaped.end))));
753
+ }
754
+ index = escaped.end - 1;
755
+ continue;
756
+ }
757
+ const codePoint = character.codePointAt(0);
758
+ if (!this.isBidirectionalControl(codePoint) && this.isForbiddenLiteralControl(codePoint)) {
759
+ const start = sourceOffset(index);
760
+ this.diagnostics.push(diagnostic("VEL1009", `Control character U+${codePoint.toString(16).toUpperCase().padStart(4, "0")} must be written with a '\\u{...}' escape inside a string literal`, span(start, sourceOffset(index + 1))));
761
+ }
762
+ if (!scanned.interpolated || character !== "{")
763
+ continue;
764
+ if (scanned.content[index - 1] === "$")
765
+ continue;
766
+ if (next === "{") {
767
+ index += 1;
768
+ continue;
769
+ }
770
+ const close = findInterpolatedExpressionEnd(scanned.content, index + 1);
771
+ if (close < 0)
772
+ break;
773
+ index = close;
774
+ }
775
+ }
776
+ decodeStringText(value, raw, quote, layout) {
777
+ let decoded = "";
778
+ for (let index = 0; index < value.length; index += 1) {
779
+ const character = value[index];
780
+ const next = value[index + 1];
781
+ if (raw && !layout && character === quote && next === quote) {
782
+ decoded += quote;
783
+ index += 1;
784
+ }
785
+ else if (!raw && character === "\\" && next !== undefined) {
786
+ const escaped = scanStringEscape(value, index);
787
+ decoded += escaped.value ?? next;
788
+ index = escaped.end - 1;
789
+ }
790
+ else {
791
+ decoded += character;
792
+ }
793
+ }
794
+ return decoded;
795
+ }
796
+ legacyTripleQuotePrefix() {
797
+ for (const prefix of ["rf", "fr", "f", "r", ""]) {
798
+ if (!this.text.startsWith(`${prefix}\"\"\"`, this.index))
799
+ continue;
800
+ return {
801
+ prefix,
802
+ interpolated: prefix === "f" || prefix === "rf" || prefix === "fr",
803
+ raw: prefix === "r" || prefix === "rf" || prefix === "fr",
804
+ };
805
+ }
806
+ return null;
807
+ }
808
+ readLegacyTripleQuote(options) {
809
+ const start = this.index;
810
+ this.index += options.prefix.length + 3;
811
+ const contentStart = this.index;
812
+ while (!this.isAtEnd() && !this.text.startsWith('\"\"\"', this.index))
813
+ this.index += 1;
814
+ const closed = !this.isAtEnd();
815
+ const contentEnd = this.index;
816
+ if (closed)
817
+ this.index += 3;
818
+ const canonicalPrefix = options.prefix === "fr" ? "rf" : options.prefix;
819
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", `Use a ${canonicalPrefix ? `'${canonicalPrefix}\"'` : "'\"'"} layout string; VelarScript uses indentation rather than triple-quote delimiters`, span(start, this.index)));
820
+ if (!closed)
821
+ this.diagnostics.push(diagnostic("VEL1003", "Unterminated legacy triple-quoted string", span(start, this.index)));
822
+ this.tokens.push({
823
+ kind: options.interpolated ? "fstring" : "string",
824
+ value: this.text.slice(contentStart, contentEnd),
825
+ span: span(start, this.index),
826
+ ...(options.interpolated ? {
827
+ payload: { prefixLength: options.prefix.length, quote: '"', raw: options.raw, layout: true },
828
+ } : {}),
829
+ });
830
+ }
831
+ readExtensionToken() {
832
+ for (const scanner of this.extensionScanners) {
833
+ const result = scanner({
834
+ source: this.text,
835
+ offset: this.index,
836
+ currentIndent: this.indentStack.at(-1) ?? 0,
837
+ tokens: this.tokens,
838
+ });
839
+ if (!result)
840
+ continue;
841
+ if (result.token.kind !== "extensionToken" || result.nextOffset <= this.index || result.nextOffset > this.text.length) {
842
+ throw new Error("A compiler lexical extension returned an invalid token boundary");
843
+ }
844
+ this.tokens.push(result.token);
845
+ this.diagnostics.push(...result.diagnostics ?? []);
846
+ this.index = result.nextOffset;
847
+ this.atLineStart = result.startsLine ?? false;
848
+ return true;
849
+ }
850
+ return false;
851
+ }
852
+ operator(single, compound, start) {
853
+ this.simple(this.peek(1) === "=" ? compound : single, start, this.peek(1) === "=" ? 2 : 1);
854
+ }
855
+ /**
856
+ * The rewrite of a symbol operator to its word spelling. A word needs air on
857
+ * either side that a symbol did not: 'a&&b' becomes 'a and b', while
858
+ * 'a && b' keeps the spacing it already had.
859
+ */
860
+ wordOperatorFix(start, end, word, title) {
861
+ const before = this.text[start - 1];
862
+ const after = this.text[end];
863
+ const left = before !== undefined && !/[\s([{,]/u.test(before) ? " " : "";
864
+ const right = after !== undefined && !/[\s)\]},]/u.test(after) ? " " : "";
865
+ return mechanicalFix(span(start, end), `${left}${word}${right}`, title);
866
+ }
867
+ skipHorizontalWhitespace(index) {
868
+ let end = index;
869
+ while (this.text[end] === " " || this.text[end] === "\t")
870
+ end += 1;
871
+ return end;
872
+ }
873
+ /**
874
+ * The deletion of a line-ending semicolon, including the blank space it would
875
+ * leave behind. A semicolon followed by anything except further semicolons,
876
+ * spaces, or a comment separates two statements: putting those on their own
877
+ * lines is a change of layout rather than of spelling, so it carries no fix.
878
+ */
879
+ trailingSemicolonFix(start) {
880
+ let end = start + 1;
881
+ while (this.text[end] === ";" || this.text[end] === " " || this.text[end] === "\t")
882
+ end += 1;
883
+ const rest = this.text.slice(end, this.lineEnd(end));
884
+ if (rest.length > 0 && !rest.startsWith("//") && !rest.startsWith("/*"))
885
+ return undefined;
886
+ let from = start;
887
+ while (from > 0 && (this.text[from - 1] === " " || this.text[from - 1] === "\t"))
888
+ from -= 1;
889
+ // Indentation is not the semicolon's whitespace to take.
890
+ return mechanicalFix(span(Math.max(from, this.lineStart(start)), start + 1), "", "Remove the semicolon");
891
+ }
892
+ simple(kind, start, length) {
893
+ this.index += length;
894
+ this.tokens.push({ kind, value: this.text.slice(start, this.index), span: span(start, this.index) });
895
+ }
896
+ // A bare hex color such as '#3478f6' is guided to its quoted-string
897
+ // spelling and recovered as that string token, so the digits never fall
898
+ // into number lexing and produce a misleading unknown-numeric-unit error.
899
+ readHexColor(start) {
900
+ let length = 0;
901
+ while (/[0-9a-fA-F]/.test(this.peek(1 + length)))
902
+ length += 1;
903
+ if ((length !== 3 && length !== 4 && length !== 6 && length !== 8) || this.isIdentifierPart(this.peek(1 + length))) {
904
+ return false;
905
+ }
906
+ const end = start + 1 + length;
907
+ const text = this.text.slice(start, end);
908
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", `Use '"${text}"'; VelarScript writes hex colors as quoted strings or color builders such as rgb(...)`, span(start, end), mechanicalFix(span(start, end), `"${text}"`, `Quote the hex color as '"${text}"'`)));
909
+ this.tokens.push({ kind: "string", value: text, span: span(start, end) });
910
+ this.index = end;
911
+ return true;
912
+ }
913
+ readJavaScriptPrivateIdentifier(start) {
914
+ const previous = this.tokens.at(-1);
915
+ const memberAccess = previous?.kind === "dot" || previous?.kind === "optionalDot";
916
+ const declaration = previous?.kind === "let" || previous?.kind === "const" || previous?.kind === "def"
917
+ || (previous?.kind === "identifier" && previous.value === "get");
918
+ if ((!memberAccess && !declaration) || !this.isIdentifierStart(this.peek(1)))
919
+ return false;
920
+ this.index = start + 1;
921
+ const nameStart = this.index;
922
+ while (this.isIdentifierPart(this.peek()))
923
+ this.advance();
924
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Remove '#'; VelarScript owns class privacy and does not expose JavaScript private identifiers", span(start, start + 1), mechanicalFix(span(start, start + 1), "", "Remove the JavaScript private marker")));
925
+ this.tokens.push({ kind: "identifier", value: this.text.slice(nameStart, this.index), span: span(nameStart, this.index) });
926
+ return true;
927
+ }
928
+ // A '#' that starts a line is a Python-style comment: it receives "use //"
929
+ // guidance and the rest of the line is skipped like a comment, so the
930
+ // commented text never produces its own error cascade. Bare hex colors were
931
+ // already consumed by readHexColor before this check runs.
932
+ readHashComment(start) {
933
+ const previous = this.tokens.at(-1)?.kind;
934
+ const lineStart = previous === undefined || previous === "newline" || previous === "indent" || previous === "dedent";
935
+ if (!lineStart)
936
+ return false;
937
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use '//' for comments; VelarScript comments start with '//'", span(start, start + 1), mechanicalFix(span(start, start + 1), "//", "Use '//' to start the comment")));
938
+ this.index = start;
939
+ this.advance();
940
+ this.readComment();
941
+ return true;
942
+ }
943
+ diagnoseForbiddenSourceCharacters() {
944
+ for (let index = 0; index < this.text.length; index += 1) {
945
+ const codePoint = this.text.codePointAt(index);
946
+ if (!this.isBidirectionalControl(codePoint)) {
947
+ if (codePoint > 0xffff)
948
+ index += 1;
949
+ continue;
950
+ }
951
+ this.diagnosedBidirectionalOffsets.add(index);
952
+ const point = codePoint.toString(16).toUpperCase().padStart(4, "0");
953
+ this.diagnostics.push(diagnostic("VEL1009", `Bidirectional control U+${point} cannot appear directly in VelarScript source; write it inside a string as '\\u{${point}}' so the source remains reviewable`, span(index, index + 1)));
954
+ }
955
+ }
956
+ isBidirectionalControl(codePoint) {
957
+ return bidirectionalControls.has(codePoint);
958
+ }
959
+ isForbiddenLiteralControl(codePoint) {
960
+ // Physical CR/LF are structural content in layout strings. Every other C0
961
+ // control, DEL, and the C1 block must use the visible escape spelling.
962
+ return (codePoint >= 0 && codePoint <= 0x1f && codePoint !== 0x0a && codePoint !== 0x0d)
963
+ || (codePoint >= 0x7f && codePoint <= 0x9f);
964
+ }
965
+ lineStart(index) {
966
+ while (index > 0 && this.text[index - 1] !== "\n" && this.text[index - 1] !== "\r")
967
+ index -= 1;
968
+ return index;
969
+ }
970
+ lineEnd(index) {
971
+ while (index < this.text.length && this.text[index] !== "\n" && this.text[index] !== "\r")
972
+ index += 1;
973
+ return index;
974
+ }
975
+ invalidCharacter(character, start) {
976
+ this.advance();
977
+ if (this.diagnosedBidirectionalOffsets.has(start) || this.isBidirectionalControl(character.codePointAt(0)))
978
+ return;
979
+ this.diagnostics.push(diagnostic("VEL1001", character === "\uFEFF"
980
+ ? "Unexpected UTF-8 BOM (U+FEFF); remove the BOM or save the file as UTF-8 without BOM"
981
+ : `Unexpected character '${character}'`, span(start, this.index)));
982
+ }
983
+ isAtEnd() {
984
+ return this.index >= this.text.length;
985
+ }
986
+ peek(distance = 0) {
987
+ return this.text[this.index + distance] ?? "\0";
988
+ }
989
+ advance() {
990
+ const character = this.peek();
991
+ this.index += 1;
992
+ return character;
993
+ }
994
+ isIdentifierStart(character) {
995
+ return isSourceIdentifierStart(character);
996
+ }
997
+ isIdentifierPart(character) {
998
+ return isSourceIdentifierPart(character);
999
+ }
1000
+ isDigit(character) {
1001
+ return character >= "0" && character <= "9";
1002
+ }
1003
+ }
1004
+ //# sourceMappingURL=lexer.js.map