@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.
- package/LICENSE +201 -0
- package/README.md +70 -0
- package/dist/analyzer.d.ts +1175 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +12130 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/application-package-host.d.ts +28 -0
- package/dist/application-package-host.d.ts.map +1 -0
- package/dist/application-package-host.js +2 -0
- package/dist/application-package-host.js.map +1 -0
- package/dist/ast.d.ts +853 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +245 -0
- package/dist/ast.js.map +1 -0
- package/dist/class-runtime.d.ts +6 -0
- package/dist/class-runtime.d.ts.map +1 -0
- package/dist/class-runtime.js +45 -0
- package/dist/class-runtime.js.map +1 -0
- package/dist/collection-lowering-runtime.d.ts +7 -0
- package/dist/collection-lowering-runtime.d.ts.map +1 -0
- package/dist/collection-lowering-runtime.js +952 -0
- package/dist/collection-lowering-runtime.js.map +1 -0
- package/dist/collection-runtime.d.ts +16 -0
- package/dist/collection-runtime.d.ts.map +1 -0
- package/dist/collection-runtime.js +231 -0
- package/dist/collection-runtime.js.map +1 -0
- package/dist/core-vocabulary.d.ts +141 -0
- package/dist/core-vocabulary.d.ts.map +1 -0
- package/dist/core-vocabulary.js +88 -0
- package/dist/core-vocabulary.js.map +1 -0
- package/dist/diagnostic.d.ts +42 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +66 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/embedded-javascript.d.ts +74 -0
- package/dist/embedded-javascript.d.ts.map +1 -0
- package/dist/embedded-javascript.js +341 -0
- package/dist/embedded-javascript.js.map +1 -0
- package/dist/embedded-module.d.ts +11 -0
- package/dist/embedded-module.d.ts.map +1 -0
- package/dist/embedded-module.js +67 -0
- package/dist/embedded-module.js.map +1 -0
- package/dist/embedded-source.d.ts +19 -0
- package/dist/embedded-source.d.ts.map +1 -0
- package/dist/embedded-source.js +61 -0
- package/dist/embedded-source.js.map +1 -0
- package/dist/emitter.d.ts +188 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +3254 -0
- package/dist/emitter.js.map +1 -0
- package/dist/error-runtime.d.ts +17 -0
- package/dist/error-runtime.d.ts.map +1 -0
- package/dist/error-runtime.js +109 -0
- package/dist/error-runtime.js.map +1 -0
- package/dist/extension.d.ts +373 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +25 -0
- package/dist/extension.js.map +1 -0
- package/dist/formatter.d.ts +13 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +1440 -0
- package/dist/formatter.js.map +1 -0
- package/dist/framework-host.d.ts +87 -0
- package/dist/framework-host.d.ts.map +1 -0
- package/dist/framework-host.js +2 -0
- package/dist/framework-host.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +932 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolated-string.d.ts +47 -0
- package/dist/interpolated-string.d.ts.map +1 -0
- package/dist/interpolated-string.js +344 -0
- package/dist/interpolated-string.js.map +1 -0
- package/dist/json-runtime.d.ts +2 -0
- package/dist/json-runtime.d.ts.map +1 -0
- package/dist/json-runtime.js +193 -0
- package/dist/json-runtime.js.map +1 -0
- package/dist/language-guidance.d.ts +31 -0
- package/dist/language-guidance.d.ts.map +1 -0
- package/dist/language-guidance.js +187 -0
- package/dist/language-guidance.js.map +1 -0
- package/dist/lexer.d.ts +85 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +1004 -0
- package/dist/lexer.js.map +1 -0
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -0
- package/dist/mechanical-fix.d.ts +26 -0
- package/dist/mechanical-fix.d.ts.map +1 -0
- package/dist/mechanical-fix.js +61 -0
- package/dist/mechanical-fix.js.map +1 -0
- package/dist/narrowing-runtime.d.ts +5 -0
- package/dist/narrowing-runtime.d.ts.map +1 -0
- package/dist/narrowing-runtime.js +27 -0
- package/dist/narrowing-runtime.js.map +1 -0
- package/dist/number-runtime.d.ts +3 -0
- package/dist/number-runtime.d.ts.map +1 -0
- package/dist/number-runtime.js +37 -0
- package/dist/number-runtime.js.map +1 -0
- package/dist/parser.d.ts +245 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +3582 -0
- package/dist/parser.js.map +1 -0
- package/dist/primitive-runtime.d.ts +4 -0
- package/dist/primitive-runtime.d.ts.map +1 -0
- package/dist/primitive-runtime.js +39 -0
- package/dist/primitive-runtime.js.map +1 -0
- package/dist/promise-runtime.d.ts +6 -0
- package/dist/promise-runtime.d.ts.map +1 -0
- package/dist/promise-runtime.js +86 -0
- package/dist/promise-runtime.js.map +1 -0
- package/dist/reactive-bridge-runtime.d.ts +7 -0
- package/dist/reactive-bridge-runtime.d.ts.map +1 -0
- package/dist/reactive-bridge-runtime.js +112 -0
- package/dist/reactive-bridge-runtime.js.map +1 -0
- package/dist/runtime-abi.d.ts +24 -0
- package/dist/runtime-abi.d.ts.map +1 -0
- package/dist/runtime-abi.js +24 -0
- package/dist/runtime-abi.js.map +1 -0
- package/dist/semantic.d.ts +142 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +845 -0
- package/dist/semantic.js.map +1 -0
- package/dist/source-names.d.ts +32 -0
- package/dist/source-names.d.ts.map +1 -0
- package/dist/source-names.js +101 -0
- package/dist/source-names.js.map +1 -0
- package/dist/source.d.ts +19 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +64 -0
- package/dist/source.js.map +1 -0
- package/dist/text-runtime.d.ts +2 -0
- package/dist/text-runtime.d.ts.map +1 -0
- package/dist/text-runtime.js +285 -0
- package/dist/text-runtime.js.map +1 -0
- package/dist/token.d.ts +19 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +51 -0
- package/dist/token.js.map +1 -0
- package/dist/type-registry-runtime.d.ts +7 -0
- package/dist/type-registry-runtime.d.ts.map +1 -0
- package/dist/type-registry-runtime.js +51 -0
- package/dist/type-registry-runtime.js.map +1 -0
- package/dist/type-validation-runtime.d.ts +17 -0
- package/dist/type-validation-runtime.d.ts.map +1 -0
- package/dist/type-validation-runtime.js +150 -0
- package/dist/type-validation-runtime.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1170 -0
- package/dist/types.js.map +1 -0
- package/dist/utf8-runtime.d.ts +7 -0
- package/dist/utf8-runtime.d.ts.map +1 -0
- package/dist/utf8-runtime.js +43 -0
- package/dist/utf8-runtime.js.map +1 -0
- 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
|