skir 0.0.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/README.md +447 -0
- package/dist/casing.d.ts +8 -0
- package/dist/casing.d.ts.map +1 -0
- package/dist/casing.js +49 -0
- package/dist/casing.js.map +1 -0
- package/dist/casing.test.d.ts +2 -0
- package/dist/casing.test.d.ts.map +1 -0
- package/dist/casing.test.js +134 -0
- package/dist/casing.test.js.map +1 -0
- package/dist/command_line_parser.d.ts +33 -0
- package/dist/command_line_parser.d.ts.map +1 -0
- package/dist/command_line_parser.js +171 -0
- package/dist/command_line_parser.js.map +1 -0
- package/dist/command_line_parser.test.d.ts +2 -0
- package/dist/command_line_parser.test.d.ts.map +1 -0
- package/dist/command_line_parser.test.js +302 -0
- package/dist/command_line_parser.test.js.map +1 -0
- package/dist/compatibility_checker.d.ts +68 -0
- package/dist/compatibility_checker.d.ts.map +1 -0
- package/dist/compatibility_checker.js +328 -0
- package/dist/compatibility_checker.js.map +1 -0
- package/dist/compatibility_checker.test.d.ts +2 -0
- package/dist/compatibility_checker.test.d.ts.map +1 -0
- package/dist/compatibility_checker.test.js +528 -0
- package/dist/compatibility_checker.test.js.map +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +358 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/definition_finder.d.ts +12 -0
- package/dist/definition_finder.d.ts.map +1 -0
- package/dist/definition_finder.js +180 -0
- package/dist/definition_finder.js.map +1 -0
- package/dist/definition_finder.test.d.ts +2 -0
- package/dist/definition_finder.test.d.ts.map +1 -0
- package/dist/definition_finder.test.js +164 -0
- package/dist/definition_finder.test.js.map +1 -0
- package/dist/encoding.d.ts +2 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +38 -0
- package/dist/encoding.js.map +1 -0
- package/dist/encoding.test.d.ts +2 -0
- package/dist/encoding.test.d.ts.map +1 -0
- package/dist/encoding.test.js +23 -0
- package/dist/encoding.test.js.map +1 -0
- package/dist/error_renderer.d.ts +10 -0
- package/dist/error_renderer.d.ts.map +1 -0
- package/dist/error_renderer.js +247 -0
- package/dist/error_renderer.js.map +1 -0
- package/dist/formatter.d.ts +3 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +263 -0
- package/dist/formatter.js.map +1 -0
- package/dist/formatter.test.d.ts +2 -0
- package/dist/formatter.test.d.ts.map +1 -0
- package/dist/formatter.test.js +156 -0
- package/dist/formatter.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +14 -0
- package/dist/index.test.js.map +1 -0
- package/dist/io.d.ts +13 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +22 -0
- package/dist/io.js.map +1 -0
- package/dist/language_server.d.ts +15 -0
- package/dist/language_server.d.ts.map +1 -0
- package/dist/language_server.js +248 -0
- package/dist/language_server.js.map +1 -0
- package/dist/literals.d.ts +13 -0
- package/dist/literals.d.ts.map +1 -0
- package/dist/literals.js +100 -0
- package/dist/literals.js.map +1 -0
- package/dist/literals.test.d.ts +2 -0
- package/dist/literals.test.d.ts.map +1 -0
- package/dist/literals.test.js +149 -0
- package/dist/literals.test.js.map +1 -0
- package/dist/module_collector.d.ts +3 -0
- package/dist/module_collector.d.ts.map +1 -0
- package/dist/module_collector.js +22 -0
- package/dist/module_collector.js.map +1 -0
- package/dist/module_set.d.ts +44 -0
- package/dist/module_set.d.ts.map +1 -0
- package/dist/module_set.js +1025 -0
- package/dist/module_set.js.map +1 -0
- package/dist/module_set.test.d.ts +2 -0
- package/dist/module_set.test.d.ts.map +1 -0
- package/dist/module_set.test.js +1330 -0
- package/dist/module_set.test.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +971 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +1366 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/snapshotter.d.ts +6 -0
- package/dist/snapshotter.d.ts.map +1 -0
- package/dist/snapshotter.js +107 -0
- package/dist/snapshotter.js.map +1 -0
- package/dist/tokenizer.d.ts +4 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +192 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/tokenizer.test.d.ts +2 -0
- package/dist/tokenizer.test.d.ts.map +1 -0
- package/dist/tokenizer.test.js +425 -0
- package/dist/tokenizer.test.js.map +1 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/casing.ts +64 -0
- package/src/command_line_parser.ts +249 -0
- package/src/compatibility_checker.ts +470 -0
- package/src/compiler.ts +435 -0
- package/src/config.ts +28 -0
- package/src/definition_finder.ts +221 -0
- package/src/encoding.ts +32 -0
- package/src/error_renderer.ts +278 -0
- package/src/formatter.ts +274 -0
- package/src/index.ts +6 -0
- package/src/io.ts +33 -0
- package/src/language_server.ts +301 -0
- package/src/literals.ts +120 -0
- package/src/module_collector.ts +22 -0
- package/src/module_set.ts +1175 -0
- package/src/parser.ts +1122 -0
- package/src/snapshotter.ts +136 -0
- package/src/tokenizer.ts +216 -0
- package/src/types.ts +518 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
import * as casing from "./casing.js";
|
|
2
|
+
import type {
|
|
3
|
+
Declaration,
|
|
4
|
+
ErrorSink,
|
|
5
|
+
FieldPath,
|
|
6
|
+
Import,
|
|
7
|
+
ImportAlias,
|
|
8
|
+
MutableConstant,
|
|
9
|
+
MutableDeclaration,
|
|
10
|
+
MutableField,
|
|
11
|
+
MutableMethod,
|
|
12
|
+
MutableModule,
|
|
13
|
+
MutableModuleLevelDeclaration,
|
|
14
|
+
MutableObjectEntry,
|
|
15
|
+
MutableRecord,
|
|
16
|
+
MutableRecordLevelDeclaration,
|
|
17
|
+
MutableRecordLocation,
|
|
18
|
+
MutableValue,
|
|
19
|
+
Numbering,
|
|
20
|
+
Primitive,
|
|
21
|
+
Record,
|
|
22
|
+
Removed,
|
|
23
|
+
Result,
|
|
24
|
+
SkirError,
|
|
25
|
+
Token,
|
|
26
|
+
UnresolvedArrayType,
|
|
27
|
+
UnresolvedRecordRef,
|
|
28
|
+
UnresolvedType,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
|
|
31
|
+
/** Runs syntactic analysis on a module. */
|
|
32
|
+
export function parseModule(
|
|
33
|
+
tokens: readonly Token[],
|
|
34
|
+
modulePath: string,
|
|
35
|
+
sourceCode: string,
|
|
36
|
+
): Result<MutableModule> {
|
|
37
|
+
const errors: SkirError[] = [];
|
|
38
|
+
const it = new TokenIterator(tokens, errors);
|
|
39
|
+
const declarations = parseDeclarations(it, "module");
|
|
40
|
+
it.expectThenMove([""]);
|
|
41
|
+
// Create a mappinng from names to declarations, and check for duplicates.
|
|
42
|
+
const nameToDeclaration: { [name: string]: MutableModuleLevelDeclaration } =
|
|
43
|
+
{};
|
|
44
|
+
for (const declaration of declarations) {
|
|
45
|
+
let nameTokens: Token[];
|
|
46
|
+
if (declaration.kind === "import") {
|
|
47
|
+
nameTokens = declaration.importedNames;
|
|
48
|
+
} else {
|
|
49
|
+
nameTokens = [declaration.name];
|
|
50
|
+
}
|
|
51
|
+
for (const nameToken of nameTokens) {
|
|
52
|
+
const name = nameToken.text;
|
|
53
|
+
if (name in nameToDeclaration) {
|
|
54
|
+
errors.push({
|
|
55
|
+
token: nameToken,
|
|
56
|
+
message: `Duplicate identifier "${name}"`,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
nameToDeclaration[name] = declaration;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const methods = declarations.filter(
|
|
64
|
+
(d): d is MutableMethod => d.kind === "method",
|
|
65
|
+
);
|
|
66
|
+
const constants = declarations.filter(
|
|
67
|
+
(d): d is MutableConstant => d.kind === "constant",
|
|
68
|
+
);
|
|
69
|
+
return {
|
|
70
|
+
result: {
|
|
71
|
+
kind: "module",
|
|
72
|
+
path: modulePath,
|
|
73
|
+
sourceCode: sourceCode,
|
|
74
|
+
nameToDeclaration: nameToDeclaration,
|
|
75
|
+
declarations: declarations,
|
|
76
|
+
// Populated right below.
|
|
77
|
+
records: collectModuleRecords(declarations),
|
|
78
|
+
pathToImportedNames: {},
|
|
79
|
+
methods: methods,
|
|
80
|
+
constants: constants,
|
|
81
|
+
},
|
|
82
|
+
errors: errors,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseDeclarations(
|
|
87
|
+
it: TokenIterator,
|
|
88
|
+
parentNode: "module",
|
|
89
|
+
): MutableModuleLevelDeclaration[];
|
|
90
|
+
|
|
91
|
+
function parseDeclarations(
|
|
92
|
+
it: TokenIterator,
|
|
93
|
+
parentNode: "struct" | "enum",
|
|
94
|
+
): MutableRecordLevelDeclaration[];
|
|
95
|
+
|
|
96
|
+
function parseDeclarations(
|
|
97
|
+
it: TokenIterator,
|
|
98
|
+
parentNode: "module" | "struct" | "enum",
|
|
99
|
+
): MutableDeclaration[] {
|
|
100
|
+
const result: MutableDeclaration[] = [];
|
|
101
|
+
// Returns true on a next token if it indicates that the statement is over.
|
|
102
|
+
const isEndToken = (t: string): boolean =>
|
|
103
|
+
t === "" || (parentNode !== "module" && t === "}");
|
|
104
|
+
// Returns true if the token may be the last token of a valid statement.
|
|
105
|
+
const isLastToken = (t: string): boolean => t === "}" || t === ";";
|
|
106
|
+
while (!isEndToken(it.peek())) {
|
|
107
|
+
const startIndex = it.index;
|
|
108
|
+
const declaration = parseDeclaration(it, parentNode);
|
|
109
|
+
if (declaration !== null) {
|
|
110
|
+
result.push(declaration);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// We have an invalid statement. An error was already registered. Perhaps
|
|
114
|
+
// the statement was parsed entirely but was incorrect (`removed 1, 1;`), or
|
|
115
|
+
// zero tokens were consumed (`a`), or a few tokens were consumed but did
|
|
116
|
+
// not form a statement. We want to recover from whichever scenario to avoid
|
|
117
|
+
// showing unhelpful extra error messages.
|
|
118
|
+
const noTokenWasConsumed = it.index === startIndex;
|
|
119
|
+
if (noTokenWasConsumed) {
|
|
120
|
+
it.next();
|
|
121
|
+
if (isLastToken(it.peekBack())) {
|
|
122
|
+
// For example: two semicolons in a row.
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
noTokenWasConsumed ||
|
|
128
|
+
(it.peek() !== "" && !isLastToken(it.peekBack()))
|
|
129
|
+
) {
|
|
130
|
+
let nestedLevel = 0;
|
|
131
|
+
while (true) {
|
|
132
|
+
const token = it.peek();
|
|
133
|
+
if (token === "") {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
it.next();
|
|
137
|
+
if (token === "{") {
|
|
138
|
+
++nestedLevel;
|
|
139
|
+
} else if (token === "}") {
|
|
140
|
+
--nestedLevel;
|
|
141
|
+
}
|
|
142
|
+
if (nestedLevel <= 0 && isLastToken(token)) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseDeclaration(
|
|
152
|
+
it: TokenIterator,
|
|
153
|
+
parentNode: "module" | "struct" | "enum",
|
|
154
|
+
): MutableDeclaration | null {
|
|
155
|
+
let recordType: "struct" | "enum" = "enum";
|
|
156
|
+
const parentIsRoot = parentNode === "module";
|
|
157
|
+
const expected = [
|
|
158
|
+
/*0:*/ "struct",
|
|
159
|
+
/*1:*/ "enum",
|
|
160
|
+
/*2:*/ parentIsRoot ? null : "removed",
|
|
161
|
+
/*3:*/ parentIsRoot ? null : TOKEN_IS_IDENTIFIER,
|
|
162
|
+
/*4:*/ parentIsRoot ? "import" : null,
|
|
163
|
+
/*5:*/ parentIsRoot ? "method" : null,
|
|
164
|
+
/*6:*/ parentIsRoot ? "const" : null,
|
|
165
|
+
];
|
|
166
|
+
const match = it.expectThenMove(expected);
|
|
167
|
+
switch (match.case) {
|
|
168
|
+
case 0:
|
|
169
|
+
recordType = "struct";
|
|
170
|
+
// Falls through.
|
|
171
|
+
case 1:
|
|
172
|
+
return parseRecord(it, recordType);
|
|
173
|
+
case 2:
|
|
174
|
+
return parseRemoved(it, match.token);
|
|
175
|
+
case 3:
|
|
176
|
+
return parseField(it, match.token, parentNode as "struct" | "enum");
|
|
177
|
+
case 4:
|
|
178
|
+
return parseImport(it);
|
|
179
|
+
case 5:
|
|
180
|
+
return parseMethod(it);
|
|
181
|
+
case 6:
|
|
182
|
+
return parseConstant(it);
|
|
183
|
+
default:
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class RecordBuilder {
|
|
189
|
+
constructor(
|
|
190
|
+
private readonly recordName: Token,
|
|
191
|
+
private readonly recordType: "struct" | "enum",
|
|
192
|
+
private readonly stableId: number | null,
|
|
193
|
+
private readonly errors: ErrorSink,
|
|
194
|
+
) {}
|
|
195
|
+
|
|
196
|
+
addDeclaration(declaration: MutableRecordLevelDeclaration): void {
|
|
197
|
+
if (this.numbering === "broken") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let nameToken: Token | undefined;
|
|
202
|
+
let errorToken: Token;
|
|
203
|
+
let numbers: readonly number[] = [];
|
|
204
|
+
let newNumbering = this.numbering;
|
|
205
|
+
|
|
206
|
+
// Unless explicitly specified, the number assigned to the first field of an
|
|
207
|
+
// enum is 1.
|
|
208
|
+
const nextImplicitNumber =
|
|
209
|
+
this.numbers.size + (this.recordType === "enum" ? 1 : 0);
|
|
210
|
+
|
|
211
|
+
switch (declaration.kind) {
|
|
212
|
+
case "field": {
|
|
213
|
+
nameToken = declaration.name;
|
|
214
|
+
errorToken = nameToken;
|
|
215
|
+
newNumbering = declaration.number < 0 ? "implicit" : "explicit";
|
|
216
|
+
if (declaration.number < 0) {
|
|
217
|
+
declaration = { ...declaration, number: nextImplicitNumber };
|
|
218
|
+
}
|
|
219
|
+
numbers = [declaration.number];
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "record": {
|
|
223
|
+
nameToken = declaration.name;
|
|
224
|
+
errorToken = nameToken;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "removed": {
|
|
228
|
+
errorToken = declaration.removedToken;
|
|
229
|
+
if (declaration.numbers.length) {
|
|
230
|
+
newNumbering = "explicit";
|
|
231
|
+
numbers = declaration.numbers;
|
|
232
|
+
} else {
|
|
233
|
+
newNumbering = "implicit";
|
|
234
|
+
numbers = [nextImplicitNumber];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Make sure we're not mixing implicit and explicit numbering.
|
|
240
|
+
if (this.numbering === "") {
|
|
241
|
+
this.numbering = newNumbering;
|
|
242
|
+
} else if (this.numbering !== newNumbering) {
|
|
243
|
+
this.errors.push({
|
|
244
|
+
token: errorToken,
|
|
245
|
+
message: "Cannot mix implicit and explicit numbering",
|
|
246
|
+
});
|
|
247
|
+
this.numbering = "broken";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Register the record/field name and make sure it's unique.
|
|
251
|
+
if (nameToken !== undefined) {
|
|
252
|
+
const name = nameToken.text;
|
|
253
|
+
if (name in this.nameToDeclaration) {
|
|
254
|
+
this.errors.push({
|
|
255
|
+
token: nameToken,
|
|
256
|
+
message: `Duplicate identifier "${name}"`,
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.nameToDeclaration[name] = declaration;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Register the field number and make sure it's unique.
|
|
264
|
+
for (const number of numbers) {
|
|
265
|
+
if (this.numbers.has(number)) {
|
|
266
|
+
this.errors.push({
|
|
267
|
+
token: errorToken,
|
|
268
|
+
message: `Duplicate field number ${number}`,
|
|
269
|
+
});
|
|
270
|
+
this.numbering = "broken";
|
|
271
|
+
return;
|
|
272
|
+
} else if (number === 0 && this.recordType === "enum") {
|
|
273
|
+
this.errors.push({
|
|
274
|
+
token: errorToken,
|
|
275
|
+
message: "Number 0 is reserved for UNKNOWN field",
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.numbers.add(number);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Register the removed field numbers.
|
|
283
|
+
if (declaration.kind === "removed") {
|
|
284
|
+
this.removedNumbers.push(...numbers);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
build(): MutableRecord {
|
|
289
|
+
const isStruct = this.recordType === "struct";
|
|
290
|
+
|
|
291
|
+
// If the record is a struct, make sure that all field numbers are
|
|
292
|
+
// consecutive starting from 0. The fields of an enum, on the other hand,
|
|
293
|
+
// can be sparse.
|
|
294
|
+
if (isStruct) {
|
|
295
|
+
for (let i = 0; i < this.numbers.size; ++i) {
|
|
296
|
+
if (this.numbers.has(i)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
this.errors.push({
|
|
300
|
+
token: this.recordName,
|
|
301
|
+
message: `Missing field number ${i}`,
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const declarations = Object.values(this.nameToDeclaration);
|
|
308
|
+
const fields = declarations.filter(
|
|
309
|
+
(d): d is MutableField => d.kind === "field",
|
|
310
|
+
);
|
|
311
|
+
const nestedRecords = declarations.filter(
|
|
312
|
+
(d): d is MutableRecord => d.kind === "record",
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const { recordName } = this;
|
|
316
|
+
const key = `${recordName.line.modulePath}:${recordName.position}`;
|
|
317
|
+
|
|
318
|
+
const numSlots =
|
|
319
|
+
isStruct && fields.length
|
|
320
|
+
? Math.max(...fields.map((f) => f.number)) + 1
|
|
321
|
+
: 0;
|
|
322
|
+
const numSlotsInclRemovedNumbers = isStruct ? this.numbers.size : 0;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
kind: "record",
|
|
326
|
+
key: key,
|
|
327
|
+
name: this.recordName,
|
|
328
|
+
recordType: this.recordType,
|
|
329
|
+
nameToDeclaration: this.nameToDeclaration,
|
|
330
|
+
declarations: Object.values(this.nameToDeclaration),
|
|
331
|
+
fields: fields,
|
|
332
|
+
nestedRecords: nestedRecords,
|
|
333
|
+
numbering: this.numbering,
|
|
334
|
+
removedNumbers: this.removedNumbers.sort(),
|
|
335
|
+
recordNumber: this.stableId,
|
|
336
|
+
numSlots: numSlots,
|
|
337
|
+
numSlotsInclRemovedNumbers: numSlotsInclRemovedNumbers,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private nameToDeclaration: { [n: string]: MutableRecordLevelDeclaration } =
|
|
342
|
+
{};
|
|
343
|
+
private numbers = new Set<number>();
|
|
344
|
+
private numbering: Numbering = "";
|
|
345
|
+
private removedNumbers: number[] = [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseRecord(
|
|
349
|
+
it: TokenIterator,
|
|
350
|
+
recordType: "struct" | "enum",
|
|
351
|
+
): MutableRecord | null {
|
|
352
|
+
// A struct or an enum.
|
|
353
|
+
const nameMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
354
|
+
if (nameMatch.case < 0) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
casing.validate(nameMatch.token, "UpperCamel", it.errors);
|
|
358
|
+
let stableId: number | null = null;
|
|
359
|
+
if (it.peek() === "(") {
|
|
360
|
+
it.next();
|
|
361
|
+
stableId = parseUint32(it);
|
|
362
|
+
if (stableId < 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
if (it.expectThenMove([")"]).case < 0) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (it.expectThenMove(["{"]).case < 0) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const declarations = parseDeclarations(it, recordType);
|
|
373
|
+
it.expectThenMove(["}"]);
|
|
374
|
+
const builder = new RecordBuilder(
|
|
375
|
+
nameMatch.token,
|
|
376
|
+
recordType,
|
|
377
|
+
stableId,
|
|
378
|
+
it.errors,
|
|
379
|
+
);
|
|
380
|
+
for (const declaration of declarations) {
|
|
381
|
+
builder.addDeclaration(declaration);
|
|
382
|
+
}
|
|
383
|
+
return builder.build();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseField(
|
|
387
|
+
it: TokenIterator,
|
|
388
|
+
name: Token,
|
|
389
|
+
recordType: "struct" | "enum",
|
|
390
|
+
): MutableField | null {
|
|
391
|
+
// May only be undefined if the type is an enum.
|
|
392
|
+
let type: UnresolvedType | undefined = undefined;
|
|
393
|
+
let number = -1;
|
|
394
|
+
|
|
395
|
+
while (true) {
|
|
396
|
+
const typeAllowed = type === undefined && number < 0;
|
|
397
|
+
const endAllowed = type !== undefined || recordType === "enum";
|
|
398
|
+
const numberAllowed = number < 0 && endAllowed;
|
|
399
|
+
const expected = [
|
|
400
|
+
/*0:*/ typeAllowed ? ":" : null,
|
|
401
|
+
/*1:*/ numberAllowed ? "=" : null,
|
|
402
|
+
/*2:*/ endAllowed ? ";" : null,
|
|
403
|
+
];
|
|
404
|
+
const match = it.expectThenMove(expected);
|
|
405
|
+
switch (match.case) {
|
|
406
|
+
case 0: {
|
|
407
|
+
type = parseType(it);
|
|
408
|
+
if (type === undefined) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case 1: {
|
|
414
|
+
number = parseUint32(it);
|
|
415
|
+
if (number < 0) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case 2: {
|
|
421
|
+
const expectedCasing = type ? "lower_underscore" : "UPPER_UNDERSCORE";
|
|
422
|
+
casing.validate(name, expectedCasing, it.errors);
|
|
423
|
+
if (recordType === "enum" && name.text === "UNKNOWN") {
|
|
424
|
+
it.errors.push({
|
|
425
|
+
token: name,
|
|
426
|
+
message: `Cannot name field of enum: UNKNOWN`,
|
|
427
|
+
});
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
kind: "field",
|
|
432
|
+
name: name,
|
|
433
|
+
number: number,
|
|
434
|
+
unresolvedType: type,
|
|
435
|
+
// Will be populated at a later stage.
|
|
436
|
+
type: undefined,
|
|
437
|
+
// Will be populated at a later stage.
|
|
438
|
+
isRecursive: false,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
case -1:
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const PRIMITIVE_TYPES: ReadonlySet<string> = new Set<Primitive>([
|
|
448
|
+
"bool",
|
|
449
|
+
"int32",
|
|
450
|
+
"int64",
|
|
451
|
+
"uint64",
|
|
452
|
+
"float32",
|
|
453
|
+
"float64",
|
|
454
|
+
"timestamp",
|
|
455
|
+
"string",
|
|
456
|
+
"bytes",
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
function parseType(it: TokenIterator): UnresolvedType | undefined {
|
|
460
|
+
const match = it.expectThenMove([
|
|
461
|
+
/*0:*/ "[",
|
|
462
|
+
/*1:*/ TOKEN_IS_IDENTIFIER,
|
|
463
|
+
/*2:*/ ".",
|
|
464
|
+
]);
|
|
465
|
+
let value: UnresolvedType | undefined;
|
|
466
|
+
switch (match.case) {
|
|
467
|
+
case 0: {
|
|
468
|
+
// Left square bracket.
|
|
469
|
+
value = parseArrayType(it);
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case 1:
|
|
473
|
+
// An identifier.
|
|
474
|
+
if (PRIMITIVE_TYPES.has(match.token.text)) {
|
|
475
|
+
value = {
|
|
476
|
+
kind: "primitive",
|
|
477
|
+
primitive: match.token.text as Primitive,
|
|
478
|
+
};
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
// Falls through.
|
|
482
|
+
case 2: {
|
|
483
|
+
// Dot.
|
|
484
|
+
value = parseRecordRef(it, match.token);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
default:
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (value === undefined) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
if (it.peek() === "?") {
|
|
494
|
+
it.next();
|
|
495
|
+
return { kind: "optional", other: value };
|
|
496
|
+
} else {
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parseArrayType(it: TokenIterator): UnresolvedArrayType | undefined {
|
|
502
|
+
const item = parseType(it);
|
|
503
|
+
if (item === undefined) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
let key: FieldPath | undefined = undefined;
|
|
507
|
+
while (true) {
|
|
508
|
+
const keyAllowed = !key && item.kind === "record";
|
|
509
|
+
const match = it.expectThenMove([
|
|
510
|
+
/*0:*/ keyAllowed ? "|" : null,
|
|
511
|
+
/*1:*/ "]",
|
|
512
|
+
]);
|
|
513
|
+
switch (match.case) {
|
|
514
|
+
case 0: {
|
|
515
|
+
// '|'
|
|
516
|
+
key = parseFieldPath(it, match.token);
|
|
517
|
+
if (key === null) {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case 1:
|
|
523
|
+
return { kind: "array", item: item, key: key };
|
|
524
|
+
default:
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function parseFieldPath(
|
|
531
|
+
it: TokenIterator,
|
|
532
|
+
pipeToken: Token,
|
|
533
|
+
): FieldPath | undefined {
|
|
534
|
+
const fieldNames: Token[] = [];
|
|
535
|
+
while (true) {
|
|
536
|
+
const match = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
537
|
+
if (match.case < 0) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
fieldNames.push(match.token);
|
|
541
|
+
if (it.peek() === ".") {
|
|
542
|
+
it.next();
|
|
543
|
+
} else {
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const path = fieldNames.map((name) => ({
|
|
548
|
+
name: name,
|
|
549
|
+
}));
|
|
550
|
+
return {
|
|
551
|
+
pipeToken: pipeToken,
|
|
552
|
+
path,
|
|
553
|
+
// Just because we need to provide a value.
|
|
554
|
+
// The correct value will be populated at a later stage.
|
|
555
|
+
keyType: { kind: "primitive", primitive: "bool" },
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function parseRecordRef(
|
|
560
|
+
it: TokenIterator,
|
|
561
|
+
nameOrDot: Token,
|
|
562
|
+
): UnresolvedRecordRef | undefined {
|
|
563
|
+
const absolute = nameOrDot.text === ".";
|
|
564
|
+
const nameParts: Token[] = [];
|
|
565
|
+
if (nameOrDot.text === ".") {
|
|
566
|
+
const match = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
567
|
+
if (match.case < 0) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
nameParts.push(match.token);
|
|
571
|
+
} else {
|
|
572
|
+
nameParts.push(nameOrDot);
|
|
573
|
+
}
|
|
574
|
+
while (it.peek() === ".") {
|
|
575
|
+
it.next();
|
|
576
|
+
const match = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
577
|
+
if (match.case < 0) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
nameParts.push(match.token);
|
|
581
|
+
}
|
|
582
|
+
return { kind: "record", nameParts: nameParts, absolute: absolute };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function parseUint32(it: TokenIterator): number {
|
|
586
|
+
const match = it.expectThenMove([TOKEN_IS_POSITIVE_INT]);
|
|
587
|
+
if (match.case < 0) {
|
|
588
|
+
return -1;
|
|
589
|
+
}
|
|
590
|
+
const { text } = match.token;
|
|
591
|
+
const valueAsBigInt = BigInt(text);
|
|
592
|
+
if (valueAsBigInt < BigInt(2 ** 32)) {
|
|
593
|
+
return +text;
|
|
594
|
+
} else {
|
|
595
|
+
it.errors.push({
|
|
596
|
+
token: match.token,
|
|
597
|
+
message: "Value out of uint32 range",
|
|
598
|
+
});
|
|
599
|
+
return -1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Parses the "removed" declaration.
|
|
604
|
+
// Assumes the current token is the token after "removed".
|
|
605
|
+
function parseRemoved(it: TokenIterator, removedToken: Token): Removed | null {
|
|
606
|
+
const numbers: number[] = [];
|
|
607
|
+
// The 5 states are:
|
|
608
|
+
// · '?': expect a number or a semicolon
|
|
609
|
+
// · ',': expect a comma or a semicolon
|
|
610
|
+
// · '..': expect a comma, a semicolon or a '..'
|
|
611
|
+
// · '0': expect a single number or the lower bound of a range
|
|
612
|
+
// · '1': expect the upper bound of a range
|
|
613
|
+
let expect: "?" | "," | ".." | "0" | "1" = "?";
|
|
614
|
+
let lowerBound: number | undefined;
|
|
615
|
+
loop: while (true) {
|
|
616
|
+
const expected: Array<string | TokenPredicate | null> = [
|
|
617
|
+
/*0:*/ expect === "," || expect === ".." ? "," : null,
|
|
618
|
+
/*1:*/ expect === "?" || expect === "0" || expect === "1"
|
|
619
|
+
? TOKEN_IS_POSITIVE_INT
|
|
620
|
+
: null,
|
|
621
|
+
/*2:*/ expect === "?" || expect === "," || expect === ".." ? ";" : null,
|
|
622
|
+
/*3:*/ expect === ".." ? ".." : null,
|
|
623
|
+
];
|
|
624
|
+
const match = it.expectThenMove(expected);
|
|
625
|
+
switch (match.case) {
|
|
626
|
+
case 0: {
|
|
627
|
+
// A comma.
|
|
628
|
+
expect = "0";
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 1: {
|
|
632
|
+
// A number.
|
|
633
|
+
const number = +match.token.text;
|
|
634
|
+
if (lowerBound === undefined) {
|
|
635
|
+
expect = "..";
|
|
636
|
+
numbers.push(number);
|
|
637
|
+
} else {
|
|
638
|
+
expect = ",";
|
|
639
|
+
if (number <= lowerBound) {
|
|
640
|
+
it.errors.push({
|
|
641
|
+
token: removedToken,
|
|
642
|
+
message: "Upper bound must be greater than lower bound",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
for (let n = lowerBound; n <= number; ++n) {
|
|
646
|
+
numbers.push(n);
|
|
647
|
+
}
|
|
648
|
+
lowerBound = undefined;
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case 2: {
|
|
653
|
+
// A semicolon.
|
|
654
|
+
break loop;
|
|
655
|
+
}
|
|
656
|
+
case 3: {
|
|
657
|
+
// A '..'
|
|
658
|
+
expect = "1";
|
|
659
|
+
lowerBound = numbers.pop()!;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case -1:
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Make sure we don't have a duplicate number.
|
|
667
|
+
const seenNumbers = new Set<number>();
|
|
668
|
+
for (const number of numbers) {
|
|
669
|
+
if (seenNumbers.has(number)) {
|
|
670
|
+
it.errors.push({
|
|
671
|
+
token: removedToken,
|
|
672
|
+
message: `Duplicate field number ${number}`,
|
|
673
|
+
});
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
seenNumbers.add(number);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
kind: "removed",
|
|
681
|
+
removedToken: removedToken,
|
|
682
|
+
numbers: numbers,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function parseImport(it: TokenIterator): Import | ImportAlias | null {
|
|
687
|
+
const tokenMatch = it.expectThenMove(["*", TOKEN_IS_IDENTIFIER]);
|
|
688
|
+
switch (tokenMatch.case) {
|
|
689
|
+
case 0:
|
|
690
|
+
return parseImportAs(it);
|
|
691
|
+
case 1:
|
|
692
|
+
return parseImportGivenNames(tokenMatch.token, it);
|
|
693
|
+
default:
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function parseImportAs(it: TokenIterator): ImportAlias | null {
|
|
699
|
+
if (it.expectThenMove(["as"]).case < 0) return null;
|
|
700
|
+
const aliasMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
701
|
+
if (aliasMatch.case < 0) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
casing.validate(aliasMatch.token, "lower_underscore", it.errors);
|
|
705
|
+
if (it.expectThenMove(["from"]).case < 0) return null;
|
|
706
|
+
const modulePathMatch = it.expectThenMove([TOKEN_IS_STRING_LITERAL]);
|
|
707
|
+
if (modulePathMatch.case < 0) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
it.expectThenMove([";"]);
|
|
711
|
+
const modulePath = modulePathMatch.token;
|
|
712
|
+
return {
|
|
713
|
+
kind: "import-alias",
|
|
714
|
+
name: aliasMatch.token,
|
|
715
|
+
modulePath,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function parseImportGivenNames(
|
|
720
|
+
firstName: Token,
|
|
721
|
+
it: TokenIterator,
|
|
722
|
+
): Import | null {
|
|
723
|
+
const importedNames = [firstName];
|
|
724
|
+
while (it.peek() === ",") {
|
|
725
|
+
it.next();
|
|
726
|
+
const nameMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
727
|
+
if (nameMatch.case < 0) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
importedNames.push(nameMatch.token);
|
|
731
|
+
}
|
|
732
|
+
if (it.expectThenMove(["from"]).case < 0) return null;
|
|
733
|
+
const modulePathMatch = it.expectThenMove([TOKEN_IS_STRING_LITERAL]);
|
|
734
|
+
if (modulePathMatch.case < 0) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
it.expectThenMove([";"]);
|
|
738
|
+
const modulePath = modulePathMatch.token;
|
|
739
|
+
return {
|
|
740
|
+
kind: "import",
|
|
741
|
+
importedNames,
|
|
742
|
+
modulePath,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function parseMethod(it: TokenIterator): MutableMethod | null {
|
|
747
|
+
const nameMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
748
|
+
if (nameMatch.case < 0) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
casing.validate(nameMatch.token, "UpperCamel", it.errors);
|
|
752
|
+
if (it.expectThenMove(["("]).case < 0) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
const requestType = parseType(it);
|
|
756
|
+
if (!requestType) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
if (it.expectThenMove([")"]).case < 0 || it.expectThenMove([":"]).case < 0) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const responseType = parseType(it);
|
|
763
|
+
if (!responseType) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const explicitNumber = it.expectThenMove(["=", ";"]).case === 0;
|
|
768
|
+
let number: number;
|
|
769
|
+
if (explicitNumber) {
|
|
770
|
+
number = parseUint32(it);
|
|
771
|
+
if (number < 0) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
it.expectThenMove([";"]);
|
|
775
|
+
} else {
|
|
776
|
+
const methodName = nameMatch.token.text;
|
|
777
|
+
const { modulePath } = nameMatch.token.line;
|
|
778
|
+
number = simpleHash(`${modulePath}:${methodName}`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
kind: "method",
|
|
783
|
+
name: nameMatch.token,
|
|
784
|
+
unresolvedRequestType: requestType,
|
|
785
|
+
unresolvedResponseType: responseType,
|
|
786
|
+
// Will be populated at a later stage.
|
|
787
|
+
requestType: undefined,
|
|
788
|
+
// Will be populated at a later stage.
|
|
789
|
+
responseType: undefined,
|
|
790
|
+
number: number,
|
|
791
|
+
hasExplicitNumber: explicitNumber,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function parseConstant(it: TokenIterator): MutableConstant | null {
|
|
796
|
+
const nameMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
797
|
+
if (nameMatch.case < 0) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
casing.validate(nameMatch.token, "UPPER_UNDERSCORE", it.errors);
|
|
801
|
+
if (it.expectThenMove([":"]).case < 0) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const type = parseType(it);
|
|
805
|
+
if (!type) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
if (it.expectThenMove(["="]).case < 0) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
const value = parseValue(it);
|
|
812
|
+
if (value === null) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
it.expectThenMove([";"]);
|
|
816
|
+
return {
|
|
817
|
+
kind: "constant",
|
|
818
|
+
name: nameMatch.token,
|
|
819
|
+
unresolvedType: type,
|
|
820
|
+
type: undefined,
|
|
821
|
+
value: value,
|
|
822
|
+
valueAsDenseJson: undefined,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function parseValue(it: TokenIterator): MutableValue | null {
|
|
827
|
+
const expected = [
|
|
828
|
+
/*0:*/ "{",
|
|
829
|
+
/*1:*/ "{|",
|
|
830
|
+
/*2:*/ "[",
|
|
831
|
+
/*3:*/ "false",
|
|
832
|
+
/*4:*/ "true",
|
|
833
|
+
/*5:*/ "null",
|
|
834
|
+
/*6:*/ TOKEN_IS_NUMBER,
|
|
835
|
+
/*7:*/ TOKEN_IS_STRING_LITERAL,
|
|
836
|
+
];
|
|
837
|
+
const match = it.expectThenMove(expected);
|
|
838
|
+
switch (match.case) {
|
|
839
|
+
case 0:
|
|
840
|
+
case 1: {
|
|
841
|
+
const partial = match.case === 1;
|
|
842
|
+
const entries = parseObjectValue(it, partial);
|
|
843
|
+
if (entries === null) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
kind: "object",
|
|
848
|
+
token: match.token,
|
|
849
|
+
entries: entries,
|
|
850
|
+
partial: partial,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
case 2: {
|
|
854
|
+
const items = parseArrayValue(it);
|
|
855
|
+
if (items === null) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
kind: "array",
|
|
860
|
+
token: match.token,
|
|
861
|
+
items: items,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
case 3:
|
|
865
|
+
case 4:
|
|
866
|
+
case 5:
|
|
867
|
+
case 6:
|
|
868
|
+
case 7:
|
|
869
|
+
return {
|
|
870
|
+
kind: "literal",
|
|
871
|
+
token: match.token,
|
|
872
|
+
};
|
|
873
|
+
default:
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function parseObjectValue(
|
|
879
|
+
it: TokenIterator,
|
|
880
|
+
partial: boolean,
|
|
881
|
+
): { [f: string]: MutableObjectEntry } | null {
|
|
882
|
+
const closingToken = partial ? "|}" : "}";
|
|
883
|
+
const entries: { [f: string]: MutableObjectEntry } = {};
|
|
884
|
+
while (true) {
|
|
885
|
+
if (it.peek() === closingToken) {
|
|
886
|
+
it.next();
|
|
887
|
+
return entries;
|
|
888
|
+
}
|
|
889
|
+
const fieldNameMatch = it.expectThenMove([TOKEN_IS_IDENTIFIER]);
|
|
890
|
+
if (fieldNameMatch.case < 0) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const fieldNameToken = fieldNameMatch.token;
|
|
894
|
+
const fieldName = fieldNameMatch.token.text;
|
|
895
|
+
if (it.expectThenMove([":"]).case < 0) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const value = parseValue(it);
|
|
899
|
+
if (value === null) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
if (fieldName in entries) {
|
|
903
|
+
it.errors.push({
|
|
904
|
+
token: fieldNameMatch.token,
|
|
905
|
+
message: "Duplicate field",
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
entries[fieldName] = {
|
|
909
|
+
name: fieldNameToken,
|
|
910
|
+
value: value,
|
|
911
|
+
};
|
|
912
|
+
const endMatch = it.expectThenMove([",", closingToken]);
|
|
913
|
+
if (endMatch.case < 0) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
if (endMatch.token.text === closingToken) {
|
|
917
|
+
return entries;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function parseArrayValue(it: TokenIterator): MutableValue[] | null {
|
|
923
|
+
if (it.peek() === "]") {
|
|
924
|
+
it.next();
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
const items: MutableValue[] = [];
|
|
928
|
+
while (true) {
|
|
929
|
+
const item = parseValue(it);
|
|
930
|
+
if (item === null) {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
items.push(item);
|
|
934
|
+
const match = it.expectThenMove([",", "]"]);
|
|
935
|
+
if (match.case < 0) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
if (match.token.text === "]") {
|
|
939
|
+
return items;
|
|
940
|
+
}
|
|
941
|
+
if (it.peek() === "]") {
|
|
942
|
+
it.next();
|
|
943
|
+
return items;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
abstract class TokenPredicate {
|
|
949
|
+
abstract matches(token: string): boolean;
|
|
950
|
+
abstract what(): string;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
class TokenIsIdentifier extends TokenPredicate {
|
|
954
|
+
override matches(token: string): boolean {
|
|
955
|
+
return /^\w/.test(token);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
override what(): string {
|
|
959
|
+
return "identifier";
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const TOKEN_IS_IDENTIFIER = new TokenIsIdentifier();
|
|
964
|
+
|
|
965
|
+
class TokenIsPositiveInt extends TokenPredicate {
|
|
966
|
+
override matches(token: string): boolean {
|
|
967
|
+
return /^[0-9]+$/.test(token);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
override what(): string {
|
|
971
|
+
return "positive integer";
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const TOKEN_IS_POSITIVE_INT = new TokenIsPositiveInt();
|
|
976
|
+
|
|
977
|
+
class TokenIsNumber extends TokenPredicate {
|
|
978
|
+
override matches(token: string): boolean {
|
|
979
|
+
return /^[0-9-]/.test(token);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
override what(): string {
|
|
983
|
+
return "number";
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const TOKEN_IS_NUMBER = new TokenIsNumber();
|
|
988
|
+
|
|
989
|
+
class TokenIsStringLiteral extends TokenPredicate {
|
|
990
|
+
override matches(token: string): boolean {
|
|
991
|
+
return /^["']/.test(token);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
override what(): string {
|
|
995
|
+
return "string literal";
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const TOKEN_IS_STRING_LITERAL = new TokenIsStringLiteral();
|
|
1000
|
+
|
|
1001
|
+
interface TokenMatch {
|
|
1002
|
+
case: number;
|
|
1003
|
+
token: Token;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
class TokenIterator {
|
|
1007
|
+
constructor(
|
|
1008
|
+
private readonly tokens: readonly Token[],
|
|
1009
|
+
readonly errors: ErrorSink,
|
|
1010
|
+
) {}
|
|
1011
|
+
|
|
1012
|
+
// Returns both:
|
|
1013
|
+
// · the index of the first predicate matching the current token, or -1 if
|
|
1014
|
+
// there is none
|
|
1015
|
+
// · the current token (before the move)
|
|
1016
|
+
//
|
|
1017
|
+
// If the current token matches any predicate, i.e. if the index is not -1,
|
|
1018
|
+
// moves to the next token before returning. Otherwise, registers an error.
|
|
1019
|
+
expectThenMove(
|
|
1020
|
+
expected: ReadonlyArray<string | TokenPredicate | null>,
|
|
1021
|
+
): TokenMatch {
|
|
1022
|
+
const token = this.tokens[this.tokenIndex]!;
|
|
1023
|
+
for (let i = 0; i < expected.length; ++i) {
|
|
1024
|
+
const e = expected[i];
|
|
1025
|
+
if (e === null) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const match =
|
|
1029
|
+
e instanceof TokenPredicate ? e.matches(token.text) : token.text === e;
|
|
1030
|
+
if (!match) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
++this.tokenIndex;
|
|
1034
|
+
return {
|
|
1035
|
+
case: i,
|
|
1036
|
+
token: token,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// No match: register an error.
|
|
1041
|
+
const expectedParts: string[] = [];
|
|
1042
|
+
for (let i = 0; i < expected.length; ++i) {
|
|
1043
|
+
const e = expected[i];
|
|
1044
|
+
if (e === null) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
expectedParts.push(e instanceof TokenPredicate ? e.what() : `"${e}"`);
|
|
1048
|
+
}
|
|
1049
|
+
const expectedMsg =
|
|
1050
|
+
expectedParts.length === 1
|
|
1051
|
+
? expectedParts[0]!
|
|
1052
|
+
: `one of: ${expectedParts.join(", ")}`;
|
|
1053
|
+
|
|
1054
|
+
this.errors.push({
|
|
1055
|
+
token: token,
|
|
1056
|
+
expected: expectedMsg,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
case: -1,
|
|
1061
|
+
token: token,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
peek(): string {
|
|
1066
|
+
return this.tokens[this.tokenIndex]!.text;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
peekBack(): string {
|
|
1070
|
+
return this.tokens[this.tokenIndex - 1]!.text;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
next(): void {
|
|
1074
|
+
++this.tokenIndex;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
get index(): number {
|
|
1078
|
+
return this.tokenIndex;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private tokenIndex = 0;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** Returns a uint32 hash of the given string. */
|
|
1085
|
+
export function simpleHash(input: string): number {
|
|
1086
|
+
// From https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
|
|
1087
|
+
let hash = 0;
|
|
1088
|
+
for (let i = 0; i < input.length; i++) {
|
|
1089
|
+
const char = input.charCodeAt(i);
|
|
1090
|
+
hash = (hash << 5) - hash + char;
|
|
1091
|
+
hash |= 0;
|
|
1092
|
+
}
|
|
1093
|
+
// Signed int32 to unsigned int32.
|
|
1094
|
+
return hash >>> 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function collectModuleRecords(
|
|
1098
|
+
declarations: readonly Declaration[],
|
|
1099
|
+
): MutableRecordLocation[] {
|
|
1100
|
+
const result: MutableRecordLocation[] = [];
|
|
1101
|
+
const collect = (
|
|
1102
|
+
declarations: readonly Declaration[],
|
|
1103
|
+
ancestors: readonly Record[],
|
|
1104
|
+
): void => {
|
|
1105
|
+
for (const record of declarations) {
|
|
1106
|
+
if (record.kind !== "record") continue;
|
|
1107
|
+
const updatedRecordAncestors = ancestors.concat([record]);
|
|
1108
|
+
const modulePath = record.name.line.modulePath;
|
|
1109
|
+
const recordLocation: MutableRecordLocation = {
|
|
1110
|
+
kind: "record-location",
|
|
1111
|
+
record: record,
|
|
1112
|
+
recordAncestors: updatedRecordAncestors,
|
|
1113
|
+
modulePath: modulePath,
|
|
1114
|
+
};
|
|
1115
|
+
// We want depth-first.
|
|
1116
|
+
collect(record.declarations, updatedRecordAncestors);
|
|
1117
|
+
result.push(recordLocation);
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
collect(declarations, []);
|
|
1121
|
+
return result;
|
|
1122
|
+
}
|